diff --git a/.changeset/server-components-external.md b/.changeset/server-components-external.md new file mode 100644 index 0000000..488c9ff --- /dev/null +++ b/.changeset/server-components-external.md @@ -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'`. diff --git a/README.md b/README.md index 9fb660f..7f02fdd 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/examples/start-ssr/package.json b/examples/start-ssr/package.json index 7cb86b4..34f4a12 100644 --- a/examples/start-ssr/package.json +++ b/examples/start-ssr/package.json @@ -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", diff --git a/examples/start-ssr/test/components-warning.mjs b/examples/start-ssr/test/components-warning.mjs new file mode 100644 index 0000000..fd9029e --- /dev/null +++ b/examples/start-ssr/test/components-warning.mjs @@ -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); +} diff --git a/src/index.ts b/src/index.ts index 0dbce2d..49dac99 100644 --- a/src/index.ts +++ b/src/index.ts @@ -573,8 +573,9 @@ export default function solidPlugin(options: Partial = {}): 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). @@ -906,14 +907,25 @@ export default function solidPlugin(options: Partial = {}): 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 = diff --git a/src/server-functions/index.ts b/src/server-functions/index.ts index 54fd2b6..d234a80 100644 --- a/src/server-functions/index.ts +++ b/src/server-functions/index.ts @@ -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}';