From 95483267664cb6056b3d3005fccdf14dfae0f9ef Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Thu, 8 Oct 2026 13:14:44 -0700 Subject: [PATCH] fix: define __SOLID_SERVER_COMPONENTS__ for every environment Libraries can drop server-component-only client code when the flag is false, and an absent identifier cannot be eliminated. Co-authored-by: Cursor --- .changeset/server-components-define.md | 5 + examples/ssr/package.json | 2 +- examples/ssr/test/fixtures/sc-define.ts | 11 ++ .../ssr/test/server-components-define.mjs | 160 ++++++++++++++++++ src/index.ts | 29 ++++ 5 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 .changeset/server-components-define.md create mode 100644 examples/ssr/test/fixtures/sc-define.ts create mode 100644 examples/ssr/test/server-components-define.mjs diff --git a/.changeset/server-components-define.md b/.changeset/server-components-define.md new file mode 100644 index 0000000..cc69bbb --- /dev/null +++ b/.changeset/server-components-define.md @@ -0,0 +1,5 @@ +--- +'@solidjs/vite-plugin': patch +--- + +The build now defines `__SOLID_SERVER_COMPONENTS__` so libraries can drop server-component-only client code (#396). The value is `"true"` when `serverFunctions.components` is set (including `'external'`) and `"false"` otherwise, and it is always defined — an absent identifier cannot be eliminated. It is set on Vite's `define` (build, and dev source via `/@vite/env`) and on every environment's `optimizeDeps.rolldownOptions.transform.define`, because the optimizer ignores top-level `define` and only the `client` environment inherits top-level `optimizeDeps`. A user-provided `define` value wins, and so does a value already set on that environment's optimizer. diff --git a/examples/ssr/package.json b/examples/ssr/package.json index 421f0c7..4f44ff9 100644 --- a/examples/ssr/package.json +++ b/examples/ssr/package.json @@ -8,7 +8,7 @@ "build:client": "vite build --outDir dist/client", "build:server": "vite build --ssr src/entry-server.tsx --outDir dist/server", "serve": "NODE_ENV=production node server.js", - "test": "node test/run.mjs && node test/boundary.mjs && node test/scan.mjs" + "test": "node test/run.mjs && node test/boundary.mjs && node test/scan.mjs && node test/server-components-define.mjs" }, "devDependencies": { "vite": "^8.2.1", diff --git a/examples/ssr/test/fixtures/sc-define.ts b/examples/ssr/test/fixtures/sc-define.ts new file mode 100644 index 0000000..fe7fd3a --- /dev/null +++ b/examples/ssr/test/fixtures/sc-define.ts @@ -0,0 +1,11 @@ +// Build fixture for #396. Not imported by the app. Vite's `define` replaces +// `__SOLID_SERVER_COMPONENTS__` with a boolean literal, and the unused branch +// has to fold away — that is the whole point of a define versus a runtime flag. +declare const __SOLID_SERVER_COMPONENTS__: boolean | undefined; + +const marker = + typeof __SOLID_SERVER_COMPONENTS__ !== 'undefined' && __SOLID_SERVER_COMPONENTS__ + ? 'SC_DEFINE_ON' + : 'SC_DEFINE_OFF'; + +console.log(marker); diff --git a/examples/ssr/test/server-components-define.mjs b/examples/ssr/test/server-components-define.mjs new file mode 100644 index 0000000..ae61e39 --- /dev/null +++ b/examples/ssr/test/server-components-define.mjs @@ -0,0 +1,160 @@ +// `__SOLID_SERVER_COMPONENTS__` is defined for every build so libraries can +// drop server-component-only client code (#396). Asserts, in one process: +// - resolved config: the flag is `"true"` or `"false"` (never absent) on +// Vite's `define` and on every environment's dep-scan transform, `"true"` +// when `serverFunctions.components` is set including `'external'`, and a +// user-provided value wins on both the top-level define and a single +// environment's optimizer, +// - build: the unused branch of `if (__SOLID_SERVER_COMPONENTS__)` is gone +// from the bundle, off and on. +// +// Requires the plugin built (pnpm build at the repo root). No browser. +// Usage: node test/server-components-define.mjs + +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; +import { build, resolveConfig } from 'vite'; +import solidPlugin from '@solidjs/vite-plugin'; + +const KEY = '__SOLID_SERVER_COMPONENTS__'; +const exampleDir = path.dirname(path.dirname(fileURLToPath(import.meta.url))); +const fixture = path.join(exampleDir, 'test/fixtures/sc-define.ts'); + +const results = []; +function record(name, ok, detail = '') { + results.push({ name, ok, detail }); + console.log(` [define] ${ok ? 'PASS' : 'FAIL'} ${name}${detail && !ok ? ` — ${detail}` : ''}`); +} + +function flags(config) { + const read = (define) => define?.[KEY]; + return { + client: read(config.environments.client.define), + ssr: read(config.environments.ssr.define), + clientOpt: read(config.environments.client.optimizeDeps?.rolldownOptions?.transform?.define), + ssrOpt: read(config.environments.ssr.optimizeDeps?.rolldownOptions?.transform?.define), + }; +} + +async function resolved(inline) { + return resolveConfig( + { root: exampleDir, configFile: false, logLevel: 'silent', ...inline }, + 'serve', + ); +} + +function expectFlags(name, actual, expected) { + const mismatches = Object.entries(expected).filter(([key, value]) => actual[key] !== value); + record( + name, + mismatches.length === 0, + mismatches.map(([key]) => `${key}=${JSON.stringify(actual[key])}`).join(', ') || + JSON.stringify(actual), + ); +} + +{ + const config = await resolved({ plugins: [solidPlugin()] }); + expectFlags('off by default, on define and both optimizers', flags(config), { + client: 'false', + ssr: 'false', + clientOpt: 'false', + ssrOpt: 'false', + }); + record( + 'client scanner jsx stays classic', + config.environments.client.optimizeDeps.rolldownOptions?.transform?.jsx?.runtime === 'classic', + ); +} +{ + const config = await resolved({ + plugins: [solidPlugin({ serverFunctions: { components: true } })], + }); + expectFlags('components: true defines the flag true', flags(config), { + client: 'true', + ssr: 'true', + clientOpt: 'true', + ssrOpt: 'true', + }); +} +{ + const config = await resolved({ + plugins: [solidPlugin({ serverFunctions: { components: 'external' } })], + }); + expectFlags("components: 'external' defines the flag true", flags(config), { + client: 'true', + ssr: 'true', + clientOpt: 'true', + ssrOpt: 'true', + }); +} +{ + const config = await resolved({ + define: { [KEY]: 'true' }, + plugins: [solidPlugin()], + }); + expectFlags('a user define wins, including the optimizers', flags(config), { + client: 'true', + ssr: 'true', + clientOpt: 'true', + ssrOpt: 'true', + }); +} +{ + const config = await resolved({ + plugins: [solidPlugin()], + environments: { + ssr: { + optimizeDeps: { rolldownOptions: { transform: { define: { [KEY]: 'true' } } } }, + }, + }, + }); + expectFlags('a per-environment optimizer define wins only there', flags(config), { + client: 'false', + ssr: 'false', + clientOpt: 'false', + ssrOpt: 'true', + }); +} + +async function builtMarker(plugins) { + const result = await build({ + root: exampleDir, + configFile: false, + logLevel: 'silent', + plugins, + build: { + write: false, + minify: false, + rollupOptions: { input: fixture }, + }, + }); + const outputs = Array.isArray(result) ? result : [result]; + return outputs + .flatMap((output) => ('output' in output ? output.output : [])) + .map((chunk) => (chunk.type === 'chunk' ? chunk.code : '')) + .join('\n'); +} + +{ + const off = await builtMarker([solidPlugin()]); + record( + 'a false define drops the server-component branch', + off.includes('SC_DEFINE_OFF') && !off.includes('SC_DEFINE_ON'), + off.includes('SC_DEFINE_ON') ? 'both markers survived' : 'off marker missing', + ); + const on = await builtMarker([solidPlugin({ serverFunctions: { components: true } })]); + record( + 'a true define drops the fallback branch', + on.includes('SC_DEFINE_ON') && !on.includes('SC_DEFINE_OFF'), + on.includes('SC_DEFINE_OFF') ? 'both markers survived' : 'on marker missing', + ); +} + +const failures = results.filter((r) => !r.ok); +console.log(`\n${results.length - failures.length}/${results.length} define assertions passed`); +if (failures.length) { + console.log('\nFailures:'); + for (const f of failures) console.log(` ${f.name} — ${f.detail}`); +} +process.exit(failures.length ? 1 : 0); diff --git a/src/index.ts b/src/index.ts index 057c778..6a03f12 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1097,6 +1097,14 @@ function collapseDotRuns( }; } +/** + * Build-time flag libraries use to drop server-component-only client code + * (#396). Always defined (`"true"` or `"false"`): an absent identifier cannot + * be eliminated. `"true"` when `serverFunctions.components` is set, including + * `'external'`. A user-provided `define` value wins. + */ +const SERVER_COMPONENTS_DEFINE = '__SOLID_SERVER_COMPONENTS__'; + export default function solidPlugin(options: Partial = {}): Plugin[] { if (typeof options.ssr === 'object') { throw new Error( @@ -1112,6 +1120,9 @@ export default function solidPlugin(options: Partial = {}): Plugin[] { const serverComponentsOption = typeof options.serverFunctions === 'object' ? options.serverFunctions.components : undefined; const serverComponents = !!serverComponentsOption; + // Replaced in `config` when the user already defined the flag. Read by + // `configEnvironment`, which runs after `config` resolves. + let serverComponentsDefine = JSON.stringify(serverComponents); // The client runtime compiled server-function references import (the // server-functions plugin's own default unless `runtime` is set), kept only // when it names a package. A relative path or an alias is app source: the @@ -1545,6 +1556,9 @@ export default function solidPlugin(options: Partial = {}): Plugin[] { } } + const userDefine = userConfig.define?.[SERVER_COMPONENTS_DEFINE]; + if (typeof userDefine === 'string') serverComponentsDefine = userDefine; + return { /** * We only need esbuild on .ts or .js files. @@ -1552,6 +1566,11 @@ export default function solidPlugin(options: Partial = {}): Plugin[] { */ // esbuild: { include: /\.ts$/ }, // resolve.conditions is handled per-environment in configEnvironment. + // Build and dev source (via /@vite/env). The optimizer ignores this + // and gets the same flag in configEnvironment. + define: { + [SERVER_COMPONENTS_DEFINE]: serverComponentsDefine, + }, resolve: { dedupe, }, @@ -1611,6 +1630,16 @@ export default function solidPlugin(options: Partial = {}): Plugin[] { }, configEnvironment(name, config, opts) { + // The optimizer does not apply the top-level `define`, and Vite only + // seeds the `client` environment from the top-level `optimizeDeps`. + // Every environment's pre-bundle therefore gets the flag here, unless + // that environment already set its own value. #396 + const optimizeDeps = (config.optimizeDeps ??= {}); + const rolldownOptions = (optimizeDeps.rolldownOptions ??= {}); + const transform = (rolldownOptions.transform ??= {}); + const define = (transform.define ??= {}); + define[SERVER_COMPONENTS_DEFINE] ??= serverComponentsDefine; + config.resolve ??= {}; // Emulate Vite default fallback for `resolve.conditions` if not set if (config.resolve.conditions == null) {