@shipeasy/sdk 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -118,6 +118,15 @@ declare function getShipeasyClient(): FlagsClientBrowser | null;
118
118
  * Not part of the documented surface; production code should never call this.
119
119
  */
120
120
  declare function _resetShipeasyForTests(): void;
121
+ interface BootstrapPayload {
122
+ flags: Record<string, boolean>;
123
+ configs: Record<string, unknown>;
124
+ experiments: Record<string, {
125
+ inExperiment: boolean;
126
+ group: string;
127
+ params: Record<string, unknown>;
128
+ }>;
129
+ }
121
130
  /**
122
131
  * Universal flags facade. Methods return safe defaults when the singleton
123
132
  * hasn't been configured yet (false / undefined / `notIn` experiment), so
@@ -126,12 +135,24 @@ declare function _resetShipeasyForTests(): void;
126
135
  declare const flags: {
127
136
  configure(opts: FlagsClientBrowserOptions): void;
128
137
  identify(user: User): Promise<void>;
129
- /** Read a feature gate. Returns false until identify() resolves. */
138
+ /**
139
+ * Read a feature gate.
140
+ * Priority: URL override → server bootstrap (window.__SE_BOOTSTRAP) → CDN-fetched (post-mount) → false.
141
+ * The _mountedAndReady gate still applies for the CDN path to prevent hydration
142
+ * mismatches on force-static pages; bootstrap data is safe to read immediately
143
+ * because the server rendered with the same values.
144
+ */
130
145
  get(name: string): boolean;
131
146
  getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
132
147
  getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P, variants?: Record<string, Partial<P>>): ExperimentResult<P>;
133
148
  track(eventName: string, props?: Record<string, unknown>): void;
134
149
  flush(): Promise<void>;
150
+ /**
151
+ * Called by FlagsBoundary after React hydration to unlock flag reads.
152
+ * Dispatches se:override:change so subscribers (FlagsBoundary) re-render
153
+ * once with real values — URL overrides and server-evaluated flags.
154
+ */
155
+ notifyMounted(): void;
135
156
  /** Subscribe for change notifications (identify/override). Used by framework adapters. */
136
157
  subscribe(listener: () => void): () => void;
137
158
  /** True once identify() has completed and flags are available. */
@@ -148,14 +169,14 @@ interface LabelAttrs {
148
169
  "data-label-desc"?: string;
149
170
  }
150
171
  declare function labelAttrs(key: string, variables?: Record<string, string | number>, desc?: string): LabelAttrs;
172
+ /**
173
+ * Universal i18n facade. Backed by the `window.i18n` global the loader
174
+ * script installs. Returns the key itself when the loader hasn't run
175
+ * (SSR, missing script tag, before profile fetch completes), so call
176
+ * sites never need to null-check.
177
+ */
151
178
  declare const i18n: {
152
- /**
153
- * Look up `key` in the active translation profile. When the profile
154
- * hasn't been fetched yet (SSR, CDN downtime, missing key), interpolate
155
- * `fallback` instead — `fallback` is the source-of-truth English copy
156
- * and is mandatory so the page never renders a raw key.
157
- */
158
- t(key: string, fallback: string, variables?: Record<string, string | number>): string;
179
+ t(key: string, variables?: Record<string, string | number>): string;
159
180
  /**
160
181
  * Translate a key and return a framework element (e.g. React <span>)
161
182
  * carrying `data-label` / `data-variables` attributes so the ShipEasy
@@ -181,4 +202,4 @@ declare const i18n: {
181
202
  onUpdate(cb: () => void): () => void;
182
203
  };
183
204
 
184
- export { type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type ShipeasySdkBridge, type User, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, version };
205
+ export { type BootstrapPayload, type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type ShipeasySdkBridge, type User, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, version };
@@ -118,6 +118,15 @@ declare function getShipeasyClient(): FlagsClientBrowser | null;
118
118
  * Not part of the documented surface; production code should never call this.
119
119
  */
120
120
  declare function _resetShipeasyForTests(): void;
121
+ interface BootstrapPayload {
122
+ flags: Record<string, boolean>;
123
+ configs: Record<string, unknown>;
124
+ experiments: Record<string, {
125
+ inExperiment: boolean;
126
+ group: string;
127
+ params: Record<string, unknown>;
128
+ }>;
129
+ }
121
130
  /**
122
131
  * Universal flags facade. Methods return safe defaults when the singleton
123
132
  * hasn't been configured yet (false / undefined / `notIn` experiment), so
@@ -126,12 +135,24 @@ declare function _resetShipeasyForTests(): void;
126
135
  declare const flags: {
127
136
  configure(opts: FlagsClientBrowserOptions): void;
128
137
  identify(user: User): Promise<void>;
129
- /** Read a feature gate. Returns false until identify() resolves. */
138
+ /**
139
+ * Read a feature gate.
140
+ * Priority: URL override → server bootstrap (window.__SE_BOOTSTRAP) → CDN-fetched (post-mount) → false.
141
+ * The _mountedAndReady gate still applies for the CDN path to prevent hydration
142
+ * mismatches on force-static pages; bootstrap data is safe to read immediately
143
+ * because the server rendered with the same values.
144
+ */
130
145
  get(name: string): boolean;
131
146
  getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
132
147
  getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P, variants?: Record<string, Partial<P>>): ExperimentResult<P>;
