Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/start-client-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@solidjs/vite-plugin': patch
---

New `start.clientSetup` option: a client module the generated client entry runs before it hydrates or mounts the app. Use it to install a server-function transport wrapper, a `vite:preloadError` listener, or an error reporter before the first render, without writing your own `entry-client` / `entry-server` pair. The module may use top-level `await`, and `hydrate()` / `render()` waits for it. Combining it with an authored `entry-client` is a config error.

The start options list in the README now also names `instrument`.
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,8 @@ same server functions.
The object form carries the options (`start: true` is pure sugar for
`start: {}` — both mean the identical start mode with defaults, and
`false`/absent means off): `app`, `document`, `entryServer`, `entryClient`,
`middleware`, `setup`, `renderMode`, `env`, `devtools`, `errorBoundary`,
`middleware`, `setup`, `instrument`, `clientSetup`, `renderMode`, `env`,
`devtools`, `errorBoundary`,
`css`, `external`, `node`, all documented below.

Install `@solidjs/start-devtools` as a development dependency to add the
Expand Down Expand Up @@ -556,6 +557,39 @@ instrument.mjs` dance. The module may be async and needs no exports; the
server build must keep code splitting on (the default), since inlining
dynamic imports would hoist the handler graph back above the instrument.

**`clientSetup`** points at a client module that the generated client entry
runs before it hydrates (SSR mode) or mounts (client mode) the app. Use it
for browser setup that must be in place before the first render:

- a server-function transport wrapper or request-header hook,
- a `vite:preloadError` listener,
- an error reporter's `init()`.

```ts
// vite.config.ts
solid({ start: { clientSetup: './src/client-setup.ts' }, ssr: true });

// src/client-setup.ts
window.addEventListener('vite:preloadError', () => window.location.reload());
```

How it runs:

- The generated entry imports the module for its side effects, ahead of the
app. A synchronous module finishes before the app's modules evaluate.
- The module may use top-level `await`. `hydrate()` / `render()` waits for
it to settle. The app's modules may evaluate while it waits, but nothing
renders.
- Because it can wait, it can read a global that a classic `<script>` in
`<body>` defines: await `DOMContentLoaded` first.
- It stays in the entry's static module graph, so the build preloads the
app's chunks and links its stylesheets as before.
- Editing it in dev reloads the page.

The module needs no exports. It only applies to a generated client entry.
An authored `entry-client` can run its own code before `hydrate()`, so
configuring both is an error.

**`renderMode`** — how a page render becomes a response body: `'stream'`
(the default) or `'async'`.

Expand Down
16 changes: 16 additions & 0 deletions examples/start-ssr/src/ClientSetupApp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// App root for the client-setup mode (SSR_CLIENT_SETUP=1): wraps the real
// App and records, in the register src/client-setup.ts creates, when this
// module evaluated and whether the setup module had settled by the time the
// app rendered during hydration.
import { isServer } from '@solidjs/web';
import App from './App';

if (!isServer) globalThis.__solidClientSetup?.order.push('app:module');

export default function ClientSetupApp() {
if (!isServer) {
const state = globalThis.__solidClientSetup;
state?.order.push(`app:render(setupDone:${state.done})`);
}
return <App />;
}
32 changes: 32 additions & 0 deletions examples/start-ssr/src/client-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// `start.clientSetup` (SSR_CLIENT_SETUP=1, client-setup mode): the module
// the generated client entry runs before `hydrate()`. This fake proves the
// contract without a real transport wrapper:
// - it starts FIRST: the ordering register it creates is empty, so nothing
// from the app graph (src/ClientSetupApp.tsx appends at its module top
// level) has evaluated yet,
// - `hydrate()` waits for it: real async work here (a timer) finishes
// before the app renders, so ClientSetupApp finds `done` true when it
// renders during hydration,
// - `marker` is what the HMR check edits on disk: the module has no accept
// boundary, so an edit must reload the page with the new value.
declare global {
// eslint-disable-next-line no-var
var __solidClientSetup:
| { order: string[]; done: boolean; evaluations: number; marker: string }
| undefined;
}

const state = (globalThis.__solidClientSetup ??= {
order: [],
done: false,
evaluations: 0,
marker: '',
});
state.evaluations++;
state.marker = 'client-setup-v1';
state.order.push(`setup:start(before:${state.order.length})`);
await new Promise((resolve) => setTimeout(resolve, 50));
state.order.push('setup:done');
state.done = true;

export {};
Loading
Loading