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.
- package/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +142 -0
- package/dist/editor.cjs +247 -0
- package/dist/editor.cjs.map +1 -0
- package/dist/editor.d.cts +80 -0
- package/dist/editor.d.ts +80 -0
- package/dist/editor.js +225 -0
- package/dist/editor.js.map +1 -0
- package/dist/index.cjs +1265 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +580 -0
- package/dist/index.d.ts +580 -0
- package/dist/index.js +1192 -0
- package/dist/index.js.map +1 -0
- package/dist/server.cjs +287 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +142 -0
- package/dist/server.d.ts +142 -0
- package/dist/server.js +282 -0
- package/dist/server.js.map +1 -0
- package/dist/shared.js +30 -0
- package/dist/shared.js.map +1 -0
- package/docs/01-getting-started.md +244 -0
- package/docs/02-module-registry.md +487 -0
- package/docs/03-sharing-your-app-libraries.md +206 -0
- package/docs/04-scaling.md +234 -0
- package/docs/05-security.md +204 -0
- package/docs/06-api-reference.md +337 -0
- package/docs/07-troubleshooting.md +289 -0
- package/docs/08-integration-guide.md +340 -0
- package/docs/09-non-ui-snippets.md +124 -0
- package/docs/10-validating-in-ci.md +192 -0
- package/docs/README.md +76 -0
- package/package.json +105 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# Scaling to many apps
|
|
2
|
+
|
|
3
|
+
[← Sharing libraries](./03-sharing-your-app-libraries.md) · [Docs index](./README.md) · [Security →](./05-security.md)
|
|
4
|
+
|
|
5
|
+
For an app hosting hundreds of snippets with a large SDK surface, this is where
|
|
6
|
+
things go wrong, and it is mostly about *how* you register, not *how much*.
|
|
7
|
+
|
|
8
|
+
## Register loaders, not values
|
|
9
|
+
|
|
10
|
+
Registering a module **by value** means your page imports it statically, so it
|
|
11
|
+
ships to every visitor whether or not any snippet uses it:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import * as charts from 'my-charts'; // ❌ in the page bundle, always
|
|
15
|
+
modules={{ 'my-charts': charts }}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Register a **loader** and it becomes a dynamic import, which the bundler
|
|
19
|
+
code-splits and fetches only when a snippet imports that specifier:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
modules={{ 'my-charts': defineLoader(() => import('my-charts')) }} // ✅
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Measured on the playground with a 192 KB vendor module:
|
|
26
|
+
|
|
27
|
+
| | initial JS for the page |
|
|
28
|
+
|---|---|
|
|
29
|
+
| registered by value | 827 KB |
|
|
30
|
+
| registered as a loader | **666 KB** |
|
|
31
|
+
|
|
32
|
+
With the loader, the module is absent from every initially-loaded chunk, and the
|
|
33
|
+
browser fetches its chunk at the moment a snippet imports it - verified by
|
|
34
|
+
watching network activity, not inferred.
|
|
35
|
+
|
|
36
|
+
**A registry of 300 loaders costs nothing.** `next-live` only resolves
|
|
37
|
+
specifiers that appear in the compiled snippet, so unused entries are never
|
|
38
|
+
touched. Your provider can list everything without penalty.
|
|
39
|
+
|
|
40
|
+
> If your app already imports the library for its own use, the loader costs
|
|
41
|
+
> nothing extra either, it hands over the already-loaded module. See
|
|
42
|
+
> [Sharing libraries](./03-sharing-your-app-libraries.md#no-second-download-either).
|
|
43
|
+
|
|
44
|
+
## The library's own weight
|
|
45
|
+
|
|
46
|
+
Before your registry, what does `next-live` itself cost a page? Measured with
|
|
47
|
+
esbuild, React externalised, code-splitting on:
|
|
48
|
+
|
|
49
|
+
| Page imports | Entry chunk |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `LiveProvider` + `LivePreview` + `LiveError` | **16.1 KB** |
|
|
52
|
+
| the above plus `LiveEditor` from `next-live/editor` | 102.1 KB |
|
|
53
|
+
|
|
54
|
+
The difference is `prism-react-renderer`. It is only needed to syntax-highlight
|
|
55
|
+
an editor, so it lives on its own entry and is an optional peer dependency -
|
|
56
|
+
a page that merely *runs* stored snippets never downloads or installs it.
|
|
57
|
+
|
|
58
|
+
Sucrase is not in either number: it is fetched as a separate chunk on first
|
|
59
|
+
compile, or skipped entirely if you
|
|
60
|
+
[precompile on the server](#compile-cost-and-skipping-the-transpiler).
|
|
61
|
+
|
|
62
|
+
## Design an SDK surface, not a mirror of your codebase
|
|
63
|
+
|
|
64
|
+
The bigger question is *what* to register.
|
|
65
|
+
|
|
66
|
+
Everything in the registry is a public contract with your snippet authors. Expose
|
|
67
|
+
500 internal functions and you can never rename or move any of them again -
|
|
68
|
+
every stored snippet becomes a reason not to refactor.
|
|
69
|
+
|
|
70
|
+
Prefer a small, deliberate set of stable namespaces:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
@app/store @app/ui @app/data @app/charts
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
each a thin re-export of the real implementation:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// lib/live-sdk/modules/charts.ts
|
|
80
|
+
export { BarChart, LineChart } from 'my-charts';
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
This, more than any technique below, is what stops the provider growing without
|
|
84
|
+
bound. It is a design decision, not a technical one.
|
|
85
|
+
|
|
86
|
+
## Compose the registry from small files
|
|
87
|
+
|
|
88
|
+
`createRegistry` merges groups, so the surface lives in one file per domain
|
|
89
|
+
rather than one object that grows forever:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
// lib/live-sdk/index.ts
|
|
93
|
+
import { createRegistry } from 'next-live';
|
|
94
|
+
import { vendorModules } from './vendor';
|
|
95
|
+
import { storeModules } from './stores';
|
|
96
|
+
import { uiModules } from './ui';
|
|
97
|
+
|
|
98
|
+
export const liveModules = createRegistry(vendorModules, storeModules, uiModules);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
<LiveProvider code={source} modules={liveModules} />
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Adding a capability means adding a group, not editing the component that renders
|
|
106
|
+
the provider. Later groups win, and a key defined by two groups logs a warning in
|
|
107
|
+
development - a silent override is painful to debug.
|
|
108
|
+
|
|
109
|
+
## Generate entries from the filesystem
|
|
110
|
+
|
|
111
|
+
For a directory that is genuinely one-file-per-thing, `registryFromGlob` removes
|
|
112
|
+
the hand-maintenance entirely. Turbopack's `import.meta.glob` already returns
|
|
113
|
+
`{ path: () => import(path) }` - lazy thunks, exactly the shape a registry needs:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
// lib/live-sdk/modules.ts
|
|
117
|
+
import { registryFromGlob } from 'next-live';
|
|
118
|
+
|
|
119
|
+
export const storeModules = registryFromGlob(
|
|
120
|
+
import.meta.glob('./modules/*.ts'),
|
|
121
|
+
(path) => {
|
|
122
|
+
const name = path.split('/').pop()!.replace(/\.tsx?$/, '');
|
|
123
|
+
return name ? `@app/${name}` : null; // return null to omit a file
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Add a file to `./modules` and snippets can import it immediately, still lazily.
|
|
129
|
+
This file never has to change again.
|
|
130
|
+
|
|
131
|
+
### The directory rule that will cost you an hour
|
|
132
|
+
|
|
133
|
+
**The pattern must be at or below the calling file's own directory.**
|
|
134
|
+
|
|
135
|
+
Turbopack resolves the pattern relative to the file calling it, and a `../`
|
|
136
|
+
pattern **silently matches nothing**. It returns an empty object rather than
|
|
137
|
+
erroring, so your registry quietly has zero entries and every snippet fails with
|
|
138
|
+
"module is not registered".
|
|
139
|
+
|
|
140
|
+
Verified against Turbopack 16.3.5:
|
|
141
|
+
|
|
142
|
+
| Pattern | Result |
|
|
143
|
+
|---|---|
|
|
144
|
+
| `'./*.ts'` | ✅ matches |
|
|
145
|
+
| `'./modules/*.ts'` | ✅ matches |
|
|
146
|
+
| `'../*.ts'` | ❌ empty |
|
|
147
|
+
| `'../store.ts'` (explicit file) | ❌ empty |
|
|
148
|
+
|
|
149
|
+
That constraint pushes you toward the better shape anyway: keep a dedicated
|
|
150
|
+
directory of modules exposed to snippets, each a thin re-export. Your SDK
|
|
151
|
+
surface becomes visible in the filesystem instead of buried in a filter list.
|
|
152
|
+
|
|
153
|
+
`import.meta.glob` requires **Turbopack** - it does not exist under webpack.
|
|
154
|
+
Under webpack, build the equivalent `{ path: () => import(path) }` object
|
|
155
|
+
yourself and pass that to `registryFromGlob`.
|
|
156
|
+
|
|
157
|
+
## Deep subpaths
|
|
158
|
+
|
|
159
|
+
Some packages are used through hundreds of deep modules rather than a barrel -
|
|
160
|
+
`big-lib/charts/BarChart`, `big-lib/format/currency`, and so on. A **prefix
|
|
161
|
+
entry** - a key ending in `/`, serves the whole subtree from one line,
|
|
162
|
+
receiving the full specifier:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
'big-lib/': defineLoader((specifier) => {
|
|
166
|
+
const subpath = specifier.slice('big-lib/'.length);
|
|
167
|
+
return import(`big-lib/${subpath}`);
|
|
168
|
+
}),
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Verified with Turbopack: deep subpaths resolve, and each arrives as its own
|
|
172
|
+
chunk fetched on demand.
|
|
173
|
+
|
|
174
|
+
### Know the trade-off
|
|
175
|
+
|
|
176
|
+
A template-literal import compiles to a **context** - the bundler emits a chunk
|
|
177
|
+
for every module matching the pattern, including ones no snippet ever imports.
|
|
178
|
+
Confirmed in the playground: a module imported by no app still had chunks
|
|
179
|
+
generated for it.
|
|
180
|
+
|
|
181
|
+
Those chunks are not in your initial bundle, so page load is unaffected. But
|
|
182
|
+
build time and output file count grow with the size of the package. This was
|
|
183
|
+
verified on a small package, measure it yourself before pointing a prefix entry
|
|
184
|
+
at something with hundreds of modules.
|
|
185
|
+
|
|
186
|
+
If build times suffer, narrow the scope:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
// Only the subtrees you actually use
|
|
190
|
+
'big-lib/charts/': defineLoader(/* … */),
|
|
191
|
+
'big-lib/format/': defineLoader(/* … */),
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
or list the specific modules explicitly. An explicit list is more verbose but is
|
|
195
|
+
also an allowlist, which some teams prefer for exactly that reason - and it is
|
|
196
|
+
the approach the [recommended SDK pattern](#design-an-sdk-surface-not-a-mirror-of-your-codebase)
|
|
197
|
+
gives you for free.
|
|
198
|
+
|
|
199
|
+
## Compile cost, and skipping the transpiler
|
|
200
|
+
|
|
201
|
+
Compilation is a few milliseconds, and results are not shared between page
|
|
202
|
+
loads. If you serve many stored snippets, transpile once on the server and cache
|
|
203
|
+
by content hash, the browser then never downloads Sucrase at all:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
// app/api/apps/[id]/route.ts
|
|
207
|
+
import { precompile } from 'next-live/server';
|
|
208
|
+
|
|
209
|
+
const result = precompile(source, { filePath: `${id}.tsx` });
|
|
210
|
+
// result.hash is a stable cache key / ETag
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
import { precompiledTransform } from 'next-live';
|
|
215
|
+
|
|
216
|
+
<LiveProvider code={source} transform={precompiledTransform(compiled)} />
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`precompiledTransform` returns a constant closure over the server result. Only
|
|
220
|
+
apply it while `code` still matches the source that was precompiled - as soon as
|
|
221
|
+
an author edits the snippet, drop back to client transpile or re-precompile.
|
|
222
|
+
See [Troubleshooting: precompile ignores edits](./07-troubleshooting.md#precompile-ignores-my-edits).
|
|
223
|
+
|
|
224
|
+
You can also warm the transpiler chunk during idle time so the first compile is
|
|
225
|
+
not gated on a network round trip:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import { preloadTranspiler } from 'next-live';
|
|
229
|
+
useEffect(() => preloadTranspiler(), []);
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
[← Sharing libraries](./03-sharing-your-app-libraries.md) · [Docs index](./README.md) · [Security →](./05-security.md)
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
[← Scaling](./04-scaling.md) · [Docs index](./README.md) · [API reference →](./06-api-reference.md)
|
|
4
|
+
|
|
5
|
+
Read this before you deploy. It is short, and the first rule is the one that
|
|
6
|
+
matters.
|
|
7
|
+
|
|
8
|
+
## The model in one paragraph
|
|
9
|
+
|
|
10
|
+
`next-live` runs code with `new Function` on your page. That code has the page's
|
|
11
|
+
full authority - cookies, storage, DOM, and your APIs as the signed-in user.
|
|
12
|
+
This is safe when snippet authors are people you trust, and unsafe when they are
|
|
13
|
+
not. **Everything below assumes the first.**
|
|
14
|
+
|
|
15
|
+
## 1. Feed the evaluator only from your own API
|
|
16
|
+
|
|
17
|
+
This is the rule that protects you.
|
|
18
|
+
|
|
19
|
+
Snippet source must come from your own authenticated API. Never from a query
|
|
20
|
+
parameter, hash fragment, `localStorage`, `postMessage`, or any other channel a
|
|
21
|
+
visitor can influence.
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
// ✅ from your API
|
|
25
|
+
const { source } = await fetch(`/api/apps/${id}`).then((r) => r.json());
|
|
26
|
+
|
|
27
|
+
// ❌ never
|
|
28
|
+
const source = new URLSearchParams(location.search).get('code');
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
If an attacker can control what reaches `code`, they run arbitrary JavaScript in
|
|
32
|
+
your origin - and no CSP setting prevents it, because the execution is by
|
|
33
|
+
design. Every other measure here assumes this one holds.
|
|
34
|
+
|
|
35
|
+
## 2. Treat stored snippets as code
|
|
36
|
+
|
|
37
|
+
A row in your snippets table is now equivalent to a deploy. Give it the controls
|
|
38
|
+
a deploy gets:
|
|
39
|
+
|
|
40
|
+
- **Authorization on the write path.** Whoever can save a snippet can run
|
|
41
|
+
JavaScript on your site. That endpoint deserves your strictest check.
|
|
42
|
+
- **An audit trail**, who changed what, when.
|
|
43
|
+
- **Version history**, so you can roll back a bad snippet the way you roll back
|
|
44
|
+
a bad deploy.
|
|
45
|
+
|
|
46
|
+
## 3. Scope `'unsafe-eval'` to the routes that run snippets
|
|
47
|
+
|
|
48
|
+
`new Function` requires `'unsafe-eval'` in `script-src`. Next's CSP guide gates
|
|
49
|
+
that directive behind a dev-only check, because "Neither React nor Next.js use
|
|
50
|
+
`eval` in production by default" - `next-live` does.
|
|
51
|
+
|
|
52
|
+
It does **not** have to apply to your whole application. In `proxy.ts`:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
56
|
+
|
|
57
|
+
/** Only these routes evaluate snippets. */
|
|
58
|
+
const RUNNER_ROUTES = ['/apps', '/playground'];
|
|
59
|
+
|
|
60
|
+
export function proxy(request: NextRequest) {
|
|
61
|
+
const isDev = process.env.NODE_ENV !== 'production';
|
|
62
|
+
// React uses eval in development for server error stacks, so dev needs the
|
|
63
|
+
// directive everywhere regardless of route.
|
|
64
|
+
const needsEval =
|
|
65
|
+
isDev || RUNNER_ROUTES.some((r) => request.nextUrl.pathname.startsWith(r));
|
|
66
|
+
|
|
67
|
+
const csp = [
|
|
68
|
+
"default-src 'self'",
|
|
69
|
+
`script-src 'self' 'unsafe-inline'${needsEval ? " 'unsafe-eval'" : ''}`,
|
|
70
|
+
"style-src 'self' 'unsafe-inline'",
|
|
71
|
+
"img-src 'self' blob: data:",
|
|
72
|
+
"font-src 'self' data:",
|
|
73
|
+
"connect-src 'self'",
|
|
74
|
+
"object-src 'none'",
|
|
75
|
+
"base-uri 'self'",
|
|
76
|
+
"form-action 'self'",
|
|
77
|
+
"frame-ancestors 'none'",
|
|
78
|
+
...(isDev ? [] : ['upgrade-insecure-requests']),
|
|
79
|
+
].join('; ');
|
|
80
|
+
|
|
81
|
+
const response = NextResponse.next();
|
|
82
|
+
response.headers.set('Content-Security-Policy', csp);
|
|
83
|
+
return response;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const config = {
|
|
87
|
+
// Without a matcher this runs on every request, including static assets.
|
|
88
|
+
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
|
89
|
+
};
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
A working copy is in `apps/playground/proxy.ts`. The `/rsc-check` route there is
|
|
93
|
+
a deliberate control: it is *not* a runner route, so evaluation is blocked and
|
|
94
|
+
you can see what the failure looks like.
|
|
95
|
+
|
|
96
|
+
> CSP moved to `proxy.ts` in Next 16 - `middleware.ts` was renamed.
|
|
97
|
+
|
|
98
|
+
### The catch: a CSP does not follow a client-side navigation
|
|
99
|
+
|
|
100
|
+
Route-scoped CSP and client-side routing do not compose, and the failure is
|
|
101
|
+
quiet.
|
|
102
|
+
|
|
103
|
+
A policy is attached to a **document**. A Next `<Link>` navigation fetches no
|
|
104
|
+
new document, so the policy from wherever the visitor first landed stays in
|
|
105
|
+
force for the rest of the session. Land on `/` (no `'unsafe-eval'`), click a
|
|
106
|
+
`<Link>` to `/apps`, and every snippet fails to compile, even though `/apps`
|
|
107
|
+
would have been served the right policy had it been loaded directly.
|
|
108
|
+
|
|
109
|
+
It works in development, because React needs `'unsafe-eval'` there anyway and
|
|
110
|
+
the dev branch grants it everywhere. It works if you reload directly onto a
|
|
111
|
+
runner route. It breaks on the path most visitors actually take.
|
|
112
|
+
|
|
113
|
+
Cross that boundary with a real page load:
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
// A link into a runner route must not be a client-side navigation.
|
|
117
|
+
<a href="/apps">Open the app</a> // ✅ new document, correct policy
|
|
118
|
+
<Link href="/apps">Open the app</Link> // ❌ keeps the previous page's policy
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The playground wraps this in a `RunnerLink` component that reads the same
|
|
122
|
+
`RUNNER_ROUTES` list the proxy uses, so the two cannot drift. See
|
|
123
|
+
`apps/playground/components/RunnerLink.tsx` and `lib/runner-routes.ts`.
|
|
124
|
+
|
|
125
|
+
The cost is one full page load when entering the runner section. The
|
|
126
|
+
alternative is granting `'unsafe-eval'` application-wide, which is the thing
|
|
127
|
+
this whole section exists to avoid.
|
|
128
|
+
|
|
129
|
+
One consequence worth planning for: every page a visitor can reach *from* a
|
|
130
|
+
runner route by soft navigation also needs the runner policy, or it needs its
|
|
131
|
+
own hard link back. Keeping the runner section contiguous (one prefix, as the
|
|
132
|
+
playground does with `/docs`) is simpler than scattering eval-enabled pages
|
|
133
|
+
through the app.
|
|
134
|
+
|
|
135
|
+
## 4. Keep the rest of the policy strict
|
|
136
|
+
|
|
137
|
+
`'unsafe-eval'` sounds worse than it is. It gates **only** string-to-code APIs
|
|
138
|
+
(`eval`, `Function`, `setTimeout("…")`). It does **not** permit loading external
|
|
139
|
+
scripts, so `script-src 'self' 'unsafe-eval'` still blocks attacker-hosted code.
|
|
140
|
+
|
|
141
|
+
**`'unsafe-inline'` in `script-src` is the one weakening worth naming.** The
|
|
142
|
+
policy above carries it because Next injects inline bootstrap and streaming
|
|
143
|
+
scripts; without it a stock App Router page does not run. On a runner route it
|
|
144
|
+
costs you little extra, since `'unsafe-eval'` is already present and is the
|
|
145
|
+
stronger capability of the two. Removing it means adopting a nonce, which is
|
|
146
|
+
worth doing on your non-runner routes if you can accept the cost described
|
|
147
|
+
below, and which buys you nothing on the runner routes themselves.
|
|
148
|
+
|
|
149
|
+
Two further directives are worth particular attention:
|
|
150
|
+
|
|
151
|
+
- **`connect-src`** bounds a misbehaving snippet: it can read whatever the page
|
|
152
|
+
can, but it cannot send it anywhere you did not allow. Widen it per-host,
|
|
153
|
+
deliberately.
|
|
154
|
+
- **`object-src 'none'`** and **`base-uri 'self'`** close well-known bypasses and
|
|
155
|
+
cost nothing.
|
|
156
|
+
|
|
157
|
+
### Two things that will not help
|
|
158
|
+
|
|
159
|
+
- **A nonce is not a substitute.** `'nonce-…'` and `'strict-dynamic'` authorize
|
|
160
|
+
script *elements*; `new Function` is governed solely by `'unsafe-eval'`.
|
|
161
|
+
- **A `blob:` URL is not safer.** It runs in the same origin with the same
|
|
162
|
+
powers, and `'strict-dynamic'` causes scheme allowlists like `blob:` to be
|
|
163
|
+
ignored anyway.
|
|
164
|
+
|
|
165
|
+
Also note that adopting a nonce-based CSP forces fully dynamic rendering and is
|
|
166
|
+
incompatible with PPR / `cacheComponents` - a real cost worth weighing.
|
|
167
|
+
|
|
168
|
+
If CSP does block evaluation, `next-live` detects it and reports what to change
|
|
169
|
+
instead of surfacing the browser's raw `EvalError`.
|
|
170
|
+
|
|
171
|
+
## What is contained, and what is not
|
|
172
|
+
|
|
173
|
+
**Contained.** Runtime errors and render loops. `<LiveErrorBoundary>` keeps a
|
|
174
|
+
broken snippet from taking down the host app, and a render-rate breaker stops
|
|
175
|
+
runaway `setState` loops - the common real-world hang. React's own "Maximum
|
|
176
|
+
update depth" guard catches the synchronous case, but not an effect that updates
|
|
177
|
+
state on every commit; the breaker catches that one.
|
|
178
|
+
|
|
179
|
+
**Not contained.** A snippet runs with the page's full authority. A synchronous
|
|
180
|
+
`while (true)` - in module scope, in render, or in a handler - will hang the tab,
|
|
181
|
+
and no timer, `AbortController`, or `Promise.race` can interrupt it, because
|
|
182
|
+
JavaScript cannot interrupt synchronous code in its own realm.
|
|
183
|
+
|
|
184
|
+
The [module registry](./02-module-registry.md) does not change this. It bounds
|
|
185
|
+
what snippets can *conveniently* reach, not what they *can* reach, `window`,
|
|
186
|
+
`fetch`, and the DOM are always there.
|
|
187
|
+
|
|
188
|
+
## If your authors stop being trusted
|
|
189
|
+
|
|
190
|
+
Same-realm evaluation is a stability aid for cooperative authors, not a security
|
|
191
|
+
boundary. If snippets ever come from a public marketplace, user-to-user sharing,
|
|
192
|
+
or tenants writing code that runs for other tenants' users, none of the above is
|
|
193
|
+
sufficient.
|
|
194
|
+
|
|
195
|
+
What you would need is the preview running in an iframe on a **separate origin**,
|
|
196
|
+
which cannot read your cookies or DOM. Know the cost before choosing it: props
|
|
197
|
+
would have to cross by structured clone, so you could no longer pass a store, a
|
|
198
|
+
library, a function, or any live object by reference - see
|
|
199
|
+
[Sharing libraries](./03-sharing-your-app-libraries.md). That is a different
|
|
200
|
+
product, and this library does not pretend to be it.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
[← Scaling](./04-scaling.md) · [Docs index](./README.md) · [API reference →](./06-api-reference.md)
|