flagmint-monorepo 1.0.11
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/.github/workflows/publish.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +609 -0
- package/package.json +13 -0
- package/packages/react-flagmint/package.json +43 -0
- package/packages/react-flagmint/src/hooks/index.ts +66 -0
- package/packages/react-flagmint/src/index.ts +1 -0
- package/packages/react-flagmint/src/providers/FlagmintProvider.tsx +113 -0
- package/packages/react-flagmint/src/store/store.ts +20 -0
- package/packages/react-flagmint/src/types.ts +46 -0
- package/packages/react-flagmint/tsconfig.json +16 -0
- package/packages/react-flagmint/tsup.config.ts +17 -0
- package/pnpm-workspace.yaml +3 -0
- package/tsconfig.base.json +24 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Publish Package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published] # add "prereleased" if you publish pre-releases too
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
|
|
14
|
+
- uses: pnpm/action-setup@v3
|
|
15
|
+
with:
|
|
16
|
+
version: 9
|
|
17
|
+
run_install: false
|
|
18
|
+
|
|
19
|
+
- uses: actions/setup-node@v4
|
|
20
|
+
with:
|
|
21
|
+
node-version: 20
|
|
22
|
+
registry-url: https://registry.npmjs.org
|
|
23
|
+
cache: pnpm
|
|
24
|
+
|
|
25
|
+
- run: pnpm -v # sanity check
|
|
26
|
+
- run: pnpm install --frozen-lockfile
|
|
27
|
+
- run: pnpm build
|
|
28
|
+
- run: pnpm test || true
|
|
29
|
+
- run: npm publish --access public
|
|
30
|
+
env:
|
|
31
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Israel Edet
|
|
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
ADDED
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
# flagmint-react-sdk
|
|
2
|
+
|
|
3
|
+
A React wrapper for [flagmint-js-sdk](https://www.npmjs.com/package/flagmint-js-sdk) that provides React-specific hooks and context with fine-grained reactivity powered by Zustand.
|
|
4
|
+
|
|
5
|
+
## ✨ Features
|
|
6
|
+
|
|
7
|
+
- 🎯 **Fine-grained reactivity** - Only components using specific flags re-render when those flags change
|
|
8
|
+
- 🚀 **SSR-safe** - No global state leakage between server requests
|
|
9
|
+
- 📦 **TypeScript support** - Full type safety with generics for flag values and context
|
|
10
|
+
- 🔀 **Dual export strategy** - Separate server-safe types and client-only hooks
|
|
11
|
+
- ⚡ **Auto-initialization** - Direct integration with FlagClient from flagmint-js-sdk
|
|
12
|
+
- 🔐 **Deferred initialization** - Perfect for authentication flows
|
|
13
|
+
- 📱 **Framework agnostic** - Works with Next.js, Remix, Vite, and more
|
|
14
|
+
|
|
15
|
+
## 📦 Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install react-flagmint flagmint-js-sdk
|
|
19
|
+
# or
|
|
20
|
+
pnpm add react-flagmint flagmint-js-sdk
|
|
21
|
+
# or
|
|
22
|
+
yarn add react-flagmint flagmint-js-sdk
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 🚀 Quick Start
|
|
26
|
+
|
|
27
|
+
### 1. Basic Setup
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
// app/providers.tsx (Next.js App Router)
|
|
31
|
+
'use client'
|
|
32
|
+
import { FlagmintProvider } from 'react-flagmint/client'
|
|
33
|
+
|
|
34
|
+
export default function Providers({ children }: { children: React.ReactNode }) {
|
|
35
|
+
return (
|
|
36
|
+
<FlagmintProvider
|
|
37
|
+
options={{
|
|
38
|
+
apiKey: process.env.NEXT_PUBLIC_FLAGMINT_API_KEY!,
|
|
39
|
+
transportMode: 'websocket',
|
|
40
|
+
context: {
|
|
41
|
+
userId: 'user-123',
|
|
42
|
+
plan: 'premium'
|
|
43
|
+
}
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
{children}
|
|
47
|
+
</FlagmintProvider>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
// app/layout.tsx
|
|
54
|
+
import Providers from './providers'
|
|
55
|
+
|
|
56
|
+
export default function RootLayout({
|
|
57
|
+
children,
|
|
58
|
+
}: {
|
|
59
|
+
children: React.ReactNode
|
|
60
|
+
}) {
|
|
61
|
+
return (
|
|
62
|
+
<html>
|
|
63
|
+
<body>
|
|
64
|
+
<Providers>
|
|
65
|
+
{children}
|
|
66
|
+
</Providers>
|
|
67
|
+
</body>
|
|
68
|
+
</html>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 2. Using Flags in Components
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
// components/FeatureComponent.tsx
|
|
77
|
+
'use client'
|
|
78
|
+
import { useFlag, useFlags, useFlagmintReady } from 'react-flagmint/client'
|
|
79
|
+
|
|
80
|
+
export default function FeatureComponent() {
|
|
81
|
+
const showNewFeature = useFlag('new-dashboard', false)
|
|
82
|
+
const userPlan = useFlag('user-plan', 'free')
|
|
83
|
+
const isReady = useFlagmintReady()
|
|
84
|
+
|
|
85
|
+
if (!isReady) {
|
|
86
|
+
return <div>Loading flags...</div>
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div>
|
|
91
|
+
{showNewFeature && <NewDashboard />}
|
|
92
|
+
<div>Current plan: {userPlan}</div>
|
|
93
|
+
</div>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## 📚 API Reference
|
|
99
|
+
|
|
100
|
+
### FlagmintProvider
|
|
101
|
+
|
|
102
|
+
The provider component that initializes the FlagClient and provides flag context to your app.
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
<FlagmintProvider
|
|
106
|
+
options={FlagClientOptions}
|
|
107
|
+
initialFlags={{}}
|
|
108
|
+
deferInitialization={false}
|
|
109
|
+
>
|
|
110
|
+
{children}
|
|
111
|
+
</FlagmintProvider>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### Props
|
|
115
|
+
|
|
116
|
+
| Prop | Type | Default | Description |
|
|
117
|
+
|------|------|---------|-------------|
|
|
118
|
+
| `options` | `FlagClientOptions` | Required | Configuration for the FlagClient |
|
|
119
|
+
| `initialFlags` | `Flags` | `{}` | Initial flag values for SSR/hydration |
|
|
120
|
+
| `deferInitialization` | `boolean` | `false` | If true, wait for manual initialization |
|
|
121
|
+
|
|
122
|
+
#### FlagClientOptions
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
interface FlagClientOptions<C extends Record<string, any> = Record<string, any>> {
|
|
126
|
+
apiKey: string
|
|
127
|
+
context?: C
|
|
128
|
+
transportMode?: 'auto' | 'websocket' | 'long-polling'
|
|
129
|
+
enableOfflineCache?: boolean
|
|
130
|
+
persistContext?: boolean
|
|
131
|
+
onError?: (error: Error) => void
|
|
132
|
+
previewMode?: boolean
|
|
133
|
+
rawFlags?: Record<string, FlagValue>
|
|
134
|
+
deferInitialization?: boolean
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Hooks
|
|
139
|
+
|
|
140
|
+
#### useFlag(key, fallback?)
|
|
141
|
+
|
|
142
|
+
Get a specific flag value with fine-grained reactivity.
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
const showFeature = useFlag('feature-name', false)
|
|
146
|
+
const userTier = useFlag('user-tier', 'free')
|
|
147
|
+
const config = useFlag('app-config', { theme: 'light' })
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Parameters:**
|
|
151
|
+
- `key: string` - The flag key
|
|
152
|
+
- `fallback?: T` - Default value if flag is not found
|
|
153
|
+
|
|
154
|
+
**Returns:** The flag value or fallback
|
|
155
|
+
|
|
156
|
+
#### useFlags()
|
|
157
|
+
|
|
158
|
+
Get all currently loaded flags.
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
const allFlags = useFlags()
|
|
162
|
+
console.log(allFlags) // { 'feature-a': true, 'user-tier': 'pro' }
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Returns:** Object containing all loaded flags
|
|
166
|
+
|
|
167
|
+
#### useFlagmint()
|
|
168
|
+
|
|
169
|
+
Get access to the FlagClient instance and utilities.
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
const { client, isReady, isInitialized, updateContext } = useFlagmint()
|
|
173
|
+
|
|
174
|
+
// Update user context
|
|
175
|
+
await updateContext({ userId: 'new-user', plan: 'enterprise' })
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Returns:**
|
|
179
|
+
```tsx
|
|
180
|
+
{
|
|
181
|
+
client: FlagClient | null
|
|
182
|
+
isReady: boolean
|
|
183
|
+
isInitialized: boolean
|
|
184
|
+
updateContext: (context: Record<string, any>) => Promise<void>
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
#### useFlagmintReady()
|
|
189
|
+
|
|
190
|
+
Check if the FlagClient is ready to serve flags.
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
const isReady = useFlagmintReady()
|
|
194
|
+
|
|
195
|
+
if (!isReady) {
|
|
196
|
+
return <LoadingSpinner />
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
#### useFlagClient()
|
|
201
|
+
|
|
202
|
+
Get direct access to the FlagClient instance.
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
const client = useFlagClient<MyFlagTypes>()
|
|
206
|
+
const specificFlag = client?.getFlag('feature-x', false)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## 🔧 Usage Patterns
|
|
210
|
+
|
|
211
|
+
### 1. Auto-initialization (Default)
|
|
212
|
+
|
|
213
|
+
Best when user context is available immediately:
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
function App() {
|
|
217
|
+
return (
|
|
218
|
+
<FlagmintProvider
|
|
219
|
+
options={{
|
|
220
|
+
apiKey: 'your-api-key',
|
|
221
|
+
context: {
|
|
222
|
+
userId: getCurrentUser().id,
|
|
223
|
+
plan: getCurrentUser().plan
|
|
224
|
+
}
|
|
225
|
+
}}
|
|
226
|
+
>
|
|
227
|
+
<Dashboard />
|
|
228
|
+
</FlagmintProvider>
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### 2. Deferred Initialization
|
|
234
|
+
|
|
235
|
+
Perfect for authentication flows:
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
// Provider with deferred initialization
|
|
239
|
+
<FlagmintProvider
|
|
240
|
+
options={{
|
|
241
|
+
apiKey: 'your-api-key',
|
|
242
|
+
context: {} // Empty initially
|
|
243
|
+
}}
|
|
244
|
+
deferInitialization={true}
|
|
245
|
+
>
|
|
246
|
+
<App />
|
|
247
|
+
</FlagmintProvider>
|
|
248
|
+
|
|
249
|
+
// Login component
|
|
250
|
+
function LoginPage() {
|
|
251
|
+
const { updateContext } = useFlagmint()
|
|
252
|
+
|
|
253
|
+
const handleLogin = async (user) => {
|
|
254
|
+
// First update context with user info
|
|
255
|
+
await updateContext({
|
|
256
|
+
userId: user.id,
|
|
257
|
+
plan: user.plan,
|
|
258
|
+
locale: user.locale
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
// Flags are now available with user context
|
|
262
|
+
navigate('/dashboard')
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return <LoginForm onLogin={handleLogin} />
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### 3. TypeScript Usage
|
|
270
|
+
|
|
271
|
+
Define your flag types for better type safety:
|
|
272
|
+
|
|
273
|
+
```tsx
|
|
274
|
+
// types/flags.ts
|
|
275
|
+
export interface AppFlags {
|
|
276
|
+
'show-beta-feature': boolean
|
|
277
|
+
'user-plan': 'free' | 'pro' | 'enterprise'
|
|
278
|
+
'theme-config': {
|
|
279
|
+
primaryColor: string
|
|
280
|
+
darkMode: boolean
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export interface UserContext {
|
|
285
|
+
userId: string
|
|
286
|
+
plan: string
|
|
287
|
+
locale: string
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// components/TypedComponent.tsx
|
|
291
|
+
import { useFlag } from 'react-flagmint/client'
|
|
292
|
+
import type { AppFlags } from '../types/flags'
|
|
293
|
+
|
|
294
|
+
export default function TypedComponent() {
|
|
295
|
+
// TypeScript knows this returns boolean
|
|
296
|
+
const showBeta = useFlag<AppFlags['show-beta-feature']>('show-beta-feature', false)
|
|
297
|
+
|
|
298
|
+
// TypeScript knows this returns the union type
|
|
299
|
+
const plan = useFlag<AppFlags['user-plan']>('user-plan', 'free')
|
|
300
|
+
|
|
301
|
+
return (
|
|
302
|
+
<div>
|
|
303
|
+
{showBeta && <BetaFeature />}
|
|
304
|
+
<div>Plan: {plan}</div>
|
|
305
|
+
</div>
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### 4. Server-Side Rendering (SSR)
|
|
311
|
+
|
|
312
|
+
#### Next.js App Router
|
|
313
|
+
|
|
314
|
+
```tsx
|
|
315
|
+
// app/providers.tsx
|
|
316
|
+
'use client'
|
|
317
|
+
import { FlagmintProvider } from 'react-flagmint/client'
|
|
318
|
+
|
|
319
|
+
export default function Providers({ children, initialFlags = {} }) {
|
|
320
|
+
return (
|
|
321
|
+
<FlagmintProvider
|
|
322
|
+
options={{
|
|
323
|
+
apiKey: process.env.NEXT_PUBLIC_FLAGMINT_API_KEY!,
|
|
324
|
+
transportMode: 'websocket'
|
|
325
|
+
}}
|
|
326
|
+
initialFlags={initialFlags}
|
|
327
|
+
>
|
|
328
|
+
{children}
|
|
329
|
+
</FlagmintProvider>
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// app/page.tsx
|
|
334
|
+
import { FlagClient } from 'flagmint-js-sdk'
|
|
335
|
+
import Providers from './providers'
|
|
336
|
+
import ClientComponent from './ClientComponent'
|
|
337
|
+
|
|
338
|
+
export default async function Page() {
|
|
339
|
+
// Optionally preload flags on server
|
|
340
|
+
let initialFlags = {}
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
const serverClient = new FlagClient({
|
|
344
|
+
apiKey: process.env.FLAGMINT_API_KEY!,
|
|
345
|
+
context: { server: true }
|
|
346
|
+
})
|
|
347
|
+
await serverClient.ready()
|
|
348
|
+
initialFlags = await serverClient.getFlags() // If this method exists
|
|
349
|
+
} catch (error) {
|
|
350
|
+
console.warn('Failed to load initial flags:', error)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return (
|
|
354
|
+
<Providers initialFlags={initialFlags}>
|
|
355
|
+
<ClientComponent />
|
|
356
|
+
</Providers>
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### 5. Context Updates
|
|
362
|
+
|
|
363
|
+
Update user context dynamically:
|
|
364
|
+
|
|
365
|
+
```tsx
|
|
366
|
+
function UserSettings() {
|
|
367
|
+
const { updateContext } = useFlagmint()
|
|
368
|
+
const currentTheme = useFlag('user-theme', 'light')
|
|
369
|
+
|
|
370
|
+
const handleThemeChange = async (newTheme: string) => {
|
|
371
|
+
// Update context - flags will be re-evaluated
|
|
372
|
+
await updateContext({ theme: newTheme })
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const handlePlanUpgrade = async (newPlan: string) => {
|
|
376
|
+
await updateContext({ plan: newPlan })
|
|
377
|
+
// Component will re-render with new plan-based flags
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return (
|
|
381
|
+
<div>
|
|
382
|
+
<ThemeSelector onChange={handleThemeChange} />
|
|
383
|
+
<PlanUpgrade onUpgrade={handlePlanUpgrade} />
|
|
384
|
+
</div>
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### 6. Loading States
|
|
390
|
+
|
|
391
|
+
Handle loading states gracefully:
|
|
392
|
+
|
|
393
|
+
```tsx
|
|
394
|
+
function FeaturePage() {
|
|
395
|
+
const isReady = useFlagmintReady()
|
|
396
|
+
const showPremiumFeature = useFlag('premium-feature', false)
|
|
397
|
+
|
|
398
|
+
if (!isReady) {
|
|
399
|
+
return (
|
|
400
|
+
<div className="loading-container">
|
|
401
|
+
<Spinner />
|
|
402
|
+
<p>Loading personalized features...</p>
|
|
403
|
+
</div>
|
|
404
|
+
)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return (
|
|
408
|
+
<div>
|
|
409
|
+
<h1>Features</h1>
|
|
410
|
+
{showPremiumFeature ? (
|
|
411
|
+
<PremiumFeature />
|
|
412
|
+
) : (
|
|
413
|
+
<UpgradeBanner />
|
|
414
|
+
)}
|
|
415
|
+
</div>
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### 7. Error Handling
|
|
421
|
+
|
|
422
|
+
```tsx
|
|
423
|
+
<FlagmintProvider
|
|
424
|
+
options={{
|
|
425
|
+
apiKey: 'your-api-key',
|
|
426
|
+
onError: (error) => {
|
|
427
|
+
console.error('Flagmint error:', error)
|
|
428
|
+
// Send to error reporting service
|
|
429
|
+
errorReporting.captureException(error)
|
|
430
|
+
}
|
|
431
|
+
}}
|
|
432
|
+
>
|
|
433
|
+
<App />
|
|
434
|
+
</FlagmintProvider>
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
## 🏗️ Framework Examples
|
|
438
|
+
|
|
439
|
+
### Next.js (App Router)
|
|
440
|
+
|
|
441
|
+
```tsx
|
|
442
|
+
// app/layout.tsx
|
|
443
|
+
import Providers from './providers'
|
|
444
|
+
|
|
445
|
+
export default function RootLayout({ children }) {
|
|
446
|
+
return (
|
|
447
|
+
<html lang="en">
|
|
448
|
+
<body>
|
|
449
|
+
<Providers>{children}</Providers>
|
|
450
|
+
</body>
|
|
451
|
+
</html>
|
|
452
|
+
)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// app/providers.tsx
|
|
456
|
+
'use client'
|
|
457
|
+
import { FlagmintProvider } from 'react-flagmint/client'
|
|
458
|
+
|
|
459
|
+
export default function Providers({ children }) {
|
|
460
|
+
return (
|
|
461
|
+
<FlagmintProvider
|
|
462
|
+
options={{
|
|
463
|
+
apiKey: process.env.NEXT_PUBLIC_FLAGMINT_API_KEY!,
|
|
464
|
+
transportMode: 'long-polling'
|
|
465
|
+
}}
|
|
466
|
+
>
|
|
467
|
+
{children}
|
|
468
|
+
</FlagmintProvider>
|
|
469
|
+
)
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### Vite + React
|
|
474
|
+
|
|
475
|
+
```tsx
|
|
476
|
+
// src/main.tsx
|
|
477
|
+
import React from 'react'
|
|
478
|
+
import ReactDOM from 'react-dom/client'
|
|
479
|
+
import { FlagmintProvider } from 'react-flagmint/client'
|
|
480
|
+
import App from './App'
|
|
481
|
+
|
|
482
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
483
|
+
<React.StrictMode>
|
|
484
|
+
<FlagmintProvider
|
|
485
|
+
options={{
|
|
486
|
+
apiKey: import.meta.env.VITE_FLAGMINT_API_KEY,
|
|
487
|
+
transportMode: 'websocket'
|
|
488
|
+
}}
|
|
489
|
+
>
|
|
490
|
+
<App />
|
|
491
|
+
</FlagmintProvider>
|
|
492
|
+
</React.StrictMode>
|
|
493
|
+
)
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### Remix
|
|
497
|
+
|
|
498
|
+
```tsx
|
|
499
|
+
// app/root.tsx
|
|
500
|
+
import { FlagmintProvider } from 'react-flagmint/client'
|
|
501
|
+
|
|
502
|
+
export default function App() {
|
|
503
|
+
return (
|
|
504
|
+
<html>
|
|
505
|
+
<head>
|
|
506
|
+
<Meta />
|
|
507
|
+
<Links />
|
|
508
|
+
</head>
|
|
509
|
+
<body>
|
|
510
|
+
<FlagmintProvider
|
|
511
|
+
options={{
|
|
512
|
+
apiKey: process.env.FLAGMINT_API_KEY!,
|
|
513
|
+
transportMode: 'long-polling'
|
|
514
|
+
}}
|
|
515
|
+
>
|
|
516
|
+
<Outlet />
|
|
517
|
+
</FlagmintProvider>
|
|
518
|
+
<Scripts />
|
|
519
|
+
</body>
|
|
520
|
+
</html>
|
|
521
|
+
)
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
## ⚡ Performance
|
|
526
|
+
|
|
527
|
+
### Fine-grained Reactivity
|
|
528
|
+
|
|
529
|
+
Only components that use specific flags will re-render when those flags change:
|
|
530
|
+
|
|
531
|
+
```tsx
|
|
532
|
+
// ✅ Good: Only re-renders when 'feature-a' changes
|
|
533
|
+
function ComponentA() {
|
|
534
|
+
const featureA = useFlag('feature-a', false)
|
|
535
|
+
return <div>{featureA && <FeatureA />}</div>
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// ✅ Good: Only re-renders when 'feature-b' changes
|
|
539
|
+
function ComponentB() {
|
|
540
|
+
const featureB = useFlag('feature-b', false)
|
|
541
|
+
return <div>{featureB && <FeatureB />}</div>
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// ❌ Avoid: Re-renders when ANY flag changes
|
|
545
|
+
function ComponentAll() {
|
|
546
|
+
const allFlags = useFlags()
|
|
547
|
+
return <div>{allFlags['feature-a'] && <FeatureA />}</div>
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### Bundle Size
|
|
552
|
+
|
|
553
|
+
- **react-flagmint**: ~3KB gzipped
|
|
554
|
+
- **zustand**: ~2KB gzipped
|
|
555
|
+
- **Total overhead**: ~5KB gzipped
|
|
556
|
+
|
|
557
|
+
## 🔧 Development
|
|
558
|
+
|
|
559
|
+
### Local Development
|
|
560
|
+
|
|
561
|
+
```bash
|
|
562
|
+
# Clone the monorepo
|
|
563
|
+
git clone <repo-url>
|
|
564
|
+
cd flagmint-monorepo
|
|
565
|
+
|
|
566
|
+
# Install dependencies
|
|
567
|
+
pnpm install
|
|
568
|
+
|
|
569
|
+
# Build the library
|
|
570
|
+
pnpm --filter react-flagmint build
|
|
571
|
+
|
|
572
|
+
# Run the Next.js example
|
|
573
|
+
pnpm dev:next
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### Testing
|
|
577
|
+
|
|
578
|
+
```bash
|
|
579
|
+
# Run tests
|
|
580
|
+
pnpm test
|
|
581
|
+
|
|
582
|
+
# Type checking
|
|
583
|
+
pnpm typecheck
|
|
584
|
+
|
|
585
|
+
# Lint
|
|
586
|
+
pnpm lint
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
## 📄 License
|
|
590
|
+
|
|
591
|
+
MIT
|
|
592
|
+
|
|
593
|
+
## 🤝 Contributing
|
|
594
|
+
|
|
595
|
+
1. Fork the repository
|
|
596
|
+
2. Create a feature branch
|
|
597
|
+
3. Make your changes
|
|
598
|
+
4. Add tests
|
|
599
|
+
5. Submit a pull request
|
|
600
|
+
|
|
601
|
+
## 📞 Support
|
|
602
|
+
|
|
603
|
+
- 📧 Email: support@flagmint.com
|
|
604
|
+
- 🐛 Issues: [GitHub Issues](https://github.com/jtad009/flagmint-react-sdk/issues)
|
|
605
|
+
- 📖 Docs: [Documentation](https://docs.flagmint.com)
|
|
606
|
+
|
|
607
|
+
---
|
|
608
|
+
|
|
609
|
+
**Made with ❤️ by the Flagmint team**
|
package/package.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "flagmint-monorepo",
|
|
3
|
+
"version": "1.0.11",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "pnpm --filter react-flagmint build",
|
|
7
|
+
"dev:next": "pnpm --filter nextjs-app dev",
|
|
8
|
+
"clean": "pnpm -r exec rm -rf dist node_modules"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"typescript": "^5.0.0"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "flagmint-react-sdk",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "React wrapper for flagmint-js-sdk",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.mjs",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.mjs",
|
|
14
|
+
"require": "./dist/index.cjs"
|
|
15
|
+
},
|
|
16
|
+
"./client": {
|
|
17
|
+
"types": "./dist/client.d.ts",
|
|
18
|
+
"import": "./dist/client.mjs"
|
|
19
|
+
},
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"prepublishOnly": "pnpm run build"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"react": ">=18.0.0",
|
|
31
|
+
"react-dom": ">=18.0.0"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"zustand": "^4.4.0",
|
|
35
|
+
"flagmint-js-sdk": "1.0.11"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"tsup": "^8.0.0",
|
|
39
|
+
"typescript": "^5.0.0",
|
|
40
|
+
"@types/react": "^18.0.0",
|
|
41
|
+
"@types/react-dom": "^18.0.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { FlagValue } from "flagmint-js-sdk"
|
|
3
|
+
import { useContext } from "react"
|
|
4
|
+
import { StoreApi, useStore } from "zustand"
|
|
5
|
+
import { FlagmintStoreContext } from "@/providers/FlagmintProvider"
|
|
6
|
+
import { FlagStore, Flags } from "@/types"
|
|
7
|
+
|
|
8
|
+
// Helper to get the store from context
|
|
9
|
+
export function useFlagmintStore(): StoreApi<FlagStore> {
|
|
10
|
+
const store = useContext(FlagmintStoreContext)
|
|
11
|
+
if (!store) {
|
|
12
|
+
throw new Error('useFlagmintStore must be used within a FlagmintProvider')
|
|
13
|
+
}
|
|
14
|
+
return store
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Hook to get the client instance and update context
|
|
18
|
+
export function useFlagmint() {
|
|
19
|
+
const store = useFlagmintStore()
|
|
20
|
+
const client = useStore(store, (state) => state.client)
|
|
21
|
+
const isInitialized = useStore(store, (state) => state.isInitialized)
|
|
22
|
+
|
|
23
|
+
const updateContext = async (context: Record<string, any>) => {
|
|
24
|
+
if (!client) {
|
|
25
|
+
throw new Error('Flagmint client not initialized')
|
|
26
|
+
}
|
|
27
|
+
await client.updateContext(context)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
client,
|
|
32
|
+
isInitialized,
|
|
33
|
+
updateContext
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Hook to get all flags - only re-renders when flags change
|
|
38
|
+
export function useFlags(): Flags {
|
|
39
|
+
const store = useFlagmintStore()
|
|
40
|
+
return useStore(store, (state) => state.flags)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Simplified useFlag - just read from the reactive store
|
|
44
|
+
export function useFlag<T = FlagValue>(
|
|
45
|
+
key: string,
|
|
46
|
+
fallback?: T
|
|
47
|
+
): T {
|
|
48
|
+
const store = useFlagmintStore()
|
|
49
|
+
|
|
50
|
+
// Subscribe to this specific flag in the store
|
|
51
|
+
const flagValue = useStore(store, (state) => state.flags[key])
|
|
52
|
+
|
|
53
|
+
// Return the flag value or fallback
|
|
54
|
+
return (flagValue ?? fallback) as T
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Hook to check if the client is ready
|
|
58
|
+
export function useFlagmintReady(): boolean {
|
|
59
|
+
const store = useFlagmintStore()
|
|
60
|
+
|
|
61
|
+
// Subscribe to both isReady and isInitialized
|
|
62
|
+
const isReady = useStore(store, (state) => state.isReady)
|
|
63
|
+
const isInitialized = useStore(store, (state) => state.isInitialized)
|
|
64
|
+
|
|
65
|
+
return isReady && isInitialized
|
|
66
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './types';
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { FlagClient } from "flagmint-js-sdk"
|
|
3
|
+
import { useMemo, useRef, useEffect, createContext } from "react"
|
|
4
|
+
import { createFlagStore } from "@/store/store"
|
|
5
|
+
import { FlagmintProviderProps, Flags, FlagStore } from "@/types"
|
|
6
|
+
import { StoreApi } from "zustand"
|
|
7
|
+
|
|
8
|
+
export const FlagmintStoreContext = createContext<StoreApi<FlagStore> | null>(null)
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Provides the Flagmint flag store to React components and keeps it synced with FlagClient updates.
|
|
12
|
+
* Lifecycle overview:
|
|
13
|
+
* - App loads → FlagClient constructed → initialize() begins.
|
|
14
|
+
* - Cached flags applied → subscribers notified → React renders cached values.
|
|
15
|
+
* - Transport set up → initial fetch → store updated → subscribers notified.
|
|
16
|
+
* - Subsequent server updates propagate via client.subscribe to re-render consumers.
|
|
17
|
+
* - When context changes (updateContext), flags are re-fetched and the store is refreshed.
|
|
18
|
+
*
|
|
19
|
+
* @param props Component props: children, client options, optional initial flags, and deferInitialization flag.
|
|
20
|
+
* @returns A provider that supplies the flag store context to its children.
|
|
21
|
+
*/
|
|
22
|
+
export function FlagmintProvider<
|
|
23
|
+
T extends FlagClient<T, C>,
|
|
24
|
+
C extends Record<string, any> = Record<string, any>
|
|
25
|
+
>({
|
|
26
|
+
children,
|
|
27
|
+
options,
|
|
28
|
+
initialFlags = {},
|
|
29
|
+
deferInitialization = false
|
|
30
|
+
}: FlagmintProviderProps<T, C>) {
|
|
31
|
+
// Create a new store instance for each provider (per-request in SSR)
|
|
32
|
+
const store = useMemo(() => createFlagStore(initialFlags), [initialFlags])
|
|
33
|
+
|
|
34
|
+
// Use ref to avoid recreating client on every render
|
|
35
|
+
const clientRef = useRef<FlagClient<T, C> | null>(null)
|
|
36
|
+
const initPromiseRef = useRef<Promise<FlagClient<T, C>> | null>(null)
|
|
37
|
+
const unsubscribeRef = useRef<(() => void) | null>(null)
|
|
38
|
+
|
|
39
|
+
// Initialization function similar to Vue plugin
|
|
40
|
+
const init = async (): Promise<FlagClient<T, C>> => {
|
|
41
|
+
if (clientRef.current) return clientRef.current // Avoid re-init
|
|
42
|
+
|
|
43
|
+
// Return existing promise if init is already in progress
|
|
44
|
+
if (initPromiseRef.current) return initPromiseRef.current
|
|
45
|
+
|
|
46
|
+
const { setClient, setReady, setInitialized, setFlags } = store.getState()
|
|
47
|
+
|
|
48
|
+
initPromiseRef.current = (async () => {
|
|
49
|
+
try {
|
|
50
|
+
// Create the FlagClient
|
|
51
|
+
const client = new FlagClient<T, C>(options)
|
|
52
|
+
|
|
53
|
+
// Wait for client to be ready
|
|
54
|
+
await client.ready()
|
|
55
|
+
|
|
56
|
+
// Get initial flags from client
|
|
57
|
+
const initialClientFlags = client.getFlags()
|
|
58
|
+
setFlags(initialClientFlags)
|
|
59
|
+
|
|
60
|
+
// Subscribe to flag changes - THIS IS THE KEY FIX
|
|
61
|
+
const unsubscribe = client.subscribe((updatedFlags: Flags) => {
|
|
62
|
+
console.log('FlagmintProvider - flags updated:', updatedFlags)
|
|
63
|
+
setFlags(updatedFlags)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
unsubscribeRef.current = unsubscribe
|
|
67
|
+
|
|
68
|
+
clientRef.current = client
|
|
69
|
+
setClient(client)
|
|
70
|
+
setReady(true)
|
|
71
|
+
setInitialized(true)
|
|
72
|
+
|
|
73
|
+
console.log('FlagmintProvider - initialized with flags:', initialClientFlags)
|
|
74
|
+
|
|
75
|
+
return client
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('Failed to initialize Flagmint client:', error)
|
|
78
|
+
throw error
|
|
79
|
+
}
|
|
80
|
+
})()
|
|
81
|
+
|
|
82
|
+
return initPromiseRef.current
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!deferInitialization) {
|
|
87
|
+
init().catch(error => {
|
|
88
|
+
console.error('Auto-initialization failed:', error)
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return () => {
|
|
93
|
+
// Unsubscribe from flag changes
|
|
94
|
+
if (unsubscribeRef.current) {
|
|
95
|
+
unsubscribeRef.current()
|
|
96
|
+
unsubscribeRef.current = null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Destroy client
|
|
100
|
+
if (clientRef.current) {
|
|
101
|
+
clientRef.current.destroy?.()
|
|
102
|
+
clientRef.current = null
|
|
103
|
+
initPromiseRef.current = null
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}, [deferInitialization])
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<FlagmintStoreContext.Provider value={store}>
|
|
110
|
+
{children}
|
|
111
|
+
</FlagmintStoreContext.Provider>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { createStore, StoreApi } from "zustand/vanilla";
|
|
3
|
+
import { Flags, FlagStore } from "@/types";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export function createFlagStore(initialFlags: Flags = {}): StoreApi<FlagStore> {
|
|
7
|
+
return createStore<FlagStore>((set, get) => ({
|
|
8
|
+
flags: initialFlags,
|
|
9
|
+
client: null,
|
|
10
|
+
isInitialized: false,
|
|
11
|
+
isReady: false,
|
|
12
|
+
setFlags: (flags) => set({ flags }),
|
|
13
|
+
setFlag: (key, value) => set((state) => ({
|
|
14
|
+
flags: { ...state.flags, [key]: value }
|
|
15
|
+
})),
|
|
16
|
+
setClient: (client) => set({ client }),
|
|
17
|
+
setInitialized: (initialized) => set({ isInitialized: initialized }),
|
|
18
|
+
setReady: (ready) => set({ isReady: ready })
|
|
19
|
+
}))
|
|
20
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { FlagClient, FlagClientOptions } from "flagmint-js-sdk"
|
|
2
|
+
import { ReactNode } from "react"
|
|
3
|
+
|
|
4
|
+
export type FlagValue = string | number | boolean | object | null
|
|
5
|
+
|
|
6
|
+
export interface Flags {
|
|
7
|
+
[key: string]: FlagValue
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CoreClientLike {
|
|
11
|
+
getFlags(): Promise<Flags> | Flags
|
|
12
|
+
getFlag(key: string, fallback?: FlagValue): Promise<FlagValue> | FlagValue
|
|
13
|
+
subscribe(callback: (flags: Flags) => void): () => void
|
|
14
|
+
updateContext(context: Record<string, any>): Promise<void> | void
|
|
15
|
+
init(): Promise<void> | void
|
|
16
|
+
close(): Promise<void> | void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface FlagmintProviderProps {
|
|
20
|
+
children: ReactNode
|
|
21
|
+
createClient?: () => CoreClientLike
|
|
22
|
+
client: CoreClientLike
|
|
23
|
+
initialFlags?: Flags
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface FlagStore {
|
|
27
|
+
flags: Flags
|
|
28
|
+
client: FlagClient<any, any> | null
|
|
29
|
+
isInitialized: boolean
|
|
30
|
+
isReady: boolean
|
|
31
|
+
setFlags: (flags: Flags) => void
|
|
32
|
+
setFlag: (key: string, value: FlagValue) => void
|
|
33
|
+
setClient: (client: FlagClient<any, any> | null) => void
|
|
34
|
+
setInitialized: (initialized: boolean) => void
|
|
35
|
+
setReady: (ready: boolean) => void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface FlagmintProviderProps<
|
|
39
|
+
T = unknown,
|
|
40
|
+
C extends Record<string, any> = Record<string, any>
|
|
41
|
+
> {
|
|
42
|
+
children: ReactNode
|
|
43
|
+
options: FlagClientOptions<C>
|
|
44
|
+
initialFlags?: Flags
|
|
45
|
+
deferInitialization?: boolean
|
|
46
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"noEmit": false,
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"incremental": false,
|
|
9
|
+
"baseUrl": ".",
|
|
10
|
+
"paths": {
|
|
11
|
+
"@/*": ["src/*"]
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"],
|
|
15
|
+
"exclude": ["dist", "node_modules"]
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup'
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: {
|
|
5
|
+
index: 'src/index.ts',
|
|
6
|
+
},
|
|
7
|
+
format: ['esm', 'cjs'],
|
|
8
|
+
dts: true,
|
|
9
|
+
sourcemap: true,
|
|
10
|
+
clean: true,
|
|
11
|
+
treeshake: true,
|
|
12
|
+
target: 'es2019',
|
|
13
|
+
external: ['react', 'react-dom', 'zustand', 'flagmint-js-sdk'],
|
|
14
|
+
esbuildOptions(options) {
|
|
15
|
+
options.jsx = 'automatic'
|
|
16
|
+
}
|
|
17
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2019",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "es6"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [
|
|
17
|
+
{
|
|
18
|
+
"name": "next"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
23
|
+
"exclude": ["node_modules", "dist", ".next"]
|
|
24
|
+
}
|