Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/routes/(1)getting-started/(1)project-shapes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ import { handleRequest } from "./dist/server/server.js";
const response = await handleRequest(request);
```

The template's `server.js` is the Node version of that.
The `fullstack` template sets `start: { node: true }`, so the build also writes `dist/server/node.js`, the Node version of that: it serves `dist/client`, passes the rest to `handleRequest`, and listens on `PORT`.
On a fetch-native platform, map `handleRequest` to the host's request entry point and point its static asset service at `dist/client`.
Workers, Deno, and Bun each have their own module, asset, and environment configuration; [Deployment](/building-apps/deployment) covers them.

Expand Down
115 changes: 101 additions & 14 deletions src/routes/(3)building-apps/(7)deployment.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ A `bare` or `basic` project builds to `dist/client` alone, and any static host s
This page is about a project with `ssr: true` or server functions, which builds to static assets plus a server handler.
[Project shapes](/getting-started/project-shapes) says which shape produces which output; [App structure](/building-apps/app-structure) explains the entries the handler runs.

Most apps need the request handler section and one of the host sections after it: Node with the template's `server.js`, or one provider plugin.
Most apps need the request handler section and one of the host sections after it: Node with the emitted `dist/server/node.js`, or one provider plugin.
The rest is for checking a detail or wiring a host that is not listed.

## What the build produces
Expand Down Expand Up @@ -80,41 +80,122 @@ const server = createServer(async (req, res) => {
```

With the `Avoid` version, a request for `/assets/app-BpJ2g.js` renders the HTML page, because in production every request that reaches the end of the middleware chain renders; the browser receives a document where it expected a script.
With `start.node`, the emitted Node entry is the `Prefer` version; the rest of this section is for a bridge you write.

When adapting a host request, preserve the URL, method, headers, and the body of any request that is not `GET` or `HEAD`.
When adapting the result, preserve the status, the headers, each `Set-Cookie` value separately, and the streamed body.

:::caution[Set-Cookie must not be comma-joined]
A response can carry several `Set-Cookie` headers, and joining them into one comma-separated value corrupts every cookie in it.
The template's `server.js` reads them with `headers.getSetCookie()` and passes the array to Node; a bridge for another server needs the same care.
The emitted Node entry reads them with `headers.getSetCookie()` and passes the array to Node; a bridge for another server needs the same care.
:::

## Node

The fullstack templates include `server.js`, the verified Node adapter.
It converts `node:http` requests to web `Request` objects, serves `dist/client`, calls `handleRequest`, and streams the response back.
The template's start script runs that file:
Node has no server API that accepts a Fetchable module, so the plugin emits the Node server.
Set `start.node` and the build writes `dist/server/node.js` next to `dist/server/server.js`:

```ts title="vite.config.ts"
import { defineConfig } from "vite";
import solid from "@solidjs/vite-plugin";

export default defineConfig({
plugins: [
solid({
start: { node: true },
ssr: true,
}),
],
});
```

`server.js` does not change: `handleRequest` and the default `{ fetch }` export are the same as without the option, so a provider integration or a Fetch runtime keeps working from that file.

Build, then run the emitted file:

```bash
pnpm build
node dist/server/node.js
```

The server listens on `PORT`, defaulting to `3000`, and binds to `HOST` when it is set.
Those two variables are the only runtime configuration.
The fullstack templates point their start script at the file:

```json
{
"scripts": {
"start": "node --env-file-if-exists=.env server.js"
"start": "node --env-file-if-exists=.env dist/server/node.js"
}
}
```

Run `pnpm build` and then `pnpm start`, and the server listens on `PORT` or `3000`.
Any Node host that runs that command with `PORT` and the server environment set is done.

