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
9 changes: 9 additions & 0 deletions .changeset/ssr-component-names.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@solidjs/babel-plugin": patch
"@solidjs/compiler": patch
"solid-js": patch
---

`componentNames` now applies to SSR output. Under the option both compilers keep the `createComponent` call they otherwise inline to `Comp(props)` and pass the source tag name — `createComponent(Comp, props, "Comp")` — so the server runtime's observe/dev `createComponent` labels the owner and a server finding's `ownerPath` reads `<App> › <Page>` like the client's. Without the option (prod builds) SSR output is unchanged. `@solidjs/vite-plugin` already passes the option for its dev and observe postures, so app server builds pick this up with no config change.

Fixes `ssrScope` under transparent owners: the virtual hole scope swapped the current owner's id counter, but content inside a hole resolves ids by walking past transparent owners, so with one in between (the server-component scope owner; now the labelled component owner) the hole's content took ids from the enclosing counter and disagreed with the client. The scope now swaps the nearest id-bearing owner.
32 changes: 25 additions & 7 deletions documentation/plans/server-dev-build-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,13 +196,31 @@ Decision: **reuse `@solidjs/signals`'s channel, do not fork it.**
> `packages/web/test/frames-marker-corruption.spec.tsx`. Docs: RFC 08
> "Server rendering" + quick reference; the reactivity-diagnostics skill.
>
> Known gap, deliberately out of this PR: the SSR compiler inlines component
> calls (`Comp({})`) instead of `createComponent`, so compiled JSX does not
> get the label — `ownerPath` is populated for `createComponent` callers
> (the runtime's own flow components, `Dynamic`, tests) and empty for a
> plain compiled `<Comp/>` tree. The fix is the compiler emitting
> `createComponent` under the `componentNames` option for SSR output as it
> does for the client; tracked separately.
> Known gap at the time, closed in the follow-up (2026-09-14): the SSR
> compiler inlined component calls (`Comp({})`) instead of `createComponent`,
> so compiled JSX did not get the label — `ownerPath` was populated for
> `createComponent` callers (the runtime's own flow components, `Dynamic`,
> tests) and empty for a plain compiled `<Comp/>` tree. Both compilers now
> honour `componentNames` for the `ssr` generate the way they do for `dom`:
> the output keeps `createComponent(Comp, props, "Comp")` (the label has
> nowhere else to go; the prod server `createComponent` is that same
> `Comp(props)` call plus one frame), and without the option SSR still
> inlines. The vite plugin already passes `componentNames` for its dev and
> observe postures, so an app's server build labels every compiled component
> in exactly the builds whose runtime reads the argument; prod output is
> byte-identical to before. Boundaries are compiled components too, so a
> server finding raised by a boundary reads `<App> › <Errored>` — as on the
> client. Landing this surfaced a latent `ssrScope` bug: the virtual hole
> scope swapped the CURRENT owner's `id`/`_childCount`, but content inside
> the hole resolves ids by walking up past transparent owners — so with a
> transparent owner in between (the server-component scope owner in every
> tier; now the labelled `<Name>` owner under every component body) the
> reserved slot was invisible and the hole's content took fresh ids from the
> enclosing counter (`_hk=3` where the client expects `_hk=10`). The scope
> now swaps the nearest id-bearing owner
> (`packages/solid/test/server/ssr-scope.spec.ts`). The web server suite
> compiles with `componentNames` (`vite.config.server.mjs`), so its
> hydration-id and diagnostics specs run against the labelled shape.

- The server facade imports `DEV` (and the `emitDiagnostic` /
`DiagnosticEvent` types) from `@solidjs/signals`, every use behind
Expand Down
2 changes: 1 addition & 1 deletion packages/babel-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ Development output. With `hydratable`, emits the hydration walk validation helpe
- Type: `boolean`
- Default: `false`

DOM output only. Emit the tag as written in source as a third `createComponent` argument — `<Home />` compiles to `createComponent(Home, props, "Home")`, `<Ui.Button />` to `"Ui.Button"` — so the dev and observe runtimes label each component's owner (`<Home>` in diagnostic `ownerPath`s and attribution chains) even after a minifier renames the function or a `lazy()`/HMR wrapper hides it. The production runtime ignores the argument; SSR and universal output are unaffected. `@solidjs/vite-plugin` turns this on for its dev and `observe` postures.
Emit the tag as written in source as a third `createComponent` argument — `<Home />` compiles to `createComponent(Home, props, "Home")`, `<Ui.Button />` to `"Ui.Button"` — so the dev and observe runtimes label each component's owner (`<Home>` in diagnostic `ownerPath`s and attribution chains) even after a minifier renames the function or a `lazy()`/HMR wrapper hides it. Applies to DOM and SSR output; for SSR the compiler keeps the `createComponent` call it otherwise inlines to `Comp(props)`, so the server runtime labels the owner the same way (prod SSR output, without the option, is unchanged). Universal and dynamic output are unaffected. The production runtimes ignore the argument. `@solidjs/vite-plugin` turns this on for its dev and `observe` postures.

### delegateEvents

Expand Down
6 changes: 4 additions & 2 deletions packages/babel-plugin/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ export interface PluginConfig {
dev: boolean;
/** Emit the source tag name as a third `createComponent` argument
* (`createComponent(Home, props, "Home")`) so dev/observe runtimes can
* label owners after minification renames the function. DOM output only;
* the production runtime ignores the argument. */
* label owners after minification renames the function. DOM and SSR
* output (SSR keeps the `createComponent` call it would otherwise inline
* to `Comp(props)`); not universal or dynamic. The production runtimes
* ignore the argument. */
componentNames: boolean;
delegateEvents: boolean;
delegatedEvents: string[];
Expand Down
19 changes: 14 additions & 5 deletions packages/babel-plugin/src/shared/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,18 +347,27 @@ export default function transformComponent(
const componentArgs = [tagId, props[0]];
// `componentNames` carries the source tag name into the call so the
// dev/observe runtimes can label the owner after minification renames the
// function. DOM output only: SSR inlines the call below and the universal
// renderer's `createComponent` is user code with a two-argument contract.
if (config.componentNames && config.generate === "dom") {
// function — on the client (`createComponent` in solid-js's client entry)
// and on the server (its server entry's, which runs the body under a
// transparent `<Name>` owner in observe/dev so a server finding's
// `ownerPath` reads like the client's). Not for the universal renderer,
// whose `createComponent` is user code with a two-argument contract, nor
// the dynamic renderer's subtrees.
const labelled =
config.componentNames && (config.generate === "dom" || config.generate === "ssr");
if (labelled) {
componentArgs.push(t.stringLiteral(tagName));
}
// SSR's `createComponent` is literally `Comp(props || {})`. Since the
// SSR's prod `createComponent` is literally `Comp(props || {})`. Since the
// compiler always emits a real `props[0]` object expression above (see the
// `props.push(t.objectExpression(runningObject))` line), the `|| {}` fallback
// never fires in compiled output. Inline to a direct `Comp(props)` call to
// drop one function-call frame per component invocation. (DOM/dev modes
// keep the wrapper since it does real work — `untrack`, dev metadata.)
if (config.generate === "ssr") {
// With `componentNames` the wrapper IS the work — the label has nowhere
// else to go — so SSR output keeps the call; the vite-plugin turns the
// option on for dev and observe builds only, so prod output stays inlined.
if (config.generate === "ssr" && !labelled) {
exprs.push(t.callExpression(tagId, [props[0]]));
} else {
exprs.push(t.callExpression(registerImportMethod(path, "createComponent"), componentArgs));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Child, Ui, Row } from "./components";

const Component = () => <Child name="John" />;

const template = (
<div>
<Child name="Jane" {...props}>
<span>{name()}</span>
</Child>
<Ui.Button variant="primary" />
<Ui.Layout.Grid cols={2}>text</Ui.Layout.Grid>
<For each={list()}>{item => <Row item={item} />}</For>
<Show when={visible()}>
<Child />
</Show>
<this.Row />
<Comp>{() => <Child />}</Comp>
</div>
);

class Container {
render() {
return <this.Row />;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Show as _$Show } from "r-dom";
import { For as _$For } from "r-dom";
import { mergeProps as _$mergeProps } from "r-dom";
import { ssr as _$ssr } from "r-dom";
import { escape as _$escape } from "r-dom";
import { createComponent as _$createComponent } from "r-dom";
var _v$;
var _tmpl$ = ["<span>", "</span>"],
_tmpl$2 = ["<div>", "", "", "", "", "", "", "</div>"];
import { Child, Ui, Row } from "./components";
const Component = () =>
_$createComponent(
Child,
{
name: "John"
},
"Child"
);
var _v$2 = _$escape(
_$createComponent(
Child,
_$mergeProps(
{
name: "Jane"
},
props,
{
get children() {
return ((_v$ = () => _$escape(name())), _$ssr(_tmpl$, _v$));
}
}
),
"Child"
)
),
_v$3 = _$escape(
_$createComponent(
Ui.Button,
{
variant: "primary"
},
"Ui.Button"
)
),
_v$4 = _$escape(
_$createComponent(
Ui.Layout.Grid,
{
cols: 2,
children: "text"
},
"Ui.Layout.Grid"
)
),
_v$5 = _$escape(
_$createComponent(
_$For,
{
get each() {
return list();
},
children: item =>
_$createComponent(
Row,
{
item: item
},
"Row"
)
},
"For"
)
),
_v$6 = _$escape(
_$createComponent(
_$Show,
{
get when() {
return visible();
},
get children() {
return _$createComponent(Child, {}, "Child");
}
},
"Show"
)
),
_v$7 = _$escape(_$createComponent(_self$.Row, {}, "this.Row")),
_v$8 = _$escape(
_$createComponent(
Comp,
{
children: () => _$createComponent(Child, {}, "Child")
},
"Comp"
)
);
const template = (() => {
const _self$ = this;
return _$ssr(_tmpl$2, _v$2, _v$3, _v$4, _v$5, _v$6, _v$7, _v$8);
})();
class Container {
render() {
return _$createComponent(this.Row, {}, "this.Row");
}
}

This file was deleted.

This file was deleted.

2 changes: 1 addition & 1 deletion packages/compiler/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ Pass `sourceMap: true` to receive a JSON source map string in `result.map`. For
- `generate`: `"dom"`, `"ssr"`, `"universal"`, or `"dynamic"` (default `"dom"`)
- `hydratable`
- `dev`
- `componentNames`: DOM output only — emit the source tag name as `createComponent`'s third argument (`createComponent(Home, props, "Home")`) so dev/observe runtimes label owners after minification; the production runtime ignores it
- `componentNames`: emit the source tag name as `createComponent`'s third argument (`createComponent(Home, props, "Home")`) so dev/observe runtimes label owners after minification; the production runtimes ignore it. DOM and SSR output (SSR keeps the `createComponent` call it otherwise inlines to `Comp(props)`); not universal or dynamic
- `sourceMap`
- `contextToCustomElements` (default `true`)
- `delegateEvents`
Expand Down
15 changes: 14 additions & 1 deletion packages/compiler/__tests__/dom-component-names-fixtures.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const oxcFixtures = path.resolve(__dirname, "fixtures/dom-component-names");

const fixtureParity = {
components: "subset",
ssrInert: "subset"
ssr: "subset"
};

const suiteOptions = {
Expand Down Expand Up @@ -83,4 +83,17 @@ describe("AST-native Babel DOM componentNames fixture reuse", () => {
});
expect(code).not.toContain('"Child"');
});

// SSR keeps the `createComponent` wrapper only for the label; without the
// option it inlines `Comp(props)` and imports no `createComponent`.
it("SSR inlines the component call without the option", () => {
const { code } = transform(readFixture("ssr"), {
filename: "ssr.jsx",
...fixtureOptions("ssr"),
componentNames: false
});
expect(code).not.toContain("createComponent");
expect(code).not.toContain('"Child"');
expect(code).toContain("Child({");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { escape as _$escape } from "r-dom";
import { ssr as _$ssr } from "r-dom";
import { mergeProps as _$mergeProps } from "r-dom";
import { createComponent as _$createComponent } from "r-dom";
import { For as _$For } from "r-dom";
import { Show as _$Show } from "r-dom";
var _v$;
var _tmpl$ = ["<span>", "</span>"];
var _tmpl$2 = [
"<div>",
"",
"",
"",
"",
"",
"",
"</div>"
];
import { Child, Ui, Row } from "./components";
const Component = () => _$createComponent(Child, { name: "John" }, "Child");
const template = (() => {
var _v$2 = _$escape(_$createComponent(Child, _$mergeProps({ name: "Jane" }, props, { get children() {
return _v$ = () => {
return _$escape(name());
}, _$ssr(_tmpl$, _v$);
} }), "Child")), _v$3 = _$escape(_$createComponent(Ui.Button, { variant: "primary" }, "Ui.Button")), _v$4 = _$escape(_$createComponent(Ui.Layout.Grid, {
cols: 2,
children: "text"
}, "Ui.Layout.Grid")), _v$5 = _$escape(_$createComponent(_$For, {
get each() {
return list();
},
children: (item) => _$createComponent(Row, { item }, "Row")
}, "For")), _v$6 = _$escape(_$createComponent(_$Show, {
get when() {
return visible();
},
get children() {
return _$createComponent(Child, {}, "Child");
}
}, "Show")), _v$7 = _$escape(_$createComponent(_self$.Row, {}, "this.Row")), _v$8 = _$escape(_$createComponent(Comp, { children: () => _$createComponent(Child, {}, "Child") }, "Comp"));
const _self$ = this;
return _$ssr(_tmpl$2, _v$2, _v$3, _v$4, _v$5, _v$6, _v$7, _v$8);
})();
class Container {
render() {
return _$createComponent(this.Row, {}, "this.Row");
}
}

This file was deleted.

1 change: 1 addition & 0 deletions packages/compiler/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ fn compile_inner(source: &str, options: &CompileOptions) -> Result<CompileOutput
options.hydratable,
options.server_components,
options.wrap_conditionals,
options.component_names,
wrapper_name(&options.memo_wrapper, "memo"),
options.static_marker.clone(),
options.built_ins.clone(),
Expand Down
6 changes: 4 additions & 2 deletions packages/compiler/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ pub struct TransformOptions {
pub dev: Option<bool>,
/// Babel's `componentNames`: emit the source tag name as a third
/// `createComponent` argument (`createComponent(Home, props, "Home")`) so
/// dev/observe runtimes can label owners after minification. DOM output
/// only; the production runtime ignores the argument.
/// dev/observe runtimes can label owners after minification. DOM and SSR
/// output (SSR keeps the `createComponent` call it would otherwise inline
/// to `Comp(props)`); not universal or dynamic. The production runtimes
/// ignore the argument.
pub component_names: Option<bool>,
pub source_map: Option<bool>,
pub context_to_custom_elements: Option<bool>,
Expand Down
Loading
Loading