133
148
  track(eventName: string, props?: Record<string, unknown>): void;
134
149
  flush(): Promise<void>;
150
+ /**
151
+ * Called by FlagsBoundary after React hydration to unlock flag reads.
152
+ * Dispatches se:override:change so subscribers (FlagsBoundary) re-render
153
+ * once with real values — URL overrides and server-evaluated flags.
154
+ */
155
+ notifyMounted(): void;
135
156
  /** Subscribe for change notifications (identify/override). Used by framework adapters. */
136
157
  subscribe(listener: () => void): () => void;
137
158
  /** True once identify() has completed and flags are available. */
@@ -148,14 +169,14 @@ interface LabelAttrs {
148
169
  "data-label-desc"?: string;
149
170
  }
150
171
  declare function labelAttrs(key: string, variables?: Record<string, string | number>, desc?: string): LabelAttrs;
172
+ /**
173
+ * Universal i18n facade. Backed by the `window.i18n` global the loader
174
+ * script installs. Returns the key itself when the loader hasn't run
175
+ * (SSR, missing script tag, before profile fetch completes), so call
176
+ * sites never need to null-check.
177
+ */
151
178
  declare const i18n: {
152
- /**
153
- * Look up `key` in the active translation profile. When the profile
154
- * hasn't been fetched yet (SSR, CDN downtime, missing key), interpolate
155
- * `fallback` instead — `fallback` is the source-of-truth English copy
156
- * and is mandatory so the page never renders a raw key.
157
- */
158
- t(key: string, fallback: string, variables?: Record<string, string | number>): string;
179
+ t(key: string, variables?: Record<string, string | number>): string;
159
180
  /**
160
181
  * Translate a key and return a framework element (e.g. React <span>)
161
182
  * carrying `data-label` / `data-variables` attributes so the ShipEasy
@@ -181,4 +202,4 @@ declare const i18n: {
181
202
  onUpdate(cb: () => void): () => void;
182
203
  };
183
204
 
184
- export { type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type ShipeasySdkBridge, type User, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, version };
205
+ export { type BootstrapPayload, type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type ShipeasySdkBridge, type User, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, version };
@@ -454,15 +454,13 @@ var FlagsClientBrowser = class {
454
454
  this.evalResult = data;
455
455
  }
456
456
  getFlag(name) {
457
- if (this.evalResult === null) return false;
458
457
  const ov = readGateOverride(name);
459
458
  if (ov !== null) return ov;
460
- return this.evalResult.flags[name] ?? false;
459
+ return this.evalResult?.flags[name] ?? false;
461
460
  }
462
461
  getConfig(name, decode) {
463
- if (this.evalResult === null) return void 0;
464
462
  const ov = readConfigOverride(name);
465
- const raw = ov !== void 0 ? ov : this.evalResult.configs?.[name];
463
+ const raw = ov !== void 0 ? ov : this.evalResult?.configs?.[name];
466
464
  if (raw === void 0) return void 0;
467
465
  if (!decode) return raw;
468
466
  try {
@@ -661,6 +659,20 @@ function _resetShipeasyForTests() {
661
659
  _client?.destroy();
662
660
  _client = null;
663
661
  }
662
+ function getBootstrap() {
663
+ if (typeof window === "undefined") return null;
664
+ return window.__SE_BOOTSTRAP ?? null;
665
+ }
666
+ var _mountedAndReady = false;
667
+ var _standaloneListeners = /* @__PURE__ */ new Set();
668
+ var _standaloneOverrideWired = false;
669
+ function wireStandaloneOverride() {
670
+ if (_standaloneOverrideWired || typeof window === "undefined") return;
671
+ _standaloneOverrideWired = true;
672
+ window.addEventListener("se:override:change", () => {
673
+ for (const cb of _standaloneListeners) cb();
674
+ });
675
+ }
664
676
  var flags = {
665
677
  configure(opts) {
666
678
  configureShipeasy(opts);
@@ -672,12 +684,45 @@ var flags = {
672
684
  }
673
685
  return _client.identify(user);
674
686
  },
675
- /** Read a feature gate. Returns false until identify() resolves. */
687
+ /**
688
+ * Read a feature gate.
689
+ * Priority: URL override → server bootstrap (window.__SE_BOOTSTRAP) → CDN-fetched (post-mount) → false.
690
+ * The _mountedAndReady gate still applies for the CDN path to prevent hydration
691
+ * mismatches on force-static pages; bootstrap data is safe to read immediately
692
+ * because the server rendered with the same values.
693
+ */
676
694
  get(name) {
677
- return _client?.getFlag(name) ?? false;
695
+ const ov = readGateOverride(name);
696
+ if (ov !== null) return ov;
697
+ const bs = getBootstrap();
698
+ if (bs !== null && name in bs.flags) return bs.flags[name];
699
+ if (!_mountedAndReady) return false;
700
+ if (_client) return _client.getFlag(name);
701
+ return false;
678
702
  },
679
703
  getConfig(name, decode) {
680
- return _client?.getConfig(name, decode);
704
+ const ov = readConfigOverride(name);
705
+ if (ov !== void 0) {
706
+ if (!decode) return ov;
707
+ try {
708
+ return decode(ov);
709
+ } catch {
710
+ return void 0;
711
+ }
712
+ }
713
+ const bs = getBootstrap();
714
+ if (bs !== null && name in bs.configs) {
715
+ const raw = bs.configs[name];
716
+ if (!decode) return raw;
717
+ try {
718
+ return decode(raw);
719
+ } catch {
720
+ return void 0;
721
+ }
722
+ }
723
+ if (!_mountedAndReady) return void 0;
724
+ if (_client) return _client.getConfig(name, decode);
725
+ return void 0;
681
726
  },
682
727
  getExperiment(name, defaultParams, decode, variants) {
683
728
  return _client?.getExperiment(name, defaultParams, decode, variants) ?? {
@@ -692,11 +737,24 @@ var flags = {
692
737
  flush() {
693
738
  return _client?.flush() ?? Promise.resolve();
694
739
  },
740
+ /**
741
+ * Called by FlagsBoundary after React hydration to unlock flag reads.
742
+ * Dispatches se:override:change so subscribers (FlagsBoundary) re-render
743
+ * once with real values — URL overrides and server-evaluated flags.
744
+ */
745
+ notifyMounted() {
746
+ if (_mountedAndReady) return;
747
+ _mountedAndReady = true;
748
+ if (typeof window !== "undefined") {
749
+ window.dispatchEvent(new CustomEvent("se:override:change"));
750
+ }
751
+ },
695
752
  /** Subscribe for change notifications (identify/override). Used by framework adapters. */
696
753
  subscribe(listener) {
697
- if (!_client) return () => {
698
- };
699
- return _client.subscribe(listener);
754
+ if (_client) return _client.subscribe(listener);
755
+ _standaloneListeners.add(listener);
756
+ wireStandaloneOverride();
757
+ return () => _standaloneListeners.delete(listener);
700
758
  },
701
759
  /** True once identify() has completed and flags are available. */
702
760
  get ready() {
@@ -717,27 +775,10 @@ function labelAttrs(key, variables, desc) {
717
775
  return attrs;
718
776
  }
719
777
  var _createElement = null;
720
- function interpolate(template, variables) {
721
- if (!variables) return template;
722
- let out = template;
723
- for (const name of Object.keys(variables)) {
724
- out = out.replace(new RegExp(`\\{\\{${name}\\}\\}`, "g"), String(variables[name]));
725
- }
726
- return out;
727
- }
728
778
  var i18n = {
729
- /**
730
- * Look up `key` in the active translation profile. When the profile
731
- * hasn't been fetched yet (SSR, CDN downtime, missing key), interpolate
732
- * `fallback` instead — `fallback` is the source-of-truth English copy
733
- * and is mandatory so the page never renders a raw key.
734
- */
735
- t(key, fallback, variables) {
736
- if (typeof window !== "undefined" && window.i18n) {
737
- const v = window.i18n.t(key, variables);
738
- if (v !== key) return v;
739
- }
740
- return interpolate(fallback, variables);
779
+ t(key, variables) {
780
+ if (typeof window !== "undefined" && window.i18n) return window.i18n.t(key, variables);
781
+ return key;
741
782
  },
742
783
  /**
743
784
  * Translate a key and return a framework element (e.g. React <span>)
@@ -752,7 +793,7 @@ var i18n = {
752
793
  * configured (e.g. server-side or in non-JSX contexts).
753
794
  */
754
795
  tEl(key, fallback, variables, desc) {
755
- const text = this.t(key, fallback, variables);
796
+ const text = this.t(key, variables) || fallback;
756
797
  if (!_createElement) return text;
757
798
  return _createElement("span", labelAttrs(key, variables, desc), text);
758
799
  },
@@ -412,15 +412,13 @@ var FlagsClientBrowser = class {
412
412
  this.evalResult = data;
413
413
  }
414
414
  getFlag(name) {
415
- if (this.evalResult === null) return false;
416
415
  const ov = readGateOverride(name);
417
416
  if (ov !== null) return ov;
418
- return this.evalResult.flags[name] ?? false;
417
+ return this.evalResult?.flags[name] ?? false;
419
418
  }
420
419
  getConfig(name, decode) {
421
- if (this.evalResult === null) return void 0;
422
420
  const ov = readConfigOverride(name);
423
- const raw = ov !== void 0 ? ov : this.evalResult.configs?.[name];
421
+ const raw = ov !== void 0 ? ov : this.evalResult?.configs?.[name];
424
422
  if (raw === void 0) return void 0;
425
423
  if (!decode) return raw;
426
424
  try {
@@ -619,6 +617,20 @@ function _resetShipeasyForTests() {
619
617
  _client?.destroy();
620
618
  _client = null;
621
619
  }
620
+ function getBootstrap() {
621
+ if (typeof window === "undefined") return null;
622
+ return window.__SE_BOOTSTRAP ?? null;
623
+ }
624
+ var _mountedAndReady = false;
625
+ var _standaloneListeners = /* @__PURE__ */ new Set();
626
+ var _standaloneOverrideWired = false;
627
+ function wireStandaloneOverride() {
628
+ if (_standaloneOverrideWired || typeof window === "undefined") return;
629
+ _standaloneOverrideWired = true;
630
+ window.addEventListener("se:override:change", () => {
631
+ for (const cb of _standaloneListeners) cb();
632
+ });
633
+ }
622
634
  var flags = {
623
635
  configure(opts) {
624
636
  configureShipeasy(opts);
@@ -630,12 +642,45 @@ var flags = {
630
642
  }
631
643
  return _client.identify(user);
632
644
  },
633
- /** Read a feature gate. Returns false until identify() resolves. */
645
+ /**
646
+ * Read a feature gate.
647
+ * Priority: URL override → server bootstrap (window.__SE_BOOTSTRAP) → CDN-fetched (post-mount) → false.
648
+ * The _mountedAndReady gate still applies for the CDN path to prevent hydration
649
+ * mismatches on force-static pages; bootstrap data is safe to read immediately
650
+ * because the server rendered with the same values.
651
+ */
634
652
  get(name) {
635
- return _client?.getFlag(name) ?? false;
653
+ const ov = readGateOverride(name);
654
+ if (ov !== null) return ov;
655
+ const bs = getBootstrap();
656
+ if (bs !== null && name in bs.flags) return bs.flags[name];
657
+ if (!_mountedAndReady) return false;
658
+ if (_client) return _client.getFlag(name);
659
+ return false;
636
660
  },
637
661
  getConfig(name, decode) {
638
- return _client?.getConfig(name, decode);
662
+ const ov = readConfigOverride(name);
663
+ if (ov !== void 0) {
664
+ if (!decode) return ov;
665
+ try {
666
+ return decode(ov);
667
+ } catch {
668
+ return void 0;
669
+ }
670
+ }
671
+ const bs = getBootstrap();
672
+ if (bs !== null && name in bs.configs) {
673
+ const raw = bs.configs[name];
674
+ if (!decode) return raw;
675
+ try {
676
+ return decode(raw);
677
+ } catch {
678
+ return void 0;
679
+ }
680
+ }
681
+ if (!_mountedAndReady) return void 0;
682
+ if (_client) return _client.getConfig(name, decode);
683
+ return void 0;
639
684
  },
640
685
  getExperiment(name, defaultParams, decode, variants) {
641
686
  return _client?.getExperiment(name, defaultParams, decode, variants) ?? {
@@ -650,11 +695,24 @@ var flags = {
650
695
  flush() {
651
696
  return _client?.flush() ?? Promise.resolve();
652
697
  },
698
+ /**
699
+ * Called by FlagsBoundary after React hydration to unlock flag reads.
700
+ * Dispatches se:override:change so subscribers (FlagsBoundary) re-render
701
+ * once with real values — URL overrides and server-evaluated flags.
702
+ */
703
+ notifyMounted() {
704
+ if (_mountedAndReady) return;
705
+ _mountedAndReady = true;
706
+ if (typeof window !== "undefined") {
707
+ window.dispatchEvent(new CustomEvent("se:override:change"));
708
+ }
709
+ },
653
710
  /** Subscribe for change notifications (identify/override). Used by framework adapters. */
654
711
  subscribe(listener) {
655
- if (!_client) return () => {
656
- };
657
- return _client.subscribe(listener);
712
+ if (_client) return _client.subscribe(listener);
713
+ _standaloneListeners.add(listener);
714
+ wireStandaloneOverride();
715
+ return () => _standaloneListeners.delete(listener);
658
716
  },
659
717
  /** True once identify() has completed and flags are available. */
660
718
  get ready() {
@@ -675,27 +733,10 @@ function labelAttrs(key, variables, desc) {
675
733
  return attrs;
676
734
  }
677
735
  var _createElement = null;
678
- function interpolate(template, variables) {
679
- if (!variables) return template;
680
- let out = template;
681
- for (const name of Object.keys(variables)) {
682
- out = out.replace(new RegExp(`\\{\\{${name}\\}\\}`, "g"), String(variables[name]));
683
- }
684
- return out;
685
- }
686
736
  var i18n = {
687
- /**
688
- * Look up `key` in the active translation profile. When the profile
689
- * hasn't been fetched yet (SSR, CDN downtime, missing key), interpolate
690
- * `fallback` instead — `fallback` is the source-of-truth English copy
691
- * and is mandatory so the page never renders a raw key.
692
- */
693
- t(key, fallback, variables) {
694
- if (typeof window !== "undefined" && window.i18n) {
695
- const v = window.i18n.t(key, variables);
696
- if (v !== key) return v;
697
- }
698
- return interpolate(fallback, variables);
737
+ t(key, variables) {
738
+ if (typeof window !== "undefined" && window.i18n) return window.i18n.t(key, variables);
739
+ return key;
699
740
  },
700
741
  /**
701
742
  * Translate a key and return a framework element (e.g. React <span>)
@@ -710,7 +751,7 @@ var i18n = {
710
751
  * configured (e.g. server-side or in non-JSX contexts).
711
752
  */
712
753
  tEl(key, fallback, variables, desc) {
713
- const text = this.t(key, fallback, variables);
754
+ const text = this.t(key, variables) || fallback;
714
755
  if (!_createElement) return text;
715
756
  return _createElement("span", labelAttrs(key, variables, desc), text);
716
757
  },
@@ -9,6 +9,11 @@ interface ExperimentResult<P> {
9
9
  group: string;
10
10
  params: P;
11
11
  }
12
+ interface BootstrapPayload {
13
+ flags: Record<string, boolean>;
14
+ configs: Record<string, unknown>;
15
+ experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
16
+ }
12
17
  type FlagsClientEnv = "dev" | "staging" | "prod";
13
18
  interface FlagsClientOptions {
14
19
  apiKey: string;
@@ -39,6 +44,16 @@ declare class FlagsClient {
39
44
  getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
40
45
  getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
41
46
  track(userId: string, eventName: string, props?: Record<string, unknown>): void;
47
+ /**
48
+ * Evaluate all flags, configs, and experiments for a user against the locally
49
+ * cached blob (no network call). Applies ?se_ks_* / ?se_cf_* / ?se_exp_*
50
+ * overrides from the request URL when provided.
51
+ *
52
+ * Intended for SSR: call on the server, inject the result as
53
+ * `window.__SE_BOOTSTRAP` in the HTML, and the client SDK will read it
54
+ * synchronously without waiting for identify() to resolve.
55
+ */
56
+ evaluate(user: User, rawUrl?: string): BootstrapPayload;
42
57
  }
43
58
  interface LabelFile {
44
59
  v: number;
@@ -72,6 +87,12 @@ declare const flags: {
72
87
  getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
73
88
  getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
74
89
  track(userId: string, eventName: string, props?: Record<string, unknown>): void;
90
+ /**
91
+ * Evaluate all flags / configs / experiments for a user against the locally
92
+ * cached blob. Pass the request URL to apply ?se_ks_* / ?se_cf_* / ?se_exp_*
93
+ * overrides. Returns an empty payload when the blob hasn't been fetched yet.
94
+ */
95
+ evaluate(user: User, rawUrl?: string): BootstrapPayload;
75
96
  };
76
97
 
77
- export { type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type LabelFile, type User, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getShipeasyServerClient, version };
98
+ export { type BootstrapPayload, type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type LabelFile, type User, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getShipeasyServerClient, version };
@@ -9,6 +9,11 @@ interface ExperimentResult<P> {
9
9
  group: string;
10
10
  params: P;
11
11
  }
12
+ interface BootstrapPayload {
13
+ flags: Record<string, boolean>;
14
+ configs: Record<string, unknown>;
15
+ experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
16
+ }
12
17
  type FlagsClientEnv = "dev" | "staging" | "prod";
13
18
  interface FlagsClientOptions {
14
19
  apiKey: string;
@@ -39,6 +44,16 @@ declare class FlagsClient {
39
44
  getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
40
45
  getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
41
46
  track(userId: string, eventName: string, props?: Record<string, unknown>): void;
47
+ /**
48
+ * Evaluate all flags, configs, and experiments for a user against the locally
49
+ * cached blob (no network call). Applies ?se_ks_* / ?se_cf_* / ?se_exp_*
50
+ * overrides from the request URL when provided.
51
+ *
52
+ * Intended for SSR: call on the server, inject the result as
53
+ * `window.__SE_BOOTSTRAP` in the HTML, and the client SDK will read it
54
+ * synchronously without waiting for identify() to resolve.
55
+ */
56
+ evaluate(user: User, rawUrl?: string): BootstrapPayload;
42
57
  }
43
58
  interface LabelFile {
44
59
  v: number;
@@ -72,6 +87,12 @@ declare const flags: {
72
87
  getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
73
88
  getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
74
89
  track(userId: string, eventName: string, props?: Record<string, unknown>): void;
90
+ /**
91
+ * Evaluate all flags / configs / experiments for a user against the locally
92
+ * cached blob. Pass the request URL to apply ?se_ks_* / ?se_cf_* / ?se_exp_*
93
+ * overrides. Returns an empty payload when the blob hasn't been fetched yet.
94
+ */
95
+ evaluate(user: User, rawUrl?: string): BootstrapPayload;
75
96
  };
76
97
 
77
- export { type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type LabelFile, type User, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getShipeasyServerClient, version };
98
+ export { type BootstrapPayload, type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type LabelFile, type User, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getShipeasyServerClient, version };
@@ -129,6 +129,51 @@ function evalGateInternal(gate, user) {
129
129
  if (!uid) return false;
130
130
  return murmur3(`${gate.salt}:${uid}`) % 1e4 < gate.rolloutPct;
131
131
  }
132
+ var TRUE_RX = /^(true|on|1|yes)$/i;
133
+ var FALSE_RX = /^(false|off|0|no)$/i;
134
+ function parseOverrideBool(raw) {
135
+ if (TRUE_RX.test(raw)) return true;
136
+ if (FALSE_RX.test(raw)) return false;
137
+ return null;
138
+ }
139
+ function decodeOverrideConfigValue(raw) {
140
+ if (raw.startsWith("b64:")) {
141
+ try {
142
+ const json = atob(raw.slice(4).replace(/-/g, "+").replace(/_/g, "/"));
143
+ return JSON.parse(json);
144
+ } catch {
145
+ return raw;
146
+ }
147
+ }
148
+ try {
149
+ return JSON.parse(raw);
150
+ } catch {
151
+ return raw;
152
+ }
153
+ }
154
+ function parseOverrides(rawUrl) {
155
+ const gates = {};
156
+ const configs = {};
157
+ const experiments = {};
158
+ try {
159
+ const url = new URL(rawUrl, "http://localhost");
160
+ for (const [k, v] of url.searchParams) {
161
+ if (k.startsWith("se_ks_")) {
162
+ const b = parseOverrideBool(v);
163
+ if (b !== null) gates[k.slice(6)] = b;
164
+ } else if (k.startsWith("se_cf_")) {
165
+ configs[k.slice(6)] = decodeOverrideConfigValue(v);
166
+ } else if (k.startsWith("se_config_")) {
167
+ configs[k.slice(10)] = decodeOverrideConfigValue(v);
168
+ } else if (k.startsWith("se_exp_")) {
169
+ const name = k.slice(7);
170
+ if (v && v !== "default" && v !== "none") experiments[name] = v;
171
+ }
172
+ }
173
+ } catch {
174
+ }
175
+ return { gates, configs, experiments };
176
+ }
132
177
  var FlagsClient = class {
133
178
  apiKey;
134
179
  baseUrl;
@@ -271,6 +316,38 @@ var FlagsClient = class {
271
316
  body
272
317
  }).catch((err) => console.warn("[shipeasy] track failed:", String(err)));
273
318
  }
319
+ /**
320
+ * Evaluate all flags, configs, and experiments for a user against the locally
321
+ * cached blob (no network call). Applies ?se_ks_* / ?se_cf_* / ?se_exp_*
322
+ * overrides from the request URL when provided.
323
+ *
324
+ * Intended for SSR: call on the server, inject the result as
325
+ * `window.__SE_BOOTSTRAP` in the HTML, and the client SDK will read it
326
+ * synchronously without waiting for identify() to resolve.
327
+ */
328
+ evaluate(user, rawUrl) {
329
+ const flags2 = {};
330
+ const configs = {};
331
+ const experiments = {};
332
+ for (const [name, gate] of Object.entries(this.flagsBlob?.gates ?? {})) {
333
+ flags2[name] = evalGateInternal(gate, user);
334
+ }
335
+ for (const [name, entry] of Object.entries(this.flagsBlob?.configs ?? {})) {
336
+ configs[name] = entry.value;
337
+ }
338
+ for (const [name] of Object.entries(this.expsBlob?.experiments ?? {})) {
339
+ experiments[name] = this.getExperiment(name, user, {});
340
+ }
341
+ if (rawUrl) {
342
+ const ov = parseOverrides(rawUrl);
343
+ Object.assign(flags2, ov.gates);
344
+ Object.assign(configs, ov.configs);
345
+ for (const [name, group] of Object.entries(ov.experiments)) {
346
+ experiments[name] = { inExperiment: true, group, params: {} };
347
+ }
348
+ }
349
+ return { flags: flags2, configs, experiments };
350
+ }
274
351
  };
275
352
  var DEFAULT_I18N_CDN = "https://cdn.i18n.shipeasy.ai";
276
353
  async function fetchJson(url, timeoutMs = 2e3) {
@@ -351,6 +428,14 @@ var flags = {
351
428
  },
352
429
  track(userId, eventName, props) {
353
430
  _server?.track(userId, eventName, props);
431
+ },
432
+ /**
433
+ * Evaluate all flags / configs / experiments for a user against the locally
434
+ * cached blob. Pass the request URL to apply ?se_ks_* / ?se_cf_* / ?se_exp_*
435
+ * overrides. Returns an empty payload when the blob hasn't been fetched yet.
436
+ */
437
+ evaluate(user, rawUrl) {
438
+ return _server?.evaluate(user, rawUrl) ?? { flags: {}, configs: {}, experiments: {} };
354
439
  }
355
440
  };
356
441
  // Annotate the CommonJS export names for ESM import in node:
@@ -99,6 +99,51 @@ function evalGateInternal(gate, user) {
99
99
  if (!uid) return false;
100
100
  return murmur3(`${gate.salt}:${uid}`) % 1e4 < gate.rolloutPct;
101
101
  }
102
+ var TRUE_RX = /^(true|on|1|yes)$/i;
103
+ var FALSE_RX = /^(false|off|0|no)$/i;
104
+ function parseOverrideBool(raw) {
105
+ if (TRUE_RX.test(raw)) return true;
106
+ if (FALSE_RX.test(raw)) return false;
107
+ return null;
108
+ }
109
+ function decodeOverrideConfigValue(raw) {
110
+ if (raw.startsWith("b64:")) {
111
+ try {
112
+ const json = atob(raw.slice(4).replace(/-/g, "+").replace(/_/g, "/"));
113
+ return JSON.parse(json);
114
+ } catch {
115
+ return raw;
116
+ }
117
+ }
118
+ try {
119
+ return JSON.parse(raw);
120
+ } catch {
121
+ return raw;
122
+ }
123
+ }
124
+ function parseOverrides(rawUrl) {
125
+ const gates = {};
126
+ const configs = {};
127
+ const experiments = {};
128
+ try {
129
+ const url = new URL(rawUrl, "http://localhost");
130
+ for (const [k, v] of url.searchParams) {
131
+ if (k.startsWith("se_ks_")) {
132
+ const b = parseOverrideBool(v);
133
+ if (b !== null) gates[k.slice(6)] = b;
134
+ } else if (k.startsWith("se_cf_")) {
135
+ configs[k.slice(6)] = decodeOverrideConfigValue(v);
136
+ } else if (k.startsWith("se_config_")) {
137
+ configs[k.slice(10)] = decodeOverrideConfigValue(v);
138
+ } else if (k.startsWith("se_exp_")) {
139
+ const name = k.slice(7);
140
+ if (v && v !== "default" && v !== "none") experiments[name] = v;
141
+ }
142
+ }
143
+ } catch {
144
+ }
145
+ return { gates, configs, experiments };
146
+ }
102
147
  var FlagsClient = class {
103
148
  apiKey;
104
149
  baseUrl;
@@ -241,6 +286,38 @@ var FlagsClient = class {
241
286
  body
242
287
  }).catch((err) => console.warn("[shipeasy] track failed:", String(err)));
243
288
  }
289
+ /**
290
+ * Evaluate all flags, configs, and experiments for a user against the locally
291
+ * cached blob (no network call). Applies ?se_ks_* / ?se_cf_* / ?se_exp_*
292
+ * overrides from the request URL when provided.
293
+ *
294
+ * Intended for SSR: call on the server, inject the result as
295
+ * `window.__SE_BOOTSTRAP` in the HTML, and the client SDK will read it
296
+ * synchronously without waiting for identify() to resolve.
297
+ */
298
+ evaluate(user, rawUrl) {
299
+ const flags2 = {};
300
+ const configs = {};
301
+ const experiments = {};
302
+ for (const [name, gate] of Object.entries(this.flagsBlob?.gates ?? {})) {
303
+ flags2[name] = evalGateInternal(gate, user);
304
+ }
305
+ for (const [name, entry] of Object.entries(this.flagsBlob?.configs ?? {})) {
306
+ configs[name] = entry.value;
307
+ }
308
+ for (const [name] of Object.entries(this.expsBlob?.experiments ?? {})) {
309
+ experiments[name] = this.getExperiment(name, user, {});
310
+ }
311
+ if (rawUrl) {
312
+ const ov = parseOverrides(rawUrl);
313
+ Object.assign(flags2, ov.gates);
314
+ Object.assign(configs, ov.configs);
315
+ for (const [name, group] of Object.entries(ov.experiments)) {
316
+ experiments[name] = { inExperiment: true, group, params: {} };
317
+ }
318
+ }
319
+ return { flags: flags2, configs, experiments };
320
+ }
244
321
  };
245
322
  var DEFAULT_I18N_CDN = "https://cdn.i18n.shipeasy.ai";
246
323
  async function fetchJson(url, timeoutMs = 2e3) {
@@ -321,6 +398,14 @@ var flags = {
321
398
  },
322
399
  track(userId, eventName, props) {
323
400
  _server?.track(userId, eventName, props);
401
+ },
402
+ /**
403
+ * Evaluate all flags / configs / experiments for a user against the locally
404
+ * cached blob. Pass the request URL to apply ?se_ks_* / ?se_cf_* / ?se_exp_*
405
+ * overrides. Returns an empty payload when the blob hasn't been fetched yet.
406
+ */
407
+ evaluate(user, rawUrl) {
408
+ return _server?.evaluate(user, rawUrl) ?? { flags: {}, configs: {}, experiments: {} };
324
409
  }
325
410
  };
326
411
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipeasy/sdk",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "Shipeasy SDK — feature gates, runtime configs, experiments, and metrics for the Shipeasy hosted service.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://shipeasy.ai",