[2.0 rc.13] Compiled SSR without a spread writes `style=""` / `class=""` for a nullish `style` / `class`
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 45/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- rust, typescript
- Domain
- backend
Research direction
Start by examining the SSR template generation in packages/babel-plugin/src/ssr/element.ts around lines 754-755 and packages/compiler/src/ssr/transform.rs around lines 2233-2241, where static style="" and class="" attributes are emitted. Compare with ssrElementAttribute in packages/web/src/server.ts around line 4637, which correctly omits nullish values. The fix should make the non-spread path skip nullish style/class values to match the spread path behavior. Update parity fixtures if they exist, and run the reproduction script from the issue to verify.
Written by the indexing model from the issue text.
Description
Describe the bug
Low priority. On an intrinsic element without a spread, a dynamic style or class that is undefined or null renders as style="" / class="", while title with the same value is omitted. With serverComponents: true both are omitted as well.
This is the template path of #3382. #3393 fixed ssrElement (the spread path) and says "Neither compiler emits these attributes; this is runtime-only". Without a spread they do: both compilers put style=" / class=" and the closing quote in the static template (element.ts#L754-L755, transform.rs#L2233-L2241), and ssrStyle / ssrClassName fill the gap with "" for a nullish value (server.ts#L4281, #L4257). Since the quotes are template bytes, changing only those two helpers can't drop the attribute.
In Chromium 153 with style-src 'self' this shows up as a style-src-attr violation (sample ""; the console offers the hash of the empty string). Chromium reports any style="" that way, a bare <div style=""> included, and class="" doesn't trip it, so the repro skips the browser. Hydration keeps the attribute: style leaves the DOM alone while hydrating (client.ts#L860).
Your Example Website or App
Self-contained Node script under Steps (published packages, no bundler).
Steps to Reproduce the Bug or Issue
- In an empty directory:
npm i [email protected] @solidjs/[email protected] @solidjs/[email protected] @solidjs/[email protected] @babel/core - Save this as
repro.mjsin the same directory and runnode repro.mjs:
import { writeFileSync } from "node:fs";
import { transformSync } from "@babel/core";
import { transform } from "@solidjs/compiler";
import { renderToString } from "@solidjs/web";
const source = `export const App = p => <div style={p.style} class={p.class} title={p.title} />;`;
const compilers = {
oxc: options => transform(source, { filename: "app.jsx", ...options }).code,
babel: options =>
transformSync(source, {
filename: "app.jsx",
babelrc: false,
configFile: false,
plugins: [["@solidjs/babel-plugin", options]]
}).code
};
const cases = {
undefined: { style: undefined, class: undefined, title: undefined },
null: { style: null, class: null, title: null },
filled: { style: { color: "red" }, class: "a", title: "t" }
};
for (const serverComponents of [false, true]) {
const results = {};
for (const [name, compile] of Object.entries(compilers)) {
const code = compile({ moduleName: "@solidjs/web", generate: "ssr", hydratable: true, serverComponents });
const file = new URL(`app-${name}-${serverComponents}.mjs`, import.meta.url);
writeFileSync(file, code);
const { App } = await import(file.href);
const template = JSON.parse(code.match(/_tmpl\$ = (\[[\s\S]*?\]);/)[1]);
const lines = [` template: ${JSON.stringify(template)}`];
for (const [label, props] of Object.entries(cases)) {
lines.push(` ${label.padEnd(10)} ${renderToString(() => App(props))}`);
}
results[name] = lines.join("\n");
}
const same = results.oxc === results.babel;
console.log(`\nserverComponents: ${serverComponents} (oxc and babel ${same ? "print the same" : "differ"})`);
console.log(same ? results.oxc : `oxc:\n${results.oxc}\nbabel:\n${results.babel}`);
}
Output:
serverComponents: false (oxc and babel print the same)
template: ["<div"," style=\"","\" class=\"","\"","></div>"]
undefined <div _hk=0 style="" class=""></div>
null <div _hk=0 style="" class=""></div>
filled <div _hk=0 style="color:red" class="a" title="t"></div>
serverComponents: true (oxc and babel print the same)
template: ["<div","","","","></div>"]
undefined <div _hk=0></div>
null <div _hk=0></div>
filled <div _hk=0 style="color:red" class="a" title="t"></div>
Expected behavior
A nullish dynamic style or class is left out of the SSR output, like title, the spread path since #3393 and a client render.
Screenshots or Videos
N/A.
Platform
- OS: macOS 27.0
- Browser: Chromium 153.0.8010.12 (Playwright 1.63.0), CSP check only
- Node.js: v24.21.0
- Version: 2.0.0-rc.13 of
solid-js,@solidjs/web,@solidjs/compilerand@solidjs/babel-pluginfrom npm. Same output onnextat e44b2e4 with a locally built Babel plugin (I didn't build the Oxc binary there). The cited files are unchanged on the currentnext(4738c6d).
Additional context
- A possible fix: when the
style/classexpression isn't a literal (so it may be nullish), emit the whole attribute as one hole, asserverComponentsdoes withssrElementAttribute(key, x), which skips nullish (server.ts#L4637) and prints the samefilledoutput above. Object literals keep their inlined path. It touches both compilers and the parity fixtures. serverComponents: trueisn't a workaround, since it also stops inlining object literals. A spread on the element is one: the attribute then goes throughssrElementand is left out.- A static
class={undefined}rendersclass=""too, though #3382 assumed the compiler omits it (serverComponentsdoes). - Not part of this:
false,""and an all-nullish object givestyle=""in every mode, spread andserverComponentsincluded, per the server rule that only nullish means unset. The client also writesclass=""for a dynamic empty-string class (client.ts#L777-L780). inlineStyles: false(the answer in solidjs/solid-start#1996) doesn't change the SSR template.
- Dominant language
- TypeScript
- Stars
- 36.1k
- Forks
- 1.1k
- Avg merge
- 9h 33m
- Merged PRs (30d)
- 262
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from solidjs/solid
-
Difficulty 4/5 3-5 days Newbie friendliness 58/100
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 48/100
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 45/100
solidjs/solid#3766 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 48/100
Maintainers usually reply within 1 day
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 4 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day