Use the template adapter as the reference when integrating another Node server.
The bridge must stream the request body for methods other than `GET` and `HEAD`, and it must forward multiple `Set-Cookie` headers as separate values.
The emitted entry is the `Prefer` version from [the request handler section](#the-request-handler), with the details a production bridge needs:

- It serves `dist/client` first.
Hashed files under `assets/` get `Cache-Control: public, max-age=31536000, immutable`; other files get `public, max-age=0, must-revalidate` and a `Last-Modified` header.
A path containing `..` cannot leave the directory, and a path with a dot segment such as `.vite/manifest.json` is never served.
- It passes every request that matched no file to `handleRequest(request, { event: { nativeEvent: req } })` through the same Node-to-web bridge that `vite dev` and `vite preview` use.
The bridge streams the request body for methods other than `GET` and `HEAD`, forwards each `Set-Cookie` header separately, answers `HEAD` without a body, aborts the render when the client disconnects, and waits for the socket to drain before writing more.
- In client start mode with server functions, it serves `dist/client/index.html` for HTML-accepting `GET` requests that match no file, and routes the server-function endpoint to the handler.
- It speaks plain HTTP.
Terminate TLS and compress at a reverse proxy or CDN in front of it, or mount it in Express behind `compression()` as shown below.

:::tip[The raw request is one option away]
`server.js` passes the Node request into the event with `handleRequest(request, { event: { nativeEvent: req } })`.
Middleware and server functions then read `getRequestEvent().nativeEvent` for platform details such as the socket's remote address.
The emitted entry passes the Node request into the event with `handleRequest(request, { event: { nativeEvent: req } })`, so `getRequestEvent().nativeEvent` is the Node `IncomingMessage`.
Middleware and server functions read it for platform details such as the socket's remote address.
Behind a proxy, read the forwarding headers off `getRequestEvent().request` instead, and only when the proxy is trusted.
:::

### Your own Node server

Keep a hand-written entry when the app needs compression, a custom `http` server, or a place inside an existing Express or Fastify app.
`dist/server/node.js` exports three things for that: `listener`, the `(req, res)` function the emitted server runs; `createListener(options)`, which builds a listener with options; and `serve(options)`, which creates and starts an `http.Server` on `PORT` and `HOST` and returns it.
Importing the file does not start a server; only running it directly does.

```js
// server.js
import { createServer } from "node:http";
import { listener } from "./dist/server/node.js";

createServer(listener).listen(process.env.PORT || 3000);
```

With Express, either put compression in front and let the listener serve everything:

```js
// server.js
import express from "express";
import compression from "compression";
import { listener } from "./dist/server/node.js";

const app = express();
app.use(compression());
app.use(listener); // static files, pages, server functions
app.listen(process.env.PORT || 3000);
```

Or let Express own the static files and keep only the bridge:

```js
// server.js
import express from "express";
import { createListener } from "./dist/server/node.js";

const app = express();
app.use(express.static("dist/client", { immutable: true, maxAge: "1y" }));
app.use(createListener({ static: false }));
app.listen(process.env.PORT || 3000);
```

`static: false` skips the file lookup and, in client start mode, the `index.html` history fallback; Express owns both.
`createListener({ event: (req) => ({ ...fields }) })` merges extra fields next to `nativeEvent` in the request event, and `serve()` accepts `static` and `event` alongside `port` and `host`.

A bridge written against `handleRequest` from `dist/server/server.js` is the last resort, for a server that cannot mount a Node request listener.
It must meet the requirements in [the request handler section](#the-request-handler): stream the request body for methods other than `GET` and `HEAD`, forward multiple `Set-Cookie` headers as separate values, and pass the Node request as `event.nativeEvent`.

## Preview the production artifact

The start-mode integration configures `vite preview` to serve `dist/client` and dispatch the remaining requests through the built handler:
Expand All @@ -124,7 +205,7 @@ pnpm build
pnpm exec vite preview
```

The official fullstack templates name the Vite preview script `serve` and reserve `start` for the included Node server.
The official fullstack templates name the Vite preview script `serve` and reserve `start` for the emitted Node server.
Preview verifies the built handler and static assets together; it does not replace a test on the target host.

## Provider integrations
Expand Down Expand Up @@ -286,6 +367,12 @@ Under `ssr: true`, requests for pages are not reaching `handleRequest`; the host
Route every request that is not a file to the handler.
For a static-shell project, there is no handler: configure the host to serve `dist/client/index.html` for paths that are not files, as any single-page app needs.

### `Cannot find module 'dist/server/node.js'`

The build writes `dist/server/node.js` only when `start.node` is `true`.
Set `start: { node: true }` in `vite.config.ts` and run the build again; `server.js` alone is the handler, not a server.
If `dist/server` is missing altogether, see the next problem.

### `Cannot find module './dist/server/server.js'`

The build did not produce a server directory.
Expand All @@ -301,14 +388,14 @@ Client `VITE_` values are the opposite: set them on the machine that runs `vite
### Sign-in works locally but the session is missing in production

The bridge joined several `Set-Cookie` headers into one comma-separated value, which corrupts them.
Forward them as separate headers, the way `server.js` does with `getSetCookie()`.
Forward them as separate headers, the way the emitted Node entry does with `getSetCookie()`.

## Recap

- Serve `dist/client` as static files first and pass every other request to `handleRequest`.
- `handleRequest(request)` and the default `{ fetch }` export are the same handler; the default export ignores host arguments after the request.
- Preserve the method, headers, and streamed body on the way in, and the status, headers, separate `Set-Cookie` values, and streamed body on the way out.
- Run the template's `server.js` on any Node host; `pnpm build` then `pnpm start`.
- Set `start.node` and run `node dist/server/node.js` on any Node host; `PORT` and `HOST` are its only configuration.
- Set `server` environment variables on the host, because they are read at boot; `VITE_` values are fixed at build time.
- A provider plugin adopts the `ssr` environment; reach for `start.external` only when the host names or configures its server environment differently.
- `vite preview` runs the built handler and assets together, and does not replace a test on the target host.
Expand Down
2 changes: 1 addition & 1 deletion src/routes/(6)migration/(2)from-solid-start.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@ Preserve the request URL, method, headers, and body.
Preserve the response status, headers, separate `Set-Cookie` values, and streamed body.

Do not deploy the old Nitro output or reuse a SolidStart provider preset.
The pinned templates include a verified Node adapter.
For Node, set `start: { node: true }`; the build then writes `dist/server/node.js`, a server that implements this boundary and runs with `node dist/server/node.js`.
Other hosts need an adapter or Vite integration that implements this exact asset and handler boundary.

Set `start.external: true` only when a host integration owns both server build wiring and HTTP serving.
Expand Down
56 changes: 28 additions & 28 deletions src/routes/reference/(1)solid-js/(1)reactivity/create-effect.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ source_path: "packages/solid/src/client/hydration.ts"

Creates a reactive effect with **separate compute and effect phases**.

- `compute(prev)` runs reactively — *put all reactive reads here*.
- `compute(prev)` runs reactively — _put all reactive reads here_.
The returned value is passed to `effect` and is also the new
"previous" value for the next run.
- `effect(next, prev?)` runs imperatively (untracked) after the
queue flushes. *Put DOM writes / fetch / logging / subscriptions
here.* It may return a cleanup function which runs before the
queue flushes. _Put DOM writes / fetch / logging / subscriptions
here._ It may return a cleanup function which runs before the
next effect or on disposal.

Reactive reads inside `effect` will *not* re-trigger this effect —
Reactive reads inside `effect` will _not_ re-trigger this effect —
that's intentional. If you need a single-phase tracked effect, use
`createTrackedEffect` (with the tradeoffs noted there).

Expand All @@ -50,9 +50,9 @@ import { createEffect } from "solid-js";

```ts
function createEffect<T>(
compute: ComputeFunction<undefined | NoInfer<T>, T>,
effectFn: EffectFunction<NoInfer<T>, T> | EffectBundle<NoInfer<T>, T>,
options?: EffectOptions
compute: ComputeFunction<undefined | NoInfer<T>, T>,
effectFn: EffectFunction<NoInfer<T>, T> | EffectBundle<NoInfer<T>, T>,
options?: EffectOptions
): void;
```

Expand Down Expand Up @@ -87,21 +87,21 @@ Nothing. The effect is owned by the surrounding scope and disposed with it.
const [count, setCount] = createSignal(0);

createEffect(
() => count(), // compute: tracks `count`
value => console.log(value) // effect: side effect
() => count(), // compute: tracks `count`
(value) => console.log(value) // effect: side effect
);

setCount(1); // logs 1 after the next flush
```

```ts
createEffect(
() => userId(),
id => {
const ctrl = new AbortController();
fetch(`/users/${id}`, { signal: ctrl.signal });
return () => ctrl.abort(); // cleanup before next run / disposal
}
() => userId(),
(id) => {
const ctrl = new AbortController();
fetch(`/users/${id}`, { signal: ctrl.signal });
return () => ctrl.abort(); // cleanup before next run / disposal
}
);
```

Expand Down Expand Up @@ -140,16 +140,16 @@ server never created.

```ts
type ComputeFunction<Prev, Next extends Prev = Prev> = (
v: Prev
v: Prev
) => PromiseLike<Next> | AsyncIterable<Next> | Next;
```

### `EffectBundle`

```ts
type EffectBundle<Prev, Next extends Prev = Prev> = {
effect: EffectFunction<Prev, Next>;
error: (err: unknown, cleanup: () => void) => void;
effect: EffectFunction<Prev, Next>;
error: (err: unknown, cleanup: () => void) => void;
};
```

Expand All @@ -175,8 +175,8 @@ outcomes — an error that recovers before the effect phase runs the

```ts
type EffectFunction<Prev, Next extends Prev = Prev> = (
v: Next,
p?: Prev
v: Next,
p?: Prev
) => (() => void) | void;
```

Expand All @@ -186,13 +186,13 @@ Options for effect primitives that support deferring/scheduling their initial ru

```ts
interface EffectOptions extends BaseEffectOptions {
defer?: boolean;
schedule?: boolean;
sync?: boolean;
transparent?: boolean;
deferStream?: boolean;
ssrSource?: "server" | "hybrid" | "client";
};
defer?: boolean;
schedule?: boolean;
sync?: boolean;
transparent?: boolean;
deferStream?: boolean;
ssrSource?: "server" | "hybrid" | "client";
}
```

#### `defer`
Expand Down Expand Up @@ -265,7 +265,7 @@ its fallback into the HTML. Server-only; ignored on the client.
Hydration policy. Decides what initial value the client uses and
whether the compute re-runs.

- `"server"` *(default)*: client uses the serialized server value
- `"server"` _(default)_: client uses the serialized server value
as initial state. Compute does **not** re-run for the initial
value — the serialized result is authoritative. Choose this when
the compute is deterministic from server-available inputs.
Expand Down
Loading
Loading