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/client-only-lazy-import.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@solidjs/start": patch
---

Stop `client-only` from failing the build for lazily imported client components.

A module that imported `client-only` and was loaded through `clientOnly(() => import(...))` failed the server build, even though it only ever runs in the browser. The server build resolves every dynamic import to emit its chunk, so it resolved the `client-only` module and rejected it, though that module never runs on the server. `client-only` no longer fails the build. `server-only` is unchanged and still fails a client build.
8 changes: 6 additions & 2 deletions packages/start/src/config/boundary-modules.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@ describe("boundaryModules", () => {
expect(resolveWith("server-only", false)).toThrowError(/server-only.*someClient\.ts/s);
});

it("fails when a server module imports client-only", () => {
expect(resolveWith("client-only", true)).toThrowError(/client-only.*someClient\.ts/s);
it("does not fail the build when a server module resolves client-only", () => {
// The server build resolves clientOnly() dynamic imports to emit their
// chunks without ever running them, so this must not error at resolve.
const plugin = boundaryModules() as any;
const id = resolveWith("client-only", true)();
expect(plugin.load(id)).toBe("export {}");
});

it("resolves the markers to an empty module in the allowed environment", () => {
Expand Down
48 changes: 26 additions & 22 deletions packages/start/src/config/boundary-modules.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,49 @@
import type { Plugin } from "vite";

const VIRTUAL_ID = "\0solid-start:boundary-modules:id";
const EMPTY_ID = "\0solid-start:boundary-modules:empty";

/**
* Supports `server-only` and `client-only` marker modules (#2162): importing
* `server-only` from a client module (or `client-only` from a server module)
* fails at resolve time; in the allowed environment the marker resolves to an
* empty module.
* Supports the `server-only` and `client-only` marker modules (#2162).
*
* Start's own server-only entry points (`@solidjs/start/http`,
* `@solidjs/start/middleware`) import `server-only` themselves, so pulling
* them into the client bundle fails loudly instead of shipping server code to
* the browser, where it crashed hydration and broke unrelated actions/forms
* with no diagnostic (https://github.com/solidjs/solid-start/issues/2068).
* `server-only` imported from a client module fails the build at resolve time.
* A client module that reaches server-only code is always a mistake and must
* never bundle. Start's own server-only entry points (`@solidjs/start/http`,
* `@solidjs/start/middleware`) import `server-only` themselves, so pulling them
* into the client bundle fails loudly instead of shipping server code to the
* browser (https://github.com/solidjs/solid-start/issues/2068).
*
* `client-only` cannot be enforced the same way. The server build resolves
* every dynamic import to emit its chunk, so a module reached only through
* `clientOnly(() => import(...))` is resolved by the server build even though
* it never runs on the server. Failing the build there is a false positive
* that breaks the client-only lazy pattern, so `client-only` resolves to an
* empty module in both environments. Client-only code that does reach the
* server runtime still fails there on its own (e.g. a missing `window`).
*/
export function boundaryModules(): Plugin {
return {
name: "solid-start:boundary-modules",
enforce: "pre",
resolveId(id, importer, { ssr }) {
if (id === "server-only") {
if (!ssr)
if (!ssr) {
this.error(
`Attempt to import 'server-only' in a client module: ${importer}. ` +
`Code that uses this module must run only on the server: mark it with ` +
`"use server", or make sure it is only imported by server code.`,
);
} else if (id === "client-only") {
if (ssr)
this.error(
`Attempt to import 'client-only' in a server module: ${importer}. ` +
`Code that uses this module must run only in the browser: make sure it ` +
`is only imported by client code (e.g. wrap components with clientOnly()).`,
);
} else {
return null;
}
return EMPTY_ID;
}
return VIRTUAL_ID;
if (id === "client-only") {
return EMPTY_ID;
}
return null;
},
load(id) {
if (id === VIRTUAL_ID) return "export {}";
if (id === EMPTY_ID) {
return "export {}";
}
},
};
}
Loading