@replanejs/next 0.7.0 → 0.7.1
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 +426 -0
- package/dist/index.d.cts +4 -4
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/server.d.cts +1 -1
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js.map +1 -1
- package/package.json +3 -3
package/README.md
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
# @replanejs/next
|
|
2
|
+
|
|
3
|
+
Next.js SDK for Replane - feature flags and remote configuration with SSR support.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **SSR Hydration**: Fetch configs on the server, instantly hydrate on the client
|
|
8
|
+
- **Zero Loading States**: Users see correct feature flags immediately
|
|
9
|
+
- **Real-time Updates**: Optional live connection for instant config changes
|
|
10
|
+
- **Type-safe**: Full TypeScript support with generics
|
|
11
|
+
- **Next.js Optimized**: Works with App Router, Pages Router, and Server Components
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @replanejs/next
|
|
17
|
+
# or
|
|
18
|
+
pnpm add @replanejs/next
|
|
19
|
+
# or
|
|
20
|
+
yarn add @replanejs/next
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
### 1. Set up environment variables
|
|
26
|
+
|
|
27
|
+
```env
|
|
28
|
+
# Server-side (not exposed to browser)
|
|
29
|
+
REPLANE_BASE_URL=https://your-replane-instance.com
|
|
30
|
+
REPLANE_SDK_KEY=rp_your_server_sdk_key
|
|
31
|
+
|
|
32
|
+
# Client-side (exposed to browser, for real-time updates)
|
|
33
|
+
NEXT_PUBLIC_REPLANE_BASE_URL=https://your-replane-instance.com
|
|
34
|
+
NEXT_PUBLIC_REPLANE_SDK_KEY=rp_your_client_sdk_key
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. Create the provider in your layout (App Router)
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
// app/layout.tsx
|
|
41
|
+
import { getReplaneSnapshot } from "@replanejs/next/server";
|
|
42
|
+
import { ReplaneNextProvider } from "@replanejs/next";
|
|
43
|
+
|
|
44
|
+
export default async function RootLayout({
|
|
45
|
+
children,
|
|
46
|
+
}: {
|
|
47
|
+
children: React.ReactNode;
|
|
48
|
+
}) {
|
|
49
|
+
const snapshot = await getReplaneSnapshot({
|
|
50
|
+
baseUrl: process.env.REPLANE_BASE_URL!,
|
|
51
|
+
sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<html lang="en">
|
|
56
|
+
<body>
|
|
57
|
+
<ReplaneNextProvider
|
|
58
|
+
snapshot={snapshot}
|
|
59
|
+
connection={{
|
|
60
|
+
baseUrl: process.env.NEXT_PUBLIC_REPLANE_BASE_URL!,
|
|
61
|
+
sdkKey: process.env.NEXT_PUBLIC_REPLANE_SDK_KEY!,
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
{children}
|
|
65
|
+
</ReplaneNextProvider>
|
|
66
|
+
</body>
|
|
67
|
+
</html>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 3. Use configs in your components
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
// app/components/feature.tsx
|
|
76
|
+
"use client";
|
|
77
|
+
|
|
78
|
+
import { useConfig } from "@replanejs/next";
|
|
79
|
+
|
|
80
|
+
export function FeatureComponent() {
|
|
81
|
+
const isEnabled = useConfig<boolean>("my-feature");
|
|
82
|
+
const maxItems = useConfig<number>("max-items");
|
|
83
|
+
|
|
84
|
+
if (!isEnabled) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return <div>Feature enabled! Max items: {maxItems}</div>;
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## API Reference
|
|
93
|
+
|
|
94
|
+
### Server Functions
|
|
95
|
+
|
|
96
|
+
#### `getReplaneSnapshot(options)`
|
|
97
|
+
|
|
98
|
+
Fetches configs from Replane and returns a serializable snapshot for client-side hydration.
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
import { getReplaneSnapshot } from "@replanejs/next/server";
|
|
102
|
+
|
|
103
|
+
const snapshot = await getReplaneSnapshot({
|
|
104
|
+
// Required
|
|
105
|
+
baseUrl: "https://your-replane-instance.com",
|
|
106
|
+
sdkKey: "rp_your_sdk_key",
|
|
107
|
+
|
|
108
|
+
// Optional
|
|
109
|
+
fetchFn: customFetch, // Custom fetch for caching
|
|
110
|
+
requestTimeoutMs: 2000, // Request timeout (default: 2000)
|
|
111
|
+
initializationTimeoutMs: 5000, // Init timeout (default: 5000)
|
|
112
|
+
context: { userId: "123" }, // Context for override evaluation
|
|
113
|
+
required: ["feature-a", "feature-b"], // Required configs
|
|
114
|
+
fallbacks: { "feature-a": false }, // Fallback values
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
#### `getConfig(options)`
|
|
119
|
+
|
|
120
|
+
Get a single config value directly on the server.
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
import { getConfig } from "@replanejs/next/server";
|
|
124
|
+
|
|
125
|
+
const maintenanceMode = await getConfig<boolean>({
|
|
126
|
+
name: "maintenance-mode",
|
|
127
|
+
baseUrl: process.env.REPLANE_BASE_URL!,
|
|
128
|
+
sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
129
|
+
context: { region: "us-east" },
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (maintenanceMode) {
|
|
133
|
+
return <MaintenancePage />;
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Client Components
|
|
138
|
+
|
|
139
|
+
#### `ReplaneNextProvider`
|
|
140
|
+
|
|
141
|
+
Main provider component for SSR hydration.
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
import { ReplaneNextProvider } from "@replanejs/next";
|
|
145
|
+
|
|
146
|
+
<ReplaneNextProvider
|
|
147
|
+
snapshot={snapshot} // Required: from getReplaneSnapshot()
|
|
148
|
+
connection={{
|
|
149
|
+
// Optional: for real-time updates
|
|
150
|
+
baseUrl: "https://...",
|
|
151
|
+
sdkKey: "rp_...",
|
|
152
|
+
requestTimeoutMs: 2000,
|
|
153
|
+
retryDelayMs: 200,
|
|
154
|
+
inactivityTimeoutMs: 30000,
|
|
155
|
+
}}
|
|
156
|
+
context={{ userId: "123" }} // Optional: override context on client
|
|
157
|
+
>
|
|
158
|
+
{children}
|
|
159
|
+
</ReplaneNextProvider>;
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
#### `ReplaneScriptProvider`
|
|
163
|
+
|
|
164
|
+
Alternative hydration pattern using embedded scripts.
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
// In layout (Server Component)
|
|
168
|
+
import { getReplaneSnapshotScript, ReplaneScriptProvider } from "@replanejs/next";
|
|
169
|
+
|
|
170
|
+
export default async function RootLayout({ children }) {
|
|
171
|
+
const snapshot = await getReplaneSnapshot({ ... });
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<html>
|
|
175
|
+
<head>
|
|
176
|
+
<script
|
|
177
|
+
dangerouslySetInnerHTML={{
|
|
178
|
+
__html: getReplaneSnapshotScript(snapshot),
|
|
179
|
+
}}
|
|
180
|
+
/>
|
|
181
|
+
</head>
|
|
182
|
+
<body>
|
|
183
|
+
<ReplaneScriptProvider connection={{ baseUrl, sdkKey }}>
|
|
184
|
+
{children}
|
|
185
|
+
</ReplaneScriptProvider>
|
|
186
|
+
</body>
|
|
187
|
+
</html>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Hooks
|
|
193
|
+
|
|
194
|
+
#### `useConfig<T>(name, options?)`
|
|
195
|
+
|
|
196
|
+
Subscribe to a specific config with reactive updates.
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
import { useConfig } from "@replanejs/next";
|
|
200
|
+
|
|
201
|
+
function MyComponent() {
|
|
202
|
+
// Basic usage
|
|
203
|
+
const feature = useConfig<boolean>("feature-flag");
|
|
204
|
+
|
|
205
|
+
// With context override
|
|
206
|
+
const price = useConfig<number>("pricing", {
|
|
207
|
+
context: { plan: "premium" },
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
return <div>{feature ? "Enabled" : "Disabled"}</div>;
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
#### `useReplane()`
|
|
215
|
+
|
|
216
|
+
Access the underlying Replane client.
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
import { useReplane } from "@replanejs/next";
|
|
220
|
+
|
|
221
|
+
function MyComponent() {
|
|
222
|
+
const { client } = useReplane();
|
|
223
|
+
|
|
224
|
+
// Access client methods
|
|
225
|
+
const snapshot = client.getSnapshot();
|
|
226
|
+
|
|
227
|
+
return <div>...</div>;
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Advanced Usage
|
|
232
|
+
|
|
233
|
+
### Next.js Caching
|
|
234
|
+
|
|
235
|
+
Use Next.js fetch caching with `getReplaneSnapshot`:
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
// ISR: Revalidate every 60 seconds
|
|
239
|
+
const snapshot = await getReplaneSnapshot({
|
|
240
|
+
baseUrl: process.env.REPLANE_BASE_URL!,
|
|
241
|
+
sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
242
|
+
fetchFn: (url, init) =>
|
|
243
|
+
fetch(url, {
|
|
244
|
+
...init,
|
|
245
|
+
next: { revalidate: 60 },
|
|
246
|
+
}),
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
```tsx
|
|
251
|
+
// On-demand revalidation with tags
|
|
252
|
+
const snapshot = await getReplaneSnapshot({
|
|
253
|
+
baseUrl: process.env.REPLANE_BASE_URL!,
|
|
254
|
+
sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
255
|
+
fetchFn: (url, init) =>
|
|
256
|
+
fetch(url, {
|
|
257
|
+
...init,
|
|
258
|
+
next: { tags: ["replane-config"] },
|
|
259
|
+
}),
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// In a server action or route handler:
|
|
263
|
+
// revalidateTag('replane-config');
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Static Snapshot (No Real-time Updates)
|
|
267
|
+
|
|
268
|
+
For static sites or when real-time updates aren't needed:
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
<ReplaneNextProvider snapshot={snapshot}>
|
|
272
|
+
{/* No connection prop = no live updates */}
|
|
273
|
+
{children}
|
|
274
|
+
</ReplaneNextProvider>
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Pages Router Support
|
|
278
|
+
|
|
279
|
+
```tsx
|
|
280
|
+
// pages/_app.tsx
|
|
281
|
+
import { ReplaneNextProvider } from "@replanejs/next";
|
|
282
|
+
import type { AppProps } from "next/app";
|
|
283
|
+
|
|
284
|
+
export default function App({ Component, pageProps }: AppProps) {
|
|
285
|
+
return (
|
|
286
|
+
<ReplaneNextProvider
|
|
287
|
+
snapshot={pageProps.replaneSnapshot}
|
|
288
|
+
connection={{
|
|
289
|
+
baseUrl: process.env.NEXT_PUBLIC_REPLANE_BASE_URL!,
|
|
290
|
+
sdkKey: process.env.NEXT_PUBLIC_REPLANE_SDK_KEY!,
|
|
291
|
+
}}
|
|
292
|
+
>
|
|
293
|
+
<Component {...pageProps} />
|
|
294
|
+
</ReplaneNextProvider>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
// pages/index.tsx
|
|
301
|
+
import { getReplaneSnapshot } from "@replanejs/next/server";
|
|
302
|
+
import type { GetServerSideProps } from "next";
|
|
303
|
+
|
|
304
|
+
export const getServerSideProps: GetServerSideProps = async () => {
|
|
305
|
+
const replaneSnapshot = await getReplaneSnapshot({
|
|
306
|
+
baseUrl: process.env.REPLANE_BASE_URL!,
|
|
307
|
+
sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
props: { replaneSnapshot },
|
|
312
|
+
};
|
|
313
|
+
};
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Context-based Overrides
|
|
317
|
+
|
|
318
|
+
Pass user context for personalized config values:
|
|
319
|
+
|
|
320
|
+
```tsx
|
|
321
|
+
// Server-side: include context in snapshot
|
|
322
|
+
const snapshot = await getReplaneSnapshot({
|
|
323
|
+
baseUrl: process.env.REPLANE_BASE_URL!,
|
|
324
|
+
sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
325
|
+
context: {
|
|
326
|
+
userId: user.id,
|
|
327
|
+
plan: user.subscription,
|
|
328
|
+
country: user.country,
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Client-side: override or extend context
|
|
333
|
+
<ReplaneNextProvider
|
|
334
|
+
snapshot={snapshot}
|
|
335
|
+
context={{
|
|
336
|
+
// Add client-specific context
|
|
337
|
+
browser: navigator.userAgent,
|
|
338
|
+
screenSize: window.innerWidth > 768 ? "desktop" : "mobile",
|
|
339
|
+
}}
|
|
340
|
+
>
|
|
341
|
+
{children}
|
|
342
|
+
</ReplaneNextProvider>
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Required Configs
|
|
346
|
+
|
|
347
|
+
Ensure specific configs are loaded before rendering:
|
|
348
|
+
|
|
349
|
+
```tsx
|
|
350
|
+
const snapshot = await getReplaneSnapshot({
|
|
351
|
+
baseUrl: process.env.REPLANE_BASE_URL!,
|
|
352
|
+
sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
353
|
+
required: ["critical-feature", "api-endpoint"],
|
|
354
|
+
// Or with default values:
|
|
355
|
+
required: {
|
|
356
|
+
"critical-feature": true,
|
|
357
|
+
"api-endpoint": "https://default.api.com",
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Error Handling with Fallbacks
|
|
363
|
+
|
|
364
|
+
Provide fallback values for resilience:
|
|
365
|
+
|
|
366
|
+
```tsx
|
|
367
|
+
const snapshot = await getReplaneSnapshot({
|
|
368
|
+
baseUrl: process.env.REPLANE_BASE_URL!,
|
|
369
|
+
sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
370
|
+
fallbacks: {
|
|
371
|
+
"feature-flag": false,
|
|
372
|
+
"max-items": 10,
|
|
373
|
+
"api-endpoint": "https://fallback.api.com",
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## TypeScript
|
|
379
|
+
|
|
380
|
+
Define your config types for full type safety:
|
|
381
|
+
|
|
382
|
+
```tsx
|
|
383
|
+
// types/replane.ts
|
|
384
|
+
export interface ReplaneConfigs {
|
|
385
|
+
"feature-flag": boolean;
|
|
386
|
+
"max-items": number;
|
|
387
|
+
"api-endpoint": string;
|
|
388
|
+
theme: {
|
|
389
|
+
primaryColor: string;
|
|
390
|
+
darkMode: boolean;
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
```tsx
|
|
396
|
+
// Use with generics
|
|
397
|
+
import type { ReplaneConfigs } from "./types/replane";
|
|
398
|
+
|
|
399
|
+
const snapshot = await getReplaneSnapshot<ReplaneConfigs>({
|
|
400
|
+
baseUrl: process.env.REPLANE_BASE_URL!,
|
|
401
|
+
sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// In components
|
|
405
|
+
const theme = useConfig<ReplaneConfigs["theme"]>("theme");
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
## Why SSR Hydration?
|
|
409
|
+
|
|
410
|
+
The snapshot pattern minimizes latency by:
|
|
411
|
+
|
|
412
|
+
1. **Server Fetch**: Configs are fetched during SSR (no client-side request delay)
|
|
413
|
+
2. **Instant Hydration**: Client instantly has all config values (no loading states)
|
|
414
|
+
3. **Optional Live Updates**: Real-time connection established after hydration
|
|
415
|
+
|
|
416
|
+
This means users see correct feature flags immediately without any loading states or flashes of incorrect content.
|
|
417
|
+
|
|
418
|
+
## Requirements
|
|
419
|
+
|
|
420
|
+
- Next.js >= 13.0.0
|
|
421
|
+
- React >= 18.0.0
|
|
422
|
+
- Node.js >= 18.0.0
|
|
423
|
+
|
|
424
|
+
## License
|
|
425
|
+
|
|
426
|
+
MIT
|
package/dist/index.d.cts
CHANGED
|
@@ -38,7 +38,7 @@ interface ReplaneConnectionOptions {
|
|
|
38
38
|
/**
|
|
39
39
|
* Props for ReplaneNextProvider.
|
|
40
40
|
*/
|
|
41
|
-
interface ReplaneNextProviderProps<T extends object =
|
|
41
|
+
interface ReplaneNextProviderProps<T extends object = object> {
|
|
42
42
|
/**
|
|
43
43
|
* Serializable snapshot from the server.
|
|
44
44
|
* Obtained from `getReplaneSnapshot()` in a Server Component or getServerSideProps.
|
|
@@ -118,7 +118,7 @@ interface ReplaneNextProviderProps<T extends object = any> {
|
|
|
118
118
|
* </ReplaneNextProvider>
|
|
119
119
|
* ```
|
|
120
120
|
*/
|
|
121
|
-
declare function ReplaneNextProvider<T extends object
|
|
121
|
+
declare function ReplaneNextProvider<T extends object = Record<string, unknown>>({
|
|
122
122
|
snapshot,
|
|
123
123
|
connection,
|
|
124
124
|
context,
|
|
@@ -128,7 +128,7 @@ declare function ReplaneNextProvider<T extends object>({
|
|
|
128
128
|
//#endregion
|
|
129
129
|
//#region src/script.d.ts
|
|
130
130
|
declare const REPLANE_SNAPSHOT_KEY = "__REPLANE_SNAPSHOT__";
|
|
131
|
-
type AnyConfig = Record<string,
|
|
131
|
+
type AnyConfig = Record<string, unknown>;
|
|
132
132
|
declare global {
|
|
133
133
|
interface Window {
|
|
134
134
|
[REPLANE_SNAPSHOT_KEY]?: ReplaneSnapshot$1<AnyConfig>;
|
|
@@ -167,7 +167,7 @@ declare global {
|
|
|
167
167
|
* }
|
|
168
168
|
* ```
|
|
169
169
|
*/
|
|
170
|
-
declare function getReplaneSnapshotScript<T extends object>(snapshot: ReplaneSnapshot$1<T>): string;
|
|
170
|
+
declare function getReplaneSnapshotScript<T extends object = AnyConfig>(snapshot: ReplaneSnapshot$1<T>): string;
|
|
171
171
|
/**
|
|
172
172
|
* Props for ReplaneScriptProvider.
|
|
173
173
|
*/
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts","../src/provider.tsx","../src/script.tsx"],"sourcesContent":[],"mappings":";;;;;;;;;;UAMiB,wBAAA;;AAAjB;
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts","../src/provider.tsx","../src/script.tsx"],"sourcesContent":[],"mappings":";;;;;;;;;;UAMiB,wBAAA;;AAAjB;AAmCA;;EAAyC,OAKb,EAAA,MAAA;EAAC;;;;EA2BR,MAAA,EAAA,MAAA;;;;ACpBrB;EAAmC,gBAAA,CAAA,EAAA,MAAA;EAAA;;;;EAG1B,YACP,CAAA,EAAA,MAAA;EAAQ;;;AACoB;;;;ACrD0B;AAG9B;AAEH,UF+BN,wBE/BM,CAAA,UAAA,MAAA,GAAA,MAAA,CAAA,CAAA;EAAA;;;;EAIE,QAAA,EFgCb,iBEhCa,CFgCG,CEhCH,CAAA;EAAA;AAqCzB;;;;;AAC2B;AAU3B;;;;;AAaqB;AAyBrB;;;;EAEU,UACR,CAAA,EFtCa,wBEsCb;EAAQ;;AACmB;;YFjCjB;YAEA;;;;;;;;;;;AAnEZ;AAmCA;;;;;;;AAgCqB;;;;ACpBrB;;;;;;;;;;AAK8B;;;;ACrD0B;AAG9B;AAEH;;;;;;AAIE;AAqCzB;;;;;AAC2B,iBDCX,mBCDW,CAAA,UAAA,MAAA,GDC4B,MCD5B,CAAA,MAAA,EAAA,OAAA,CAAA,CAAA,CAAA;EAAA,QAAA;EAAA,UAAA;EAAA,OAAA;EAAA;AAAA,CAAA,EDMxB,wBCNwB,CDMC,CCND,CAAA,CAAA,EDMG,oBAAA,CAAA,GAAA,CAAA,OCNH;AAU3B;;;cAtDM,oBAAA;KAED,SAAA,GAAY;;;IFJA,CEQZ,oBAAA,EFRoC,EEQZ,iBFRY,CEQI,SFRJ,CAAA;EAmCxB;;;;;;;AAgCI;;;;ACpBrB;;;;;;;;;;AAK8B;;;;ACrD0B;AAG9B;AAEH;;;;;;AAIE;AAqCzB;AAAwC,iBAAxB,wBAAwB,CAAA,UAAA,MAAA,GAAoB,SAApB,CAAA,CAAA,QAAA,EAC5B,iBAD4B,CACZ,CADY,CAAA,CAAA,EAAA,MAAA;;;;AACb,UAUV,0BAAA,CAVU;EAUV;;;;EAWK,UAEV,CAAA,EARG,wBAQH;EAAS;AAyBrB;;;EACY,QACV,CAAA,EA7BW,SA6BX;EAAQ,QACR,EA5BU,SA4BV;;;AAC2B;;;;;;;;;;;;;;;;;;;;;iBAJb,qBAAA;;;;GAIb,6BAA0B,kBAAA,CAAA,GAAA,CAAA"}
|
package/dist/index.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ interface ReplaneConnectionOptions {
|
|
|
38
38
|
/**
|
|
39
39
|
* Props for ReplaneNextProvider.
|
|
40
40
|
*/
|
|
41
|
-
interface ReplaneNextProviderProps<T extends object =
|
|
41
|
+
interface ReplaneNextProviderProps<T extends object = object> {
|
|
42
42
|
/**
|
|
43
43
|
* Serializable snapshot from the server.
|
|
44
44
|
* Obtained from `getReplaneSnapshot()` in a Server Component or getServerSideProps.
|
|
@@ -118,7 +118,7 @@ interface ReplaneNextProviderProps<T extends object = any> {
|
|
|
118
118
|
* </ReplaneNextProvider>
|
|
119
119
|
* ```
|
|
120
120
|
*/
|
|
121
|
-
declare function ReplaneNextProvider<T extends object
|
|
121
|
+
declare function ReplaneNextProvider<T extends object = Record<string, unknown>>({
|
|
122
122
|
snapshot,
|
|
123
123
|
connection,
|
|
124
124
|
context,
|
|
@@ -128,7 +128,7 @@ declare function ReplaneNextProvider<T extends object>({
|
|
|
128
128
|
//#endregion
|
|
129
129
|
//#region src/script.d.ts
|
|
130
130
|
declare const REPLANE_SNAPSHOT_KEY = "__REPLANE_SNAPSHOT__";
|
|
131
|
-
type AnyConfig = Record<string,
|
|
131
|
+
type AnyConfig = Record<string, unknown>;
|
|
132
132
|
declare global {
|
|
133
133
|
interface Window {
|
|
134
134
|
[REPLANE_SNAPSHOT_KEY]?: ReplaneSnapshot$1<AnyConfig>;
|
|
@@ -167,7 +167,7 @@ declare global {
|
|
|
167
167
|
* }
|
|
168
168
|
* ```
|
|
169
169
|
*/
|
|
170
|
-
declare function getReplaneSnapshotScript<T extends object>(snapshot: ReplaneSnapshot$1<T>): string;
|
|
170
|
+
declare function getReplaneSnapshotScript<T extends object = AnyConfig>(snapshot: ReplaneSnapshot$1<T>): string;
|
|
171
171
|
/**
|
|
172
172
|
* Props for ReplaneScriptProvider.
|
|
173
173
|
*/
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/provider.tsx","../src/script.tsx"],"sourcesContent":[],"mappings":";;;;;;;;;;UAMiB,wBAAA;;AAAjB;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/provider.tsx","../src/script.tsx"],"sourcesContent":[],"mappings":";;;;;;;;;;UAMiB,wBAAA;;AAAjB;AAmCA;;EAAyC,OAKb,EAAA,MAAA;EAAC;;;;EA2BR,MAAA,EAAA,MAAA;;;;ACpBrB;EAAmC,gBAAA,CAAA,EAAA,MAAA;EAAA;;;;EAG1B,YACP,CAAA,EAAA,MAAA;EAAQ;;;AACoB;;;;ACrD0B;AAG9B;AAEH,UF+BN,wBE/BM,CAAA,UAAA,MAAA,GAAA,MAAA,CAAA,CAAA;EAAA;;;;EAIE,QAAA,EFgCb,iBEhCa,CFgCG,CEhCH,CAAA;EAAA;AAqCzB;;;;;AAC2B;AAU3B;;;;;AAaqB;AAyBrB;;;;EAEU,UACR,CAAA,EFtCa,wBEsCb;EAAQ;;AACmB;;YFjCjB;YAEA;;;;;;;;;;;AAnEZ;AAmCA;;;;;;;AAgCqB;;;;ACpBrB;;;;;;;;;;AAK8B;;;;ACrD0B;AAG9B;AAEH;;;;;;AAIE;AAqCzB;;;;;AAC2B,iBDCX,mBCDW,CAAA,UAAA,MAAA,GDC4B,MCD5B,CAAA,MAAA,EAAA,OAAA,CAAA,CAAA,CAAA;EAAA,QAAA;EAAA,UAAA;EAAA,OAAA;EAAA;AAAA,CAAA,EDMxB,wBCNwB,CDMC,CCND,CAAA,CAAA,EDMG,oBAAA,CAAA,GAAA,CAAA,OCNH;AAU3B;;;cAtDM,oBAAA;KAED,SAAA,GAAY;;;IFJA,CEQZ,oBAAA,EFRoC,EEQZ,iBFRY,CEQI,SFRJ,CAAA;EAmCxB;;;;;;;AAgCI;;;;ACpBrB;;;;;;;;;;AAK8B;;;;ACrD0B;AAG9B;AAEH;;;;;;AAIE;AAqCzB;AAAwC,iBAAxB,wBAAwB,CAAA,UAAA,MAAA,GAAoB,SAApB,CAAA,CAAA,QAAA,EAC5B,iBAD4B,CACZ,CADY,CAAA,CAAA,EAAA,MAAA;;;;AACb,UAUV,0BAAA,CAVU;EAUV;;;;EAWK,UAEV,CAAA,EARG,wBAQH;EAAS;AAyBrB;;;EACY,QACV,CAAA,EA7BW,SA6BX;EAAQ,QACR,EA5BU,SA4BV;;;AAC2B;;;;;;;;;;;;;;;;;;;;;iBAJb,qBAAA;;;;GAIb,6BAA0B,kBAAA,CAAA,GAAA,CAAA"}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["snapshot: ReplaneSnapshot<T>"],"sources":["../src/provider.tsx","../src/script.tsx"],"sourcesContent":["\"use client\";\n\nimport { useMemo, useRef, useEffect } from \"react\";\nimport { ReplaneProvider } from \"@replanejs/react\";\nimport { restoreReplaneClient, type ReplaneClient } from \"@replanejs/sdk\";\nimport type { ReplaneNextProviderProps } from \"./types\";\n\n/**\n * Next.js-optimized Replane provider with SSR hydration support.\n *\n * This component:\n * 1. Restores the Replane client from a server-side snapshot instantly (no loading state)\n * 2. Optionally connects to Replane for real-time updates\n * 3. Preserves the client across re-renders for minimal latency\n *\n * @example\n * ```tsx\n * // app/layout.tsx\n * import { getReplaneSnapshot } from \"@replanejs/next/server\";\n * import { ReplaneNextProvider } from \"@replanejs/next\";\n *\n * export default async function RootLayout({ children }) {\n * const snapshot = await getReplaneSnapshot({\n * baseUrl: process.env.REPLANE_BASE_URL!,\n * sdkKey: process.env.REPLANE_SDK_KEY!,\n * });\n *\n * return (\n * <html>\n * <body>\n * <ReplaneNextProvider\n * snapshot={snapshot}\n * connection={{\n * baseUrl: process.env.NEXT_PUBLIC_REPLANE_BASE_URL!,\n * sdkKey: process.env.NEXT_PUBLIC_REPLANE_SDK_KEY!,\n * }}\n * >\n * {children}\n * </ReplaneNextProvider>\n * </body>\n * </html>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Without real-time updates (static snapshot only)\n * <ReplaneNextProvider snapshot={snapshot}>\n * {children}\n * </ReplaneNextProvider>\n * ```\n */\nexport function ReplaneNextProvider<T extends object>({\n snapshot,\n connection,\n context,\n children,\n}: ReplaneNextProviderProps<T>) {\n // Use a ref to store the client to preserve it across re-renders\n // This is important for minimizing latency - we don't want to recreate\n // the client on every render\n const clientRef = useRef<ReplaneClient<T> | null>(null);\n\n // Create a stable key for the client based on snapshot and connection\n // We only recreate the client if these change\n const clientKey = useMemo(() => {\n const snapshotKey = JSON.stringify(snapshot.configs.map((c) => c.name).sort());\n const connectionKey = connection ? `${connection.baseUrl}:${connection.sdkKey}` : \"no-connection\";\n const contextKey = context ? JSON.stringify(context) : \"no-context\";\n return `${snapshotKey}:${connectionKey}:${contextKey}`;\n }, [snapshot, connection, context]);\n\n // Memoize client creation\n const client = useMemo(() => {\n // If we have a cached client with the same key, reuse it\n if (clientRef.current) {\n // Check if we need to create a new client\n // For simplicity, we always create a new client if the key changes\n // This happens when snapshot or connection changes\n }\n\n const newClient = restoreReplaneClient<T>({\n snapshot,\n connection: connection\n ? {\n baseUrl: connection.baseUrl,\n sdkKey: connection.sdkKey,\n requestTimeoutMs: connection.requestTimeoutMs,\n retryDelayMs: connection.retryDelayMs,\n inactivityTimeoutMs: connection.inactivityTimeoutMs,\n }\n : undefined,\n context,\n });\n\n clientRef.current = newClient;\n return newClient;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [clientKey]);\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n if (clientRef.current) {\n clientRef.current.close();\n clientRef.current = null;\n }\n };\n }, []);\n\n return <ReplaneProvider client={client}>{children}</ReplaneProvider>;\n}\n","\"use client\";\n\nimport { useEffect, useState, useMemo, useRef, type ReactNode } from \"react\";\nimport { ReplaneProvider } from \"@replanejs/react\";\nimport { restoreReplaneClient, type ReplaneSnapshot, type ReplaneClient } from \"@replanejs/sdk\";\nimport type { ReplaneConnectionOptions } from \"./types\";\n\n// Global variable name for the snapshot\nconst REPLANE_SNAPSHOT_KEY = \"__REPLANE_SNAPSHOT__\";\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype AnyConfig = Record<string, any>;\n\ndeclare global {\n interface Window {\n [REPLANE_SNAPSHOT_KEY]?: ReplaneSnapshot<AnyConfig>;\n }\n}\n\n/**\n * Generate the script content for embedding the snapshot.\n *\n * Use this in a Server Component to embed the snapshot in the page:\n *\n * @example\n * ```tsx\n * // app/layout.tsx\n * import { getReplaneSnapshot } from \"@replanejs/next/server\";\n * import { getReplaneSnapshotScript, ReplaneScriptProvider } from \"@replanejs/next\";\n *\n * export default async function RootLayout({ children }) {\n * const snapshot = await getReplaneSnapshot({ ... });\n *\n * return (\n * <html>\n * <head>\n * <script\n * dangerouslySetInnerHTML={{\n * __html: getReplaneSnapshotScript(snapshot),\n * }}\n * />\n * </head>\n * <body>\n * <ReplaneScriptProvider connection={{ ... }}>\n * {children}\n * </ReplaneScriptProvider>\n * </body>\n * </html>\n * );\n * }\n * ```\n */\nexport function getReplaneSnapshotScript<T extends object>(snapshot: ReplaneSnapshot<T>): string {\n // Escape script closing tags in JSON to prevent XSS\n const json = JSON.stringify(snapshot).replace(/<\\/script>/gi, \"<\\\\/script>\");\n return `window.${REPLANE_SNAPSHOT_KEY}=${json};`;\n}\n\n/**\n * Props for ReplaneScriptProvider.\n */\nexport interface ReplaneScriptProviderProps {\n /**\n * Connection options for real-time updates.\n * If not provided, the client will only use the snapshot data (no live updates).\n */\n connection?: ReplaneConnectionOptions;\n\n /**\n * Fallback to render while waiting for the snapshot.\n * This should rarely be needed since the script is in the head.\n */\n fallback?: ReactNode;\n\n children: ReactNode;\n}\n\n/**\n * Provider that reads the snapshot from a script tag.\n *\n * Use this with `getReplaneSnapshotScript()` for an alternative hydration pattern\n * where the snapshot is embedded in a script tag instead of passed as a prop.\n *\n * This pattern can be useful for:\n * - Pages with heavy component trees where prop drilling is inconvenient\n * - Partial hydration scenarios\n * - When you want the snapshot to be available before React hydrates\n *\n * @example\n * ```tsx\n * // In app/layout.tsx (Server Component)\n * <script dangerouslySetInnerHTML={{ __html: getReplaneSnapshotScript(snapshot) }} />\n *\n * // In a client component\n * <ReplaneScriptProvider connection={{ baseUrl, sdkKey }}>\n * <App />\n * </ReplaneScriptProvider>\n * ```\n */\nexport function ReplaneScriptProvider({\n connection,\n fallback,\n children,\n}: ReplaneScriptProviderProps) {\n const [snapshot, setSnapshot] = useState<ReplaneSnapshot<AnyConfig> | null>(() => {\n // Try to get snapshot from window on initial render (SSR-safe)\n if (typeof window !== \"undefined\" && window[REPLANE_SNAPSHOT_KEY]) {\n return window[REPLANE_SNAPSHOT_KEY];\n }\n return null;\n });\n\n const clientRef = useRef<ReplaneClient<AnyConfig> | null>(null);\n\n // Check for snapshot on mount (in case the script runs after initial render)\n useEffect(() => {\n if (!snapshot && typeof window !== \"undefined\" && window[REPLANE_SNAPSHOT_KEY]) {\n setSnapshot(window[REPLANE_SNAPSHOT_KEY]);\n }\n }, [snapshot]);\n\n // Create client from snapshot\n const client = useMemo(() => {\n if (!snapshot) return null;\n\n const newClient = restoreReplaneClient({\n snapshot,\n connection: connection\n ? {\n baseUrl: connection.baseUrl,\n sdkKey: connection.sdkKey,\n requestTimeoutMs: connection.requestTimeoutMs,\n retryDelayMs: connection.retryDelayMs,\n inactivityTimeoutMs: connection.inactivityTimeoutMs,\n }\n : undefined,\n });\n\n clientRef.current = newClient;\n return newClient;\n }, [snapshot, connection]);\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n if (clientRef.current) {\n clientRef.current.close();\n clientRef.current = null;\n }\n };\n }, []);\n\n if (!client) {\n return <>{fallback ?? null}</>;\n }\n\n return <ReplaneProvider client={client}>{children}</ReplaneProvider>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqDA,SAAgB,oBAAsC,EACpD,UACA,YACA,SACA,UAC4B,EAAE;CAI9B,MAAM,YAAY,OAAgC,KAAK;CAIvD,MAAM,YAAY,QAAQ,MAAM;EAC9B,MAAM,cAAc,KAAK,UAAU,SAAS,QAAQ,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC;EAC9E,MAAM,gBAAgB,cAAc,EAAE,WAAW,QAAQ,GAAG,WAAW,OAAO,IAAI;EAClF,MAAM,aAAa,UAAU,KAAK,UAAU,QAAQ,GAAG;AACvD,UAAQ,EAAE,YAAY,GAAG,cAAc,GAAG,WAAW;CACtD,GAAE;EAAC;EAAU;EAAY;CAAQ,EAAC;CAGnC,MAAM,SAAS,QAAQ,MAAM;AAE3B,MAAI,UAAU,SAAS,CAItB;EAED,MAAM,YAAY,uBAAwB;GACxC;GACA,YAAY,aACR;IACE,SAAS,WAAW;IACpB,QAAQ,WAAW;IACnB,kBAAkB,WAAW;IAC7B,cAAc,WAAW;IACzB,qBAAqB,WAAW;GACjC;GAEL;EACD,EAAC;AAEF,YAAU,UAAU;AACpB,SAAO;CAER,GAAE,CAAC,SAAU,EAAC;AAGf,WAAU,MAAM;AACd,SAAO,MAAM;AACX,OAAI,UAAU,SAAS;AACrB,cAAU,QAAQ,OAAO;AACzB,cAAU,UAAU;GACrB;EACF;CACF,GAAE,CAAE,EAAC;AAEN,wBAAO,IAAC;EAAwB;EAAS;GAA2B;AACrE;;;;ACxGD,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4C7B,SAAgB,yBAA2CA,UAAsC;CAE/F,MAAM,OAAO,KAAK,UAAU,SAAS,CAAC,QAAQ,gBAAgB,cAAc;AAC5E,SAAQ,SAAS,qBAAqB,GAAG,KAAK;AAC/C;;;;;;;;;;;;;;;;;;;;;;;AA2CD,SAAgB,sBAAsB,EACpC,YACA,UACA,UAC2B,EAAE;CAC7B,MAAM,CAAC,UAAU,YAAY,GAAG,SAA4C,MAAM;AAEhF,aAAW,WAAW,eAAe,OAAO,sBAC1C,QAAO,OAAO;AAEhB,SAAO;CACR,EAAC;CAEF,MAAM,YAAY,OAAwC,KAAK;AAG/D,WAAU,MAAM;AACd,OAAK,mBAAmB,WAAW,eAAe,OAAO,sBACvD,aAAY,OAAO,sBAAsB;CAE5C,GAAE,CAAC,QAAS,EAAC;CAGd,MAAM,SAAS,QAAQ,MAAM;AAC3B,OAAK,SAAU,QAAO;EAEtB,MAAM,YAAY,uBAAqB;GACrC;GACA,YAAY,aACR;IACE,SAAS,WAAW;IACpB,QAAQ,WAAW;IACnB,kBAAkB,WAAW;IAC7B,cAAc,WAAW;IACzB,qBAAqB,WAAW;GACjC;EAEN,EAAC;AAEF,YAAU,UAAU;AACpB,SAAO;CACR,GAAE,CAAC,UAAU,UAAW,EAAC;AAG1B,WAAU,MAAM;AACd,SAAO,MAAM;AACX,OAAI,UAAU,SAAS;AACrB,cAAU,QAAQ,OAAO;AACzB,cAAU,UAAU;GACrB;EACF;CACF,GAAE,CAAE,EAAC;AAEN,MAAK,OACH,wBAAO,0BAAG,YAAY,OAAQ;AAGhC,wBAAO,IAAC;EAAwB;EAAS;GAA2B;AACrE"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["snapshot: ReplaneSnapshot<T>"],"sources":["../src/provider.tsx","../src/script.tsx"],"sourcesContent":["\"use client\";\n\nimport { useMemo, useRef, useEffect } from \"react\";\nimport { ReplaneProvider } from \"@replanejs/react\";\nimport { restoreReplaneClient, type ReplaneClient } from \"@replanejs/sdk\";\nimport type { ReplaneNextProviderProps } from \"./types\";\n\n/**\n * Next.js-optimized Replane provider with SSR hydration support.\n *\n * This component:\n * 1. Restores the Replane client from a server-side snapshot instantly (no loading state)\n * 2. Optionally connects to Replane for real-time updates\n * 3. Preserves the client across re-renders for minimal latency\n *\n * @example\n * ```tsx\n * // app/layout.tsx\n * import { getReplaneSnapshot } from \"@replanejs/next/server\";\n * import { ReplaneNextProvider } from \"@replanejs/next\";\n *\n * export default async function RootLayout({ children }) {\n * const snapshot = await getReplaneSnapshot({\n * baseUrl: process.env.REPLANE_BASE_URL!,\n * sdkKey: process.env.REPLANE_SDK_KEY!,\n * });\n *\n * return (\n * <html>\n * <body>\n * <ReplaneNextProvider\n * snapshot={snapshot}\n * connection={{\n * baseUrl: process.env.NEXT_PUBLIC_REPLANE_BASE_URL!,\n * sdkKey: process.env.NEXT_PUBLIC_REPLANE_SDK_KEY!,\n * }}\n * >\n * {children}\n * </ReplaneNextProvider>\n * </body>\n * </html>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Without real-time updates (static snapshot only)\n * <ReplaneNextProvider snapshot={snapshot}>\n * {children}\n * </ReplaneNextProvider>\n * ```\n */\nexport function ReplaneNextProvider<T extends object = Record<string, unknown>>({\n snapshot,\n connection,\n context,\n children,\n}: ReplaneNextProviderProps<T>) {\n // Use a ref to store the client to preserve it across re-renders\n // This is important for minimizing latency - we don't want to recreate\n // the client on every render\n const clientRef = useRef<ReplaneClient<T> | null>(null);\n\n // Create a stable key for the client based on snapshot and connection\n // We only recreate the client if these change\n const clientKey = useMemo(() => {\n const snapshotKey = JSON.stringify(snapshot.configs.map((c) => c.name).sort());\n const connectionKey = connection ? `${connection.baseUrl}:${connection.sdkKey}` : \"no-connection\";\n const contextKey = context ? JSON.stringify(context) : \"no-context\";\n return `${snapshotKey}:${connectionKey}:${contextKey}`;\n }, [snapshot, connection, context]);\n\n // Memoize client creation\n const client = useMemo(() => {\n // If we have a cached client with the same key, reuse it\n if (clientRef.current) {\n // Check if we need to create a new client\n // For simplicity, we always create a new client if the key changes\n // This happens when snapshot or connection changes\n }\n\n const newClient = restoreReplaneClient<T>({\n snapshot,\n connection: connection\n ? {\n baseUrl: connection.baseUrl,\n sdkKey: connection.sdkKey,\n requestTimeoutMs: connection.requestTimeoutMs,\n retryDelayMs: connection.retryDelayMs,\n inactivityTimeoutMs: connection.inactivityTimeoutMs,\n }\n : undefined,\n context,\n });\n\n clientRef.current = newClient;\n return newClient;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [clientKey]);\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n if (clientRef.current) {\n clientRef.current.close();\n clientRef.current = null;\n }\n };\n }, []);\n\n return <ReplaneProvider client={client}>{children}</ReplaneProvider>;\n}\n","\"use client\";\n\nimport { useEffect, useState, useMemo, useRef, type ReactNode } from \"react\";\nimport { ReplaneProvider } from \"@replanejs/react\";\nimport { restoreReplaneClient, type ReplaneSnapshot, type ReplaneClient } from \"@replanejs/sdk\";\nimport type { ReplaneConnectionOptions } from \"./types\";\n\n// Global variable name for the snapshot\nconst REPLANE_SNAPSHOT_KEY = \"__REPLANE_SNAPSHOT__\";\n\ntype AnyConfig = Record<string, unknown>;\n\ndeclare global {\n interface Window {\n [REPLANE_SNAPSHOT_KEY]?: ReplaneSnapshot<AnyConfig>;\n }\n}\n\n/**\n * Generate the script content for embedding the snapshot.\n *\n * Use this in a Server Component to embed the snapshot in the page:\n *\n * @example\n * ```tsx\n * // app/layout.tsx\n * import { getReplaneSnapshot } from \"@replanejs/next/server\";\n * import { getReplaneSnapshotScript, ReplaneScriptProvider } from \"@replanejs/next\";\n *\n * export default async function RootLayout({ children }) {\n * const snapshot = await getReplaneSnapshot({ ... });\n *\n * return (\n * <html>\n * <head>\n * <script\n * dangerouslySetInnerHTML={{\n * __html: getReplaneSnapshotScript(snapshot),\n * }}\n * />\n * </head>\n * <body>\n * <ReplaneScriptProvider connection={{ ... }}>\n * {children}\n * </ReplaneScriptProvider>\n * </body>\n * </html>\n * );\n * }\n * ```\n */\nexport function getReplaneSnapshotScript<T extends object = AnyConfig>(\n snapshot: ReplaneSnapshot<T>\n): string {\n // Escape script closing tags in JSON to prevent XSS\n const json = JSON.stringify(snapshot).replace(/<\\/script>/gi, \"<\\\\/script>\");\n return `window.${REPLANE_SNAPSHOT_KEY}=${json};`;\n}\n\n/**\n * Props for ReplaneScriptProvider.\n */\nexport interface ReplaneScriptProviderProps {\n /**\n * Connection options for real-time updates.\n * If not provided, the client will only use the snapshot data (no live updates).\n */\n connection?: ReplaneConnectionOptions;\n\n /**\n * Fallback to render while waiting for the snapshot.\n * This should rarely be needed since the script is in the head.\n */\n fallback?: ReactNode;\n\n children: ReactNode;\n}\n\n/**\n * Provider that reads the snapshot from a script tag.\n *\n * Use this with `getReplaneSnapshotScript()` for an alternative hydration pattern\n * where the snapshot is embedded in a script tag instead of passed as a prop.\n *\n * This pattern can be useful for:\n * - Pages with heavy component trees where prop drilling is inconvenient\n * - Partial hydration scenarios\n * - When you want the snapshot to be available before React hydrates\n *\n * @example\n * ```tsx\n * // In app/layout.tsx (Server Component)\n * <script dangerouslySetInnerHTML={{ __html: getReplaneSnapshotScript(snapshot) }} />\n *\n * // In a client component\n * <ReplaneScriptProvider connection={{ baseUrl, sdkKey }}>\n * <App />\n * </ReplaneScriptProvider>\n * ```\n */\nexport function ReplaneScriptProvider({\n connection,\n fallback,\n children,\n}: ReplaneScriptProviderProps) {\n const [snapshot, setSnapshot] = useState<ReplaneSnapshot<AnyConfig> | null>(() => {\n // Try to get snapshot from window on initial render (SSR-safe)\n if (typeof window !== \"undefined\" && window[REPLANE_SNAPSHOT_KEY]) {\n return window[REPLANE_SNAPSHOT_KEY];\n }\n return null;\n });\n\n const clientRef = useRef<ReplaneClient<AnyConfig> | null>(null);\n\n // Check for snapshot on mount (in case the script runs after initial render)\n useEffect(() => {\n if (!snapshot && typeof window !== \"undefined\" && window[REPLANE_SNAPSHOT_KEY]) {\n setSnapshot(window[REPLANE_SNAPSHOT_KEY]);\n }\n }, [snapshot]);\n\n // Create client from snapshot\n const client = useMemo(() => {\n if (!snapshot) return null;\n\n const newClient = restoreReplaneClient({\n snapshot,\n connection: connection\n ? {\n baseUrl: connection.baseUrl,\n sdkKey: connection.sdkKey,\n requestTimeoutMs: connection.requestTimeoutMs,\n retryDelayMs: connection.retryDelayMs,\n inactivityTimeoutMs: connection.inactivityTimeoutMs,\n }\n : undefined,\n });\n\n clientRef.current = newClient;\n return newClient;\n }, [snapshot, connection]);\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n if (clientRef.current) {\n clientRef.current.close();\n clientRef.current = null;\n }\n };\n }, []);\n\n if (!client) {\n return <>{fallback ?? null}</>;\n }\n\n return <ReplaneProvider client={client}>{children}</ReplaneProvider>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqDA,SAAgB,oBAAgE,EAC9E,UACA,YACA,SACA,UAC4B,EAAE;CAI9B,MAAM,YAAY,OAAgC,KAAK;CAIvD,MAAM,YAAY,QAAQ,MAAM;EAC9B,MAAM,cAAc,KAAK,UAAU,SAAS,QAAQ,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC;EAC9E,MAAM,gBAAgB,cAAc,EAAE,WAAW,QAAQ,GAAG,WAAW,OAAO,IAAI;EAClF,MAAM,aAAa,UAAU,KAAK,UAAU,QAAQ,GAAG;AACvD,UAAQ,EAAE,YAAY,GAAG,cAAc,GAAG,WAAW;CACtD,GAAE;EAAC;EAAU;EAAY;CAAQ,EAAC;CAGnC,MAAM,SAAS,QAAQ,MAAM;AAE3B,MAAI,UAAU,SAAS,CAItB;EAED,MAAM,YAAY,uBAAwB;GACxC;GACA,YAAY,aACR;IACE,SAAS,WAAW;IACpB,QAAQ,WAAW;IACnB,kBAAkB,WAAW;IAC7B,cAAc,WAAW;IACzB,qBAAqB,WAAW;GACjC;GAEL;EACD,EAAC;AAEF,YAAU,UAAU;AACpB,SAAO;CAER,GAAE,CAAC,SAAU,EAAC;AAGf,WAAU,MAAM;AACd,SAAO,MAAM;AACX,OAAI,UAAU,SAAS;AACrB,cAAU,QAAQ,OAAO;AACzB,cAAU,UAAU;GACrB;EACF;CACF,GAAE,CAAE,EAAC;AAEN,wBAAO,IAAC;EAAwB;EAAS;GAA2B;AACrE;;;;ACxGD,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2C7B,SAAgB,yBACdA,UACQ;CAER,MAAM,OAAO,KAAK,UAAU,SAAS,CAAC,QAAQ,gBAAgB,cAAc;AAC5E,SAAQ,SAAS,qBAAqB,GAAG,KAAK;AAC/C;;;;;;;;;;;;;;;;;;;;;;;AA2CD,SAAgB,sBAAsB,EACpC,YACA,UACA,UAC2B,EAAE;CAC7B,MAAM,CAAC,UAAU,YAAY,GAAG,SAA4C,MAAM;AAEhF,aAAW,WAAW,eAAe,OAAO,sBAC1C,QAAO,OAAO;AAEhB,SAAO;CACR,EAAC;CAEF,MAAM,YAAY,OAAwC,KAAK;AAG/D,WAAU,MAAM;AACd,OAAK,mBAAmB,WAAW,eAAe,OAAO,sBACvD,aAAY,OAAO,sBAAsB;CAE5C,GAAE,CAAC,QAAS,EAAC;CAGd,MAAM,SAAS,QAAQ,MAAM;AAC3B,OAAK,SAAU,QAAO;EAEtB,MAAM,YAAY,uBAAqB;GACrC;GACA,YAAY,aACR;IACE,SAAS,WAAW;IACpB,QAAQ,WAAW;IACnB,kBAAkB,WAAW;IAC7B,cAAc,WAAW;IACzB,qBAAqB,WAAW;GACjC;EAEN,EAAC;AAEF,YAAU,UAAU;AACpB,SAAO;CACR,GAAE,CAAC,UAAU,UAAW,EAAC;AAG1B,WAAU,MAAM;AACd,SAAO,MAAM;AACX,OAAI,UAAU,SAAS;AACrB,cAAU,QAAQ,OAAO;AACzB,cAAU,UAAU;GACrB;EACF;CACF,GAAE,CAAE,EAAC;AAEN,MAAK,OACH,wBAAO,0BAAG,YAAY,OAAQ;AAGhC,wBAAO,IAAC;EAAwB;EAAS;GAA2B;AACrE"}
|
package/dist/server.d.cts
CHANGED
|
@@ -2,7 +2,7 @@ import { ReplaneClientOptions, ReplaneContext, ReplaneSnapshot } from "@replanej
|
|
|
2
2
|
|
|
3
3
|
//#region src/server.d.ts
|
|
4
4
|
|
|
5
|
-
interface GetReplaneSnapshotOptions<T extends object
|
|
5
|
+
interface GetReplaneSnapshotOptions<T extends object = Record<string, unknown>> {
|
|
6
6
|
/**
|
|
7
7
|
* Base URL of the Replane instance (no trailing slash).
|
|
8
8
|
*/
|
package/dist/server.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.cts","names":[],"sources":["../src/server.ts"],"sourcesContent":[],"mappings":";;;;UAiDiB;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"server.d.cts","names":[],"sources":["../src/server.ts"],"sourcesContent":[],"mappings":";;;;UAiDiB,6CAA6C;;;;;;;;;;;;;;;;mBAkB3C;;;;;;;;;;;;;;;YAkBP;;;;aAKC,qBAAqB;;;;cAKpB,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAmCb,sCAAsC,kCACjD,0BAA0B,KAClC,QAAQ,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;iBA0CL;;;;mBAIH;;;YAGP;IACR,QAAQ"}
|
package/dist/server.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { ReplaneClientOptions, ReplaneContext, ReplaneSnapshot } from "@replanej
|
|
|
2
2
|
|
|
3
3
|
//#region src/server.d.ts
|
|
4
4
|
|
|
5
|
-
interface GetReplaneSnapshotOptions<T extends object
|
|
5
|
+
interface GetReplaneSnapshotOptions<T extends object = Record<string, unknown>> {
|
|
6
6
|
/**
|
|
7
7
|
* Base URL of the Replane instance (no trailing slash).
|
|
8
8
|
*/
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","names":[],"sources":["../src/server.ts"],"sourcesContent":[],"mappings":";;;;UAiDiB;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"server.d.ts","names":[],"sources":["../src/server.ts"],"sourcesContent":[],"mappings":";;;;UAiDiB,6CAA6C;;;;;;;;;;;;;;;;mBAkB3C;;;;;;;;;;;;;;;YAkBP;;;;aAKC,qBAAqB;;;;cAKpB,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAmCb,sCAAsC,kCACjD,0BAA0B,KAClC,QAAQ,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;iBA0CL;;;;mBAIH;;;YAGP;IACR,QAAQ"}
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","names":["options: GetReplaneSnapshotOptions<T>","options: {\n name: string;\n baseUrl: string;\n sdkKey: string;\n fetchFn?: typeof fetch;\n requestTimeoutMs?: number;\n initializationTimeoutMs?: number;\n context?: ReplaneContext;\n}"],"sources":["../src/server.ts"],"sourcesContent":["/**\n * Server-side utilities for Replane in Next.js.\n *\n * These functions are designed to be used in:\n * - React Server Components (RSC)\n * - getServerSideProps\n * - API routes\n * - Server Actions\n *\n * @example\n * ```tsx\n * // app/layout.tsx (Server Component)\n * import { getReplaneSnapshot } from \"@replanejs/next/server\";\n * import { ReplaneNextProvider } from \"@replanejs/next\";\n *\n * export default async function RootLayout({ children }) {\n * const snapshot = await getReplaneSnapshot({\n * baseUrl: process.env.REPLANE_BASE_URL!,\n * sdkKey: process.env.REPLANE_SDK_KEY!,\n * });\n *\n * return (\n * <html>\n * <body>\n * <ReplaneNextProvider\n * snapshot={snapshot}\n * connection={{\n * baseUrl: process.env.NEXT_PUBLIC_REPLANE_BASE_URL!,\n * sdkKey: process.env.NEXT_PUBLIC_REPLANE_SDK_KEY!,\n * }}\n * >\n * {children}\n * </ReplaneNextProvider>\n * </body>\n * </html>\n * );\n * }\n * ```\n */\n\nimport {\n createReplaneClient,\n type ReplaneClientOptions,\n type ReplaneSnapshot,\n type ReplaneContext,\n} from \"@replanejs/sdk\";\n\nexport type { ReplaneSnapshot, ReplaneContext };\n\nexport interface GetReplaneSnapshotOptions<T extends object
|
|
1
|
+
{"version":3,"file":"server.js","names":["options: GetReplaneSnapshotOptions<T>","options: {\n name: string;\n baseUrl: string;\n sdkKey: string;\n fetchFn?: typeof fetch;\n requestTimeoutMs?: number;\n initializationTimeoutMs?: number;\n context?: ReplaneContext;\n}"],"sources":["../src/server.ts"],"sourcesContent":["/**\n * Server-side utilities for Replane in Next.js.\n *\n * These functions are designed to be used in:\n * - React Server Components (RSC)\n * - getServerSideProps\n * - API routes\n * - Server Actions\n *\n * @example\n * ```tsx\n * // app/layout.tsx (Server Component)\n * import { getReplaneSnapshot } from \"@replanejs/next/server\";\n * import { ReplaneNextProvider } from \"@replanejs/next\";\n *\n * export default async function RootLayout({ children }) {\n * const snapshot = await getReplaneSnapshot({\n * baseUrl: process.env.REPLANE_BASE_URL!,\n * sdkKey: process.env.REPLANE_SDK_KEY!,\n * });\n *\n * return (\n * <html>\n * <body>\n * <ReplaneNextProvider\n * snapshot={snapshot}\n * connection={{\n * baseUrl: process.env.NEXT_PUBLIC_REPLANE_BASE_URL!,\n * sdkKey: process.env.NEXT_PUBLIC_REPLANE_SDK_KEY!,\n * }}\n * >\n * {children}\n * </ReplaneNextProvider>\n * </body>\n * </html>\n * );\n * }\n * ```\n */\n\nimport {\n createReplaneClient,\n type ReplaneClientOptions,\n type ReplaneSnapshot,\n type ReplaneContext,\n} from \"@replanejs/sdk\";\n\nexport type { ReplaneSnapshot, ReplaneContext };\n\nexport interface GetReplaneSnapshotOptions<T extends object = Record<string, unknown>> {\n /**\n * Base URL of the Replane instance (no trailing slash).\n */\n baseUrl: string;\n\n /**\n * Project SDK key for authorization.\n */\n sdkKey: string;\n\n /**\n * Custom fetch implementation (useful for caching configuration).\n * In Next.js, you can use the fetch options for caching:\n * ```ts\n * fetchFn: (url, init) => fetch(url, { ...init, next: { revalidate: 60 } })\n * ```\n */\n fetchFn?: typeof fetch;\n\n /**\n * Optional timeout in ms for the request.\n * @default 2000\n */\n requestTimeoutMs?: number;\n\n /**\n * Optional timeout in ms for initialization.\n * @default 5000\n */\n initializationTimeoutMs?: number;\n\n /**\n * Default context used for override evaluation.\n * This context will be included in the snapshot.\n */\n context?: ReplaneContext;\n\n /**\n * Config names that must be present before the client is ready.\n */\n required?: ReplaneClientOptions<T>[\"required\"];\n\n /**\n * Fallback values to use if configs are not available.\n */\n fallbacks?: ReplaneClientOptions<T>[\"fallbacks\"];\n}\n\n/**\n * Fetch configs from Replane and return a serializable snapshot.\n *\n * This is the primary server-side function for Next.js integration.\n * It creates a Replane client, waits for initialization, captures a snapshot,\n * and closes the client.\n *\n * The snapshot can be passed to `ReplaneNextProvider` for client-side hydration.\n *\n * @example\n * ```tsx\n * // In a Server Component\n * const snapshot = await getReplaneSnapshot({\n * baseUrl: process.env.REPLANE_BASE_URL!,\n * sdkKey: process.env.REPLANE_SDK_KEY!,\n * context: { userId: user.id },\n * });\n * ```\n *\n * @example\n * ```tsx\n * // With Next.js caching\n * const snapshot = await getReplaneSnapshot({\n * baseUrl: process.env.REPLANE_BASE_URL!,\n * sdkKey: process.env.REPLANE_SDK_KEY!,\n * fetchFn: (url, init) => fetch(url, {\n * ...init,\n * next: { revalidate: 60 }, // Revalidate every 60 seconds\n * }),\n * });\n * ```\n */\nexport async function getReplaneSnapshot<T extends object = Record<string, unknown>>(\n options: GetReplaneSnapshotOptions<T>\n): Promise<ReplaneSnapshot<T>> {\n const client = await createReplaneClient<T>({\n baseUrl: options.baseUrl,\n sdkKey: options.sdkKey,\n fetchFn: options.fetchFn,\n requestTimeoutMs: options.requestTimeoutMs,\n initializationTimeoutMs: options.initializationTimeoutMs,\n context: options.context,\n required: options.required,\n fallbacks: options.fallbacks,\n });\n\n try {\n return client.getSnapshot();\n } finally {\n client.close();\n }\n}\n\n/**\n * Get a config value directly on the server.\n *\n * This is useful when you need a single config value in a Server Component\n * or server-side code and don't need the full snapshot/hydration flow.\n *\n * Note: This creates a new client for each call, so prefer `getReplaneSnapshot`\n * if you need multiple configs or client-side hydration.\n *\n * @example\n * ```tsx\n * // In a Server Component\n * const maintenanceMode = await getConfig<boolean>({\n * name: \"maintenance-mode\",\n * baseUrl: process.env.REPLANE_BASE_URL!,\n * sdkKey: process.env.REPLANE_SDK_KEY!,\n * });\n *\n * if (maintenanceMode) {\n * return <MaintenancePage />;\n * }\n * ```\n */\nexport async function getConfig<T>(options: {\n name: string;\n baseUrl: string;\n sdkKey: string;\n fetchFn?: typeof fetch;\n requestTimeoutMs?: number;\n initializationTimeoutMs?: number;\n context?: ReplaneContext;\n}): Promise<T> {\n const client = await createReplaneClient({\n baseUrl: options.baseUrl,\n sdkKey: options.sdkKey,\n fetchFn: options.fetchFn,\n requestTimeoutMs: options.requestTimeoutMs,\n initializationTimeoutMs: options.initializationTimeoutMs,\n context: options.context,\n });\n\n try {\n return client.get(options.name, { context: options.context }) as T;\n } finally {\n client.close();\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkIA,eAAsB,mBACpBA,SAC6B;CAC7B,MAAM,SAAS,MAAM,oBAAuB;EAC1C,SAAS,QAAQ;EACjB,QAAQ,QAAQ;EAChB,SAAS,QAAQ;EACjB,kBAAkB,QAAQ;EAC1B,yBAAyB,QAAQ;EACjC,SAAS,QAAQ;EACjB,UAAU,QAAQ;EAClB,WAAW,QAAQ;CACpB,EAAC;AAEF,KAAI;AACF,SAAO,OAAO,aAAa;CAC5B,UAAS;AACR,SAAO,OAAO;CACf;AACF;;;;;;;;;;;;;;;;;;;;;;;;AAyBD,eAAsB,UAAaC,SAQpB;CACb,MAAM,SAAS,MAAM,oBAAoB;EACvC,SAAS,QAAQ;EACjB,QAAQ,QAAQ;EAChB,SAAS,QAAQ;EACjB,kBAAkB,QAAQ;EAC1B,yBAAyB,QAAQ;EACjC,SAAS,QAAQ;CAClB,EAAC;AAEF,KAAI;AACF,SAAO,OAAO,IAAI,QAAQ,MAAM,EAAE,SAAS,QAAQ,QAAS,EAAC;CAC9D,UAAS;AACR,SAAO,OAAO;CACf;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@replanejs/next",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Next.js SDK for Replane - feature flags and remote configuration with SSR support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"react": ">=18.0.0"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@replanejs/
|
|
54
|
-
"@replanejs/
|
|
53
|
+
"@replanejs/react": "^0.7.1",
|
|
54
|
+
"@replanejs/sdk": "^0.7.1"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@testing-library/jest-dom": "^6.9.1",
|