@shipeasy/sdk 5.2.0 → 5.4.0

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/README.md CHANGED
@@ -53,6 +53,47 @@ const client = new FlagsClient({ sdkKey: process.env.SHIPEASY_SERVER_KEY! });
53
53
  const cfg = await client.getConfig("plan_limits", { user_id: "u-1" });
54
54
  ```
55
55
 
56
+ ## SSR bootstrap (flags on first paint)
57
+
58
+ Server-render the evaluated flags / configs / experiments into the page so the
59
+ browser SDK reads them **synchronously on first paint** — no flash, no extra
60
+ round-trip. The `shipeasy()` server handle emits two declarative `<script>`
61
+ tags. **No SDK key is embedded** in the bootstrap tag (the server key never
62
+ reaches the browser).
63
+
64
+ ```tsx
65
+ // app/layout.tsx — Next.js root layout (React Server Component)
66
+ import { shipeasy } from "@shipeasy/sdk/server";
67
+
68
+ export default async function RootLayout({ children }: { children: React.ReactNode }) {
69
+ const se = await shipeasy({ serverKey: process.env.SHIPEASY_SERVER_KEY ?? "" });
70
+ const boot = se.getBootstrapData({
71
+ // public client key — lets the i18n loader revalidate strings at runtime
72
+ clientKey: process.env.NEXT_PUBLIC_SHIPEASY_CLIENT_KEY,
73
+ });
74
+ return (
75
+ <html>
76
+ <body>
77
+ {/* Render REAL <script> elements — scripts set via
78
+ dangerouslySetInnerHTML do NOT execute. */}
79
+ <script src={boot.bootstrap.src} {...boot.bootstrap.attrs} />
80
+ {boot.i18nLoader && <script src={boot.i18nLoader.src} {...boot.i18nLoader.attrs} />}
81
+ {children}
82
+ </body>
83
+ </html>
84
+ );
85
+ }
86
+ ```
87
+
88
+ `bootstrap.src` is `https://cdn.shipeasy.ai/sdk/bootstrap.js` — a static loader
89
+ that reads its own `data-*` attributes, hydrates `window.__SE_BOOTSTRAP`, and
90
+ persists the `__se_anon_id` cookie so the browser buckets **identically** to the
91
+ server. The i18n loader tag carries the SSR strings (`data-strings`) for a
92
+ no-flash first paint plus the public client key for runtime revalidation.
93
+
94
+ For non-React SSR (Express, raw templates), `se.getBootstrapTags()` returns the
95
+ same two tags as an HTML string you can drop straight into the served markup.
96
+
56
97
  ## Error tracking — `see()`
57
98
 
58
99
  `see` (shipeasy error) is the structured error reporter — opes-style: every
@@ -530,8 +530,8 @@ interface BootstrapPayload {
530
530
  /** i18n profile the server rendered with, so the client loader matches. No key is embedded. */
531
531
  i18nProfile?: string;
532
532
  apiUrl?: string;
533
- /** When true, tEl() returns marker-wrapped strings for devtools label editing. */
534
- editLabels?: boolean;
533
+ /** Stable anonymous bucketing id the server evaluated against (cross-SDK contract). */
534
+ anonId?: string;
535
535
  }
536
536
  /**
537
537
  * Universal flags facade. Methods return safe defaults when the singleton
@@ -530,8 +530,8 @@ interface BootstrapPayload {
530
530
  /** i18n profile the server rendered with, so the client loader matches. No key is embedded. */
531
531
  i18nProfile?: string;
532
532
  apiUrl?: string;
533
- /** When true, tEl() returns marker-wrapped strings for devtools label editing. */
534
- editLabels?: boolean;
533
+ /** Stable anonymous bucketing id the server evaluated against (cross-SDK contract). */
534
+ anonId?: string;
535
535
  }
