@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.
- package/dist/client/index.d.mts +30 -9
- package/dist/client/index.d.ts +30 -9
- package/dist/client/index.js +72 -31
- package/dist/client/index.mjs +72 -31
- package/dist/server/index.d.mts +22 -1
- package/dist/server/index.d.ts +22 -1
- package/dist/server/index.js +85 -0
- package/dist/server/index.mjs +85 -0
- package/package.json +1 -1
package/dist/client/index.d.mts
CHANGED
|
@@ -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
|
-
/**
|
|
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 };
|
package/dist/client/index.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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 };
|
package/dist/client/index.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
698
|
-
|
|
699
|
-
|
|
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
|
-
|
|
731
|
-
|
|
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,
|
|
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
|
},
|
package/dist/client/index.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
689
|
-
|
|
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,
|
|
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
|
},
|
package/dist/server/index.d.mts
CHANGED
|
@@ -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 };
|
package/dist/server/index.d.ts
CHANGED
|
@@ -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 };
|
package/dist/server/index.js
CHANGED
|
@@ -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:
|
package/dist/server/index.mjs
CHANGED
|
@@ -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.
|
|
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",
|