Skip to content
Open
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/merge-memo-unstable-output.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/signals": patch
---

`UNSTABLE_MEMO_OUTPUT` no longer reports the memo `merge()` creates for a function source, such as a compiled conditional component spread (`<Child {...(on() ? { tone } : {})} />`). No one can pass that memo `equals`, so the warning had no fix (#4256). In observe builds the memo is now named `merge`. Production output is unchanged.
3 changes: 3 additions & 0 deletions packages/signals/src/core/attribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import {
CONFIG_GUESS,
CONFIG_OVERRIDE,
CONFIG_KEY_BAG,
CONFIG_PLUMBING,
CONFIG_WIDE,
CONFIG_SUPERSEDES,
Expand Down Expand Up @@ -1778,6 +1779,8 @@ function checkUnstableOutput(el: Computed<any>, prevValue: unknown, newValue: un
// typeof guard: an explicit `unstableMemos: undefined` in enable() options
// clobbers the default through the spread — treat any non-number as off.
if (typeof limit !== "number") return;
// merge()'s function-source memo: a key bag no user configures (#4256).
if (el._config & CONFIG_KEY_BAG) return;
const node = el as AttributedNode;
if (
prevValue === newValue || // paranoia: changed runs should never hit this
Expand Down
7 changes: 7 additions & 0 deletions packages/signals/src/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,13 @@ export const CONFIG_WIDE = 1 << 26;
* creation by dev builds only (the observe artifact mangles the option name,
* as it does `_wide`). */
export const CONFIG_SUPERSEDES = 1 << 27;
/** Observe tiers only: the memo `merge()` creates for a function source — a
* key bag readers consume key by key, built by the framework (a compiled
* component spread) with options no user can reach. The attribution engine's
* UNSTABLE_MEMO_OUTPUT skips it: a fresh-but-equal bag is the idiom
* (`{...(on() ? { a } : {})}`), and the advice (`equals`) cannot be applied
* (#4256). Set through `_extraConfig` by observe builds only. */
export const CONFIG_KEY_BAG = 1 << 28;

export const STATUS_NONE = 0;
export const STATUS_PENDING = 1 << 0;
Expand Down
10 changes: 8 additions & 2 deletions packages/signals/src/store/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SUPPORTS_PROXY } from "../core/constants.js";
import { CONFIG_KEY_BAG, SUPPORTS_PROXY } from "../core/constants.js";
import { createMemo } from "../signals.js";
import { $PROXY, $RECORD, $TARGET, ownEnumerableKeys } from "./types.js";

Expand Down Expand Up @@ -991,7 +991,13 @@ export function merge<T extends unknown[]>(...sources: T): Merge<T> {
count++;
only = s;
if (typeof s === "function") {
flattened[n] = createMemo(s as () => any) as any;
// Observe tiers name it and exempt it from UNSTABLE_MEMO_OUTPUT (see
// CONFIG_KEY_BAG); prod passes no options.
flattened[n] = (
__OBSERVE__
? createMemo(s as () => any, { name: "merge", _extraConfig: CONFIG_KEY_BAG } as {})
: createMemo(s as () => any)
) as any;
kinds[n++] = SOURCE_MEMO;
continue;
}
Expand Down
48 changes: 48 additions & 0 deletions packages/signals/tests/attribution-unstable-merge-4256.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { afterEach, expect, it, vi } from "vitest";
import { attribution } from "../src/attribution.js";
import { createEffect, createRoot, createSignal, flush, merge, OBSERVE } from "../src/index.js";
import type { DiagnosticEvent } from "../src/core/dev.js";

afterEach(() => {
attribution.disable();
flush();
vi.restoreAllMocks();
});

function captureUnstable() {
vi.spyOn(console, "warn").mockImplementation(() => {});
attribution.enable({ log: false, hotRuns: false, hotTime: false, unstableMemos: 4 });
const events: DiagnosticEvent[] = [];
OBSERVE!.diagnostics.subscribe(e => {
if (e.code === "UNSTABLE_MEMO_OUTPUT") events.push(e);
});
return events;
}

// #4256: a compiled conditional component spread (`<Child {...(on() ? { tone } : {})} />`)
// is `merge(() => …)`. The memo merge() wraps the function in returns a fresh
// key bag each run, and no user can pass it `equals`.
it("merge()'s function-source memo is not reported as unstable output (#4256)", () => {
const [n, setN] = createSignal(0, { name: "n" });
let seen: unknown;
createRoot(() => {
const props = merge(() => (n() >= 0 ? { tone: "info" } : {})) as { tone?: string };
createEffect(
() => props.tone,
v => {
seen = v;
},
{ name: "reader" }
);
});
flush();

const events = captureUnstable();
for (let i = 1; i <= 8; i++) {
setN(i);
flush();
}

expect(seen).toBe("info");
expect(events).toHaveLength(0);
});
Loading