fauxbase 0.1.0 → 0.1.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.
Files changed (2) hide show
  1. package/README.md +630 -0
  2. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,630 @@
1
+ # Fauxbase
2
+
3
+ **Start with fake. Ship with real. Change nothing.**
4
+
5
+ A frontend data layer that simulates your backend — entities, services, business logic, 13 query operators, auth, role-based access — all running in the browser. When your backend is ready, swap one config line.
6
+
7
+ ```
8
+ npm install @fauxbase/core
9
+ ```
10
+
11
+ ---
12
+
13
+ ## The Problem
14
+
15
+ Every frontend project does this:
16
+
17
+ ```
18
+ 1. Backend not ready → hardcode mock data in components
19
+ 2. Mock data grows → copy-paste, no structure, spaghetti
20
+ 3. Need filtering/search → hack together Array.filter()
21
+ 4. Need pagination → hack together Array.slice()
22
+ 5. Need auth → fake login with useState
23
+ 6. Backend arrives → REWRITE all data fetching
24
+ 7. Partial backend → half mock, half real, chaos
25
+ 8. Migration bugs → deadline missed
26
+ ```
27
+
28
+ Existing tools don't solve this:
29
+
30
+ | Tool | What it does | The gap |
31
+ |------|-------------|---------|
32
+ | **MSW** | Intercepts HTTP at network level | You mock the *transport*, not the *data layer*. No query engine, no auth simulation. |
33
+ | **json-server** | Fake REST from a JSON file | No query operators, no auth, no hooks, separate process. |
34
+ | **MirageJS** | In-browser mock server | No typed entities, limited query operators, no auth sim, largely abandoned. |
35
+ | **Zustand/Redux** | State management | Just state — no CRUD contract, no query engine, no migration path. |
36
+
37
+ **Fauxbase fills the gap**: a structured, type-safe data layer with query capabilities and auth that runs locally during development and transparently switches to your real backend.
38
+
39
+ ---
40
+
41
+ ## Architecture
42
+
43
+ ```mermaid
44
+ graph TB
45
+ subgraph "Your React App"
46
+ C[Components]
47
+ end
48
+
49
+ subgraph "Fauxbase"
50
+ S[Service Layer<br/>hooks, validation, CRUD]
51
+ QE[QueryEngine<br/>13 operators, sort, paginate]
52
+
53
+ subgraph "Driver Abstraction"
54
+ LD[LocalDriver<br/>memory / localStorage / indexedDB]
55
+ HD[HttpDriver<br/>fetch + preset]
56
+ end
57
+ end
58
+
59
+ subgraph "Storage"
60
+ MEM[(Memory)]
61
+ LS[(localStorage)]
62
+ API[(REST API)]
63
+ end
64
+
65
+ C -->|fb.product.list, create, ...| S
66
+ S --> LD
67
+ S --> HD
68
+ LD --> QE
69
+ LD --> MEM
70
+ LD --> LS
71
+ HD -->|preset translates| API
72
+
73
+ style C fill:#61dafb,color:#000
74
+ style S fill:#f5c842,color:#000
75
+ style QE fill:#f5c842,color:#000
76
+ style LD fill:#4caf50,color:#fff
77
+ style HD fill:#2196f3,color:#fff
78
+ ```
79
+
80
+ **Key insight**: Components always talk to the Service layer. The Service delegates to a Driver. The Driver is swappable. Your components never know whether they're hitting localStorage or a REST API.
81
+
82
+ ---
83
+
84
+ ## Quick Start
85
+
86
+ ### 1. Define your entities
87
+
88
+ ```typescript
89
+ import { Entity, field, relation, computed } from '@fauxbase/core';
90
+
91
+ export class Product extends Entity {
92
+ @field({ required: true }) name!: string;
93
+ @field({ required: true, min: 0 }) price!: number;
94
+ @field({ default: 0 }) stock!: number;
95
+ @field({ default: true }) isActive!: boolean;
96
+
97
+ @relation('category') categoryId!: string;
98
+
99
+ @computed(p => p.stock > 0 && p.isActive)
100
+ available!: boolean;
101
+ }
102
+ ```
103
+
104
+ The `Entity` base class gives you these fields automatically:
105
+
106
+ | Field | Type | Description |
107
+ |-------|------|-------------|
108
+ | `id` | `string` | Auto-generated UUID |
109
+ | `createdAt` | `string` | ISO timestamp, set on create |
110
+ | `updatedAt` | `string` | ISO timestamp, updated on every change |
111
+ | `createdById` | `string?` | User who created (when auth active) |
112
+ | `createdByName` | `string?` | Display name of creator |
113
+ | `updatedById` | `string?` | User who last updated |
114
+ | `updatedByName` | `string?` | Display name of updater |
115
+ | `deletedAt` | `string?` | Soft delete timestamp (null = not deleted) |
116
+ | `version` | `number` | Auto-incremented on every update |
117
+
118
+ ### 2. Define services with business logic
119
+
120
+ ```typescript
121
+ import { Service, beforeCreate, beforeUpdate } from '@fauxbase/core';
122
+ import { Product } from './entities/product';
123
+
124
+ export class ProductService extends Service<Product> {
125
+ entity = Product;
126
+ endpoint = '/products';
127
+
128
+ @beforeCreate()
129
+ ensureUniqueName(data: Partial<Product>, existing: Product[]) {
130
+ if (existing.some(p => p.name === data.name)) {
131
+ throw new ConflictError(`Product "${data.name}" already exists`);
132
+ }
133
+ }
134
+
135
+ @beforeUpdate()
136
+ preventNegativeStock(_id: string, data: Partial<Product>) {
137
+ if (data.stock !== undefined && data.stock < 0) {
138
+ throw new ValidationError('Stock cannot be negative');
139
+ }
140
+ }
141
+
142
+ // Custom methods — available on both drivers
143
+ async getByCategory(categoryId: string, page = 1) {
144
+ return this.list({
145
+ filter: { categoryId, isActive: true },
146
+ sort: { field: 'name', direction: 'asc' },
147
+ page, size: 20,
148
+ });
149
+ }
150
+ }
151
+ ```
152
+
153
+ Services give you:
154
+
155
+ | Method | Description |
156
+ |--------|-------------|
157
+ | `list(query)` | Filtered, sorted, paginated list |
158
+ | `get(id)` | Get by ID |
159
+ | `create(data)` | Create with hooks + validation |
160
+ | `update(id, data)` | Update with hooks |
161
+ | `delete(id)` | Soft delete |
162
+ | `count(filter?)` | Count matching records |
163
+ | `bulk.create([])` | Batch insert |
164
+ | `bulk.update([])` | Batch update |
165
+ | `bulk.delete([])` | Batch delete |
166
+
167
+ ### 3. Define seed data
168
+
169
+ ```typescript
170
+ import { seed } from '@fauxbase/core';
171
+ import { Product } from './entities/product';
172
+
173
+ export const productSeed = seed(Product, [
174
+ { name: 'Hair Clay', price: 185000, categoryId: 'seed:category:0', stock: 50 },
175
+ { name: 'Beard Oil', price: 125000, categoryId: 'seed:category:1', stock: 30 },
176
+ ]);
177
+ ```
178
+
179
+ ### 4. Create the client
180
+
181
+ ```typescript
182
+ import { createClient } from '@fauxbase/core';
183
+
184
+ export const fb = createClient({
185
+ driver: import.meta.env.VITE_API_URL
186
+ ? { type: 'http', baseUrl: import.meta.env.VITE_API_URL, preset: 'lightwind' }
187
+ : { type: 'local', persist: 'localStorage' },
188
+
189
+ services: {
190
+ product: ProductService,
191
+ category: CategoryService,
192
+ order: OrderService,
193
+ },
194
+
195
+ seeds: [categorySeed, productSeed],
196
+ });
197
+
198
+ // Type-safe:
199
+ // fb.product.list(...) → ProductService
200
+ // fb.category.get(...) → CategoryService
201
+ // fb.product.getByCategory() → custom method, fully typed
202
+ ```
203
+
204
+ ### 5. Use it
205
+
206
+ ```typescript
207
+ // List with filtering
208
+ const result = await fb.product.list({
209
+ filter: { price__gte: 100000, name__contains: 'hair' },
210
+ sort: { field: 'price', direction: 'desc' },
211
+ page: 1,
212
+ size: 20,
213
+ });
214
+
215
+ // Create
216
+ const { data } = await fb.product.create({
217
+ name: 'New Product',
218
+ price: 150000,
219
+ categoryId: 'seed:category:0',
220
+ });
221
+
222
+ // Update
223
+ await fb.product.update(data.id, { stock: 100 });
224
+
225
+ // Delete (soft)
226
+ await fb.product.delete(data.id);
227
+ ```
228
+
229
+ ---
230
+
231
+ ## Query Engine — 13 Operators
232
+
233
+ Every filter operator works identically on the local driver (in-memory) and the HTTP driver (translated to URL params).
234
+
235
+ ```typescript
236
+ const result = await fb.product.list({
237
+ filter: {
238
+ price__gte: 100000, // price >= 100000
239
+ name__contains: 'pomade', // case-insensitive substring
240
+ categoryId__in: ['cat-1', 'cat-2'], // value in list
241
+ stock__between: [10, 100], // 10 <= stock <= 100
242
+ isActive: true, // exact match (eq implied)
243
+ description__isnull: false, // is not null
244
+ },
245
+ sort: { field: 'price', direction: 'desc' },
246
+ page: 1,
247
+ size: 20,
248
+ });
249
+ ```
250
+
251
+ ### All operators
252
+
253
+ | Operator | Syntax | Description | Example |
254
+ |----------|--------|-------------|---------|
255
+ | `eq` | `field` or `field__eq` | Exact match | `{ isActive: true }` |
256
+ | `ne` | `field__ne` | Not equal | `{ status__ne: 'deleted' }` |
257
+ | `gt` | `field__gt` | Greater than | `{ price__gt: 100 }` |
258
+ | `gte` | `field__gte` | Greater than or equal | `{ price__gte: 100 }` |
259
+ | `lt` | `field__lt` | Less than | `{ stock__lt: 10 }` |
260
+ | `lte` | `field__lte` | Less than or equal | `{ stock__lte: 100 }` |
261
+ | `like` | `field__like` | Case-insensitive substring | `{ name__like: 'hair' }` |
262
+ | `contains` | `field__contains` | Same as `like` | `{ name__contains: 'hair' }` |
263
+ | `startswith` | `field__startswith` | Case-insensitive prefix | `{ name__startswith: 'ha' }` |
264
+ | `endswith` | `field__endswith` | Case-insensitive suffix | `{ email__endswith: '@gmail.com' }` |
265
+ | `between` | `field__between` | Inclusive range | `{ price__between: [100, 500] }` |
266
+ | `in` | `field__in` | Value in list | `{ status__in: ['active', 'pending'] }` |
267
+ | `isnull` | `field__isnull` | Null/undefined check | `{ deletedAt__isnull: true }` |
268
+
269
+ ### How queries flow
270
+
271
+ ```mermaid
272
+ flowchart LR
273
+ subgraph "Input"
274
+ Q["{ filter: { price__gte: 100 },<br/>sort: { field: 'name', direction: 'asc' },<br/>page: 1, size: 20 }"]
275
+ end
276
+
277
+ subgraph "QueryEngine"
278
+ F[Filter<br/>13 operators]
279
+ S[Sort<br/>asc/desc, null-safe]
280
+ P[Paginate<br/>slice + meta]
281
+ end
282
+
283
+ subgraph "Output"
284
+ R["{ items: [...],<br/>meta: { page, size,<br/>totalItems, totalPages } }"]
285
+ end
286
+
287
+ Q --> F --> S --> P --> R
288
+ ```
289
+
290
+ Soft-deleted records (where `deletedAt` is set) are automatically excluded before any filtering.
291
+
292
+ ---
293
+
294
+ ## Drivers
295
+
296
+ ### Local Driver (development)
297
+
298
+ Runs entirely in the browser. No server needed.
299
+
300
+ ```typescript
301
+ driver: { type: 'local', persist: 'memory' } // volatile, fastest
302
+ driver: { type: 'local', persist: 'localStorage' } // persists across refresh
303
+ ```
304
+
305
+ | Store | Persists? | Limit | Best for |
306
+ |-------|-----------|-------|----------|
307
+ | `memory` | No | RAM | Unit tests, throwaway demos |
308
+ | `localStorage` | Yes | ~5MB | Default, small-medium datasets |
309
+
310
+ ### HTTP Driver (production)
311
+
312
+ Translates Fauxbase calls to fetch requests. Uses **presets** to speak your backend's language.
313
+
314
+ ```typescript
315
+ driver: {
316
+ type: 'http',
317
+ baseUrl: 'https://api.example.com',
318
+ preset: 'lightwind',
319
+ }
320
+ ```
321
+
322
+ ### Hybrid Driver (gradual migration)
323
+
324
+ When your backend is partially ready — migrate one service at a time:
325
+
326
+ ```typescript
327
+ const fb = createClient({
328
+ driver: { type: 'local' }, // default for all
329
+
330
+ overrides: {
331
+ product: { driver: { type: 'http', baseUrl: 'https://api.example.com', preset: 'lightwind' } },
332
+ // category, order → still local
333
+ },
334
+ });
335
+ ```
336
+
337
+ ### Driver flow
338
+
339
+ ```mermaid
340
+ flowchart TB
341
+ C[Component<br/>fb.product.list]
342
+
343
+ subgraph "Local Driver"
344
+ LS[(Storage<br/>memory/localStorage)]
345
+ QE1[QueryEngine<br/>in-memory processing]
346
+ end
347
+
348
+ subgraph "HTTP Driver"
349
+ PR[Preset<br/>serialize/parse]
350
+ FE[fetch<br/>GET /products?...]
351
+ API[(REST API)]
352
+ end
353
+
354
+ C -->|dev| LS --> QE1 --> R1[Normalized Response]
355
+ C -->|prod| PR --> FE --> API --> PR --> R2[Normalized Response]
356
+ ```
357
+
358
+ Components always get the same normalized response shape, regardless of driver.
359
+
360
+ ---
361
+
362
+ ## Backend Presets
363
+
364
+ A preset tells the HTTP driver how to talk to your backend — how to serialize queries, parse responses, and handle auth.
365
+
366
+ ### Built-in presets
367
+
368
+ | Preset | Framework | Filter Style | Auth |
369
+ |--------|-----------|-------------|------|
370
+ | `lightwind` | Lightwind (Quarkus) | `?price__gte=100` | `/auth/login` |
371
+ | `spring-boot` | Spring Boot | `?price.gte=100` | `/api/auth/signin` |
372
+ | `nestjs` | NestJS | `?filter.price.$gte=100` | `/auth/login` |
373
+ | `laravel` | Laravel | `?filter[price_gte]=100` | `/api/login` |
374
+ | `django` | Django REST Framework | `?price__gte=100` | `/api/token/` |
375
+ | `express` | Express.js | `?price__gte=100` | `/auth/login` |
376
+ | `fastapi` | FastAPI | `?price__gte=100` | `/api/auth/token` |
377
+ | `rails` | Ruby on Rails | `?q[price_gteq]=100` | `/api/login` |
378
+ | `go-gin` | Go (Gin) | `?price__gte=100` | `/api/auth/login` |
379
+
380
+ ### Custom presets
381
+
382
+ ```typescript
383
+ import { definePreset } from '@fauxbase/core';
384
+
385
+ const fb = createClient({
386
+ driver: {
387
+ type: 'http',
388
+ baseUrl: 'https://api.myapp.com',
389
+ preset: definePreset({
390
+ name: 'my-backend',
391
+ response: {
392
+ single: (raw) => ({ data: raw.result }),
393
+ list: (raw) => ({
394
+ items: raw.results,
395
+ meta: {
396
+ page: raw.page,
397
+ size: raw.per_page,
398
+ totalItems: raw.total,
399
+ totalPages: raw.pages,
400
+ },
401
+ }),
402
+ },
403
+ query: {
404
+ filterStyle: 'django',
405
+ pageParam: 'page',
406
+ sizeParam: 'per_page',
407
+ },
408
+ auth: {
409
+ loginUrl: '/api/v1/login',
410
+ tokenField: 'access_token',
411
+ },
412
+ }),
413
+ },
414
+ });
415
+ ```
416
+
417
+ ### How presets work
418
+
419
+ ```mermaid
420
+ sequenceDiagram
421
+ participant Component
422
+ participant Service
423
+ participant HttpDriver
424
+ participant Preset
425
+ participant Backend
426
+
427
+ Component->>Service: fb.product.list({ filter: { price__gte: 100 } })
428
+ Service->>HttpDriver: list("product", query)
429
+ HttpDriver->>Preset: serialize query
430
+ Preset-->>HttpDriver: GET /products?price__gte=100&page=1&size=20
431
+ HttpDriver->>Backend: fetch(url, { headers: { Authorization } })
432
+ Backend-->>HttpDriver: { code: 200, data: { items, meta } }
433
+ HttpDriver->>Preset: parse response
434
+ Preset-->>HttpDriver: { items: [...], meta: { page, size, totalItems, totalPages } }
435
+ HttpDriver-->>Service: normalized PagedResponse
436
+ Service-->>Component: same shape as local driver
437
+ ```
438
+
439
+ ---
440
+
441
+ ## Seeding Strategy
442
+
443
+ Seed data and runtime data are tracked separately.
444
+
445
+ ```mermaid
446
+ stateDiagram-v2
447
+ [*] --> FirstLoad : App starts
448
+ FirstLoad --> HasData : Seeds applied
449
+
450
+ HasData --> HasData : Reloads (seeds unchanged)
451
+ note right of HasData : Seeds SKIPPED, data preserved
452
+
453
+ HasData --> SeedChanged : Reloads (seeds modified)
454
+ SeedChanged --> HasData : Upsert seed records
455
+
456
+ HasData --> ResetSeeds : Reset Seeds
457
+ ResetSeeds --> HasData : Wipe + re-seed
458
+
459
+ HasData --> RefreshSeeds : Refresh Seeds
460
+ RefreshSeeds --> HasData : Re-seed only
461
+ ```
462
+
463
+ **How it works:**
464
+
465
+ - Seed records get deterministic IDs: `seed:product:0`, `seed:product:1`, ...
466
+ - Runtime records (created during development) get normal UUIDs: `a3f1b2c4-...`
467
+ - Fauxbase tracks a `_seedVersion` hash — if your seed definitions change, only seed records are re-applied
468
+ - Runtime records are never touched during re-seeding
469
+ - On HTTP driver, seeding is disabled entirely — the backend owns the data
470
+
471
+ ---
472
+
473
+ ## Response Format
474
+
475
+ All operations return a normalized format. Components always see the same shape regardless of driver.
476
+
477
+ ```typescript
478
+ // Single item (get, create, update)
479
+ interface ApiResponse<T> {
480
+ data: T;
481
+ }
482
+
483
+ // List (list)
484
+ interface PagedResponse<T> {
485
+ items: T[];
486
+ meta: {
487
+ page: number;
488
+ size: number;
489
+ totalItems: number;
490
+ totalPages: number;
491
+ };
492
+ }
493
+
494
+ // Errors (thrown as exceptions)
495
+ class NotFoundError // code: 'NOT_FOUND'
496
+ class ConflictError // code: 'CONFLICT'
497
+ class ValidationError // code: 'VALIDATION', details: { field: message }
498
+ class ForbiddenError // code: 'FORBIDDEN'
499
+ ```
500
+
501
+ ---
502
+
503
+ ## Decorators
504
+
505
+ ### Entity decorators
506
+
507
+ | Decorator | Purpose | Example |
508
+ |-----------|---------|---------|
509
+ | `@field(options?)` | Mark entity field with validation | `@field({ required: true, min: 0 })` |
510
+ | `@relation(entity)` | Foreign key relation | `@relation('category')` |
511
+ | `@computed(fn)` | Derived value, recalculated on access | `@computed(p => p.stock > 0)` |
512
+
513
+ ### Service hook decorators
514
+
515
+ | Decorator | Signature | Purpose |
516
+ |-----------|-----------|---------|
517
+ | `@beforeCreate()` | `(data, existingItems) => void` | Validate/mutate before create |
518
+ | `@beforeUpdate()` | `(id, data) => void` | Validate/mutate before update |
519
+ | `@afterCreate()` | `(entity) => void` | Side effects after create |
520
+ | `@afterUpdate()` | `(entity) => void` | Side effects after update |
521
+
522
+ Hooks can throw errors to abort operations:
523
+
524
+ ```typescript
525
+ @beforeCreate()
526
+ ensureUnique(data: Partial<Product>, existing: Product[]) {
527
+ if (existing.some(p => p.name === data.name)) {
528
+ throw new ConflictError('Name must be unique');
529
+ }
530
+ }
531
+ ```
532
+
533
+ ---
534
+
535
+ ## The Migration Timeline
536
+
537
+ ```mermaid
538
+ gantt
539
+ title Frontend Development with Fauxbase
540
+ dateFormat YYYY-MM-DD
541
+ axisFormat Week %W
542
+
543
+ section Setup
544
+ Install Fauxbase + define entities/services :a1, 2024-01-01, 1d
545
+ Define seed data :a2, after a1, 1d
546
+
547
+ section Build (no backend)
548
+ Build full UI with local driver :b1, after a2, 28d
549
+ CRUD, filtering, pagination, auth — all work :b2, after a2, 28d
550
+
551
+ section Migrate (backend arrives)
552
+ Products API ready → hybrid mode :c1, after b1, 7d
553
+ All APIs ready → full HTTP driver :c2, after c1, 7d
554
+ Remove local driver config :c3, after c2, 1d
555
+ ```
556
+
557
+ ```
558
+ Week 1: npm install @fauxbase/core
559
+ Define entities, services, seeds
560
+ Build UI with local driver — everything works
561
+
562
+ Week 2-4: Building features at full speed
563
+ No blocking on backend, no mock data spaghetti
564
+
565
+ Week 5: "Products API is ready"
566
+ → switch products to HTTP driver (hybrid mode)
567
+ → zero component changes
568
+
569
+ Week 6: "All APIs ready"
570
+ → set VITE_API_URL globally
571
+ → done
572
+ ```
573
+
574
+ ---
575
+
576
+ ## Project Structure
577
+
578
+ Recommended structure in your app:
579
+
580
+ ```
581
+ src/
582
+ ├── fauxbase/
583
+ │ ├── entities/
584
+ │ │ ├── product.ts ← class Product extends Entity
585
+ │ │ ├── category.ts
586
+ │ │ └── user.ts
587
+ │ ├── services/
588
+ │ │ ├── product.ts ← class ProductService extends Service<Product>
589
+ │ │ ├── category.ts
590
+ │ │ └── user.ts
591
+ │ ├── seeds/
592
+ │ │ ├── product.ts ← seed(Product, [...])
593
+ │ │ └── category.ts
594
+ │ └── index.ts ← createClient({ ... })
595
+ ├── components/
596
+ │ └── ProductList.tsx ← fb.product.list({ ... })
597
+ └── main.tsx
598
+ ```
599
+
600
+ ---
601
+
602
+ ## Technical Details
603
+
604
+ | Aspect | Detail |
605
+ |--------|--------|
606
+ | **Package** | `@fauxbase/core` (~8KB gzipped) |
607
+ | **Runtime deps** | Zero |
608
+ | **TypeScript** | First-class, full type inference |
609
+ | **Decorators** | `experimentalDecorators` (legacy TS decorators) |
610
+ | **Query operators** | 13: eq, ne, gt, gte, lt, lte, like, contains, startswith, endswith, between, in, isnull |
611
+ | **Storage** | memory, localStorage (indexedDB planned) |
612
+ | **Auth** | Planned (Phase 2) |
613
+ | **Presets** | lightwind, spring-boot, nestjs, laravel, express, django, fastapi, rails, go-gin, custom |
614
+ | **Build** | tsup (ESM + CJS + DTS) |
615
+ | **Tests** | vitest, 140+ tests, 97%+ coverage |
616
+
617
+ ---
618
+
619
+ ## Roadmap
620
+
621
+ - [x] **v0.1** — Core: Entity, Service, QueryEngine, LocalDriver, Seeds
622
+ - [ ] **v0.2** — React hooks (`useList`, `useGet`, `useMutation`, `useAuth`) + Auth simulation
623
+ - [ ] **v0.3** — HTTP Driver + Backend Presets + DevTools
624
+ - [ ] **v0.4** — IndexedDB, CLI (`npx fauxbase init`), Vue/Svelte adapters
625
+
626
+ ---
627
+
628
+ ## License
629
+
630
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fauxbase",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",