@usels/vite-plugin-legend-memo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +561 -0
- package/dist/index.d.mts +31 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +68 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +33 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +36 -0
- package/src/index.ts +63 -0
- package/tsconfig.json +10 -0
- package/tsup.config.ts +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
# @usels/vite-plugin-legend-memo
|
|
2
|
+
|
|
3
|
+
A Vite plugin that applies [`@usels/babel-plugin-legend-memo`](../babel) during the transform phase. Automatically wraps Legend-State observable `.get()` calls in JSX with reactive `<Memo>` boundaries for fine-grained reactivity — without any boilerplate.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
// You write this naturally
|
|
7
|
+
<div>{count$.get()}</div>
|
|
8
|
+
|
|
9
|
+
// Plugin transforms to this automatically
|
|
10
|
+
import { Memo } from "@legendapp/state/react";
|
|
11
|
+
<div><Memo>{() => count$.get()}</Memo></div>
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
**One plugin replaces two** — no longer need `@legendapp/state/babel` alongside another auto-wrap plugin.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Table of Contents
|
|
19
|
+
|
|
20
|
+
- [Installation](#installation)
|
|
21
|
+
- [Setup](#setup)
|
|
22
|
+
- [Plugin Order (Critical)](#plugin-order-critical)
|
|
23
|
+
- [Configuration](#configuration)
|
|
24
|
+
- [Features](#features)
|
|
25
|
+
- [Writing Components](#writing-components)
|
|
26
|
+
- [API Reference](#api-reference)
|
|
27
|
+
- [Troubleshooting](#troubleshooting)
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install -D @usels/vite-plugin-legend-memo @usels/babel-plugin-legend-memo @babel/core
|
|
35
|
+
# or
|
|
36
|
+
pnpm add -D @usels/vite-plugin-legend-memo @usels/babel-plugin-legend-memo @babel/core
|
|
37
|
+
# or
|
|
38
|
+
yarn add -D @usels/vite-plugin-legend-memo @usels/babel-plugin-legend-memo @babel/core
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Setup
|
|
44
|
+
|
|
45
|
+
### Basic
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// vite.config.ts
|
|
49
|
+
import { defineConfig } from 'vite';
|
|
50
|
+
import react from '@vitejs/plugin-react';
|
|
51
|
+
import { autoWrap } from '@usels/vite-plugin-legend-memo';
|
|
52
|
+
|
|
53
|
+
export default defineConfig({
|
|
54
|
+
plugins: [
|
|
55
|
+
autoWrap(), // ← Must be BEFORE react()
|
|
56
|
+
react(),
|
|
57
|
+
],
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### With options
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// vite.config.ts
|
|
65
|
+
import { defineConfig } from 'vite';
|
|
66
|
+
import react from '@vitejs/plugin-react';
|
|
67
|
+
import { autoWrap } from '@usels/vite-plugin-legend-memo';
|
|
68
|
+
|
|
69
|
+
export default defineConfig({
|
|
70
|
+
plugins: [
|
|
71
|
+
autoWrap({
|
|
72
|
+
// Detect all .get() regardless of $ suffix (default: false)
|
|
73
|
+
allGet: false,
|
|
74
|
+
|
|
75
|
+
// Auto-wrap Memo/Show/Computed children (default: true)
|
|
76
|
+
wrapReactiveChildren: true,
|
|
77
|
+
|
|
78
|
+
// Custom wrapper component (default: "Memo")
|
|
79
|
+
componentName: 'Memo',
|
|
80
|
+
|
|
81
|
+
// Import source (default: "@legendapp/state/react")
|
|
82
|
+
importSource: '@legendapp/state/react',
|
|
83
|
+
}),
|
|
84
|
+
react(),
|
|
85
|
+
],
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### With a different observable library
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { defineConfig } from 'vite';
|
|
93
|
+
import react from '@vitejs/plugin-react';
|
|
94
|
+
import { autoWrap } from '@usels/vite-plugin-legend-memo';
|
|
95
|
+
|
|
96
|
+
export default defineConfig({
|
|
97
|
+
plugins: [
|
|
98
|
+
autoWrap({
|
|
99
|
+
componentName: 'Auto',
|
|
100
|
+
importSource: '@usels/core',
|
|
101
|
+
}),
|
|
102
|
+
react(),
|
|
103
|
+
],
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Plugin Order (Critical)
|
|
110
|
+
|
|
111
|
+
**`autoWrap()` MUST be placed BEFORE `react()` in the plugins array.**
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// ✅ Correct — autoWrap processes JSX before React plugin
|
|
115
|
+
plugins: [autoWrap(), react()]
|
|
116
|
+
|
|
117
|
+
// ❌ Wrong — JSX is already transpiled when autoWrap runs
|
|
118
|
+
plugins: [react(), autoWrap()]
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Why order matters
|
|
122
|
+
|
|
123
|
+
The plugin uses `enforce: 'pre'` to run before `@vitejs/plugin-react`:
|
|
124
|
+
|
|
125
|
+
1. **`autoWrap()` runs first** — processes `.jsx`/`.tsx` files while JSX syntax is intact
|
|
126
|
+
2. **React plugin runs** — converts JSX to `React.createElement()` calls
|
|
127
|
+
3. **esbuild bundles** — produces the final output
|
|
128
|
+
|
|
129
|
+
If `react()` runs before `autoWrap()`, the JSX is already converted to function calls and the plugin cannot find JSX expressions to wrap.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Configuration
|
|
134
|
+
|
|
135
|
+
All options from `@usels/babel-plugin-legend-memo` are supported:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
interface PluginOptions {
|
|
139
|
+
/**
|
|
140
|
+
* Wrapper component name
|
|
141
|
+
* @default "Memo"
|
|
142
|
+
*/
|
|
143
|
+
componentName?: string;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Import source for the wrapper component
|
|
147
|
+
* @default "@legendapp/state/react"
|
|
148
|
+
*/
|
|
149
|
+
importSource?: string;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Detect all .get() calls regardless of $ suffix
|
|
153
|
+
* @default false
|
|
154
|
+
*/
|
|
155
|
+
allGet?: boolean;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Additional method names to detect beyond "get"
|
|
159
|
+
* @default ["get"]
|
|
160
|
+
*/
|
|
161
|
+
methodNames?: string[];
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Additional reactive component names to skip
|
|
165
|
+
* Merged with defaults: Auto, For, Show, Memo, Computed, Switch
|
|
166
|
+
*/
|
|
167
|
+
reactiveComponents?: string[];
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Observer HOC function names — skip content inside these
|
|
171
|
+
* @default ["observer"]
|
|
172
|
+
*/
|
|
173
|
+
observerNames?: string[];
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Auto-wrap non-function children of Memo/Show/Computed in () =>
|
|
177
|
+
* Replaces the need for @legendapp/state/babel plugin
|
|
178
|
+
* @default true
|
|
179
|
+
*/
|
|
180
|
+
wrapReactiveChildren?: boolean;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Additional component names whose children should be auto-wrapped
|
|
184
|
+
* Merged with defaults: Memo, Show, Computed
|
|
185
|
+
*/
|
|
186
|
+
wrapReactiveChildrenComponents?: string[];
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Features
|
|
193
|
+
|
|
194
|
+
### 1. Auto-wrap `.get()` calls in JSX expressions
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
// Input
|
|
198
|
+
<div>{count$.get()}</div>
|
|
199
|
+
|
|
200
|
+
// Output
|
|
201
|
+
import { Memo } from "@legendapp/state/react";
|
|
202
|
+
<div><Memo>{() => count$.get()}</Memo></div>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Complex expressions, ternaries, and conditionals are all handled:
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
// Multiple observables → one Memo
|
|
209
|
+
<p>{a$.get() + " " + b$.get()}</p>
|
|
210
|
+
// → <p><Memo>{() => a$.get() + " " + b$.get()}</Memo></p>
|
|
211
|
+
|
|
212
|
+
// Ternary
|
|
213
|
+
<div>{isActive$.get() ? "ON" : "OFF"}</div>
|
|
214
|
+
// → <div><Memo>{() => isActive$.get() ? "ON" : "OFF"}</Memo></div>
|
|
215
|
+
|
|
216
|
+
// Conditional rendering
|
|
217
|
+
<div>{show$.get() && <Modal />}</div>
|
|
218
|
+
// → <div><Memo>{() => show$.get() && <Modal />}</Memo></div>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### 2. Auto-wrap `.get()` calls in JSX attributes
|
|
222
|
+
|
|
223
|
+
When an element's props contain `.get()`, the entire element is wrapped:
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
// Input
|
|
227
|
+
<Component value={obs$.get()} />
|
|
228
|
+
|
|
229
|
+
// Output
|
|
230
|
+
<Memo>{() => <Component value={obs$.get()} />}</Memo>
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Attributes and children together → one `<Memo>`:
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
// Input
|
|
237
|
+
<div className={theme$.get()}>
|
|
238
|
+
{count$.get()}
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
// Output
|
|
242
|
+
<Memo>{() =>
|
|
243
|
+
<div className={theme$.get()}>
|
|
244
|
+
{count$.get()}
|
|
245
|
+
</div>
|
|
246
|
+
}</Memo>
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### 3. Auto-wrap children of Memo/Show/Computed
|
|
250
|
+
|
|
251
|
+
Non-function children are automatically wrapped in `() =>`. This replaces `@legendapp/state/babel`:
|
|
252
|
+
|
|
253
|
+
```tsx
|
|
254
|
+
// Input
|
|
255
|
+
<Memo>{count$.get()}</Memo>
|
|
256
|
+
<Show if={cond$}>{count$.get()}</Show>
|
|
257
|
+
<Computed>{price$.get() * qty$.get()}</Computed>
|
|
258
|
+
|
|
259
|
+
// Output
|
|
260
|
+
<Memo>{() => count$.get()}</Memo>
|
|
261
|
+
<Show if={cond$}>{() => count$.get()}</Show>
|
|
262
|
+
<Computed>{() => price$.get() * qty$.get()}</Computed>
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Direct JSX children and multiple children:
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
// Direct JSX child
|
|
269
|
+
<Memo><div>{count$.get()}</div></Memo>
|
|
270
|
+
// → <Memo>{() => <div>{count$.get()}</div>}</Memo>
|
|
271
|
+
|
|
272
|
+
// Multiple children → Fragment
|
|
273
|
+
<Memo><Header /><Body /></Memo>
|
|
274
|
+
// → <Memo>{() => <><Header /><Body /></>}</Memo>
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Writing Components
|
|
280
|
+
|
|
281
|
+
### Basic principle: write `.get()` naturally, plugin handles wrapping
|
|
282
|
+
|
|
283
|
+
```tsx
|
|
284
|
+
import { observable } from '@legendapp/state';
|
|
285
|
+
import { Show, Memo, For } from '@legendapp/state/react';
|
|
286
|
+
|
|
287
|
+
const count$ = observable(0);
|
|
288
|
+
const isVisible$ = observable(true);
|
|
289
|
+
const user$ = observable({ name: 'Alice', age: 30 });
|
|
290
|
+
const items$ = observable([{ id: 1, name: 'Item 1' }]);
|
|
291
|
+
|
|
292
|
+
export function App() {
|
|
293
|
+
return (
|
|
294
|
+
<div>
|
|
295
|
+
{/* Simple expressions — plugin wraps each */}
|
|
296
|
+
<h1>Count: {count$.get()}</h1>
|
|
297
|
+
<p>User: {user$.name.get()}</p>
|
|
298
|
+
|
|
299
|
+
{/* Memo children — plugin auto-wraps in () => */}
|
|
300
|
+
<Memo>
|
|
301
|
+
<div className="card">{count$.get()}</div>
|
|
302
|
+
</Memo>
|
|
303
|
+
|
|
304
|
+
{/* Show children — plugin auto-wraps in () => */}
|
|
305
|
+
<Show if={isVisible$}>
|
|
306
|
+
{user$.name.get()}
|
|
307
|
+
</Show>
|
|
308
|
+
|
|
309
|
+
{/* For — handles list reactivity, plugin skips inside */}
|
|
310
|
+
<For each={items$}>
|
|
311
|
+
{(item$) => <li>{item$.name.get()}</li>}
|
|
312
|
+
</For>
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Use `$` suffix for observables (required by default)
|
|
319
|
+
|
|
320
|
+
```tsx
|
|
321
|
+
// ✅ Detected automatically — use $ suffix
|
|
322
|
+
const count$ = observable(0);
|
|
323
|
+
const profile$ = observable({ name: '', email: '' });
|
|
324
|
+
|
|
325
|
+
// ❌ Without $ — won't be wrapped (use allGet: true to override)
|
|
326
|
+
const count = observable(0);
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Use `observer()` for component-level reactivity
|
|
330
|
+
|
|
331
|
+
When the whole component is reactive, wrap with `observer()`. Plugin skips content inside — no double-wrapping:
|
|
332
|
+
|
|
333
|
+
```tsx
|
|
334
|
+
import { observer } from '@legendapp/state/react';
|
|
335
|
+
|
|
336
|
+
// ✅ Entire component is reactive — no individual Memo wrappers needed
|
|
337
|
+
const Profile = observer(() => {
|
|
338
|
+
return (
|
|
339
|
+
<div>
|
|
340
|
+
<h2>{user$.name.get()}</h2>
|
|
341
|
+
<p>{user$.bio.get()}</p>
|
|
342
|
+
<span>{user$.age.get()} years old</span>
|
|
343
|
+
</div>
|
|
344
|
+
);
|
|
345
|
+
});
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
**When to use `observer()`:** When the entire component needs reactivity and you want simpler code without individual `<Memo>` boundaries.
|
|
349
|
+
|
|
350
|
+
**When to use auto-wrap (default):** When you want fine-grained reactivity — only the specific expressions that use observables update, not the whole component.
|
|
351
|
+
|
|
352
|
+
### Reactive attributes
|
|
353
|
+
|
|
354
|
+
```tsx
|
|
355
|
+
const theme$ = observable({ color: '#007bff', size: 'lg' });
|
|
356
|
+
const isDark$ = observable(false);
|
|
357
|
+
|
|
358
|
+
function ThemedButton({ label }: { label: string }) {
|
|
359
|
+
return (
|
|
360
|
+
<button
|
|
361
|
+
className={`btn-${theme$.size.get()}`}
|
|
362
|
+
style={{ backgroundColor: theme$.color.get() }}
|
|
363
|
+
aria-pressed={isDark$.get()}
|
|
364
|
+
>
|
|
365
|
+
{label}
|
|
366
|
+
</button>
|
|
367
|
+
// ↑ Plugin wraps entire <button> since attributes have .get()
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Conditional rendering patterns
|
|
373
|
+
|
|
374
|
+
```tsx
|
|
375
|
+
const auth$ = observable({ isLoggedIn: false, username: '' });
|
|
376
|
+
|
|
377
|
+
function Header() {
|
|
378
|
+
return (
|
|
379
|
+
<header>
|
|
380
|
+
{/* Show removes from DOM when false */}
|
|
381
|
+
<Show if={auth$.isLoggedIn}>
|
|
382
|
+
{/* Plugin auto-wraps children */}
|
|
383
|
+
Welcome, {auth$.username.get()}
|
|
384
|
+
</Show>
|
|
385
|
+
|
|
386
|
+
{/* Ternary with .get() */}
|
|
387
|
+
<nav>
|
|
388
|
+
{auth$.isLoggedIn.get()
|
|
389
|
+
? <a href="/profile">Profile</a>
|
|
390
|
+
: <a href="/login">Login</a>
|
|
391
|
+
}
|
|
392
|
+
</nav>
|
|
393
|
+
</header>
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Reactive lists with `For`
|
|
399
|
+
|
|
400
|
+
```tsx
|
|
401
|
+
const todos$ = observable([
|
|
402
|
+
{ id: 1, text: 'Learn Legend-State', done: false },
|
|
403
|
+
]);
|
|
404
|
+
|
|
405
|
+
function TodoList() {
|
|
406
|
+
return (
|
|
407
|
+
<ul>
|
|
408
|
+
<For each={todos$}>
|
|
409
|
+
{(todo$) => (
|
|
410
|
+
// item$ is already reactive — plugin skips inside For
|
|
411
|
+
<li
|
|
412
|
+
style={{ textDecoration: todo$.done.get() ? 'line-through' : 'none' }}
|
|
413
|
+
>
|
|
414
|
+
{todo$.text.get()}
|
|
415
|
+
</li>
|
|
416
|
+
)}
|
|
417
|
+
</For>
|
|
418
|
+
</ul>
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### Computed derived values
|
|
424
|
+
|
|
425
|
+
```tsx
|
|
426
|
+
import { computed } from '@legendapp/state';
|
|
427
|
+
|
|
428
|
+
const price$ = observable(100);
|
|
429
|
+
const qty$ = observable(2);
|
|
430
|
+
const discount$ = observable(0.1);
|
|
431
|
+
|
|
432
|
+
// Derived value
|
|
433
|
+
const total$ = computed(() =>
|
|
434
|
+
price$.get() * qty$.get() * (1 - discount$.get())
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
function OrderSummary() {
|
|
438
|
+
return (
|
|
439
|
+
<div>
|
|
440
|
+
<p>Price: {price$.get()}</p>
|
|
441
|
+
<p>Qty: {qty$.get()}</p>
|
|
442
|
+
<p>Total: {total$.get()}</p>
|
|
443
|
+
{/* Plugin wraps each expression individually */}
|
|
444
|
+
</div>
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
---
|
|
450
|
+
|
|
451
|
+
## API Reference
|
|
452
|
+
|
|
453
|
+
### `autoWrap(options?: PluginOptions): Plugin`
|
|
454
|
+
|
|
455
|
+
Returns a Vite plugin that transforms `.jsx` and `.tsx` files using `@usels/babel-plugin-legend-memo`.
|
|
456
|
+
|
|
457
|
+
**Parameters:**
|
|
458
|
+
- `options` (optional) — Configuration object. See [Configuration](#configuration).
|
|
459
|
+
|
|
460
|
+
**Returns:** Vite `Plugin` object
|
|
461
|
+
|
|
462
|
+
**Example:**
|
|
463
|
+
|
|
464
|
+
```typescript
|
|
465
|
+
import { autoWrap } from '@usels/vite-plugin-legend-memo';
|
|
466
|
+
|
|
467
|
+
const plugin = autoWrap({
|
|
468
|
+
componentName: 'Memo',
|
|
469
|
+
importSource: '@legendapp/state/react',
|
|
470
|
+
wrapReactiveChildren: true,
|
|
471
|
+
});
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### Type exports
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
import type { PluginOptions } from '@usels/vite-plugin-legend-memo';
|
|
478
|
+
|
|
479
|
+
const options: PluginOptions = {
|
|
480
|
+
allGet: false,
|
|
481
|
+
wrapReactiveChildren: true,
|
|
482
|
+
};
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## Troubleshooting
|
|
488
|
+
|
|
489
|
+
### `.get()` calls aren't being wrapped
|
|
490
|
+
|
|
491
|
+
1. Check plugin order — `autoWrap()` must come before `react()`
|
|
492
|
+
2. Check that your observable uses `$` suffix (or enable `allGet: true`)
|
|
493
|
+
3. Check if code is inside `observer()` — this is intentional (observer makes whole component reactive)
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
// Check 1: plugin order
|
|
497
|
+
plugins: [autoWrap(), react()] // ✅ correct order
|
|
498
|
+
|
|
499
|
+
// Check 2: $ suffix
|
|
500
|
+
const count$ = observable(0); // ✅ will be wrapped
|
|
501
|
+
const count = observable(0); // ❌ won't be wrapped (add allGet: true)
|
|
502
|
+
|
|
503
|
+
// Check 3: observer() is expected
|
|
504
|
+
const Comp = observer(() => {
|
|
505
|
+
return <div>{count$.get()}</div>; // intentionally not wrapped
|
|
506
|
+
});
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
### `Memo` is not defined error
|
|
510
|
+
|
|
511
|
+
The plugin auto-adds `import { Memo } from "@legendapp/state/react"` when wrapping. If you see this error:
|
|
512
|
+
|
|
513
|
+
1. Ensure `@legendapp/state` is installed: `npm install @legendapp/state`
|
|
514
|
+
2. If using a different import source, configure it: `autoWrap({ importSource: '...' })`
|
|
515
|
+
|
|
516
|
+
### Source maps not working
|
|
517
|
+
|
|
518
|
+
The plugin preserves source maps automatically. If DevTools shows incorrect locations:
|
|
519
|
+
|
|
520
|
+
```typescript
|
|
521
|
+
// vite.config.ts
|
|
522
|
+
export default defineConfig({
|
|
523
|
+
build: {
|
|
524
|
+
sourcemap: true, // Enable for production builds
|
|
525
|
+
},
|
|
526
|
+
});
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
### Performance: too many re-renders
|
|
530
|
+
|
|
531
|
+
If you see many components re-rendering, consider using `observer()` for entire components instead of fine-grained `<Memo>` boundaries:
|
|
532
|
+
|
|
533
|
+
```tsx
|
|
534
|
+
// Fine-grained (default) — each expression gets its own Memo
|
|
535
|
+
function Component() {
|
|
536
|
+
return <div>{a$.get()} {b$.get()} {c$.get()}</div>;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Component-level (use observer if whole component should update together)
|
|
540
|
+
const Component = observer(() => {
|
|
541
|
+
return <div>{a$.get()} {b$.get()} {c$.get()}</div>;
|
|
542
|
+
});
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
---
|
|
546
|
+
|
|
547
|
+
## Peer Dependencies
|
|
548
|
+
|
|
549
|
+
| Package | Required Version |
|
|
550
|
+
|---------|-----------------|
|
|
551
|
+
| `vite` | `>=4.0.0` |
|
|
552
|
+
| `@babel/core` | `>=7.0.0` |
|
|
553
|
+
| `@usels/babel-plugin-legend-memo` | `workspace:*` |
|
|
554
|
+
|
|
555
|
+
---
|
|
556
|
+
|
|
557
|
+
## See Also
|
|
558
|
+
|
|
559
|
+
- [@usels/babel-plugin-legend-memo](../babel) — The underlying Babel plugin and its full documentation
|
|
560
|
+
- [Legend-State Documentation](https://legendapp.com/dev/state/v3/)
|
|
561
|
+
- [Vite Plugin API](https://vitejs.dev/guide/api-plugin.html)
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Plugin } from 'vite';
|
|
2
|
+
import { PluginOptions } from '@usels/babel-plugin-legend-memo';
|
|
3
|
+
export { PluginOptions } from '@usels/babel-plugin-legend-memo';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Vite plugin that applies @usels/babel-plugin-legend-memo during transform.
|
|
7
|
+
*
|
|
8
|
+
* Wraps Legend-State observable .get() calls in JSX with <Auto> component
|
|
9
|
+
* for fine-grained reactive rendering.
|
|
10
|
+
*
|
|
11
|
+
* IMPORTANT: Must be placed BEFORE @vitejs/plugin-react in the plugins array,
|
|
12
|
+
* because `enforce: "pre"` ensures this runs before esbuild JSX transform.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* // vite.config.ts
|
|
17
|
+
* import { defineConfig } from 'vite';
|
|
18
|
+
* import react from '@vitejs/plugin-react';
|
|
19
|
+
* import { autoWrap } from '@usels/vite-plugin-legend-memo';
|
|
20
|
+
*
|
|
21
|
+
* export default defineConfig({
|
|
22
|
+
* plugins: [
|
|
23
|
+
* autoWrap({ importSource: '@usels/core' }),
|
|
24
|
+
* react(),
|
|
25
|
+
* ],
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
declare function autoWrap(opts?: PluginOptions): Plugin;
|
|
30
|
+
|
|
31
|
+
export { autoWrap, autoWrap as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Plugin } from 'vite';
|
|
2
|
+
import { PluginOptions } from '@usels/babel-plugin-legend-memo';
|
|
3
|
+
export { PluginOptions } from '@usels/babel-plugin-legend-memo';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Vite plugin that applies @usels/babel-plugin-legend-memo during transform.
|
|
7
|
+
*
|
|
8
|
+
* Wraps Legend-State observable .get() calls in JSX with <Auto> component
|
|
9
|
+
* for fine-grained reactive rendering.
|
|
10
|
+
*
|
|
11
|
+
* IMPORTANT: Must be placed BEFORE @vitejs/plugin-react in the plugins array,
|
|
12
|
+
* because `enforce: "pre"` ensures this runs before esbuild JSX transform.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* // vite.config.ts
|
|
17
|
+
* import { defineConfig } from 'vite';
|
|
18
|
+
* import react from '@vitejs/plugin-react';
|
|
19
|
+
* import { autoWrap } from '@usels/vite-plugin-legend-memo';
|
|
20
|
+
*
|
|
21
|
+
* export default defineConfig({
|
|
22
|
+
* plugins: [
|
|
23
|
+
* autoWrap({ importSource: '@usels/core' }),
|
|
24
|
+
* react(),
|
|
25
|
+
* ],
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
declare function autoWrap(opts?: PluginOptions): Plugin;
|
|
30
|
+
|
|
31
|
+
export { autoWrap, autoWrap as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
autoWrap: () => autoWrap,
|
|
34
|
+
default: () => index_default
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
function autoWrap(opts = {}) {
|
|
38
|
+
return {
|
|
39
|
+
name: "@usels/vite-plugin-legend-memo",
|
|
40
|
+
// Must run before esbuild JSX transform (before @vitejs/plugin-react)
|
|
41
|
+
enforce: "pre",
|
|
42
|
+
async transform(code, id) {
|
|
43
|
+
if (!/\.[jt]sx$/.test(id)) return null;
|
|
44
|
+
const babel = await import("@babel/core");
|
|
45
|
+
const result = await babel.transformAsync(code, {
|
|
46
|
+
filename: id,
|
|
47
|
+
plugins: [["@usels/babel-plugin-legend-memo", opts]],
|
|
48
|
+
parserOpts: {
|
|
49
|
+
plugins: ["jsx", "typescript"]
|
|
50
|
+
},
|
|
51
|
+
sourceMaps: true,
|
|
52
|
+
configFile: false,
|
|
53
|
+
babelrc: false
|
|
54
|
+
});
|
|
55
|
+
if (!result || !result.code) return null;
|
|
56
|
+
return {
|
|
57
|
+
code: result.code,
|
|
58
|
+
map: result.map ?? void 0
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
var index_default = autoWrap;
|
|
64
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
65
|
+
0 && (module.exports = {
|
|
66
|
+
autoWrap
|
|
67
|
+
});
|
|
68
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Plugin } from 'vite';\nimport type { PluginOptions } from '@usels/babel-plugin-legend-memo';\n\n/**\n * Vite plugin that applies @usels/babel-plugin-legend-memo during transform.\n *\n * Wraps Legend-State observable .get() calls in JSX with <Auto> component\n * for fine-grained reactive rendering.\n *\n * IMPORTANT: Must be placed BEFORE @vitejs/plugin-react in the plugins array,\n * because `enforce: \"pre\"` ensures this runs before esbuild JSX transform.\n *\n * @example\n * ```typescript\n * // vite.config.ts\n * import { defineConfig } from 'vite';\n * import react from '@vitejs/plugin-react';\n * import { autoWrap } from '@usels/vite-plugin-legend-memo';\n *\n * export default defineConfig({\n * plugins: [\n * autoWrap({ importSource: '@usels/core' }),\n * react(),\n * ],\n * });\n * ```\n */\nexport function autoWrap(opts: PluginOptions = {}): Plugin {\n return {\n name: '@usels/vite-plugin-legend-memo',\n // Must run before esbuild JSX transform (before @vitejs/plugin-react)\n enforce: 'pre',\n\n async transform(code, id) {\n // Only process .jsx and .tsx files\n if (!/\\.[jt]sx$/.test(id)) return null;\n\n // Lazy import @babel/core to avoid bundling it\n const babel = await import('@babel/core');\n\n const result = await babel.transformAsync(code, {\n filename: id,\n plugins: [['@usels/babel-plugin-legend-memo', opts]],\n parserOpts: {\n plugins: ['jsx', 'typescript'],\n },\n sourceMaps: true,\n configFile: false,\n babelrc: false,\n });\n\n if (!result || !result.code) return null;\n\n return {\n code: result.code,\n map: result.map ?? undefined,\n };\n },\n };\n}\n\nexport default autoWrap;\nexport type { PluginOptions };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA2BO,SAAS,SAAS,OAAsB,CAAC,GAAW;AACzD,SAAO;AAAA,IACL,MAAM;AAAA;AAAA,IAEN,SAAS;AAAA,IAET,MAAM,UAAU,MAAM,IAAI;AAExB,UAAI,CAAC,YAAY,KAAK,EAAE,EAAG,QAAO;AAGlC,YAAM,QAAQ,MAAM,OAAO,aAAa;AAExC,YAAM,SAAS,MAAM,MAAM,eAAe,MAAM;AAAA,QAC9C,UAAU;AAAA,QACV,SAAS,CAAC,CAAC,mCAAmC,IAAI,CAAC;AAAA,QACnD,YAAY;AAAA,UACV,SAAS,CAAC,OAAO,YAAY;AAAA,QAC/B;AAAA,QACA,YAAY;AAAA,QACZ,YAAY;AAAA,QACZ,SAAS;AAAA,MACX,CAAC;AAED,UAAI,CAAC,UAAU,CAAC,OAAO,KAAM,QAAO;AAEpC,aAAO;AAAA,QACL,MAAM,OAAO;AAAA,QACb,KAAK,OAAO,OAAO;AAAA,MACrB;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,gBAAQ;","names":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
function autoWrap(opts = {}) {
|
|
3
|
+
return {
|
|
4
|
+
name: "@usels/vite-plugin-legend-memo",
|
|
5
|
+
// Must run before esbuild JSX transform (before @vitejs/plugin-react)
|
|
6
|
+
enforce: "pre",
|
|
7
|
+
async transform(code, id) {
|
|
8
|
+
if (!/\.[jt]sx$/.test(id)) return null;
|
|
9
|
+
const babel = await import("@babel/core");
|
|
10
|
+
const result = await babel.transformAsync(code, {
|
|
11
|
+
filename: id,
|
|
12
|
+
plugins: [["@usels/babel-plugin-legend-memo", opts]],
|
|
13
|
+
parserOpts: {
|
|
14
|
+
plugins: ["jsx", "typescript"]
|
|
15
|
+
},
|
|
16
|
+
sourceMaps: true,
|
|
17
|
+
configFile: false,
|
|
18
|
+
babelrc: false
|
|
19
|
+
});
|
|
20
|
+
if (!result || !result.code) return null;
|
|
21
|
+
return {
|
|
22
|
+
code: result.code,
|
|
23
|
+
map: result.map ?? void 0
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
var index_default = autoWrap;
|
|
29
|
+
export {
|
|
30
|
+
autoWrap,
|
|
31
|
+
index_default as default
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Plugin } from 'vite';\nimport type { PluginOptions } from '@usels/babel-plugin-legend-memo';\n\n/**\n * Vite plugin that applies @usels/babel-plugin-legend-memo during transform.\n *\n * Wraps Legend-State observable .get() calls in JSX with <Auto> component\n * for fine-grained reactive rendering.\n *\n * IMPORTANT: Must be placed BEFORE @vitejs/plugin-react in the plugins array,\n * because `enforce: \"pre\"` ensures this runs before esbuild JSX transform.\n *\n * @example\n * ```typescript\n * // vite.config.ts\n * import { defineConfig } from 'vite';\n * import react from '@vitejs/plugin-react';\n * import { autoWrap } from '@usels/vite-plugin-legend-memo';\n *\n * export default defineConfig({\n * plugins: [\n * autoWrap({ importSource: '@usels/core' }),\n * react(),\n * ],\n * });\n * ```\n */\nexport function autoWrap(opts: PluginOptions = {}): Plugin {\n return {\n name: '@usels/vite-plugin-legend-memo',\n // Must run before esbuild JSX transform (before @vitejs/plugin-react)\n enforce: 'pre',\n\n async transform(code, id) {\n // Only process .jsx and .tsx files\n if (!/\\.[jt]sx$/.test(id)) return null;\n\n // Lazy import @babel/core to avoid bundling it\n const babel = await import('@babel/core');\n\n const result = await babel.transformAsync(code, {\n filename: id,\n plugins: [['@usels/babel-plugin-legend-memo', opts]],\n parserOpts: {\n plugins: ['jsx', 'typescript'],\n },\n sourceMaps: true,\n configFile: false,\n babelrc: false,\n });\n\n if (!result || !result.code) return null;\n\n return {\n code: result.code,\n map: result.map ?? undefined,\n };\n },\n };\n}\n\nexport default autoWrap;\nexport type { PluginOptions };\n"],"mappings":";AA2BO,SAAS,SAAS,OAAsB,CAAC,GAAW;AACzD,SAAO;AAAA,IACL,MAAM;AAAA;AAAA,IAEN,SAAS;AAAA,IAET,MAAM,UAAU,MAAM,IAAI;AAExB,UAAI,CAAC,YAAY,KAAK,EAAE,EAAG,QAAO;AAGlC,YAAM,QAAQ,MAAM,OAAO,aAAa;AAExC,YAAM,SAAS,MAAM,MAAM,eAAe,MAAM;AAAA,QAC9C,UAAU;AAAA,QACV,SAAS,CAAC,CAAC,mCAAmC,IAAI,CAAC;AAAA,QACnD,YAAY;AAAA,UACV,SAAS,CAAC,OAAO,YAAY;AAAA,QAC/B;AAAA,QACA,YAAY;AAAA,QACZ,YAAY;AAAA,QACZ,SAAS;AAAA,MACX,CAAC;AAED,UAAI,CAAC,UAAU,CAAC,OAAO,KAAM,QAAO;AAEpC,aAAO;AAAA,QACL,MAAM,OAAO;AAAA,QACb,KAAK,OAAO,OAAO;AAAA,MACrB;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,gBAAQ;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@usels/vite-plugin-legend-memo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Vite plugin to auto-wrap Legend-State observable .get() calls in JSX with <Auto> component",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"peerDependencies": {
|
|
16
|
+
"@babel/core": ">=7.0.0",
|
|
17
|
+
"vite": ">=4.0.0",
|
|
18
|
+
"@usels/babel-plugin-legend-memo": "0.1.0"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@babel/core": "^7.24.0",
|
|
25
|
+
"@types/babel__core": "^7.20.0",
|
|
26
|
+
"tsup": "^8.0.0",
|
|
27
|
+
"typescript": "^5.0.0",
|
|
28
|
+
"vite": "^5.0.0",
|
|
29
|
+
"@usels/babel-plugin-legend-memo": "0.1.0"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsup",
|
|
33
|
+
"dev": "tsup --watch",
|
|
34
|
+
"typecheck": "tsc --noEmit"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
import type { PluginOptions } from '@usels/babel-plugin-legend-memo';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Vite plugin that applies @usels/babel-plugin-legend-memo during transform.
|
|
6
|
+
*
|
|
7
|
+
* Wraps Legend-State observable .get() calls in JSX with <Auto> component
|
|
8
|
+
* for fine-grained reactive rendering.
|
|
9
|
+
*
|
|
10
|
+
* IMPORTANT: Must be placed BEFORE @vitejs/plugin-react in the plugins array,
|
|
11
|
+
* because `enforce: "pre"` ensures this runs before esbuild JSX transform.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* // vite.config.ts
|
|
16
|
+
* import { defineConfig } from 'vite';
|
|
17
|
+
* import react from '@vitejs/plugin-react';
|
|
18
|
+
* import { autoWrap } from '@usels/vite-plugin-legend-memo';
|
|
19
|
+
*
|
|
20
|
+
* export default defineConfig({
|
|
21
|
+
* plugins: [
|
|
22
|
+
* autoWrap({ importSource: '@usels/core' }),
|
|
23
|
+
* react(),
|
|
24
|
+
* ],
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function autoWrap(opts: PluginOptions = {}): Plugin {
|
|
29
|
+
return {
|
|
30
|
+
name: '@usels/vite-plugin-legend-memo',
|
|
31
|
+
// Must run before esbuild JSX transform (before @vitejs/plugin-react)
|
|
32
|
+
enforce: 'pre',
|
|
33
|
+
|
|
34
|
+
async transform(code, id) {
|
|
35
|
+
// Only process .jsx and .tsx files
|
|
36
|
+
if (!/\.[jt]sx$/.test(id)) return null;
|
|
37
|
+
|
|
38
|
+
// Lazy import @babel/core to avoid bundling it
|
|
39
|
+
const babel = await import('@babel/core');
|
|
40
|
+
|
|
41
|
+
const result = await babel.transformAsync(code, {
|
|
42
|
+
filename: id,
|
|
43
|
+
plugins: [['@usels/babel-plugin-legend-memo', opts]],
|
|
44
|
+
parserOpts: {
|
|
45
|
+
plugins: ['jsx', 'typescript'],
|
|
46
|
+
},
|
|
47
|
+
sourceMaps: true,
|
|
48
|
+
configFile: false,
|
|
49
|
+
babelrc: false,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (!result || !result.code) return null;
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
code: result.code,
|
|
56
|
+
map: result.map ?? undefined,
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default autoWrap;
|
|
63
|
+
export type { PluginOptions };
|
package/tsconfig.json
ADDED
package/tsup.config.ts
ADDED