@lovart-open/flags 0.0.1-canary.pr4.7b8e2dd

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 ADDED
@@ -0,0 +1,279 @@
1
+ # @lovart-open/flags
2
+
3
+ [![CI](https://github.com/lovartai/statsig/actions/workflows/ci.yml/badge.svg)](https://github.com/lovartai/statsig/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/@lovart-open/flags.svg)](https://www.npmjs.com/package/@lovart-open/flags)
5
+
6
+ Type-safe Feature Flag and Parameter Store library built on Statsig, with a fully synchronous architecture.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install @lovart-open/flags
12
+ # or
13
+ pnpm add @lovart-open/flags
14
+ ```
15
+
16
+ ## Initialize Statsig
17
+
18
+ Initialize the Statsig client at your app entry point:
19
+
20
+ ```ts
21
+ import { initStatsigClient } from '@lovart-open/flags/statsig';
22
+
23
+ initStatsigClient('your-statsig-client-key', { userID: 'user-123' }, {
24
+ environment: { tier: 'production' },
25
+ });
26
+ ```
27
+
28
+ ---
29
+
30
+ # Feature Flags
31
+
32
+ ## Core Features
33
+
34
+ - **Type-safe**: Full TypeScript type inference and autocomplete
35
+ - **Synchronous**: No loading states, no skeleton screens
36
+ - **Multi-layer priority**: `URL > testOverride > override > remote > fallback(false)`
37
+
38
+ ## Define and Create
39
+
40
+ ```ts
41
+ import { createFlagStore, type FlagDefinition } from '@lovart-open/flags/statsig';
42
+
43
+ // 1. Define your flags
44
+ const MY_FLAGS = {
45
+ dark_mode: {
46
+ description: 'Dark mode toggle'
47
+ },
48
+ new_checkout: {
49
+ description: 'New checkout flow',
50
+ testOverride: true, // Force enable in E2E tests
51
+ },
52
+ beta_feature: {
53
+ description: 'Beta feature',
54
+ override: false, // Static override, ignores remote
55
+ },
56
+ } as const satisfies Record<string, FlagDefinition>;
57
+
58
+ // 2. Create type-safe store and hooks
59
+ export const {
60
+ flagStore,
61
+ useFlag,
62
+ useFlagState
63
+ } = createFlagStore(MY_FLAGS);
64
+
65
+ // 3. Export types (optional)
66
+ export type MyFlagKey = keyof typeof MY_FLAGS;
67
+ ```
68
+
69
+ ## React Usage
70
+
71
+ ```tsx
72
+ import { useFlag, useFlagState } from './my-flags';
73
+
74
+ function App() {
75
+ // Get boolean value directly
76
+ const isDark = useFlag('dark_mode'); // ✓ autocomplete
77
+
78
+ // Get full state with source info
79
+ const state = useFlagState('new_checkout');
80
+ console.log(state.flag, state.source); // true, 'remote'
81
+
82
+ return isDark ? <DarkTheme /> : <LightTheme />;
83
+ }
84
+ ```
85
+
86
+ ## Non-React Usage
87
+
88
+ ```ts
89
+ import { flagStore } from './my-flags';
90
+
91
+ // Get single flag
92
+ const enabled = flagStore.getFlag('dark_mode');
93
+
94
+ // Get snapshot of all flags
95
+ const snapshot = flagStore.snapshot;
96
+ ```
97
+
98
+ ## URL Override for Debugging
99
+
100
+ ```
101
+ ?ff.dark_mode=1 → Force enable
102
+ ?ff.dark_mode=0 → Force disable
103
+ ```
104
+
105
+ ---
106
+
107
+ # Parameter Store
108
+
109
+ ## Core Features
110
+
111
+ - **Type-safe**: Zod schema validation + TypeScript inference
112
+ - **Synchronous**: Same as Feature Flags
113
+ - **Multi-layer priority**: `URL > testOverride > override > remote > fallback`
114
+
115
+ ## Define and Create
116
+
117
+ ```ts
118
+ import { z } from 'zod';
119
+ import {
120
+ createParamStore,
121
+ defineParam,
122
+ type ParamStoreDefinition
123
+ } from '@lovart-open/flags/statsig';
124
+
125
+ // 1. Define your param stores
126
+ const MY_PARAMS = {
127
+ homepage_cta: {
128
+ description: 'Homepage CTA button',
129
+ params: {
130
+ text: defineParam({
131
+ schema: z.enum(['Learn More', 'Get Started', 'Sign Up']),
132
+ fallback: 'Learn More',
133
+ description: 'Button text',
134
+ }),
135
+ color: defineParam({
136
+ schema: z.enum(['gray', 'red', 'blue']),
137
+ fallback: 'gray',
138
+ testOverride: 'blue', // Use in E2E tests
139
+ }),
140
+ visible: defineParam({
141
+ schema: z.boolean(),
142
+ fallback: true,
143
+ }),
144
+ },
145
+ },
146
+ pricing: {
147
+ description: 'Pricing config',
148
+ params: {
149
+ discount: defineParam({
150
+ schema: z.number().min(0).max(100),
151
+ fallback: 0,
152
+ }),
153
+ currency: defineParam({
154
+ schema: z.enum(['USD', 'CNY', 'EUR']),
155
+ fallback: 'USD',
156
+ }),
157
+ },
158
+ },
159
+ } as const satisfies Record<string, ParamStoreDefinition<any>>;
160
+
161
+ // 2. Create type-safe store and hooks
162
+ export const {
163
+ paramStore,
164
+ useParam,
165
+ useParamState,
166
+ useParamStore
167
+ } = createParamStore(MY_PARAMS);
168
+ ```
169
+
170
+ ## React Usage
171
+
172
+ ```tsx
173
+ import { useParam, useParamStore } from './my-params';
174
+
175
+ function CTAButton() {
176
+ // Get value directly (with full type hints)
177
+ const text = useParam('homepage_cta', 'text'); // 'Learn More' | 'Get Started' | 'Sign Up'
178
+ const color = useParam('homepage_cta', 'color'); // 'gray' | 'red' | 'blue'
179
+
180
+ // Or get entire store handle
181
+ const store = useParamStore('homepage_cta');
182
+ const visible = store.get('visible'); // boolean
183
+
184
+ if (!visible) return null;
185
+ return <button style={{ color }}>{text}</button>;
186
+ }
187
+ ```
188
+
189
+ ## Non-React Usage
190
+
191
+ ```ts
192
+ import { paramStore } from './my-params';
193
+
194
+ // Get single param
195
+ const discount = paramStore.getParam('pricing', 'discount'); // number
196
+
197
+ // Get store handle
198
+ const store = paramStore.getStore('pricing');
199
+ store.get('currency'); // 'USD' | 'CNY' | 'EUR'
200
+ ```
201
+
202
+ ## URL Override for Debugging
203
+
204
+ ```
205
+ # Single param override
206
+ ?fp.homepage_cta.text=Get Started
207
+ ?fp.pricing.discount=20
208
+
209
+ # Entire store JSON override
210
+ ?fp.homepage_cta={"text":"Get Started","visible":false}
211
+ ```
212
+
213
+ ---
214
+
215
+ # Advanced Configuration
216
+
217
+ ## Custom Logger
218
+
219
+ ```ts
220
+ import { initStatsigClient, setLogger } from '@lovart-open/flags/statsig';
221
+
222
+ // Option 1: Pass during init
223
+ initStatsigClient('client-xxx', { userID: 'user-123' }, {
224
+ logger: (message) => myLogger.info(message),
225
+ });
226
+
227
+ // Option 2: Set globally
228
+ setLogger((message) => myLogger.info(message));
229
+ ```
230
+
231
+ ## E2E Test Support
232
+
233
+ Configure `isTestEnv` in initialization to enable `testOverride` values:
234
+
235
+ ```ts
236
+ initStatsigClient('client-xxx', { userID: 'user-123' }, {
237
+ isTestEnv: () => Boolean(window.__E2E__),
238
+ });
239
+
240
+ // playwright/cypress tests
241
+ await page.addInitScript(() => {
242
+ window.__E2E__ = true;
243
+ });
244
+ ```
245
+
246
+ ## Server-Side Bootstrap (Zero-Network Init)
247
+
248
+ ```ts
249
+ initStatsigClient('client-xxx', { userID: 'user-123' }, {
250
+ bootstrap: {
251
+ data: bootstrapDataFromServer, // Pre-fetched from BFF
252
+ },
253
+ });
254
+ ```
255
+
256
+ ## FlagDefinition Options
257
+
258
+ | Property | Type | Description |
259
+ |----------|------|-------------|
260
+ | `description` | `string` | Human-readable description |
261
+ | `testOverride` | `boolean` | Fixed value in E2E tests |
262
+ | `override` | `boolean` | Static override (priority over remote) |
263
+ | `keep` | `boolean` | Mark as kept locally (no remote needed) |
264
+
265
+ ## ParamDefinition Options
266
+
267
+ | Property | Type | Description |
268
+ |----------|------|-------------|
269
+ | `schema` | `z.ZodType` | Zod schema (required) |
270
+ | `fallback` | `T` | Default value (required) |
271
+ | `description` | `string` | Human-readable description |
272
+ | `testOverride` | `T` | Fixed value in E2E tests |
273
+ | `override` | `T` | Static override |
274
+
275
+ ---
276
+
277
+ ## License
278
+
279
+ MIT
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@lovart-open/flags",
3
+ "version": "0.0.1-canary.pr4.7b8e2dd",
4
+ "description": "Feature flag management with multi-layer priority system (client-side)",
5
+ "main": "src/index.ts",
6
+ "types": "src/index.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./src/index.ts",
10
+ "default": "./src/index.ts"
11
+ },
12
+ "./statsig": {
13
+ "types": "./src/statsig/index.ts",
14
+ "default": "./src/statsig/index.ts"
15
+ }
16
+ },
17
+ "files": [
18
+ "src"
19
+ ],
20
+ "keywords": [
21
+ "feature-flag",
22
+ "statsig",
23
+ "parameter-store"
24
+ ],
25
+ "author": "huxingyu@liblib.ai",
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "@statsig/js-client": "^3.30.0",
29
+ "@statsig/react-bindings": "^3.30.2",
30
+ "lodash": "^4.17.21",
31
+ "zod": "^3.23.0"
32
+ },
33
+ "devDependencies": {
34
+ "@testing-library/react": "^16.3.0",
35
+ "@types/lodash": "^4.17.0",
36
+ "@types/react": "^18.0.0 || ^19.0.0",
37
+ "jsdom": "^23.0.1",
38
+ "typescript": "^5.6.3",
39
+ "vitest": "^3.2.4"
40
+ },
41
+ "peerDependencies": {
42
+ "react": "^18.0.0 || ^19.0.0"
43
+ },
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "https://github.com/lovartai/flags.git"
47
+ },
48
+ "scripts": {
49
+ "typecheck": "tsc --noEmit",
50
+ "test": "vitest run"
51
+ }
52
+ }
@@ -0,0 +1,102 @@
1
+ // Mock external dependencies
2
+ const mockGetFeatureGate = vi.fn();
3
+ const mockIsTestEnv = vi.fn(() => false);
4
+ vi.mock('../statsig/client', () => ({
5
+ getStatsigClientSync: vi.fn(() => ({
6
+ getFeatureGate: mockGetFeatureGate,
7
+ })),
8
+ isTestEnv: () => mockIsTestEnv(),
9
+ }));
10
+
11
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
12
+
13
+ import { createFlagStore, type FlagDefinition } from '../statsig';
14
+
15
+ describe('FlagStore', () => {
16
+ const mockFlags = {
17
+ feature_a: { description: 'Feature A' },
18
+ feature_b: { description: 'Feature B', testOverride: true },
19
+ feature_c: { description: 'Feature C', override: false },
20
+ } as const satisfies Record<string, FlagDefinition>;
21
+
22
+ const { flagStore } = createFlagStore(mockFlags);
23
+
24
+ beforeEach(() => {
25
+ vi.resetAllMocks();
26
+ mockGetFeatureGate.mockReturnValue({
27
+ value: false,
28
+ idType: '',
29
+ });
30
+ mockIsTestEnv.mockReturnValue(false);
31
+ vi.stubGlobal('location', {
32
+ search: '',
33
+ pathname: '/',
34
+ hash: '',
35
+ });
36
+ });
37
+
38
+ afterEach(() => {
39
+ vi.unstubAllGlobals();
40
+ });
41
+
42
+ describe('Initial State', () => {
43
+ it('should initialize with fallback state', () => {
44
+ const state = flagStore.getFlagState('feature_a');
45
+ expect(state.flag).toBe(false);
46
+ expect(state.source).toBe('fallback');
47
+ });
48
+ });
49
+
50
+ describe('Priority Logic', () => {
51
+ it('Priority 1: URL Override should win', () => {
52
+ vi.stubGlobal('location', {
53
+ search: '?ff.feature_a=1',
54
+ href: 'http://localhost/?ff.feature_a=1',
55
+ });
56
+ const urlState = flagStore.resolve('feature_a', { search: '?ff.feature_a=1' });
57
+ expect(urlState.flag).toBe(true);
58
+ expect(urlState.source).toBe('url');
59
+ });
60
+
61
+ it('Priority 2: Test Override should win over code static override', () => {
62
+ mockIsTestEnv.mockReturnValue(true);
63
+ const state = flagStore.getFlagState('feature_b');
64
+ expect(state.flag).toBe(true);
65
+ expect(state.source).toBe('test');
66
+ });
67
+
68
+ it('Priority 3: Code Static Override should win over remote', () => {
69
+ mockGetFeatureGate.mockReturnValue({ value: true, idType: 'userID' });
70
+ const state = flagStore.getFlagState('feature_c');
71
+ expect(state.flag).toBe(false);
72
+ expect(state.source).toBe('override');
73
+ });
74
+
75
+ it('Priority 4: Remote Value should win over fallback', () => {
76
+ mockGetFeatureGate.mockReturnValue({ value: true, idType: 'userID' });
77
+ const state = flagStore.getFlagState('feature_a');
78
+ expect(state.source).toBe('remote');
79
+ expect(state.flag).toBe(true);
80
+ });
81
+ });
82
+
83
+ describe('Type Safety', () => {
84
+ it('should have type-safe keys', () => {
85
+ // These calls should have type hints
86
+ flagStore.getFlag('feature_a');
87
+ flagStore.getFlag('feature_b');
88
+ flagStore.getFlag('feature_c');
89
+
90
+ // Uncommenting below should cause compile error
91
+ // flagStore.getFlag('unknown_key');
92
+ });
93
+ });
94
+
95
+ describe('Edge Cases', () => {
96
+ it('should support explicit remoteValue passed to resolve', () => {
97
+ const state = flagStore.resolve('feature_a', { gate: { value: true, idType: 'useID' } as any });
98
+ expect(state.flag).toBe(true);
99
+ expect(state.source).toBe('remote');
100
+ });
101
+ });
102
+ });
@@ -0,0 +1,233 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { z } from 'zod';
3
+
4
+ // Mock statsig
5
+ const mockIsTestEnv = vi.fn(() => false);
6
+ vi.mock('../statsig/client', () => ({
7
+ getStatsigClientSync: vi.fn(() => ({
8
+ getParameterStore: vi.fn(),
9
+ })),
10
+ isTestEnv: () => mockIsTestEnv(),
11
+ }));
12
+
13
+ import { createParamStore, defineParam, type ParamStoreDefinition, getStatsigClientSync } from '../statsig';
14
+
15
+ describe('ParamStore', () => {
16
+ const testStores = {
17
+ test_store: {
18
+ description: 'Test store',
19
+ params: {
20
+ text: defineParam({ schema: z.string(), fallback: 'default text' }),
21
+ count: defineParam({ schema: z.number(), fallback: 0 }),
22
+ enabled: defineParam({ schema: z.boolean(), fallback: false }),
23
+ color: defineParam({
24
+ schema: z.enum(['red', 'blue', 'green']),
25
+ fallback: 'red' as const,
26
+ testOverride: 'blue' as const,
27
+ }),
28
+ size: defineParam({
29
+ schema: z.number(),
30
+ fallback: 10,
31
+ override: 20,
32
+ }),
33
+ },
34
+ },
35
+ } as const satisfies Record<string, ParamStoreDefinition<any>>;
36
+
37
+ const { paramStore } = createParamStore(testStores);
38
+
39
+ beforeEach(() => {
40
+ vi.resetAllMocks();
41
+ mockIsTestEnv.mockReturnValue(false);
42
+ vi.stubGlobal('location', { search: '' });
43
+ });
44
+
45
+ afterEach(() => {
46
+ vi.unstubAllGlobals();
47
+ });
48
+
49
+ describe('getParam / getParamState', () => {
50
+ it('returns fallback value when remote has no config', () => {
51
+ vi.mocked(getStatsigClientSync).mockReturnValue({
52
+ getParameterStore: vi.fn(() => ({
53
+ get: (_key: string, fallback: unknown) => fallback,
54
+ })),
55
+ } as any);
56
+
57
+ const value = paramStore.getParam('test_store', 'text');
58
+ expect(value).toBe('default text');
59
+
60
+ const state = paramStore.getParamState('test_store', 'text');
61
+ expect(state.value).toBe('default text');
62
+ expect(state.source).toBe('fallback');
63
+ });
64
+
65
+ it('returns remote value when remote has data', () => {
66
+ vi.mocked(getStatsigClientSync).mockReturnValue({
67
+ getParameterStore: vi.fn(() => ({
68
+ __configuration: {
69
+ text: { value: 'remote text' },
70
+ },
71
+ get: (key: string) => (key === 'text' ? 'remote text' : undefined),
72
+ })),
73
+ } as any);
74
+
75
+ const state = paramStore.getParamState('test_store', 'text');
76
+ expect(state.value).toBe('remote text');
77
+ expect(state.source).toBe('remote');
78
+ });
79
+
80
+ it('URL override has highest priority', () => {
81
+ vi.mocked(getStatsigClientSync).mockReturnValue({
82
+ getParameterStore: vi.fn(() => ({
83
+ __configuration: { text: { value: 'remote text' } },
84
+ get: () => 'remote text',
85
+ })),
86
+ } as any);
87
+
88
+ const state = paramStore.getParamState('test_store', 'text', {
89
+ search: '?fp.test_store.text=URL text',
90
+ });
91
+ expect(state.value).toBe('URL text');
92
+ expect(state.source).toBe('url');
93
+ });
94
+
95
+ it('testOverride works in E2E environment', () => {
96
+ mockIsTestEnv.mockReturnValue(true);
97
+
98
+ vi.mocked(getStatsigClientSync).mockReturnValue({
99
+ getParameterStore: vi.fn(() => ({
100
+ get: () => 'green',
101
+ })),
102
+ } as any);
103
+
104
+ const state = paramStore.getParamState('test_store', 'color');
105
+ expect(state.value).toBe('blue');
106
+ expect(state.source).toBe('test');
107
+ });
108
+
109
+ it('override has priority over remote', () => {
110
+ vi.mocked(getStatsigClientSync).mockReturnValue({
111
+ getParameterStore: vi.fn(() => ({
112
+ get: () => 30,
113
+ })),
114
+ } as any);
115
+
116
+ const state = paramStore.getParamState('test_store', 'size');
117
+ expect(state.value).toBe(20);
118
+ expect(state.source).toBe('override');
119
+ });
120
+
121
+ it('URL number type coercion', () => {
122
+ const state = paramStore.getParamState('test_store', 'count', {
123
+ search: '?fp.test_store.count=42',
124
+ });
125
+ expect(state.value).toBe(42);
126
+ expect(state.source).toBe('url');
127
+ });
128
+
129
+ it('URL boolean type coercion', () => {
130
+ const state = paramStore.getParamState('test_store', 'enabled', {
131
+ search: '?fp.test_store.enabled=true',
132
+ });
133
+ expect(state.value).toBe(true);
134
+ expect(state.source).toBe('url');
135
+ });
136
+ });
137
+
138
+ describe('getStore', () => {
139
+ it('returns handle object with { get, getState }', () => {
140
+ vi.mocked(getStatsigClientSync).mockReturnValue({
141
+ getParameterStore: vi.fn(() => ({
142
+ get: (_key: string, fallback: unknown) => fallback,
143
+ })),
144
+ } as any);
145
+
146
+ const store = paramStore.getStore('test_store');
147
+
148
+ expect(typeof store.get).toBe('function');
149
+ expect(typeof store.getState).toBe('function');
150
+
151
+ expect(store.get('text')).toBe('default text');
152
+ expect(store.getState('count')).toEqual({ value: 0, source: 'fallback' });
153
+ });
154
+
155
+ it('handle reuses same statsigStore', () => {
156
+ const mockGetParameterStore = vi.fn(() => ({
157
+ get: (_key: string, fallback: unknown) => fallback,
158
+ }));
159
+ vi.mocked(getStatsigClientSync).mockReturnValue({
160
+ getParameterStore: mockGetParameterStore,
161
+ } as any);
162
+
163
+ const store = paramStore.getStore('test_store');
164
+ store.get('text');
165
+ store.get('count');
166
+ store.getState('enabled');
167
+
168
+ // getParameterStore should only be called once (closure reuse)
169
+ expect(mockGetParameterStore).toHaveBeenCalledTimes(1);
170
+ });
171
+ });
172
+
173
+ describe('Priority Order', () => {
174
+ it('URL > test > override > remote > fallback', () => {
175
+ mockIsTestEnv.mockReturnValue(true);
176
+
177
+ vi.mocked(getStatsigClientSync).mockReturnValue({
178
+ getParameterStore: vi.fn(() => ({
179
+ get: () => 'green',
180
+ })),
181
+ } as any);
182
+
183
+ // color has testOverride='blue', remote='green'
184
+ // URL override should take priority
185
+ const state = paramStore.getParamState('test_store', 'color', {
186
+ search: '?fp.test_store.color=red',
187
+ });
188
+ expect(state.value).toBe('red');
189
+ expect(state.source).toBe('url');
190
+ });
191
+ });
192
+
193
+ describe('Error Handling', () => {
194
+ it('throws error for unknown store', () => {
195
+ expect(() => {
196
+ (paramStore as any).getParamState('unknown_store', 'text');
197
+ }).toThrow('[ParamStore] Unknown store: unknown_store');
198
+ });
199
+
200
+ it('throws error for unknown param', () => {
201
+ expect(() => {
202
+ (paramStore as any).getParamState('test_store', 'unknown_param');
203
+ }).toThrow('[ParamStore] Unknown param: test_store.unknown_param');
204
+ });
205
+
206
+ it('logs warning when URL value schema mismatch', () => {
207
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
208
+
209
+ // count expects number, passing non-numeric string 'abc'
210
+ paramStore.getParamState('test_store', 'count', {
211
+ search: '?fp.test_store.count=abc',
212
+ });
213
+
214
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Schema mismatch'), 'abc', expect.anything());
215
+
216
+ consoleSpy.mockRestore();
217
+ });
218
+ });
219
+
220
+ describe('Type Safety', () => {
221
+ it('should have type-safe store and param keys', () => {
222
+ // These calls should have type hints
223
+ paramStore.getParam('test_store', 'text'); // string
224
+ paramStore.getParam('test_store', 'count'); // number
225
+ paramStore.getParam('test_store', 'enabled'); // boolean
226
+ paramStore.getParam('test_store', 'color'); // 'red' | 'blue' | 'green'
227
+
228
+ // Uncommenting below should cause compile error
229
+ // paramStore.getParam('unknown_store', 'text');
230
+ // paramStore.getParam('test_store', 'unknown_param');
231
+ });
232
+ });
233
+ });
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @lovart-open/flags
3
+ *
4
+ * This package uses subpath exports. Import from:
5
+ * - @lovart-open/flags/statsig - Statsig-based feature flags and parameter stores
6
+ *
7
+ * @example
8
+ * import { createFlagStore, createParamStore } from '@lovart-open/flags/statsig';
9
+ */
10
+
11
+ // No exports from root path - use subpath imports
12
+ export {};