Skip to content
Closed
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/fix-flatten-needsunwrap-or.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/signals": patch
---

Fix `flattenArray` overwriting its `needsUnwrap` flag with a nested call's result instead of OR-ing it (#3133). Under `doNotUnwrap`, an accessor child (a `<For>`/`<Repeat>`/memo) followed at the same level by a fragment containing no functions reset the flag, so `flatten` returned a plain array with the raw accessor still inside instead of the resolving wrapper. Every renderer crashed on the raw function: universal hosts received it in `insertNode` (as reported), and the DOM renderer threw `insertBefore … parameter 1 is not of type 'Node'` — the protective function branch remembered from 1.x dom-expressions does not exist in 2.0. Reported with the fix by @antoinevanwel; also submitted by @nickshiro.
5 changes: 4 additions & 1 deletion packages/signals/src/boundaries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,10 @@ function flattenArray(
} while (typeof child === "function" && !child.length);
}
if (Array.isArray(child)) {
needsUnwrap = flattenArray(child, results, options);
// OR, don't overwrite: an accessor already pushed under doNotUnwrap
// still needs the resolving wrapper even when a later sibling
// fragment contains no functions (#3133).
needsUnwrap = flattenArray(child, results, options) || needsUnwrap;
} else if (
options?.skipNonRendered &&
(child == null || child === true || child === false || child === "")
Expand Down
40 changes: 40 additions & 0 deletions packages/signals/tests/flatten-needs-unwrap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* #3133: `flattenArray` must OR its `needsUnwrap` flag with a nested call's
* result, not overwrite it. Under `doNotUnwrap`, a function child (a
* `<For>`/`<Repeat>`/memo accessor) followed at the same level by an array
* child containing no functions (a fragment) reset the flag, so `flatten`
* returned the plain results array with the raw accessor still inside it
* instead of the resolving wrapper. `@solidjs/web` masked this with its
* insertExpression function branch; universal renderers passed the raw memo
* to the host's insertNode and crashed.
*/
import { describe, expect, it } from "vitest";
import { createMemo, createRoot, flatten } from "../src/index.js";

const OPTS = { skipNonRendered: true, doNotUnwrap: true };

describe("#3133: flatten needsUnwrap under doNotUnwrap", () => {
it("keeps the wrapper when a function-free fragment follows an accessor", () => {
createRoot(() => {
const accessor = createMemo(() => "from memo");
const out = flatten([accessor, ["a", "b"]], OPTS);
expect(typeof out).toBe("function");
expect(out()).toEqual(["from memo", "a", "b"]);
});
});

it("keeps the wrapper when the accessor is inside an earlier fragment", () => {
createRoot(() => {
const accessor = createMemo(() => "nested");
const out = flatten([[accessor], ["plain"]], OPTS);
expect(typeof out).toBe("function");
expect(out()).toEqual(["nested", "plain"]);
});
});

it("still returns a plain array when nothing needs unwrapping", () => {
const out = flatten(["a", ["b", "c"]], OPTS);
expect(Array.isArray(out)).toBe(true);
expect(out).toEqual(["a", "b", "c"]);
});
});
29 changes: 29 additions & 0 deletions packages/web/test/insert-flatten-needs-unwrap.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* @jsxImportSource @solidjs/web
* @vitest-environment jsdom
*
* #3133 at the DOM layer: flattenArray losing `needsUnwrap` when a
* function-free fragment follows an accessor in the same children array made
* `normalize` hand `insertExpression` a plain array with the raw memo still
* inside — `appendNodes` then threw `Failed to execute 'insertBefore' on
* 'Node': parameter 1 is not of type 'Node'`. The issue was reported against
* universal renderers on the belief that web's insertExpression had a
* function branch protecting it; that branch is 1.x dom-expressions — 2.0
* crashes identically.
*/
import { expect, test } from "vitest";
import { createMemo, flush } from "solid-js";
import { render } from "../src/index.js";

test("accessor followed by a fragment inside one children array renders (#3133)", () => {
const container = document.createElement("div");
function App() {
const label = createMemo(() => "from memo");
const children = [label, ["a", "b"]];
return <div>{children}</div>;
}
const dispose = render(() => <App />, container);
flush();
expect(container.textContent).toBe("from memoab");
dispose();
});
Loading