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-external.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@solidjs/vite-plugin': patch
---

`serverFunctions.components` now also accepts `'external'`: identical to `true`, but declares that a composing host (e.g. the Astro adapter or TanStack Start's Solid integration) owns the document wiring — render plugin + client-side `installServerComponents()` call — itself, so the without-SSR-start-mode warning is skipped instead of printing on every host build. The remaining warning text is also updated: it listed "the bootstrap script" as a required app-side piece, but head bootstrap injection was removed (serialized references self-bootstrap the registry), and it now points hosts at `components: 'external'`.
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -683,9 +683,18 @@ handle that automatically. Combined with SSR start mode
document wiring is emitted too: server components render inline in the
SSR'd document and are adopted
at boot with zero endpoint requests. With authored entries, the app-side
pieces (the render plugin, the bootstrap script, and the client's
`installServerComponents()` call, all from `@solidjs/web/frames`) live in
your entry files instead. See `examples/start-ssr` for a complete page.
pieces (the render plugin and the client's `installServerComponents()`
call, both from `@solidjs/web/frames`) live in your entry files instead.
See `examples/start-ssr` for a complete page.

Composing hosts (e.g. the Astro adapter or TanStack Start's Solid
integration) that emit that document wiring themselves — the render plugin
around their renders plus a client-side `installServerComponents()` call —
should set `components: 'external'` instead of `true`. It behaves
identically (all the same transforms and codegen), and declares the host
owns the wiring, so the plugin skips the warning it otherwise prints when
the option is enabled without SSR start mode. It reuses the plugin's
`external` vocabulary (cf. `start.external` — a host owns the server).

#### options.compiler

Expand Down
2 changes: 1 addition & 1 deletion examples/start-ssr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"dev": "vite",
"build": "vite build",
"serve": "NODE_ENV=production node server.js",
"test": "node test/run.mjs && node test/http-bridge.mjs"
"test": "node test/run.mjs && node test/http-bridge.mjs && node test/components-warning.mjs"
},
"devDependencies": {
"jsdom": "^26.1.0",
Expand Down
137 changes: 137 additions & 0 deletions examples/start-ssr/test/components-warning.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Config-level test for `serverFunctions.components: 'external'` (the host
// acknowledgement value) and the without-SSR-start-mode warning:
// - `components: true` without SSR start mode warns, and the text names
// the current app-side pieces (render plugin + installServerComponents())
// and points hosts at `components: 'external'` — it must NOT mention the
// bootstrap script (head bootstrap injection was removed; serialized
// references self-bootstrap the registry),
// - `components: 'external'` in the same config is silent — that's the
// whole point of the value: composing hosts (Astro adapter, TanStack
// Start Solid) own the document wiring and shouldn't ship a scary log,
// - `'external'` still behaves as enabled: the serve-time optimizeDeps
// pre-bundle of the server-components client runtime (gated on the
// derived serverComponents flag) matches `true` exactly, and stays off
// when the option is off,
// - under full SSR start mode `'external'` is redundant but harmless:
// silent, exactly like `true`.
//
// Pure resolveConfig — no dev server, no browser. Requires the plugin built
// (pnpm build at the repo root). Usage: node test/components-warning.mjs

import { fileURLToPath } from 'node:url';
import path from 'node:path';
import { resolveConfig, createLogger } from 'vite';
import solidPlugin from '@solidjs/vite-plugin';

const exampleDir = path.dirname(path.dirname(fileURLToPath(import.meta.url)));

const results = [];
function record(name, ok, detail = '') {
results.push({ name, ok, detail });
console.log(` ${ok ? 'PASS' : 'FAIL'} ${name}${detail && !ok ? ` — ${detail}` : ''}`);
}

async function resolveWith(solidOptions) {
const warnings = [];
const logger = createLogger('info', { allowClearScreen: false });
const originalWarn = logger.warn;
logger.warn = (msg, opts) => {
warnings.push(String(msg));
originalWarn(msg, opts);
};
const config = await resolveConfig(
{
root: exampleDir,
configFile: false,
customLogger: logger,
plugins: [solidPlugin(solidOptions)],
},
'serve',
);
const componentsWarnings = warnings.filter((w) =>
w.includes('serverFunctions.components is set without SSR start mode'),
);
return { config, componentsWarnings };
}

// ---- `true` without SSR start mode: warns, with the updated text ---------
{
const { config, componentsWarnings } = await resolveWith({
ssr: true,
serverFunctions: { components: true },
});
record('components: true without start mode warns', componentsWarnings.length === 1);
const text = componentsWarnings[0] ?? '';
record(
'warning names the render plugin and installServerComponents()',
text.includes('render plugin') && text.includes('installServerComponents()'),
);
record(
'warning no longer mentions the bootstrap script',
!text.toLowerCase().includes('bootstrap'),
text,
);
record(
"warning points hosts at components: 'external'",
text.includes("components: 'external'"),
text,
);
record(
'enabled: server-components client runtime pre-bundled (true)',
config.optimizeDeps.include.includes('@solidjs/web/frames') &&
config.optimizeDeps.include.includes('@solidjs/web/server-functions'),
);
}

// ---- `'external'` in the identical config: silent, still enabled ---------
{
const { config, componentsWarnings } = await resolveWith({
ssr: true,
serverFunctions: { components: 'external' },
});
record(
"components: 'external' without start mode does not warn",
componentsWarnings.length === 0,
componentsWarnings[0],
);
record(
"enabled: server-components client runtime pre-bundled ('external' = true)",
config.optimizeDeps.include.includes('@solidjs/web/frames') &&
config.optimizeDeps.include.includes('@solidjs/web/server-functions'),
);
}

// ---- option off: the pre-bundle stays off (probe is meaningful) ----------
{
const { config, componentsWarnings } = await resolveWith({
ssr: true,
serverFunctions: true,
});
record(
'off: no warning and no server-components pre-bundle',
componentsWarnings.length === 0 &&
!config.optimizeDeps.include.includes('@solidjs/web/frames'),
);
}

// ---- full SSR start mode: `'external'` is redundant but harmless ---------
{
const { componentsWarnings } = await resolveWith({
ssr: true,
start: { app: 'src/frames/FramesApp.tsx' },
serverFunctions: { components: 'external' },
});
record(
"components: 'external' under SSR start mode is silent (like true)",
componentsWarnings.length === 0,
componentsWarnings[0],
);
}

const failed = results.filter((r) => !r.ok);
console.log(`\n${results.length - failed.length}/${results.length} assertions passed`);
if (failed.length) {
console.log('\nFailures:');
for (const f of failed) console.log(` ${f.name} — ${f.detail}`);
process.exit(1);
}
26 changes: 19 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -573,8 +573,9 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
// resolve against the Vite root, not process.cwd() — running `vite` from
// outside the project would otherwise change what the filter matches.
let filter = createFilter(options.include, options.exclude);
const serverComponents =
typeof options.serverFunctions === 'object' && !!options.serverFunctions.components;
const serverComponentsOption =
typeof options.serverFunctions === 'object' ? options.serverFunctions.components : undefined;
const serverComponents = !!serverComponentsOption;
// `start: true` is sugar for the empty options bag — one start mode,
// two spellings — so normalize here and let everything downstream see a
// single shape (`false` behaves exactly like omission).
Expand Down Expand Up @@ -906,14 +907,25 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
projectRoot = config.root;
filter = createFilter(options.include, options.exclude, { resolve: projectRoot });
styleFilter = createStyleFilter(projectRoot);
if (serverComponents && !(options.start && options.ssr)) {
// `components: 'external'` is the acknowledgement that a composing
// host (e.g. the Astro adapter or TanStack Start's Solid integration)
// owns the document wiring itself — behavior is identical to `true`,
// only this warning is skipped. Under SSR start mode it's redundant
// but harmless (treated exactly as `true`).
if (
serverComponents &&
serverComponentsOption !== 'external' &&
!(options.start && options.ssr)
) {
config.logger.warn(
'[@solidjs/vite-plugin] serverFunctions.components is set without SSR start mode (the `start` ' +
'option with `ssr: true`), so the plugin only installs the endpoint response transform ' +
'(server functions returning components stream correctly). The document wiring — render ' +
'plugin, bootstrap script, and the client-side installServerComponents() call — is ' +
"emitted by SSR start mode's generated entries; without it, server components only mount " +
'from post-boot streams and your client code must call installServerComponents() itself.',
'(server functions returning components stream correctly). The document wiring — the ' +
'render plugin (with the direct-call transform) and the client-side ' +
"installServerComponents() call — is emitted by SSR start mode's generated entries; " +
'without it, server components only mount from post-boot streams and your client code ' +
'must call installServerComponents() itself. If a composing host owns that wiring, set ' +
"`components: 'external'` to acknowledge it and silence this warning.",
);
}
needHmr =
Expand Down
23 changes: 14 additions & 9 deletions src/server-functions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,21 +138,26 @@ export interface ServerFunctionsOptions {
* per-request wiring or server code.
*
* Document SSR of server components (rendered inline at t=0 and adopted
* at boot with zero endpoint requests) needs three more pieces: the
* render must run with the server-component render plugin, the document
* must carry the bootstrap script, and the client must call
* `installServerComponents()` before hydrating. With SSR start mode (the
* main plugin's `start` option with `ssr: true`) and generated entries
* the plugin emits all three. With authored entries those pieces live in
* your entry files — import them from `@solidjs/web/frames` (see the
* README).
* at boot with zero endpoint requests) needs two more pieces: the render
* must run with the server-component render plugin (plus the direct-call
* transform), and the client must call `installServerComponents()` before
* hydrating (the serialized references self-bootstrap the registry, so no
* separate bootstrap script is involved). With SSR start mode (the main
* plugin's `start` option with `ssr: true`) and generated entries the
* plugin emits both. With authored entries those pieces live in your
* entry files — import them from `@solidjs/web/frames` (see the README).
*
* `'external'` behaves exactly like `true`, and additionally declares
* that a composing host (e.g. a meta-framework adapter such as Astro's or
* TanStack Start's) owns that document wiring itself, so the plugin skips
* the without-SSR-start-mode warning.
*
* All of this is pure codegen: when the option is off, no reference to
* the server-component runtime is emitted anywhere.
*
* @default false
*/
components?: boolean;
components?: boolean | 'external';
}

const DEFAULT_INCLUDE = 'src/**/*.{jsx,tsx,ts,js,mjs,cjs}';
Expand Down
Loading