next-live 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.
@@ -0,0 +1,487 @@
1
+ # The module registry
2
+
3
+ [← Getting started](./01-getting-started.md) · [Docs index](./README.md) · [Sharing libraries →](./03-sharing-your-app-libraries.md)
4
+
5
+ This is the core concept. Everything else follows from it.
6
+
7
+ If you only remember one thing: **live snippets cannot import anything you did
8
+ not register.** You choose what they can reach, and you give each thing a name.
9
+
10
+ ## The mental model
11
+
12
+ **There is no npm in the browser.** No bundler, no package resolution, no
13
+ network fetch for `lodash`. Your application's JavaScript contains only what
14
+ *you* imported when it was built.
15
+
16
+ In normal app code:
17
+
18
+ ```tsx
19
+ import { Button } from '@/components/ui/button';
20
+ ```
21
+
22
+ Your bundler resolves that path at build time and ships the file. A live snippet
23
+ is a **string** that was not part of the build. When it says:
24
+
25
+ ```tsx
26
+ import _ from 'lodash';
27
+ ```
28
+
29
+ that does not install or fetch lodash. It means "look up `lodash` in the
30
+ registry I was given."
31
+
32
+ You hand real code over through the `modules` prop:
33
+
34
+ ```tsx
35
+ <LiveProvider
36
+ code={source}
37
+ modules={{
38
+ '@app/store': defineLoader(() => import('@/lib/store')),
39
+ }}
40
+ />
41
+ ```
42
+
43
+ `next-live` compiles `import x from 'y'` into a lookup against that map. If the
44
+ specifier is not registered, the snippet fails with a clear error naming it.
45
+
46
+ Think of `modules` as a **phone book**:
47
+
48
+ | Snippet writes | Registry key | What loads |
49
+ |---|---|---|
50
+ | `import { Button } from '@app/ui'` | `'@app/ui'` | Your UI module |
51
+ | `import { useCart } from '@app/store'` | `'@app/store'` | Your store |
52
+ | `import _ from 'lodash'` | *(missing)* | Error |
53
+
54
+ The snippet only knows the label. You decide which real code it points to.
55
+
56
+ ## Walkthrough: from string to component
57
+
58
+ **1. Host setup**
59
+
60
+ ```tsx
61
+ <LiveProvider
62
+ code={source}
63
+ modules={{
64
+ '@app/format': defineLoader(() => import('@/lib/format')),
65
+ }}
66
+ >
67
+ <LivePreview />
68
+ </LiveProvider>
69
+ ```
70
+
71
+ **2. Snippet source**
72
+
73
+ ```tsx
74
+ import { formatMoney } from '@app/format';
75
+
76
+ export default function Price() {
77
+ return <p>{formatMoney(99)}</p>;
78
+ }
79
+ ```
80
+
81
+ **3. Compile** - the import becomes a registry lookup for `'@app/format'`.
82
+
83
+ **4. Load** - the loader runs `import('@/lib/format')` (lazy, code-split).
84
+
85
+ **5. Render** - `formatMoney` from your real file is passed to the snippet.
86
+
87
+ ## `scope` vs `modules`
88
+
89
+ The `scope` prop injects globals; snippets cannot use `import`:
90
+
91
+ ```tsx
92
+ <LiveProvider scope={{ useState, Button, formatMoney }} />
93
+ ```
94
+
95
+ ```tsx
96
+ export default () => <Button>{formatMoney(42)}</Button>;
97
+ ```
98
+
99
+ **next-live** uses real imports against a registry:
100
+
101
+ ```tsx
102
+ modules={{
103
+ '@app/ui': defineLoader(() => import('@/components/ui')),
104
+ '@app/format': defineLoader(() => import('@/lib/format')),
105
+ }}
106
+ ```
107
+
108
+ ```tsx
109
+ import { Button } from '@app/ui';
110
+ import { formatMoney } from '@app/format';
111
+ ```
112
+
113
+ `react` is built in. Everything else you register. Prefer `modules` over `scope`
114
+ for new code.
115
+
116
+ ## The key is just a string
117
+
118
+ A registry key does **not** have to be a real path or a real package name. It is
119
+ whatever you want snippet authors to type. These could all point at the same
120
+ file:
121
+
122
+ ```tsx
123
+ modules={{
124
+ '@app/store': defineLoader(() => import('@/lib/store')),
125
+ '@/store': defineLoader(() => import('@/lib/store')),
126
+ 'my-store': defineLoader(() => import('@/lib/store')),
127
+ }}
128
+ ```
129
+
130
+ Pick names that read like a deliberate SDK, not a mirror of your folder tree.
131
+ See [Scaling](./04-scaling.md#design-an-sdk-surface-not-a-mirror-of-your-codebase).
132
+
133
+ ## Common mistakes
134
+
135
+ **Typo in a named export** - the module loads but the export is missing. Fix
136
+ the import or re-export from your SDK module.
137
+
138
+ **Two paths to the same library** - registering a different import path than
139
+ your app uses creates two instances (two stores, two React copies). Keep one
140
+ canonical path. See [Sharing libraries](./03-sharing-your-app-libraries.md).
141
+
142
+ **Expecting npm packages to work automatically** - register them explicitly:
143
+
144
+ ```tsx
145
+ 'date-fns': defineLoader(() => import('date-fns')),
146
+ ```
147
+
148
+ **Unused typo imports** - stripped at compile time like TypeScript. Only imports
149
+ that survive compilation are resolved.
150
+
151
+ ## Two ways to register
152
+
153
+ ### As a loader: the default choice
154
+
155
+ ```ts
156
+ '@app/store': defineLoader(() => import('@/lib/store'))
157
+ ```
158
+
159
+ A dynamic import, so the bundler code-splits it and the browser fetches it only
160
+ if a snippet actually imports that specifier.
161
+
162
+ ### As a value
163
+
164
+ ```ts
165
+ import * as store from '@/lib/store';
166
+ modules={{ '@app/store': store }}
167
+ ```
168
+
169
+ Simpler to read, but the module is now in your page bundle for every visitor,
170
+ used or not. Fine for something tiny; wrong for anything large.
171
+
172
+ **Use loaders.** A registry of 300 loaders costs nothing at runtime - only
173
+ specifiers that appear in the compiled snippet are ever resolved. See
174
+ [Scaling](./04-scaling.md) for the measured difference.
175
+
176
+ ## Organizing your registry in separate files
177
+
178
+ You do not need a giant `modules={{ … }}` on `<LiveProvider>`. Put the full list
179
+ in a dedicated SDK folder and import one object:
180
+
181
+ ```tsx
182
+ import { liveModules } from '@/lib/live-sdk';
183
+
184
+ <LiveProvider code={source} modules={liveModules}>
185
+ <LivePreview />
186
+ </LiveProvider>
187
+ ```
188
+
189
+ **100 registered loaders does not mean 100 network requests.** Each entry is a
190
+ small loader function. next-live scans the compiled snippet and only runs
191
+ loaders for specifiers the snippet actually imports.
192
+
193
+ ### Recommended layout
194
+
195
+ ```
196
+ lib/live-sdk/
197
+ index.ts # export liveModules
198
+ ui-modules.ts # manual group
199
+ format-modules.ts
200
+ store-modules.ts
201
+ vendor.ts # prefix / npm loaders
202
+ app-modules-glob.ts # optional auto-register
203
+ modules/
204
+ ui.ts # re-exports for snippets
205
+ format.ts
206
+ store.ts
207
+ ```
208
+
209
+ ### Option A: manual groups + `createRegistry`
210
+
211
+ ```ts
212
+ // lib/live-sdk/format-modules.ts
213
+ export const formatModules = {
214
+ '@app/format': defineLoader(() => import('./modules/format')),
215
+ '@app/x': defineLoader(() => import('@/lib/x')),
216
+ };
217
+
218
+ // lib/live-sdk/index.ts
219
+ export const liveModules = createRegistry(
220
+ vendorModules,
221
+ uiModules,
222
+ formatModules,
223
+ storeModules,
224
+ );
225
+ ```
226
+
227
+ ### Option B: `registryFromGlob`
228
+
229
+ ```ts
230
+ export const appModulesFromGlob = registryFromGlob(
231
+ import.meta.glob('./modules/*.ts'),
232
+ (path) => `@app/${path.split('/').pop()!.replace(/\.tsx?$/, '')}`,
233
+ );
234
+ ```
235
+
236
+ Add a file under `modules/` and it is registered automatically.
237
+
238
+ ### Option C: hybrid
239
+
240
+ Manual groups for special cases (prefix loaders, npm packages) plus glob for
241
+ the `./modules/` surface:
242
+
243
+ ```ts
244
+ export const liveModules = createRegistry(vendorModules, appModulesFromGlob);
245
+ ```
246
+
247
+ The playground uses Option A for `@app/ui`, `@app/format`, and `@app/store`,
248
+ with `app-modules-glob.ts` kept as a ready-made Option B example.
249
+
250
+ ## Every import form works
251
+
252
+ Given `modules={{ '@app/ui': uiModule }}`:
253
+
254
+ | Snippet writes | Gets |
255
+ |---|---|
256
+ | `import ui from '@app/ui'` | the module's `default`, or the module itself if it has none |
257
+ | `import { Button } from '@app/ui'` | the named export |
258
+ | `import * as ui from '@app/ui'` | the namespace |
259
+ | `import '@app/ui'` | nothing; runs for side effects |
260
+
261
+ Registered values are treated as **CommonJS exports objects**: a value with its
262
+ own `default` key is unwrapped, and anything else *is* the default. That makes
263
+ the common shapes work without configuration:
264
+
265
+ ```ts
266
+ '@app/config': { apiUrl: 'https://…' }
267
+ // import config from '@app/config' → the object
268
+ // import { apiUrl } from '@app/config' → the string
269
+ ```
270
+
271
+ ### The one ambiguous case
272
+
273
+ Because a value with its own `default` key is unwrapped, an object that
274
+ genuinely contains the word `default` gets unwrapped too:
275
+
276
+ ```ts
277
+ '@app/theme': { default: 'dark', light: '#fff' }
278
+ // import theme from '@app/theme' → 'dark', not the object
279
+ ```
280
+
281
+ When that is not what you meant, say so explicitly with `defineModule`:
282
+
283
+ ```ts
284
+ import { defineModule } from 'next-live';
285
+
286
+ modules={{
287
+ '@app/theme': defineModule({ default: { default: 'dark', light: '#fff' } }),
288
+ }}
289
+ // import theme from '@app/theme' → the whole object
290
+ ```
291
+
292
+ `default` and `exports.default` address the same slot - ESM makes no distinction
293
+ between a default export and a named export called `default`.
294
+
295
+ ## Always available
296
+
297
+ Registered for you, no configuration needed:
298
+
299
+ - `react`
300
+ - `react/jsx-runtime`
301
+ - `react/jsx-dev-runtime`
302
+
303
+ The two JSX runtimes are not optional, every JSX tag compiles to a call into
304
+ one of them, so without them no snippet would render at all. They come from
305
+ **your** React, which is what lets hooks and context work across the boundary.
306
+
307
+ Your entries merge over these, so you can substitute a React shim if you need to.
308
+
309
+ `react-dom` is deliberately not included; register it explicitly if snippets
310
+ need `createPortal`.
311
+
312
+ ## Prefix entries: one key for a whole subtree
313
+
314
+ A key ending in `/` claims everything beneath it, and its loader receives the
315
+ **full specifier**:
316
+
317
+ ```ts
318
+ 'big-lib/': defineLoader((specifier) => {
319
+ const subpath = specifier.slice('big-lib/'.length);
320
+ return import(`big-lib/${subpath}`);
321
+ }),
322
+ ```
323
+
324
+ Now `big-lib/charts/BarChart` and `big-lib/format/currency` both resolve without
325
+ enumerating them. There is a build-time cost - see
326
+ [Scaling](./04-scaling.md#deep-subpaths).
327
+
328
+ ## Subpath fallback
329
+
330
+ `resolveSubpaths` lets `pkg/Sub` resolve against a registered `pkg` by reading
331
+ `Sub` as a property:
332
+
333
+ ```tsx
334
+ <LiveProvider resolveSubpaths modules={{ 'big-lib': bigLib }} />
335
+ // 'big-lib/Chart' → bigLib.Chart
336
+ ```
337
+
338
+ **Off by default, deliberately.** It is right for barrel-shaped packages and
339
+ wrong for packages whose subpaths are not re-exported from the barrel - and a
340
+ silently wrong value is worse than a clear error.
341
+
342
+ ## Generating entries from the filesystem
343
+
344
+ ### Turbopack: `import.meta.glob`
345
+
346
+ ```ts
347
+ import { registryFromGlob } from 'next-live';
348
+
349
+ export const storeModules = registryFromGlob(
350
+ import.meta.glob('./modules/*.ts'),
351
+ (path) => {
352
+ const name = path.split('/').pop()?.replace(/\.tsx?$/, '');
353
+ return name ? `@app/${name}` : null;
354
+ },
355
+ );
356
+ ```
357
+
358
+ ### Webpack: `require.context`
359
+
360
+ `registryFromGlob` accepts any `Record<string, () => Promise<unknown>>`. Under
361
+ webpack, build that object from `require.context`:
362
+
363
+ ```ts
364
+ import { registryFromGlob } from 'next-live';
365
+
366
+ const context = require.context('./modules', false, /\.tsx?$/);
367
+
368
+ export const storeModules = registryFromGlob(
369
+ Object.fromEntries(
370
+ context.keys().map((key) => [
371
+ key,
372
+ () => Promise.resolve(context(key)),
373
+ ]),
374
+ ),
375
+ (path) => {
376
+ const name = path.replace(/^\.\//, '').replace(/\.tsx?$/, '');
377
+ return `@app/${name}`;
378
+ },
379
+ );
380
+ ```
381
+
382
+ See [Scaling](./04-scaling.md#generate-entries-from-the-filesystem) for the
383
+ directory rule that silently breaks globs.
384
+
385
+ ## Styling and Tailwind
386
+
387
+ Snippets can use `className`, but Tailwind only generates CSS for classes it
388
+ finds in your **host** source at build time. Arbitrary utility strings inside
389
+ stored snippet source will not produce styles unless you safelist them or add
390
+ a `@source` scan target.
391
+
392
+ The reliable pattern is to register UI components whose classes are already
393
+ compiled:
394
+
395
+ ```ts
396
+ // lib/live-sdk/modules/ui.ts
397
+ export { Button, Card, Badge } from '@/components/ui';
398
+ ```
399
+
400
+ ```tsx
401
+ import { Card, Button } from '@app/ui';
402
+
403
+ export default function App() {
404
+ return (
405
+ <Card>
406
+ <Button>Styled by the host bundle</Button>
407
+ </Card>
408
+ );
409
+ }
410
+ ```
411
+
412
+ See [Troubleshooting: Tailwind in snippets](./07-troubleshooting.md#tailwind-classes-in-my-snippet-do-nothing).
413
+
414
+ ## Asset imports are ignored
415
+
416
+ `import './styles.css'` resolves to an empty module rather than failing, so a
417
+ snippet pasted out of a real file still runs. Applies to `.css`, `.scss`,
418
+ `.svg`, images, and fonts.
419
+
420
+ ## Free variables: the `scope` prop
421
+
422
+ For legacy snippets, `scope` injects values as bare identifiers with
423
+ no import at all:
424
+
425
+ ```tsx
426
+ <LiveProvider scope={{ formatMoney, t }} />
427
+ ```
428
+
429
+ ```tsx
430
+ export default () => <b>{formatMoney(42)}</b>; // no import needed
431
+ ```
432
+
433
+ Both mechanisms work together. Prefer `modules` - an explicit import says where
434
+ something came from, and snippets stay closer to real files. Keys that are not
435
+ valid identifiers, or that collide with the injected names (`module`, `exports`,
436
+ `require`, `React`, `render`), are skipped with a console warning.
437
+
438
+ ## Runtime props
439
+
440
+ `props` are handed to the component **by reference**, not serialized:
441
+
442
+ ```tsx
443
+ <LivePreview props={{ panel, user, store }} />
444
+ ```
445
+
446
+ ```tsx
447
+ export default function App({ panel, user }) {
448
+ return <button onClick={() => panel.setSize(17)}>{user.name}</button>;
449
+ }
450
+ ```
451
+
452
+ Because nothing is cloned, a snippet calling `panel.setSize(17)` mutates the same
453
+ object your app holds and your UI updates. Class instances, functions, and live
454
+ handles all survive. This is only possible because snippets run in your page -
455
+ an iframe sandbox could not do it.
456
+
457
+ `props` on `<LivePreview>` merge over `props` on `<LiveProvider>`.
458
+
459
+ ## When a module is missing
460
+
461
+ ```
462
+ Module '@ui/coree' is not registered in the next-live scope.
463
+
464
+ Did you mean '@ui/core'?
465
+
466
+ Registered modules (7): react, react/jsx-runtime, react/jsx-dev-runtime,
467
+ '@app/store', '@app/ui', 'big-lib/', 'date-fns'
468
+
469
+ next-live does not bundle npm packages - pass them in explicitly:
470
+ <LiveProvider modules={{ '@ui/coree': theModule }} />
471
+ ```
472
+
473
+ One thing to know: an import a snippet never *uses* is removed by the TypeScript
474
+ transform before resolution runs, exactly as `tsc` would. So an unused typo does
475
+ not error - it simply disappears.
476
+
477
+ ## What the registry is not
478
+
479
+ It bounds what snippets can **conveniently** reach, not what they **can** reach.
480
+ Evaluated code still has `window`, `fetch`, `document`, and your cookies.
481
+
482
+ Treat the registry as module resolution and ergonomics. It is not a security
483
+ boundary, see [Security](./05-security.md).
484
+
485
+ ---
486
+
487
+ [← Getting started](./01-getting-started.md) · [Docs index](./README.md) · [Sharing libraries →](./03-sharing-your-app-libraries.md)
@@ -0,0 +1,206 @@
1
+ # Sharing libraries between your app and your snippets
2
+
3
+ [← Module registry](./02-module-registry.md) · [Docs index](./README.md) · [Scaling →](./04-scaling.md)
4
+
5
+ ## The question
6
+
7
+ Your app already uses a charting library and a store in its own components:
8
+
9
+ ```tsx
10
+ // app/dashboard/page.tsx, your normal application code
11
+ import { BarChart } from 'my-charts';
12
+ import { useAppStore } from '@/store';
13
+ ```
14
+
15
+ And you register the same things so snippets can use them:
16
+
17
+ ```tsx
18
+ modules={{
19
+ 'my-charts': defineLoader(() => import('my-charts')),
20
+ '@app/store': defineLoader(() => import('@/store')),
21
+ }}
22
+ ```
23
+
24
+ That looks like importing the same library twice. Is it?
25
+
26
+ ## The answer: one instance, not two
27
+
28
+ **No, there is exactly one copy.** JavaScript modules are instantiated once per
29
+ resolved specifier. Your static `import` and the registry's dynamic `import()`
30
+ resolve to the same module, so the bundler hands both the same object. This is
31
+ the same reason `import React from 'react'` in fifty files gives you one React.
32
+
33
+ This is verified in the playground, not assumed. The host page imports a module
34
+ and stamps a marker on it:
35
+
36
+ ```tsx
37
+ import HostWidget from '@demo/vendor/Widget';
38
+ (HostWidget as Record<string, unknown>).__owner = 'host-app';
39
+ ```
40
+
41
+ A snippet imports the *same specifier* through the registry and reads it back:
42
+
43
+ ```tsx
44
+ import Widget from '@demo/vendor/Widget';
45
+ export default () => <b>Widget.__owner = {String(Widget.__owner)}</b>;
46
+ ```
47
+
48
+ It renders `Widget.__owner = host-app`. One object. Run the **Shared instance** demo in `/playground` to see it. The **`/apps`**
49
+ shell demo shows the same pattern with `@app/store` and `@app/format` - the
50
+ header cart count updates when a snippet calls `addItem` through the registry.
51
+
52
+ ## Why this matters much more than bundle size
53
+
54
+ The obvious worry is downloading a library twice. That is real but minor. The
55
+ serious consequence is **state**.
56
+
57
+ If a snippet got its own copy of your store module, it would get its own
58
+ *store*. Your app's cart and the snippet's cart would be two unrelated objects. The snippet
59
+ would appear to work - no error, no warning, while silently sharing nothing.
60
+ Every bug report would be "my changes don't show up".
61
+
62
+ Because there is one instance, the store a snippet imports **is** your store:
63
+
64
+ ```tsx
65
+ // snippet
66
+ import { useAppStore } from '@app/store';
67
+
68
+ export default function App() {
69
+ const cart = useAppStore((s) => s.cart); // your app's cart
70
+ const add = useAppStore((s) => s.addItem); // updates your app's UI too
71
+ return <button onClick={() => add('x')}>{cart.length} items</button>;
72
+ }
73
+ ```
74
+
75
+ The same applies to React itself, React context, and any live object you pass
76
+ through `props`.
77
+
78
+ ## No second download either
79
+
80
+ When your app already imports a module statically, it is in a chunk the page has
81
+ already loaded. A snippet importing the same specifier gets the loaded module
82
+ back - the browser fetches nothing.
83
+
84
+ Measured in the playground: selecting the snippet that imports a
85
+ host-already-imported module fetched **zero** additional chunks.
86
+
87
+ The practical consequence is a nice one:
88
+
89
+ - If your app **already uses** the library, registering it as a loader costs
90
+ nothing extra - the loader just hands over what is already there.
91
+ - If your app **does not** use it, the loader keeps it out of your bundle until
92
+ a snippet asks for it.
93
+
94
+ Either way, registering as a loader is the right call. There is no case where
95
+ registering by value is better. See [Scaling](./04-scaling.md).
96
+
97
+ ## When you really do get two copies
98
+
99
+ The reassuring answer has edges. These are the cases that bite:
100
+
101
+ ### 1. Two versions installed
102
+
103
+ The most common cause. If your app depends on `zustand@4` and some other
104
+ dependency pulls `zustand@5`, npm may install both, and they are genuinely two
105
+ different modules with two different stores.
106
+
107
+ Check before you debug anything else:
108
+
109
+ ```bash
110
+ npm ls zustand
111
+ npm ls react
112
+ ```
113
+
114
+ If you see more than one version, deduplicate:
115
+
116
+ ```bash
117
+ npm dedupe
118
+ ```
119
+
120
+ or pin a single version with an `overrides` entry in your root `package.json`:
121
+
122
+ ```json
123
+ {
124
+ "overrides": {
125
+ "zustand": "5.0.2"
126
+ }
127
+ }
128
+ ```
129
+
130
+ **Symptom:** state that will not sync, or - for React, `Invalid hook call` and
131
+ "more than one copy of React".
132
+
133
+ ### 2. Different specifiers are different modules
134
+
135
+ `zustand` and `zustand/vanilla` are two modules, even though both "are Zustand".
136
+ If your app imports one and your registry maps the other, snippets get the other
137
+ one's exports.
138
+
139
+ Register the specifier your app actually uses.
140
+
141
+ ### 3. You registered a copy, not the module
142
+
143
+ Spreading a module into a new object breaks live bindings and, for a store,
144
+ hands over a snapshot rather than the store:
145
+
146
+ ```ts
147
+ '@app/store': { ...storeModule } // ❌ a copy
148
+ '@app/store': defineLoader(() => import('@/store')) // ✅ the module
149
+ ```
150
+
151
+ ### 4. Server and client are separate instances
152
+
153
+ Node and the browser instantiate modules separately. This never affects
154
+ snippets, because `next-live` only evaluates on the client - but it is worth
155
+ knowing if you keep module-level state and expect it to survive SSR.
156
+
157
+ ## The recommended pattern
158
+
159
+ Do not register third-party packages directly. Register a **thin re-export
160
+ module that you own**:
161
+
162
+ ```ts
163
+ // lib/live-sdk/modules/store.ts
164
+ export { useAppStore, addItem, clearCart } from '@/store';
165
+ ```
166
+
167
+ ```ts
168
+ // lib/live-sdk/modules/charts.ts
169
+ export { BarChart, LineChart } from 'my-charts';
170
+ ```
171
+
172
+ Four reasons this is better than pointing at the package:
173
+
174
+ 1. **One path to the thing.** There is no way to accidentally register a
175
+ different specifier than your app uses.
176
+ 2. **You control the surface.** Snippet authors get the handful of exports you
177
+ support, not every module in the package.
178
+ 3. **You can refactor.** Move or rename the real implementation and update one
179
+ re-export; every stored snippet keeps working.
180
+ 4. **It reads as an API.** `@app/charts` is a contract. A deep path into a
181
+ third-party package is an implementation detail leaking into your users' code.
182
+
183
+ ## How to check this in your own app
184
+
185
+ Drop this snippet into your control panel once, as a smoke test:
186
+
187
+ ```tsx
188
+ import { useAppStore } from '@app/store';
189
+
190
+ export default function InstanceCheck() {
191
+ const state = useAppStore((s) => s);
192
+ return (
193
+ <pre>
194
+ store keys: {Object.keys(state).join(', ')}
195
+ {'\n'}same instance as host: change something in the app UI and watch this update
196
+ </pre>
197
+ );
198
+ }
199
+ ```
200
+
201
+ If mutating state from your app's own UI updates this snippet live, you have one
202
+ instance and everything above holds.
203
+
204
+ ---
205
+
206
+ [← Module registry](./02-module-registry.md) · [Docs index](./README.md) · [Scaling →](./04-scaling.md)