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
5 changes: 5 additions & 0 deletions .changeset/server-components-define.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion examples/ssr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 11 additions & 0 deletions examples/ssr/test/fixtures/sc-define.ts
Original file line number Diff line number Diff line change
@@ -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);
160 changes: 160 additions & 0 deletions examples/ssr/test/server-components-define.mjs
Original file line number Diff line number Diff line change
@@ -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);
29 changes: 29 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Options> = {}): Plugin[] {
if (typeof options.ssr === 'object') {
throw new Error(
Expand All @@ -1112,6 +1120,9 @@ export default function solidPlugin(options: Partial<Options> = {}): 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
Expand Down Expand Up @@ -1545,13 +1556,21 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
}
}

const userDefine = userConfig.define?.[SERVER_COMPONENTS_DEFINE];
if (typeof userDefine === 'string') serverComponentsDefine = userDefine;

return {
/**
* We only need esbuild on .ts or .js files.
* .tsx & .jsx files are handled by us
*/
// 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,
},
Expand Down Expand Up @@ -1611,6 +1630,16 @@ export default function solidPlugin(options: Partial<Options> = {}): 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) {
Expand Down
Loading