536
536
  /**
537
537
  * Universal flags facade. Methods return safe defaults when the singleton
@@ -768,7 +768,7 @@ function writeStickyCookie(map) {
768
768
  function getOrCreateAnonId() {
769
769
  let id = readAnonCookie();
770
770
  if (!id && typeof window !== "undefined") {
771
- id = window.__SE_BOOTSTRAP?.anonId ?? null;
771
+ id = getBootstrap()?.anonId ?? null;
772
772
  }
773
773
  if (!id) {
774
774
  try {
@@ -1430,6 +1430,13 @@ var _i18nLoaderInjected = false;
1430
1430
  function injectI18nLoader(clientKey, baseUrl, profileOpt) {
1431
1431
  if (_i18nLoaderInjected || typeof document === "undefined") return;
1432
1432
  if (!clientKey || typeof document.createElement !== "function" || !document.head) return;
1433
+ try {
1434
+ if (document.querySelector?.('script[src*="/sdk/i18n/loader.js"][data-key]')) {
1435
+ _i18nLoaderInjected = true;
1436
+ return;
1437
+ }
1438
+ } catch {
1439
+ }
1433
1440
  _i18nLoaderInjected = true;
1434
1441
  try {
1435
1442
  const bs = getBootstrap();
@@ -1442,9 +1449,36 @@ function injectI18nLoader(clientKey, baseUrl, profileOpt) {
1442
1449
  } catch {
1443
1450
  }
1444
1451
  }
1452
+ var _bootstrapFromTag = null;
1453
+ function readBootstrapTag() {
1454
+ if (_bootstrapFromTag) return _bootstrapFromTag;
1455
+ if (typeof document === "undefined" || typeof document.querySelector !== "function") return null;
1456
+ const el = document.querySelector("script[data-se-bootstrap]");
1457
+ if (!el) return null;
1458
+ const J = (name) => {
1459
+ try {
1460
+ return JSON.parse(el.getAttribute(name) || "{}");
1461
+ } catch {
1462
+ return {};
1463
+ }
1464
+ };
1465
+ const bs = {
1466
+ flags: J("data-flags"),
1467
+ configs: J("data-configs"),
1468
+ experiments: J("data-experiments"),
1469
+ killswitches: J("data-killswitches"),
1470
+ i18nProfile: el.getAttribute("data-i18n-profile") || void 0,
1471
+ apiUrl: el.getAttribute("data-api-url") || void 0,
1472
+ anonId: el.getAttribute("data-anon-id") || void 0
1473
+ };
1474
+ _bootstrapFromTag = bs;
1475
+ return bs;
1476
+ }
1445
1477
  function getBootstrap() {
1446
1478
  if (typeof window === "undefined") return null;
1447
- return window.__SE_BOOTSTRAP ?? null;
1479
+ const g = window.__SE_BOOTSTRAP;
1480
+ if (g) return g;
1481
+ return readBootstrapTag();
1448
1482
  }
1449
1483
  var _mountedAndReady = false;
1450
1484
  var _standaloneListeners = /* @__PURE__ */ new Set();
