diff --git a/.changeset/merge-memo-unstable-output.md b/.changeset/merge-memo-unstable-output.md new file mode 100644 index 000000000..4f2a35314 --- /dev/null +++ b/.changeset/merge-memo-unstable-output.md @@ -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 (``). 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. diff --git a/packages/signals/src/core/attribution.ts b/packages/signals/src/core/attribution.ts index ef86a4d5d..07847ad77 100644 --- a/packages/signals/src/core/attribution.ts +++ b/packages/signals/src/core/attribution.ts @@ -8,6 +8,7 @@ import { import { CONFIG_GUESS, CONFIG_OVERRIDE, + CONFIG_KEY_BAG, CONFIG_PLUMBING, CONFIG_WIDE, CONFIG_SUPERSEDES, @@ -1778,6 +1779,8 @@ function checkUnstableOutput(el: Computed, 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 diff --git a/packages/signals/src/core/constants.ts b/packages/signals/src/core/constants.ts index 9cc3ed301..0529e796b 100644 --- a/packages/signals/src/core/constants.ts +++ b/packages/signals/src/core/constants.ts @@ -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; diff --git a/packages/signals/src/store/utils.ts b/packages/signals/src/store/utils.ts index a3586affe..087bf5851 100644 --- a/packages/signals/src/store/utils.ts +++ b/packages/signals/src/store/utils.ts @@ -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"; @@ -991,7 +991,13 @@ export function merge(...sources: T): Merge { 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; } diff --git a/packages/signals/tests/attribution-unstable-merge-4256.test.ts b/packages/signals/tests/attribution-unstable-merge-4256.test.ts new file mode 100644 index 000000000..025942370 --- /dev/null +++ b/packages/signals/tests/attribution-unstable-merge-4256.test.ts @@ -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 (``) +// 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); +});