@guidelytech/nextjs 0.6.1 → 0.7.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/LICENSE +21 -0
- package/README.md +49 -1
- package/dist/server.cjs +87 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +42 -0
- package/dist/server.d.ts +42 -0
- package/dist/server.js +60 -0
- package/dist/server.js.map +1 -0
- package/package.json +11 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Guidely
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -30,6 +30,54 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|
|
30
30
|
|---|---|
|
|
31
31
|
| `GuidelyNavigationListener` | Client component using `usePathname`/`useSearchParams` (wrapped in `Suspense`) to call `client.page()` on every route change. sdk-core also detects History API navigation; this is the explicit bridge. |
|
|
32
32
|
| `guidelyEnv(env?)` | Reads `NEXT_PUBLIC_GUIDELY_PROJECT_ID`, `NEXT_PUBLIC_GUIDELY_ENV`, `NEXT_PUBLIC_GUIDELY_API_URL`, `NEXT_PUBLIC_GUIDELY_RELEASE` into provider props (`release` selects the source maps uploaded with `@guidelytech/sourcemaps`). Note: Next.js inlines `process.env.NEXT_PUBLIC_*` only when referenced literally, so pass them explicitly in your own code as shown above. |
|
|
33
|
-
| everything from `@guidelytech/react` | `GuidelyProvider`, `useGuidely`, `useGuidelyReady`, `useGuidelyIdentify`, `useGuidelyEvent`, `useChecklistProgress`, `useGuideState
|
|
33
|
+
| everything from `@guidelytech/react` | `GuidelyProvider`, `useGuidely`, `useGuidelyReady`, `useGuidelyIdentify`, `useGuidelyEvent`, `useChecklistProgress`, `useGuideState`, the flag hooks (`useFlag`, `useFeatureValue`, `useFlagDetails`, `useFlags`, `useFlagsReady`, `useFlagsClient`), `<Feature>`, `<FeatureValue>`, `<FlagsProvider>`. |
|
|
34
34
|
|
|
35
35
|
The built output carries a `"use client"` banner; import it only from client components.
|
|
36
|
+
|
|
37
|
+
## Feature flags on the server — `@guidelytech/nextjs/server`
|
|
38
|
+
|
|
39
|
+
No React and no `"use client"`: works in Server Components, route handlers, server actions and
|
|
40
|
+
middleware. The same public key as the browser works with no extra configuration.
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
// app/layout.tsx (Server Component)
|
|
44
|
+
import { getFlagsBootstrap } from "@guidelytech/nextjs/server";
|
|
45
|
+
|
|
46
|
+
export default async function RootLayout({ children }) {
|
|
47
|
+
const flagsPayload = await getFlagsBootstrap(); // the browser starts from this: no flicker
|
|
48
|
+
return <html><body><Providers flagsPayload={flagsPayload}>{children}</Providers></body></html>;
|
|
49
|
+
}
|
|
50
|
+
// app/providers.tsx ("use client"): <GuidelyProvider … flagsPayload={flagsPayload} user={user}>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
// app/checkout/page.tsx, a route handler or a server action
|
|
55
|
+
import { getFlags } from "@guidelytech/nextjs/server";
|
|
56
|
+
|
|
57
|
+
const flags = await getFlags({ id: user.id, plan: user.plan });
|
|
58
|
+
if (flags.isEnabled("new-checkout")) …
|
|
59
|
+
const color = flags.getString("checkout-button-color", "blue");
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
| Export | Description |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `createServerFlags(options?)` | The process-wide flags client, created once and kept on `globalThis` (survives dev hot reloads). Options default to `GUIDELY_SDK_KEY` (a `pk_…` or server-only `gly_sk_…` key) or `NEXT_PUBLIC_GUIDELY_PROJECT_ID`, `GUIDELY_API_URL` / `NEXT_PUBLIC_GUIDELY_API_URL`, `GUIDELY_ENV` / `NEXT_PUBLIC_GUIDELY_ENV`. Polls every 60 s; fetches bypass the Next.js data cache. |
|
|
65
|
+
| `getFlags(attributes, { client?, timeoutMs? })` | Getters bound to one user (`isEnabled`, `getValue`, `getString`, `getDetails`, `getAll`…). Waits up to `timeoutMs` (1500) for the first download, then serves fallbacks. |
|
|
66
|
+
| `getFlagsBootstrap({ client?, timeoutMs? })` | The payload for `<GuidelyProvider flagsPayload>` / `<FlagsProvider payload>`. `null` for a `gly_sk_` client (its payload contains server-only flags) — bootstrap from a public-key client. |
|
|
67
|
+
|
|
68
|
+
Evaluate with the attributes the browser will have — the provider's `user` (`id`, traits,
|
|
69
|
+
`account`) plus `flagAttributes` — so server-rendered values and hydration agree. Anonymous
|
|
70
|
+
visitors have no id on the server, so percentage rollouts and experiments keyed on `id` resolve in
|
|
71
|
+
the browser (with the visitor's anonymous id) after hydration.
|
|
72
|
+
|
|
73
|
+
**Middleware and edge functions** are short-lived: refresh on read instead of polling.
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
// middleware.ts
|
|
77
|
+
import { createServerFlags, getFlags } from "@guidelytech/nextjs/server";
|
|
78
|
+
const client = createServerFlags({ pollingMode: "lazy", cacheTtlSeconds: 30 });
|
|
79
|
+
export async function middleware(req) {
|
|
80
|
+
const flags = await getFlags({ id: req.cookies.get("uid")?.value }, { client });
|
|
81
|
+
if (flags.isEnabled("maintenance-mode")) return Response.redirect(new URL("/maintenance", req.url));
|
|
82
|
+
}
|
|
83
|
+
```
|
package/dist/server.cjs
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/server.ts
|
|
21
|
+
var server_exports = {};
|
|
22
|
+
__export(server_exports, {
|
|
23
|
+
createServerFlags: () => createServerFlags,
|
|
24
|
+
getFlags: () => getFlags,
|
|
25
|
+
getFlagsBootstrap: () => getFlagsBootstrap
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(server_exports);
|
|
28
|
+
var import_flags = require("@guidelytech/flags");
|
|
29
|
+
function registry() {
|
|
30
|
+
const g = globalThis;
|
|
31
|
+
return g.__guidelyServerFlags ?? (g.__guidelyServerFlags = { clients: /* @__PURE__ */ new Map(), current: null, warned: /* @__PURE__ */ new Set() });
|
|
32
|
+
}
|
|
33
|
+
function env(name) {
|
|
34
|
+
try {
|
|
35
|
+
return globalThis.process?.env?.[name] || void 0;
|
|
36
|
+
} catch {
|
|
37
|
+
return void 0;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
var noStoreFetch = (input, init) => {
|
|
41
|
+
const original = globalThis.fetch._nextOriginalFetch;
|
|
42
|
+
return original ? original(input, init) : fetch(input, { ...init, cache: "no-store" });
|
|
43
|
+
};
|
|
44
|
+
function createServerFlags(options = {}) {
|
|
45
|
+
const sdkKey = options.sdkKey ?? options.projectId ?? env("GUIDELY_SDK_KEY") ?? env("NEXT_PUBLIC_GUIDELY_PROJECT_ID");
|
|
46
|
+
if (!sdkKey) throw new Error("[Guidely] createServerFlags: pass `sdkKey` or set GUIDELY_SDK_KEY / NEXT_PUBLIC_GUIDELY_PROJECT_ID.");
|
|
47
|
+
const apiUrl = options.apiUrl ?? env("GUIDELY_API_URL") ?? env("NEXT_PUBLIC_GUIDELY_API_URL");
|
|
48
|
+
const environment = options.environment ?? env("GUIDELY_ENV") ?? env("NEXT_PUBLIC_GUIDELY_ENV");
|
|
49
|
+
const id = `${sdkKey}|${apiUrl ?? ""}|${environment ?? ""}|${options.pollingMode ?? "auto"}`;
|
|
50
|
+
const reg = registry();
|
|
51
|
+
let client = reg.clients.get(id);
|
|
52
|
+
if (!client) {
|
|
53
|
+
client = (0, import_flags.createFlagsClient)({ storage: false, ...options, sdkKey, apiUrl, environment, fetch: options.fetch ?? noStoreFetch });
|
|
54
|
+
reg.clients.set(id, client);
|
|
55
|
+
}
|
|
56
|
+
reg.current = client;
|
|
57
|
+
return client;
|
|
58
|
+
}
|
|
59
|
+
function settle(client, timeoutMs) {
|
|
60
|
+
if (client.isReady()) return Promise.resolve();
|
|
61
|
+
return Promise.race([client.ready(), new Promise((r) => setTimeout(r, timeoutMs))]);
|
|
62
|
+
}
|
|
63
|
+
async function getFlags(attributes = {}, options = {}) {
|
|
64
|
+
const client = options.client ?? registry().current ?? createServerFlags();
|
|
65
|
+
await settle(client, options.timeoutMs ?? 1500);
|
|
66
|
+
return client.forUser(attributes);
|
|
67
|
+
}
|
|
68
|
+
async function getFlagsBootstrap(options = {}) {
|
|
69
|
+
const client = options.client ?? registry().current ?? createServerFlags();
|
|
70
|
+
if (client.sdkKey.startsWith("gly_sk_")) {
|
|
71
|
+
const reg = registry();
|
|
72
|
+
if (!reg.warned.has(client.sdkKey)) {
|
|
73
|
+
reg.warned.add(client.sdkKey);
|
|
74
|
+
console.warn("[Guidely] getFlagsBootstrap() needs a public-key client (pk_\u2026): a gly_sk_ payload includes server-only flags. Pass { client: createServerFlags({ sdkKey: process.env.NEXT_PUBLIC_GUIDELY_PROJECT_ID }) }.");
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
await settle(client, options.timeoutMs ?? 1500);
|
|
79
|
+
return client.getPayload();
|
|
80
|
+
}
|
|
81
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
82
|
+
0 && (module.exports = {
|
|
83
|
+
createServerFlags,
|
|
84
|
+
getFlags,
|
|
85
|
+
getFlagsBootstrap
|
|
86
|
+
});
|
|
87
|
+
//# sourceMappingURL=server.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server.ts"],"sourcesContent":["/**\n * `@guidelytech/nextjs/server` — feature flags on the server: React Server Components, route\n * handlers, server actions, middleware and edge functions. No React and no `\"use client\"` here.\n *\n * ```ts\n * import { getFlags, getFlagsBootstrap } from \"@guidelytech/nextjs/server\";\n * const flags = await getFlags({ id: user.id, plan: user.plan });\n * if (flags.isEnabled(\"new-dashboard-banner\")) …\n * ```\n */\nimport { createFlagsClient, type Attributes, type FlagsBootstrap, type FlagsClient, type FlagsClientOptions, type FlagsEvaluator } from \"@guidelytech/flags\";\n\nexport type { Attributes, FlagsBootstrap, FlagsClient, FlagsClientOptions, FlagsEvaluator, FlagDetails, JsonValue } from \"@guidelytech/flags\";\n\ninterface Registry {\n clients: Map<string, FlagsClient>;\n current: FlagsClient | null;\n warned: Set<string>;\n}\n\n/** Kept on `globalThis` so dev-server hot reloads reuse the client (and its timers) instead of leaking new ones. */\nfunction registry(): Registry {\n const g = globalThis as { __guidelyServerFlags?: Registry };\n return (g.__guidelyServerFlags ??= { clients: new Map(), current: null, warned: new Set() });\n}\n\nfunction env(name: string): string | undefined {\n try {\n return (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env?.[name] || undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Flag downloads must always reach Guidely and outlive the request that started them: use the\n * fetch Next.js wrapped (no data cache, no request scope), else ask for `no-store`.\n */\nconst noStoreFetch: typeof fetch = (input, init) => {\n const original = (globalThis.fetch as typeof fetch & { _nextOriginalFetch?: typeof fetch })._nextOriginalFetch;\n return original ? original(input, init) : fetch(input, { ...init, cache: \"no-store\" });\n};\n\n/**\n * The process-wide flags client (created once, then reused across requests and hot reloads).\n * Options default to `GUIDELY_SDK_KEY` (a public `pk_…` or secret `gly_sk_…` key), falling back to\n * `NEXT_PUBLIC_GUIDELY_PROJECT_ID`, plus `GUIDELY_API_URL` / `NEXT_PUBLIC_GUIDELY_API_URL` and\n * `GUIDELY_ENV` / `NEXT_PUBLIC_GUIDELY_ENV`. Polls every 60 s by default; in middleware and\n * edge functions use `{ pollingMode: \"lazy\" }` (refreshes on read once the cache is stale).\n */\nexport function createServerFlags(options: Partial<FlagsClientOptions> = {}): FlagsClient {\n const sdkKey = options.sdkKey ?? options.projectId ?? env(\"GUIDELY_SDK_KEY\") ?? env(\"NEXT_PUBLIC_GUIDELY_PROJECT_ID\");\n if (!sdkKey) throw new Error(\"[Guidely] createServerFlags: pass `sdkKey` or set GUIDELY_SDK_KEY / NEXT_PUBLIC_GUIDELY_PROJECT_ID.\");\n const apiUrl = options.apiUrl ?? env(\"GUIDELY_API_URL\") ?? env(\"NEXT_PUBLIC_GUIDELY_API_URL\");\n const environment = options.environment ?? env(\"GUIDELY_ENV\") ?? env(\"NEXT_PUBLIC_GUIDELY_ENV\");\n const id = `${sdkKey}|${apiUrl ?? \"\"}|${environment ?? \"\"}|${options.pollingMode ?? \"auto\"}`;\n const reg = registry();\n let client = reg.clients.get(id);\n if (!client) {\n client = createFlagsClient({ storage: false, ...options, sdkKey, apiUrl, environment, fetch: options.fetch ?? noStoreFetch });\n reg.clients.set(id, client);\n }\n reg.current = client;\n return client;\n}\n\nfunction settle(client: FlagsClient, timeoutMs: number): Promise<void> {\n if (client.isReady()) return Promise.resolve();\n return Promise.race([client.ready(), new Promise<void>((r) => setTimeout(r, timeoutMs))]);\n}\n\nexport interface GetFlagsOptions {\n /** A client from `createServerFlags(...)` (default: the last one created, or one from environment variables). */\n client?: FlagsClient;\n /** How long to wait for the first download before serving fallbacks (default 1500 ms). */\n timeoutMs?: number;\n}\n\n/**\n * Flag getters for one user, for this request. Waits (briefly, the first time only) for the\n * flags to load. Pass the same attributes you give the browser (`flagAttributes` / `user`) so\n * server and client agree.\n */\nexport async function getFlags(attributes: Attributes = {}, options: GetFlagsOptions = {}): Promise<FlagsEvaluator> {\n const client = options.client ?? registry().current ?? createServerFlags();\n await settle(client, options.timeoutMs ?? 1500);\n return client.forUser(attributes);\n}\n\n/**\n * The payload to pass to `<GuidelyProvider flagsPayload={…}>` / `<FlagsProvider payload={…}>` so\n * the first render matches the server. Returns `null` for a `gly_sk_` client: its payload holds\n * server-only flags that must not reach browsers — bootstrap from a public-key client instead.\n */\nexport async function getFlagsBootstrap(options: GetFlagsOptions = {}): Promise<FlagsBootstrap | null> {\n const client = options.client ?? registry().current ?? createServerFlags();\n if (client.sdkKey.startsWith(\"gly_sk_\")) {\n const reg = registry();\n if (!reg.warned.has(client.sdkKey)) {\n reg.warned.add(client.sdkKey);\n console.warn(\"[Guidely] getFlagsBootstrap() needs a public-key client (pk_…): a gly_sk_ payload includes server-only flags. Pass { client: createServerFlags({ sdkKey: process.env.NEXT_PUBLIC_GUIDELY_PROJECT_ID }) }.\");\n }\n return null;\n }\n await settle(client, options.timeoutMs ?? 1500);\n return client.getPayload();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,mBAAwI;AAWxI,SAAS,WAAqB;AAC5B,QAAM,IAAI;AACV,SAAQ,EAAE,yBAAF,EAAE,uBAAyB,EAAE,SAAS,oBAAI,IAAI,GAAG,SAAS,MAAM,QAAQ,oBAAI,IAAI,EAAE;AAC5F;AAEA,SAAS,IAAI,MAAkC;AAC7C,MAAI;AACF,WAAQ,WAA0E,SAAS,MAAM,IAAI,KAAK;AAAA,EAC5G,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMA,IAAM,eAA6B,CAAC,OAAO,SAAS;AAClD,QAAM,WAAY,WAAW,MAA+D;AAC5F,SAAO,WAAW,SAAS,OAAO,IAAI,IAAI,MAAM,OAAO,EAAE,GAAG,MAAM,OAAO,WAAW,CAAC;AACvF;AASO,SAAS,kBAAkB,UAAuC,CAAC,GAAgB;AACxF,QAAM,SAAS,QAAQ,UAAU,QAAQ,aAAa,IAAI,iBAAiB,KAAK,IAAI,gCAAgC;AACpH,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,qGAAqG;AAClI,QAAM,SAAS,QAAQ,UAAU,IAAI,iBAAiB,KAAK,IAAI,6BAA6B;AAC5F,QAAM,cAAc,QAAQ,eAAe,IAAI,aAAa,KAAK,IAAI,yBAAyB;AAC9F,QAAM,KAAK,GAAG,MAAM,IAAI,UAAU,EAAE,IAAI,eAAe,EAAE,IAAI,QAAQ,eAAe,MAAM;AAC1F,QAAM,MAAM,SAAS;AACrB,MAAI,SAAS,IAAI,QAAQ,IAAI,EAAE;AAC/B,MAAI,CAAC,QAAQ;AACX,iBAAS,gCAAkB,EAAE,SAAS,OAAO,GAAG,SAAS,QAAQ,QAAQ,aAAa,OAAO,QAAQ,SAAS,aAAa,CAAC;AAC5H,QAAI,QAAQ,IAAI,IAAI,MAAM;AAAA,EAC5B;AACA,MAAI,UAAU;AACd,SAAO;AACT;AAEA,SAAS,OAAO,QAAqB,WAAkC;AACrE,MAAI,OAAO,QAAQ,EAAG,QAAO,QAAQ,QAAQ;AAC7C,SAAO,QAAQ,KAAK,CAAC,OAAO,MAAM,GAAG,IAAI,QAAc,CAAC,MAAM,WAAW,GAAG,SAAS,CAAC,CAAC,CAAC;AAC1F;AAcA,eAAsB,SAAS,aAAyB,CAAC,GAAG,UAA2B,CAAC,GAA4B;AAClH,QAAM,SAAS,QAAQ,UAAU,SAAS,EAAE,WAAW,kBAAkB;AACzE,QAAM,OAAO,QAAQ,QAAQ,aAAa,IAAI;AAC9C,SAAO,OAAO,QAAQ,UAAU;AAClC;AAOA,eAAsB,kBAAkB,UAA2B,CAAC,GAAmC;AACrG,QAAM,SAAS,QAAQ,UAAU,SAAS,EAAE,WAAW,kBAAkB;AACzE,MAAI,OAAO,OAAO,WAAW,SAAS,GAAG;AACvC,UAAM,MAAM,SAAS;AACrB,QAAI,CAAC,IAAI,OAAO,IAAI,OAAO,MAAM,GAAG;AAClC,UAAI,OAAO,IAAI,OAAO,MAAM;AAC5B,cAAQ,KAAK,gNAA2M;AAAA,IAC1N;AACA,WAAO;AAAA,EACT;AACA,QAAM,OAAO,QAAQ,QAAQ,aAAa,IAAI;AAC9C,SAAO,OAAO,WAAW;AAC3B;","names":[]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { FlagsClient, FlagsClientOptions, Attributes, FlagsEvaluator, FlagsBootstrap } from '@guidelytech/flags';
|
|
2
|
+
export { Attributes, FlagDetails, FlagsBootstrap, FlagsClient, FlagsClientOptions, FlagsEvaluator, JsonValue } from '@guidelytech/flags';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `@guidelytech/nextjs/server` — feature flags on the server: React Server Components, route
|
|
6
|
+
* handlers, server actions, middleware and edge functions. No React and no `"use client"` here.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { getFlags, getFlagsBootstrap } from "@guidelytech/nextjs/server";
|
|
10
|
+
* const flags = await getFlags({ id: user.id, plan: user.plan });
|
|
11
|
+
* if (flags.isEnabled("new-dashboard-banner")) …
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The process-wide flags client (created once, then reused across requests and hot reloads).
|
|
17
|
+
* Options default to `GUIDELY_SDK_KEY` (a public `pk_…` or secret `gly_sk_…` key), falling back to
|
|
18
|
+
* `NEXT_PUBLIC_GUIDELY_PROJECT_ID`, plus `GUIDELY_API_URL` / `NEXT_PUBLIC_GUIDELY_API_URL` and
|
|
19
|
+
* `GUIDELY_ENV` / `NEXT_PUBLIC_GUIDELY_ENV`. Polls every 60 s by default; in middleware and
|
|
20
|
+
* edge functions use `{ pollingMode: "lazy" }` (refreshes on read once the cache is stale).
|
|
21
|
+
*/
|
|
22
|
+
declare function createServerFlags(options?: Partial<FlagsClientOptions>): FlagsClient;
|
|
23
|
+
interface GetFlagsOptions {
|
|
24
|
+
/** A client from `createServerFlags(...)` (default: the last one created, or one from environment variables). */
|
|
25
|
+
client?: FlagsClient;
|
|
26
|
+
/** How long to wait for the first download before serving fallbacks (default 1500 ms). */
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Flag getters for one user, for this request. Waits (briefly, the first time only) for the
|
|
31
|
+
* flags to load. Pass the same attributes you give the browser (`flagAttributes` / `user`) so
|
|
32
|
+
* server and client agree.
|
|
33
|
+
*/
|
|
34
|
+
declare function getFlags(attributes?: Attributes, options?: GetFlagsOptions): Promise<FlagsEvaluator>;
|
|
35
|
+
/**
|
|
36
|
+
* The payload to pass to `<GuidelyProvider flagsPayload={…}>` / `<FlagsProvider payload={…}>` so
|
|
37
|
+
* the first render matches the server. Returns `null` for a `gly_sk_` client: its payload holds
|
|
38
|
+
* server-only flags that must not reach browsers — bootstrap from a public-key client instead.
|
|
39
|
+
*/
|
|
40
|
+
declare function getFlagsBootstrap(options?: GetFlagsOptions): Promise<FlagsBootstrap | null>;
|
|
41
|
+
|
|
42
|
+
export { type GetFlagsOptions, createServerFlags, getFlags, getFlagsBootstrap };
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { FlagsClient, FlagsClientOptions, Attributes, FlagsEvaluator, FlagsBootstrap } from '@guidelytech/flags';
|
|
2
|
+
export { Attributes, FlagDetails, FlagsBootstrap, FlagsClient, FlagsClientOptions, FlagsEvaluator, JsonValue } from '@guidelytech/flags';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `@guidelytech/nextjs/server` — feature flags on the server: React Server Components, route
|
|
6
|
+
* handlers, server actions, middleware and edge functions. No React and no `"use client"` here.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { getFlags, getFlagsBootstrap } from "@guidelytech/nextjs/server";
|
|
10
|
+
* const flags = await getFlags({ id: user.id, plan: user.plan });
|
|
11
|
+
* if (flags.isEnabled("new-dashboard-banner")) …
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The process-wide flags client (created once, then reused across requests and hot reloads).
|
|
17
|
+
* Options default to `GUIDELY_SDK_KEY` (a public `pk_…` or secret `gly_sk_…` key), falling back to
|
|
18
|
+
* `NEXT_PUBLIC_GUIDELY_PROJECT_ID`, plus `GUIDELY_API_URL` / `NEXT_PUBLIC_GUIDELY_API_URL` and
|
|
19
|
+
* `GUIDELY_ENV` / `NEXT_PUBLIC_GUIDELY_ENV`. Polls every 60 s by default; in middleware and
|
|
20
|
+
* edge functions use `{ pollingMode: "lazy" }` (refreshes on read once the cache is stale).
|
|
21
|
+
*/
|
|
22
|
+
declare function createServerFlags(options?: Partial<FlagsClientOptions>): FlagsClient;
|
|
23
|
+
interface GetFlagsOptions {
|
|
24
|
+
/** A client from `createServerFlags(...)` (default: the last one created, or one from environment variables). */
|
|
25
|
+
client?: FlagsClient;
|
|
26
|
+
/** How long to wait for the first download before serving fallbacks (default 1500 ms). */
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Flag getters for one user, for this request. Waits (briefly, the first time only) for the
|
|
31
|
+
* flags to load. Pass the same attributes you give the browser (`flagAttributes` / `user`) so
|
|
32
|
+
* server and client agree.
|
|
33
|
+
*/
|
|
34
|
+
declare function getFlags(attributes?: Attributes, options?: GetFlagsOptions): Promise<FlagsEvaluator>;
|
|
35
|
+
/**
|
|
36
|
+
* The payload to pass to `<GuidelyProvider flagsPayload={…}>` / `<FlagsProvider payload={…}>` so
|
|
37
|
+
* the first render matches the server. Returns `null` for a `gly_sk_` client: its payload holds
|
|
38
|
+
* server-only flags that must not reach browsers — bootstrap from a public-key client instead.
|
|
39
|
+
*/
|
|
40
|
+
declare function getFlagsBootstrap(options?: GetFlagsOptions): Promise<FlagsBootstrap | null>;
|
|
41
|
+
|
|
42
|
+
export { type GetFlagsOptions, createServerFlags, getFlags, getFlagsBootstrap };
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// src/server.ts
|
|
2
|
+
import { createFlagsClient } from "@guidelytech/flags";
|
|
3
|
+
function registry() {
|
|
4
|
+
const g = globalThis;
|
|
5
|
+
return g.__guidelyServerFlags ?? (g.__guidelyServerFlags = { clients: /* @__PURE__ */ new Map(), current: null, warned: /* @__PURE__ */ new Set() });
|
|
6
|
+
}
|
|
7
|
+
function env(name) {
|
|
8
|
+
try {
|
|
9
|
+
return globalThis.process?.env?.[name] || void 0;
|
|
10
|
+
} catch {
|
|
11
|
+
return void 0;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
var noStoreFetch = (input, init) => {
|
|
15
|
+
const original = globalThis.fetch._nextOriginalFetch;
|
|
16
|
+
return original ? original(input, init) : fetch(input, { ...init, cache: "no-store" });
|
|
17
|
+
};
|
|
18
|
+
function createServerFlags(options = {}) {
|
|
19
|
+
const sdkKey = options.sdkKey ?? options.projectId ?? env("GUIDELY_SDK_KEY") ?? env("NEXT_PUBLIC_GUIDELY_PROJECT_ID");
|
|
20
|
+
if (!sdkKey) throw new Error("[Guidely] createServerFlags: pass `sdkKey` or set GUIDELY_SDK_KEY / NEXT_PUBLIC_GUIDELY_PROJECT_ID.");
|
|
21
|
+
const apiUrl = options.apiUrl ?? env("GUIDELY_API_URL") ?? env("NEXT_PUBLIC_GUIDELY_API_URL");
|
|
22
|
+
const environment = options.environment ?? env("GUIDELY_ENV") ?? env("NEXT_PUBLIC_GUIDELY_ENV");
|
|
23
|
+
const id = `${sdkKey}|${apiUrl ?? ""}|${environment ?? ""}|${options.pollingMode ?? "auto"}`;
|
|
24
|
+
const reg = registry();
|
|
25
|
+
let client = reg.clients.get(id);
|
|
26
|
+
if (!client) {
|
|
27
|
+
client = createFlagsClient({ storage: false, ...options, sdkKey, apiUrl, environment, fetch: options.fetch ?? noStoreFetch });
|
|
28
|
+
reg.clients.set(id, client);
|
|
29
|
+
}
|
|
30
|
+
reg.current = client;
|
|
31
|
+
return client;
|
|
32
|
+
}
|
|
33
|
+
function settle(client, timeoutMs) {
|
|
34
|
+
if (client.isReady()) return Promise.resolve();
|
|
35
|
+
return Promise.race([client.ready(), new Promise((r) => setTimeout(r, timeoutMs))]);
|
|
36
|
+
}
|
|
37
|
+
async function getFlags(attributes = {}, options = {}) {
|
|
38
|
+
const client = options.client ?? registry().current ?? createServerFlags();
|
|
39
|
+
await settle(client, options.timeoutMs ?? 1500);
|
|
40
|
+
return client.forUser(attributes);
|
|
41
|
+
}
|
|
42
|
+
async function getFlagsBootstrap(options = {}) {
|
|
43
|
+
const client = options.client ?? registry().current ?? createServerFlags();
|
|
44
|
+
if (client.sdkKey.startsWith("gly_sk_")) {
|
|
45
|
+
const reg = registry();
|
|
46
|
+
if (!reg.warned.has(client.sdkKey)) {
|
|
47
|
+
reg.warned.add(client.sdkKey);
|
|
48
|
+
console.warn("[Guidely] getFlagsBootstrap() needs a public-key client (pk_\u2026): a gly_sk_ payload includes server-only flags. Pass { client: createServerFlags({ sdkKey: process.env.NEXT_PUBLIC_GUIDELY_PROJECT_ID }) }.");
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
await settle(client, options.timeoutMs ?? 1500);
|
|
53
|
+
return client.getPayload();
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
createServerFlags,
|
|
57
|
+
getFlags,
|
|
58
|
+
getFlagsBootstrap
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server.ts"],"sourcesContent":["/**\n * `@guidelytech/nextjs/server` — feature flags on the server: React Server Components, route\n * handlers, server actions, middleware and edge functions. No React and no `\"use client\"` here.\n *\n * ```ts\n * import { getFlags, getFlagsBootstrap } from \"@guidelytech/nextjs/server\";\n * const flags = await getFlags({ id: user.id, plan: user.plan });\n * if (flags.isEnabled(\"new-dashboard-banner\")) …\n * ```\n */\nimport { createFlagsClient, type Attributes, type FlagsBootstrap, type FlagsClient, type FlagsClientOptions, type FlagsEvaluator } from \"@guidelytech/flags\";\n\nexport type { Attributes, FlagsBootstrap, FlagsClient, FlagsClientOptions, FlagsEvaluator, FlagDetails, JsonValue } from \"@guidelytech/flags\";\n\ninterface Registry {\n clients: Map<string, FlagsClient>;\n current: FlagsClient | null;\n warned: Set<string>;\n}\n\n/** Kept on `globalThis` so dev-server hot reloads reuse the client (and its timers) instead of leaking new ones. */\nfunction registry(): Registry {\n const g = globalThis as { __guidelyServerFlags?: Registry };\n return (g.__guidelyServerFlags ??= { clients: new Map(), current: null, warned: new Set() });\n}\n\nfunction env(name: string): string | undefined {\n try {\n return (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env?.[name] || undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Flag downloads must always reach Guidely and outlive the request that started them: use the\n * fetch Next.js wrapped (no data cache, no request scope), else ask for `no-store`.\n */\nconst noStoreFetch: typeof fetch = (input, init) => {\n const original = (globalThis.fetch as typeof fetch & { _nextOriginalFetch?: typeof fetch })._nextOriginalFetch;\n return original ? original(input, init) : fetch(input, { ...init, cache: \"no-store\" });\n};\n\n/**\n * The process-wide flags client (created once, then reused across requests and hot reloads).\n * Options default to `GUIDELY_SDK_KEY` (a public `pk_…` or secret `gly_sk_…` key), falling back to\n * `NEXT_PUBLIC_GUIDELY_PROJECT_ID`, plus `GUIDELY_API_URL` / `NEXT_PUBLIC_GUIDELY_API_URL` and\n * `GUIDELY_ENV` / `NEXT_PUBLIC_GUIDELY_ENV`. Polls every 60 s by default; in middleware and\n * edge functions use `{ pollingMode: \"lazy\" }` (refreshes on read once the cache is stale).\n */\nexport function createServerFlags(options: Partial<FlagsClientOptions> = {}): FlagsClient {\n const sdkKey = options.sdkKey ?? options.projectId ?? env(\"GUIDELY_SDK_KEY\") ?? env(\"NEXT_PUBLIC_GUIDELY_PROJECT_ID\");\n if (!sdkKey) throw new Error(\"[Guidely] createServerFlags: pass `sdkKey` or set GUIDELY_SDK_KEY / NEXT_PUBLIC_GUIDELY_PROJECT_ID.\");\n const apiUrl = options.apiUrl ?? env(\"GUIDELY_API_URL\") ?? env(\"NEXT_PUBLIC_GUIDELY_API_URL\");\n const environment = options.environment ?? env(\"GUIDELY_ENV\") ?? env(\"NEXT_PUBLIC_GUIDELY_ENV\");\n const id = `${sdkKey}|${apiUrl ?? \"\"}|${environment ?? \"\"}|${options.pollingMode ?? \"auto\"}`;\n const reg = registry();\n let client = reg.clients.get(id);\n if (!client) {\n client = createFlagsClient({ storage: false, ...options, sdkKey, apiUrl, environment, fetch: options.fetch ?? noStoreFetch });\n reg.clients.set(id, client);\n }\n reg.current = client;\n return client;\n}\n\nfunction settle(client: FlagsClient, timeoutMs: number): Promise<void> {\n if (client.isReady()) return Promise.resolve();\n return Promise.race([client.ready(), new Promise<void>((r) => setTimeout(r, timeoutMs))]);\n}\n\nexport interface GetFlagsOptions {\n /** A client from `createServerFlags(...)` (default: the last one created, or one from environment variables). */\n client?: FlagsClient;\n /** How long to wait for the first download before serving fallbacks (default 1500 ms). */\n timeoutMs?: number;\n}\n\n/**\n * Flag getters for one user, for this request. Waits (briefly, the first time only) for the\n * flags to load. Pass the same attributes you give the browser (`flagAttributes` / `user`) so\n * server and client agree.\n */\nexport async function getFlags(attributes: Attributes = {}, options: GetFlagsOptions = {}): Promise<FlagsEvaluator> {\n const client = options.client ?? registry().current ?? createServerFlags();\n await settle(client, options.timeoutMs ?? 1500);\n return client.forUser(attributes);\n}\n\n/**\n * The payload to pass to `<GuidelyProvider flagsPayload={…}>` / `<FlagsProvider payload={…}>` so\n * the first render matches the server. Returns `null` for a `gly_sk_` client: its payload holds\n * server-only flags that must not reach browsers — bootstrap from a public-key client instead.\n */\nexport async function getFlagsBootstrap(options: GetFlagsOptions = {}): Promise<FlagsBootstrap | null> {\n const client = options.client ?? registry().current ?? createServerFlags();\n if (client.sdkKey.startsWith(\"gly_sk_\")) {\n const reg = registry();\n if (!reg.warned.has(client.sdkKey)) {\n reg.warned.add(client.sdkKey);\n console.warn(\"[Guidely] getFlagsBootstrap() needs a public-key client (pk_…): a gly_sk_ payload includes server-only flags. Pass { client: createServerFlags({ sdkKey: process.env.NEXT_PUBLIC_GUIDELY_PROJECT_ID }) }.\");\n }\n return null;\n }\n await settle(client, options.timeoutMs ?? 1500);\n return client.getPayload();\n}\n"],"mappings":";AAUA,SAAS,yBAA+H;AAWxI,SAAS,WAAqB;AAC5B,QAAM,IAAI;AACV,SAAQ,EAAE,yBAAF,EAAE,uBAAyB,EAAE,SAAS,oBAAI,IAAI,GAAG,SAAS,MAAM,QAAQ,oBAAI,IAAI,EAAE;AAC5F;AAEA,SAAS,IAAI,MAAkC;AAC7C,MAAI;AACF,WAAQ,WAA0E,SAAS,MAAM,IAAI,KAAK;AAAA,EAC5G,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMA,IAAM,eAA6B,CAAC,OAAO,SAAS;AAClD,QAAM,WAAY,WAAW,MAA+D;AAC5F,SAAO,WAAW,SAAS,OAAO,IAAI,IAAI,MAAM,OAAO,EAAE,GAAG,MAAM,OAAO,WAAW,CAAC;AACvF;AASO,SAAS,kBAAkB,UAAuC,CAAC,GAAgB;AACxF,QAAM,SAAS,QAAQ,UAAU,QAAQ,aAAa,IAAI,iBAAiB,KAAK,IAAI,gCAAgC;AACpH,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,qGAAqG;AAClI,QAAM,SAAS,QAAQ,UAAU,IAAI,iBAAiB,KAAK,IAAI,6BAA6B;AAC5F,QAAM,cAAc,QAAQ,eAAe,IAAI,aAAa,KAAK,IAAI,yBAAyB;AAC9F,QAAM,KAAK,GAAG,MAAM,IAAI,UAAU,EAAE,IAAI,eAAe,EAAE,IAAI,QAAQ,eAAe,MAAM;AAC1F,QAAM,MAAM,SAAS;AACrB,MAAI,SAAS,IAAI,QAAQ,IAAI,EAAE;AAC/B,MAAI,CAAC,QAAQ;AACX,aAAS,kBAAkB,EAAE,SAAS,OAAO,GAAG,SAAS,QAAQ,QAAQ,aAAa,OAAO,QAAQ,SAAS,aAAa,CAAC;AAC5H,QAAI,QAAQ,IAAI,IAAI,MAAM;AAAA,EAC5B;AACA,MAAI,UAAU;AACd,SAAO;AACT;AAEA,SAAS,OAAO,QAAqB,WAAkC;AACrE,MAAI,OAAO,QAAQ,EAAG,QAAO,QAAQ,QAAQ;AAC7C,SAAO,QAAQ,KAAK,CAAC,OAAO,MAAM,GAAG,IAAI,QAAc,CAAC,MAAM,WAAW,GAAG,SAAS,CAAC,CAAC,CAAC;AAC1F;AAcA,eAAsB,SAAS,aAAyB,CAAC,GAAG,UAA2B,CAAC,GAA4B;AAClH,QAAM,SAAS,QAAQ,UAAU,SAAS,EAAE,WAAW,kBAAkB;AACzE,QAAM,OAAO,QAAQ,QAAQ,aAAa,IAAI;AAC9C,SAAO,OAAO,QAAQ,UAAU;AAClC;AAOA,eAAsB,kBAAkB,UAA2B,CAAC,GAAmC;AACrG,QAAM,SAAS,QAAQ,UAAU,SAAS,EAAE,WAAW,kBAAkB;AACzE,MAAI,OAAO,OAAO,WAAW,SAAS,GAAG;AACvC,UAAM,MAAM,SAAS;AACrB,QAAI,CAAC,IAAI,OAAO,IAAI,OAAO,MAAM,GAAG;AAClC,UAAI,OAAO,IAAI,OAAO,MAAM;AAC5B,cAAQ,KAAK,gNAA2M;AAAA,IAC1N;AACA,WAAO;AAAA,EACT;AACA,QAAM,OAAO,QAAQ,QAAQ,aAAa,IAAI;AAC9C,SAAO,OAAO,WAAW;AAC3B;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guidelytech/nextjs",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Next.js App Router helpers for Guidely",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Next.js App Router helpers for Guidely: provider, navigation bridge and server-side feature flags",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
"types": "./dist/index.d.ts",
|
|
14
14
|
"import": "./dist/index.js",
|
|
15
15
|
"require": "./dist/index.cjs"
|
|
16
|
+
},
|
|
17
|
+
"./server": {
|
|
18
|
+
"types": "./dist/server.d.ts",
|
|
19
|
+
"import": "./dist/server.js",
|
|
20
|
+
"require": "./dist/server.cjs"
|
|
16
21
|
}
|
|
17
22
|
},
|
|
18
23
|
"files": [
|
|
@@ -20,8 +25,9 @@
|
|
|
20
25
|
"README.md"
|
|
21
26
|
],
|
|
22
27
|
"dependencies": {
|
|
23
|
-
"@guidelytech/
|
|
24
|
-
"@guidelytech/
|
|
28
|
+
"@guidelytech/sdk-core": "0.7.0",
|
|
29
|
+
"@guidelytech/flags": "0.7.0",
|
|
30
|
+
"@guidelytech/react": "0.7.0"
|
|
25
31
|
},
|
|
26
32
|
"devDependencies": {
|
|
27
33
|
"tsup": "^8.5.1",
|
|
@@ -61,7 +67,7 @@
|
|
|
61
67
|
"nextjs"
|
|
62
68
|
],
|
|
63
69
|
"scripts": {
|
|
64
|
-
"build": "tsup",
|
|
70
|
+
"build": "rm -rf dist && tsup",
|
|
65
71
|
"dev": "tsup --watch",
|
|
66
72
|
"typecheck": "tsc --noEmit",
|
|
67
73
|
"test": "vitest run",
|