@@ -1623,7 +1657,7 @@ function getSSRI18nStore() {
1623
1657
  var _EDIT_MODE_FALLBACK_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode-fallback");
1624
1658
  function isEditLabelsMode() {
1625
1659
  if (typeof window !== "undefined") {
1626
- return !!window.__SE_BOOTSTRAP?.editLabels || new URLSearchParams(location.search).has("se_edit_labels");
1660
+ return new URLSearchParams(location.search).has("se_edit_labels") || /(?:^|;\s*)se_edit_labels=1(?:;|$)/.test(document.cookie);
1627
1661
  }
1628
1662
  const val = globalThis[_EDIT_MODE_SSR_SYM];
1629
1663
  if (typeof val === "boolean") return val;
@@ -719,7 +719,7 @@ function writeStickyCookie(map) {
719
719
  function getOrCreateAnonId() {
720
720
  let id = readAnonCookie();
721
721
  if (!id && typeof window !== "undefined") {
722
- id = window.__SE_BOOTSTRAP?.anonId ?? null;
722
+ id = getBootstrap()?.anonId ?? null;
723
723
  }
724
724
  if (!id) {
725
725
  try {
@@ -1381,6 +1381,13 @@ var _i18nLoaderInjected = false;
1381
1381
  function injectI18nLoader(clientKey, baseUrl, profileOpt) {
1382
1382
  if (_i18nLoaderInjected || typeof document === "undefined") return;
1383
1383
  if (!clientKey || typeof document.createElement !== "function" || !document.head) return;
1384
+ try {
1385
+ if (document.querySelector?.('script[src*="/sdk/i18n/loader.js"][data-key]')) {
1386
+ _i18nLoaderInjected = true;
1387
+ return;
1388
+ }
1389
+ } catch {
1390
+ }
1384
1391
  _i18nLoaderInjected = true;
1385
1392
  try {
1386
1393
  const bs = getBootstrap();
@@ -1393,9 +1400,36 @@ function injectI18nLoader(clientKey, baseUrl, profileOpt) {
1393
1400
  } catch {
1394
1401
  }
1395
1402
  }
1403
+ var _bootstrapFromTag = null;
1404
+ function readBootstrapTag() {
1405
+ if (_bootstrapFromTag) return _bootstrapFromTag;
1406
+ if (typeof document === "undefined" || typeof document.querySelector !== "function") return null;
1407
+ const el = document.querySelector("script[data-se-bootstrap]");
1408
+ if (!el) return null;
1409
+ const J = (name) => {
1410
+ try {
1411
+ return JSON.parse(el.getAttribute(name) || "{}");
1412
+ } catch {
1413
+ return {};
1414
+ }
1415
+ };
1416
+ const bs = {
1417
+ flags: J("data-flags"),
1418
+ configs: J("data-configs"),
1419
+ experiments: J("data-experiments"),
1420
+ killswitches: J("data-killswitches"),
1421
+ i18nProfile: el.getAttribute("data-i18n-profile") || void 0,
1422
+ apiUrl: el.getAttribute("data-api-url") || void 0,
1423
+ anonId: el.getAttribute("data-anon-id") || void 0
1424
+ };
1425
+ _bootstrapFromTag = bs;
1426
+ return bs;
1427
+ }
1396
1428
  function getBootstrap() {
1397
1429
  if (typeof window === "undefined") return null;
1398
- return window.__SE_BOOTSTRAP ?? null;
1430
+ const g = window.__SE_BOOTSTRAP;
1431
+ if (g) return g;
1432
+ return readBootstrapTag();
1399
1433
  }
1400
1434
  var _mountedAndReady = false;
1401
1435
  var _standaloneListeners = /* @__PURE__ */ new Set();
@@ -1574,7 +1608,7 @@ function getSSRI18nStore() {
1574
1608
  var _EDIT_MODE_FALLBACK_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode-fallback");
1575
1609
  function isEditLabelsMode() {
1576
1610
  if (typeof window !== "undefined") {
1577
- return !!window.__SE_BOOTSTRAP?.editLabels || new URLSearchParams(location.search).has("se_edit_labels");
1611
+ return new URLSearchParams(location.search).has("se_edit_labels") || /(?:^|;\s*)se_edit_labels=1(?:;|$)/.test(document.cookie);
1578
1612
  }
1579
1613
  const val = globalThis[_EDIT_MODE_SSR_SYM];
1580
1614
  if (typeof val === "boolean") return val;
@@ -477,8 +477,22 @@ interface ShipeasyServerHandle {
477
477
  flags: Record<string, boolean>;
478
478
  configs: Record<string, unknown>;
479
479
  experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
480
- /** Returns a vanilla-JS string for a single inline <script> tag. */
481
- getBootstrapHtml(): string;
480
+ /**
481
+ * Structured `<script>` tag specs to drop into the document head: the
482
+ * cross-platform `se-bootstrap.js` tag (hydrates window.__SE_BOOTSTRAP +
483
+ * writes the anon cookie) and, when there are SSR strings or a client key,
484
+ * the i18n loader tag. Use this in React: scripts inserted via
485
+ * `dangerouslySetInnerHTML` do NOT execute, so render real `<script>`
486
+ * elements from these specs (see apps/ui root layout).
487
+ */
488
+ getBootstrapData(emit?: BootstrapEmitOptions): BootstrapData;
489
+ /**
490
+ * The same tags rendered as an HTML string, for non-React SSR (Express, raw
491
+ * templates) where the markup is emitted directly into the served HTML (and
492
+ * therefore executes normally). This is the canonical cross-platform shape
493
+ * every server SDK mirrors.
494
+ */
495
+ getBootstrapTags(emit?: BootstrapEmitOptions): string;
482
496
  }
483
497
  /**
484
498
  * Initialise the ShipEasy server SDK, evaluate flags for this request, and
@@ -487,35 +501,51 @@ interface ShipeasyServerHandle {
487
501
  * payloads and i18n falls back to hardcoded strings.
488
502
  */
489
503
  declare function shipeasy(opts: ShipeasyServerConfig): Promise<ShipeasyServerHandle>;
490
- interface BootstrapHtmlOptions {
491
- /** i18n profile recorded in the bootstrap so the client loader matches SSR. Defaults to "en:prod". */
504
+ interface BootstrapEmitOptions {
505
+ /** i18n profile recorded on both tags so the client loader matches SSR. Defaults to "en:prod". */
492
506
  i18nProfile?: string;
493
- /** When true, tEl() embeds label markers so the devtools can highlight them. */
494
- editLabels?: boolean;
495
507
  /**
496
- * Stable anonymous bucketing id the server evaluated against. Emitted into
497
- * window.__SE_BOOTSTRAP and persisted (pre-paint) to the first-party
498
- * `__se_anon_id` cookie, so the browser SDK buckets identically to SSR.
499
- * Normally minted by edge middleware; this write is the fallback for routes
500
- * middleware doesn't cover. See experiment-platform/18-identity-bucketing.md.
508
+ * Stable anonymous bucketing id the server evaluated against. Emitted as
509
+ * `data-anon-id`; se-bootstrap.js exposes it on window.__SE_BOOTSTRAP and
510
+ * persists it (pre-paint) to the first-party `__se_anon_id` cookie, so the
511
+ * browser SDK buckets identically to SSR. Normally minted by edge
512
+ * middleware; this is the fallback for routes it doesn't cover. See
513
+ * experiment-platform/18-identity-bucketing.md.
501
514
  */
502
515
  anonId?: string;
516
+ /**
517
+ * Public client key. When provided, the i18n loader tag can revalidate
518
+ * strings at runtime (`data-key`). Optional — the bootstrap tag NEVER carries
519
+ * a key, and SSR first paint works keyless via `data-strings`.
520
+ */
521
+ clientKey?: string;
522
+ /** CDN base for the tag `src`s. Defaults to https://cdn.shipeasy.ai. */
523
+ baseUrl?: string;
524
+ }
525
+ interface ScriptTagSpec {
526
+ src: string;
527
+ /** Attribute name → value. An empty-string value renders as a bare boolean attribute (e.g. `data-se-bootstrap`). */
528
+ attrs: Record<string, string>;
529
+ }
530
+ interface BootstrapData {
531
+ /** se-bootstrap.js tag — always present. */
532
+ bootstrap: ScriptTagSpec;
533
+ /** i18n loader tag — null when there are no SSR strings and no client key. */
534
+ i18nLoader: ScriptTagSpec | null;
503
535
  }
504
536
  /**
505
- * Returns a vanilla-JS string for a single inline <script> tag. Handles
506
- * everything the client needs at startup EXCEPT the key no SDK key is ever
507
- * embedded here (the server only knows the server key, which must stay
508
- * server-side). The browser supplies its own client key via
509
- * `shipeasy({ clientKey })` from @shipeasy/sdk/client, which also injects the
510
- * runtime i18n loader. This script emits:
511
- * - window.__se_devtools_config (when devtoolsAdminUrl is set)
512
- * - window.__SE_BOOTSTRAP (flags + configs + experiments + i18n DATA + i18nProfile, NO key)
513
- * - window.i18n shim from SSR strings (prevents hydration mismatches / FOUC)
514
- * - devtools overlay loader when ?se / ?se_devtools is present
515
- *
516
- * Framework-agnostic: set innerHTML on a <script> element, nothing else required.
537
+ * Build the structured `<script>` tag specs for the SSR bootstrap. Render
538
+ * these as REAL `<script>` elements (React: scripts set via innerHTML do not
539
+ * execute). No SDK key is ever embedded in the bootstrap tag.
540
+ */
541
+ declare function getBootstrapData(bootstrap: BootstrapPayload | null, i18nData: I18nForRequest | null, opts: BootstrapEmitOptions): BootstrapData;
542
+ /**
543
+ * The bootstrap (and optional i18n loader) tags as an HTML string, for non-React
544
+ * SSR (Express, raw templates) where the markup is emitted directly into the
545
+ * served HTML and executes normally. React callers should use
546
+ * {@link getBootstrapData} and render real `<script>` elements instead.
517
547
  */
518
- declare function getBootstrapHtml(bootstrap: BootstrapPayload | null, i18nData: I18nForRequest | null, opts: BootstrapHtmlOptions): string;
548
+ declare function getBootstrapTags(bootstrap: BootstrapPayload | null, i18nData: I18nForRequest | null, opts: BootstrapEmitOptions): string;
519
549
  declare const flags: {
520
550
  configure(opts: FlagsClientOptions): void;
521
551
  /**
@@ -600,4 +630,4 @@ interface SeeApi {
600
630
  */
601
631
  declare const see: SeeApi;
602
632
 
603
- export { ANON_ID_COOKIE, type BootstrapHtmlOptions, type BootstrapPayload, type Consequence, type ExperimentResult, type ExpsBlob, FLAG_REASONS, type FetchLabelsOptions, type FlagDetail, type FlagReason, type FlagsBlob, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type GetConfigOptions, type I18nForRequest, type LabelFile, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type StickyBucketStore, type StickyEntry, type User, type Violation, _murmur3ForTests, _resetShipeasyServerForTests, configureShipeasyServer, createInMemoryStickyStore, fetchLabelsForSSR, flags, getBootstrapHtml, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
633
+ export { ANON_ID_COOKIE, type BootstrapData, type BootstrapEmitOptions, type BootstrapPayload, type Consequence, type ExperimentResult, type ExpsBlob, FLAG_REASONS, type FetchLabelsOptions, type FlagDetail, type FlagReason, type FlagsBlob, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type GetConfigOptions, type I18nForRequest, type LabelFile, type ScriptTagSpec, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type StickyBucketStore, type StickyEntry, type User, type Violation, _murmur3ForTests, _resetShipeasyServerForTests, configureShipeasyServer, createInMemoryStickyStore, fetchLabelsForSSR, flags, getBootstrapData, getBootstrapTags, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
@@ -477,8 +477,22 @@ interface ShipeasyServerHandle {
477
477
  flags: Record<string, boolean>;
478
478
  configs: Record<string, unknown>;
479
479
  experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
480
- /** Returns a vanilla-JS string for a single inline <script> tag. */
481
- getBootstrapHtml(): string;
480
+ /**
481
+ * Structured `<script>` tag specs to drop into the document head: the
482
+ * cross-platform `se-bootstrap.js` tag (hydrates window.__SE_BOOTSTRAP +
483
+ * writes the anon cookie) and, when there are SSR strings or a client key,
484
+ * the i18n loader tag. Use this in React: scripts inserted via
485
+ * `dangerouslySetInnerHTML` do NOT execute, so render real `<script>`
486
+ * elements from these specs (see apps/ui root layout).
487
+ */
488
+ getBootstrapData(emit?: BootstrapEmitOptions): BootstrapData;
489
+ /**
490
+ * The same tags rendered as an HTML string, for non-React SSR (Express, raw
491
+ * templates) where the markup is emitted directly into the served HTML (and
492
+ * therefore executes normally). This is the canonical cross-platform shape
493
+ * every server SDK mirrors.
494
+ */
495
+ getBootstrapTags(emit?: BootstrapEmitOptions): string;
482
496
  }
483
497
  /**
484
498
  * Initialise the ShipEasy server SDK, evaluate flags for this request, and
@@ -487,35 +501,51 @@ interface ShipeasyServerHandle {
487
501
  * payloads and i18n falls back to hardcoded strings.
488
502
  */
489
503
  declare function shipeasy(opts: ShipeasyServerConfig): Promise<ShipeasyServerHandle>;
490
- interface BootstrapHtmlOptions {
491
- /** i18n profile recorded in the bootstrap so the client loader matches SSR. Defaults to "en:prod". */
504
+ interface BootstrapEmitOptions {
505
+ /** i18n profile recorded on both tags so the client loader matches SSR. Defaults to "en:prod". */
492
506
  i18nProfile?: string;
493
- /** When true, tEl() embeds label markers so the devtools can highlight them. */
494
- editLabels?: boolean;
495
507
  /**
496
- * Stable anonymous bucketing id the server evaluated against. Emitted into
497
- * window.__SE_BOOTSTRAP and persisted (pre-paint) to the first-party
498
- * `__se_anon_id` cookie, so the browser SDK buckets identically to SSR.
499
- * Normally minted by edge middleware; this write is the fallback for routes
500
- * middleware doesn't cover. See experiment-platform/18-identity-bucketing.md.
508
+ * Stable anonymous bucketing id the server evaluated against. Emitted as
509
+ * `data-anon-id`; se-bootstrap.js exposes it on window.__SE_BOOTSTRAP and
510
+ * persists it (pre-paint) to the first-party `__se_anon_id` cookie, so the
511
+ * browser SDK buckets identically to SSR. Normally minted by edge
512
+ * middleware; this is the fallback for routes it doesn't cover. See
513
+ * experiment-platform/18-identity-bucketing.md.
501
514
  */
502
515
  anonId?: string;
516
+ /**
517
+ * Public client key. When provided, the i18n loader tag can revalidate
518
+ * strings at runtime (`data-key`). Optional — the bootstrap tag NEVER carries
519
+ * a key, and SSR first paint works keyless via `data-strings`.
520
+ */
521
+ clientKey?: string;
522
+ /** CDN base for the tag `src`s. Defaults to https://cdn.shipeasy.ai. */
523
+ baseUrl?: string;
524
+ }
525
+ interface ScriptTagSpec {
526
+ src: string;
527
+ /** Attribute name → value. An empty-string value renders as a bare boolean attribute (e.g. `data-se-bootstrap`). */
528
+ attrs: Record<string, string>;
529
+ }
530
+ interface BootstrapData {
531
+ /** se-bootstrap.js tag — always present. */
532
+ bootstrap: ScriptTagSpec;
533
+ /** i18n loader tag — null when there are no SSR strings and no client key. */
534
+ i18nLoader: ScriptTagSpec | null;
503
535
  }
504
536
  /**
505
- * Returns a vanilla-JS string for a single inline <script> tag. Handles
506
- * everything the client needs at startup EXCEPT the key no SDK key is ever
507
- * embedded here (the server only knows the server key, which must stay
508
- * server-side). The browser supplies its own client key via
509
- * `shipeasy({ clientKey })` from @shipeasy/sdk/client, which also injects the
510
- * runtime i18n loader. This script emits:
511
- * - window.__se_devtools_config (when devtoolsAdminUrl is set)
512
- * - window.__SE_BOOTSTRAP (flags + configs + experiments + i18n DATA + i18nProfile, NO key)
513
- * - window.i18n shim from SSR strings (prevents hydration mismatches / FOUC)
514
- * - devtools overlay loader when ?se / ?se_devtools is present
515
- *
516
- * Framework-agnostic: set innerHTML on a <script> element, nothing else required.
537
+ * Build the structured `<script>` tag specs for the SSR bootstrap. Render
538
+ * these as REAL `<script>` elements (React: scripts set via innerHTML do not
539
+ * execute). No SDK key is ever embedded in the bootstrap tag.
540
+ */
541
+ declare function getBootstrapData(bootstrap: BootstrapPayload | null, i18nData: I18nForRequest | null, opts: BootstrapEmitOptions): BootstrapData;
542
+ /**
543
+ * The bootstrap (and optional i18n loader) tags as an HTML string, for non-React
544
+ * SSR (Express, raw templates) where the markup is emitted directly into the
545
+ * served HTML and executes normally. React callers should use
546
+ * {@link getBootstrapData} and render real `<script>` elements instead.
517
547
  */
518
- declare function getBootstrapHtml(bootstrap: BootstrapPayload | null, i18nData: I18nForRequest | null, opts: BootstrapHtmlOptions): string;
548
+ declare function getBootstrapTags(bootstrap: BootstrapPayload | null, i18nData: I18nForRequest | null, opts: BootstrapEmitOptions): string;
519
549
  declare const flags: {
520
550
  configure(opts: FlagsClientOptions): void;
521
551
  /**
@@ -600,4 +630,4 @@ interface SeeApi {
600
630
  */
601
631
  declare const see: SeeApi;
602
632
 
603
- export { ANON_ID_COOKIE, type BootstrapHtmlOptions, type BootstrapPayload, type Consequence, type ExperimentResult, type ExpsBlob, FLAG_REASONS, type FetchLabelsOptions, type FlagDetail, type FlagReason, type FlagsBlob, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type GetConfigOptions, type I18nForRequest, type LabelFile, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type StickyBucketStore, type StickyEntry, type User, type Violation, _murmur3ForTests, _resetShipeasyServerForTests, configureShipeasyServer, createInMemoryStickyStore, fetchLabelsForSSR, flags, getBootstrapHtml, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
633
+ export { ANON_ID_COOKIE, type BootstrapData, type BootstrapEmitOptions, type BootstrapPayload, type Consequence, type ExperimentResult, type ExpsBlob, FLAG_REASONS, type FetchLabelsOptions, type FlagDetail, type FlagReason, type FlagsBlob, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type GetConfigOptions, type I18nForRequest, type LabelFile, type ScriptTagSpec, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type StickyBucketStore, type StickyEntry, type User, type Violation, _murmur3ForTests, _resetShipeasyServerForTests, configureShipeasyServer, createInMemoryStickyStore, fetchLabelsForSSR, flags, getBootstrapData, getBootstrapTags, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
@@ -39,7 +39,8 @@ __export(server_exports, {
39
39
  createInMemoryStickyStore: () => createInMemoryStickyStore,
40
40
  fetchLabelsForSSR: () => fetchLabelsForSSR,
41
41
  flags: () => flags,
42
- getBootstrapHtml: () => getBootstrapHtml,
42
+ getBootstrapData: () => getBootstrapData,
43
+ getBootstrapTags: () => getBootstrapTags,
43
44
  getShipeasyServerClient: () => getShipeasyServerClient,
44
45
  i18n: () => i18n,
45
46
  isExpected: () => isExpected,
@@ -1161,49 +1162,62 @@ async function shipeasy(opts) {
1161
1162
  flags: bootstrap.flags,
1162
1163
  configs: bootstrap.configs,
1163
1164
  experiments: bootstrap.experiments,
1164
- getBootstrapHtml() {
1165
- return getBootstrapHtml(bootstrap, i18nData, {
1166
- editLabels,
1165
+ getBootstrapData(emit) {
1166
+ return getBootstrapData(bootstrap, i18nData, {
1167
1167
  i18nProfile: profile,
1168
- anonId
1168
+ anonId,
1169
+ ...emit
1170
+ });
1171
+ },
1172
+ getBootstrapTags(emit) {
1173
+ return getBootstrapTags(bootstrap, i18nData, {
1174
+ i18nProfile: profile,
1175
+ anonId,
1176
+ ...emit
1169
1177
  });
1170
1178
  }
1171
1179
  };
1172
1180
  }
1173
- function getBootstrapHtml(bootstrap, i18nData, opts) {
1174
- const parts = [];
1175
- const apiUrl = "https://cdn.shipeasy.ai";
1181
+ var DEFAULT_CDN = "https://cdn.shipeasy.ai";
1182
+ function getBootstrapData(bootstrap, i18nData, opts) {
1183
+ const base = (opts.baseUrl ?? DEFAULT_CDN).replace(/\/$/, "");
1176
1184
  const profile = opts.i18nProfile ?? "en:prod";
1177
- const payload = {
1178
- flags: bootstrap?.flags ?? {},
1179
- configs: bootstrap?.configs ?? {},
1180
- experiments: bootstrap?.experiments ?? {},
1181
- // No key here — the server only knows the server key, which must never reach
1182
- // the browser. The client supplies its own client key via shipeasy({ clientKey }).
1183
- i18nProfile: profile,
1184
- apiUrl
1185
+ const attrs = {
1186
+ "data-se-bootstrap": "",
1187
+ "data-flags": JSON.stringify(bootstrap?.flags ?? {}),
1188
+ "data-configs": JSON.stringify(bootstrap?.configs ?? {}),
1189
+ "data-experiments": JSON.stringify(bootstrap?.experiments ?? {}),
1190
+ "data-killswitches": JSON.stringify(bootstrap?.killswitches ?? {}),
1191
+ "data-i18n-profile": profile,
1192
+ "data-api-url": base
1185
1193
  };
1186
- if (i18nData) payload.i18n = i18nData;
1187
- if (opts.editLabels) payload.editLabels = true;
1188
- if (opts.anonId) payload.anonId = opts.anonId;
1189
- parts.push(
1190
- `(function(){var Q=new URLSearchParams(location.search).has('se_edit_labels');var C=/(?:^|;\\s*)se_edit_labels=1(?:;|$)/.test(document.cookie);if(!Q&&!C)return;if(Q){try{document.cookie='se_edit_labels=1;path=/;max-age=86400;samesite=lax';}catch(_){}}var R;function P(v){if(!v||typeof v.t!=='function'||v.__sePatched)return;var O=v.t.bind(v);v.__sePatched=true;window._sei18n_t=O;v.t=function(k,vars){var r=O(k,vars);if(r===k)return k;var V='';try{if(vars&&typeof vars==='object'){var hasKey=false;for(var _k in vars){hasKey=true;break;}if(hasKey)V=JSON.stringify(vars);}}catch(_){V='';}return '\\uFFF9'+k+'\\uFFFA'+V+'\\uFFFA'+r+'\\uFFFB';};}Object.defineProperty(window,'i18n',{configurable:true,get:function(){return R;},set:function(v){P(v);R=v;}});})();`
1191
- );
1192
- parts.push(`window.__SE_BOOTSTRAP=${JSON.stringify(payload)};`);
1193
- if (opts.anonId) {
1194
- parts.push(
1195
- `(function(){try{var k=${JSON.stringify(ANON_ID_COOKIE)},v=${JSON.stringify(opts.anonId)};if(('; '+document.cookie).indexOf('; '+k+'=')===-1){document.cookie=k+'='+v+';path=/;max-age=31536000;samesite=lax'+(location.protocol==='https:'?';secure':'');}}catch(_){}})();`
1196
- );
1197
- }
1198
- if (i18nData?.strings && Object.keys(i18nData.strings).length > 0) {
1199
- parts.push(
1200
- `(function(){var d=window.__SE_BOOTSTRAP.i18n;if(!d)return;window.i18n={locale:d.locale,t:function(k,v){var r=d.strings[k];if(!r)return k;return v?r.replace(/\\{\\{(\\w+)\\}\\}/g,function(_,p){return v[p]!==undefined?String(v[p]):'{{'+p+'}}'}):r;},on:function(){return function(){};}};})();`
1201
- );
1194
+ if (opts.anonId) attrs["data-anon-id"] = opts.anonId;
1195
+ const bootstrapTag = { src: `${base}/sdk/bootstrap.js`, attrs };
1196
+ let i18nLoader = null;
1197
+ const hasStrings = !!(i18nData?.strings && Object.keys(i18nData.strings).length > 0);
1198
+ if (hasStrings || opts.clientKey) {
1199
+ const i18nAttrs = { "data-profile": profile };
1200
+ if (opts.clientKey) i18nAttrs["data-key"] = opts.clientKey;
1201
+ if (hasStrings) {
1202
+ i18nAttrs["data-strings"] = JSON.stringify(i18nData.strings);
1203
+ i18nAttrs["data-locale"] = i18nData.locale;
1204
+ }
1205
+ i18nLoader = { src: `${base}/sdk/i18n/loader.js`, attrs: i18nAttrs };
1202
1206
  }
1203
- parts.push(
1204
- `(function(){var p=new URLSearchParams(location.search);if(p.has('se')||p.has('se_devtools')){var d=document.createElement('script');d.src='https://shipeasy.ai/se-devtools.js';document.head.appendChild(d);}})();`
1205
- );
1206
- return parts.join("");
1207
+ return { bootstrap: bootstrapTag, i18nLoader };
1208
+ }
1209
+ function escapeAttr(v) {
1210
+ return v.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1211
+ }
1212
+ function renderScriptTag(spec) {
1213
+ const attrs = Object.entries(spec.attrs).map(([k, v]) => v === "" ? ` ${k}` : ` ${k}="${escapeAttr(v)}"`).join("");
1214
+ return `<script src="${escapeAttr(spec.src)}"${attrs}></script>`;
1215
+ }
1216
+ function getBootstrapTags(bootstrap, i18nData, opts) {
1217
+ const data = getBootstrapData(bootstrap, i18nData, opts);
1218
+ const tags = [data.bootstrap];
1219
+ if (data.i18nLoader) tags.push(data.i18nLoader);
1220
+ return tags.map(renderScriptTag).join("");
1207
1221
  }
1208
1222
  var flags = {
1209
1223
  configure(opts) {
@@ -1298,7 +1312,8 @@ var see = Object.assign(
1298
1312
  createInMemoryStickyStore,
1299
1313
  fetchLabelsForSSR,
1300
1314
  flags,
1301
- getBootstrapHtml,
1315
+ getBootstrapData,
1316
+ getBootstrapTags,
1302
1317
  getShipeasyServerClient,
1303
1318
  i18n,
1304
1319
  isExpected,
@@ -1118,49 +1118,62 @@ async function shipeasy(opts) {
1118
1118
  flags: bootstrap.flags,
1119
1119
  configs: bootstrap.configs,
1120
1120
  experiments: bootstrap.experiments,
1121
- getBootstrapHtml() {
1122
- return getBootstrapHtml(bootstrap, i18nData, {
1123
- editLabels,
1121
+ getBootstrapData(emit) {
1122
+ return getBootstrapData(bootstrap, i18nData, {
1124
1123
  i18nProfile: profile,
1125
- anonId
1124
+ anonId,
1125
+ ...emit
1126
+ });
1127
+ },
1128
+ getBootstrapTags(emit) {
1129
+ return getBootstrapTags(bootstrap, i18nData, {
1130
+ i18nProfile: profile,
1131
+ anonId,
1132
+ ...emit
1126
1133
  });
1127
1134
  }
1128
1135
  };
1129
1136
  }
1130
- function getBootstrapHtml(bootstrap, i18nData, opts) {
1131
- const parts = [];
1132
- const apiUrl = "https://cdn.shipeasy.ai";
1137
+ var DEFAULT_CDN = "https://cdn.shipeasy.ai";
1138
+ function getBootstrapData(bootstrap, i18nData, opts) {
1139
+ const base = (opts.baseUrl ?? DEFAULT_CDN).replace(/\/$/, "");
1133
1140
  const profile = opts.i18nProfile ?? "en:prod";
1134
- const payload = {
1135
- flags: bootstrap?.flags ?? {},
1136
- configs: bootstrap?.configs ?? {},
1137
- experiments: bootstrap?.experiments ?? {},
1138
- // No key here — the server only knows the server key, which must never reach
1139
- // the browser. The client supplies its own client key via shipeasy({ clientKey }).
1140
- i18nProfile: profile,
1141
- apiUrl
1141
+ const attrs = {
1142
+ "data-se-bootstrap": "",
1143
+ "data-flags": JSON.stringify(bootstrap?.flags ?? {}),
1144
+ "data-configs": JSON.stringify(bootstrap?.configs ?? {}),
1145
+ "data-experiments": JSON.stringify(bootstrap?.experiments ?? {}),
1146
+ "data-killswitches": JSON.stringify(bootstrap?.killswitches ?? {}),
1147
+ "data-i18n-profile": profile,
1148
+ "data-api-url": base
1142
1149
  };
1143
- if (i18nData) payload.i18n = i18nData;
1144
- if (opts.editLabels) payload.editLabels = true;
1145
- if (opts.anonId) payload.anonId = opts.anonId;
1146
- parts.push(
1147
- `(function(){var Q=new URLSearchParams(location.search).has('se_edit_labels');var C=/(?:^|;\\s*)se_edit_labels=1(?:;|$)/.test(document.cookie);if(!Q&&!C)return;if(Q){try{document.cookie='se_edit_labels=1;path=/;max-age=86400;samesite=lax';}catch(_){}}var R;function P(v){if(!v||typeof v.t!=='function'||v.__sePatched)return;var O=v.t.bind(v);v.__sePatched=true;window._sei18n_t=O;v.t=function(k,vars){var r=O(k,vars);if(r===k)return k;var V='';try{if(vars&&typeof vars==='object'){var hasKey=false;for(var _k in vars){hasKey=true;break;}if(hasKey)V=JSON.stringify(vars);}}catch(_){V='';}return '\\uFFF9'+k+'\\uFFFA'+V+'\\uFFFA'+r+'\\uFFFB';};}Object.defineProperty(window,'i18n',{configurable:true,get:function(){return R;},set:function(v){P(v);R=v;}});})();`
1148
- );
1149
- parts.push(`window.__SE_BOOTSTRAP=${JSON.stringify(payload)};`);
1150
- if (opts.anonId) {
1151
- parts.push(
1152
- `(function(){try{var k=${JSON.stringify(ANON_ID_COOKIE)},v=${JSON.stringify(opts.anonId)};if(('; '+document.cookie).indexOf('; '+k+'=')===-1){document.cookie=k+'='+v+';path=/;max-age=31536000;samesite=lax'+(location.protocol==='https:'?';secure':'');}}catch(_){}})();`
1153
- );
1154
- }
1155
- if (i18nData?.strings && Object.keys(i18nData.strings).length > 0) {
1156
- parts.push(
1157
- `(function(){var d=window.__SE_BOOTSTRAP.i18n;if(!d)return;window.i18n={locale:d.locale,t:function(k,v){var r=d.strings[k];if(!r)return k;return v?r.replace(/\\{\\{(\\w+)\\}\\}/g,function(_,p){return v[p]!==undefined?String(v[p]):'{{'+p+'}}'}):r;},on:function(){return function(){};}};})();`
1158
- );
1150
+ if (opts.anonId) attrs["data-anon-id"] = opts.anonId;
1151
+ const bootstrapTag = { src: `${base}/sdk/bootstrap.js`, attrs };
1152
+ let i18nLoader = null;
1153
+ const hasStrings = !!(i18nData?.strings && Object.keys(i18nData.strings).length > 0);
1154
+ if (hasStrings || opts.clientKey) {
1155
+ const i18nAttrs = { "data-profile": profile };
1156
+ if (opts.clientKey) i18nAttrs["data-key"] = opts.clientKey;
1157
+ if (hasStrings) {
1158
+ i18nAttrs["data-strings"] = JSON.stringify(i18nData.strings);
1159
+ i18nAttrs["data-locale"] = i18nData.locale;
1160
+ }
1161
+ i18nLoader = { src: `${base}/sdk/i18n/loader.js`, attrs: i18nAttrs };
1159
1162
  }
1160
- parts.push(
1161
- `(function(){var p=new URLSearchParams(location.search);if(p.has('se')||p.has('se_devtools')){var d=document.createElement('script');d.src='https://shipeasy.ai/se-devtools.js';document.head.appendChild(d);}})();`
1162
- );
1163
- return parts.join("");
1163
+ return { bootstrap: bootstrapTag, i18nLoader };
1164
+ }
1165
+ function escapeAttr(v) {
1166
+ return v.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1167
+ }
1168
+ function renderScriptTag(spec) {
1169
+ const attrs = Object.entries(spec.attrs).map(([k, v]) => v === "" ? ` ${k}` : ` ${k}="${escapeAttr(v)}"`).join("");
1170
+ return `<script src="${escapeAttr(spec.src)}"${attrs}></script>`;
1171
+ }
1172
+ function getBootstrapTags(bootstrap, i18nData, opts) {
1173
+ const data = getBootstrapData(bootstrap, i18nData, opts);
1174
+ const tags = [data.bootstrap];
1175
+ if (data.i18nLoader) tags.push(data.i18nLoader);
1176
+ return tags.map(renderScriptTag).join("");
1164
1177
  }
1165
1178
  var flags = {
1166
1179
  configure(opts) {
@@ -1254,7 +1267,8 @@ export {
1254
1267
  createInMemoryStickyStore,
1255
1268
  fetchLabelsForSSR,
1256
1269
  flags,
1257
- getBootstrapHtml,
1270
+ getBootstrapData,
1271
+ getBootstrapTags,
1258
1272
  getShipeasyServerClient,
1259
1273
  i18n,
1260
1274
  isExpected,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipeasy/sdk",
3
- "version": "5.2.0",
3
+ "version": "5.4.0",
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",