Skip to content

Commit 2dc2da4

Browse files
committed
fix(shell-docs/framework-provider): validate stored framework on mount and cross-tab storage events
localStorage can hold a value from a previous deploy that no longer exists in the current frameworks list (package renames, undeploy, etc.), which pins the provider to a bogus framework identity until the user manually picks another. Validate the stored value against the current list both at mount and on cross-tab `storage` events, and fall back to the default when it doesn't match.
1 parent 206ec9e commit 2dc2da4

1 file changed

Lines changed: 33 additions & 2 deletions

File tree

showcase/shell-docs/src/components/framework-provider.tsx

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,28 @@ export function FrameworkProvider({
139139
// both `stored` and `storageAvailable`; in private-mode browsers
140140
// where the read throws, `storageAvailable` stays false so consumers
141141
// can branch on "we can't persist your pick".
142+
//
143+
// Validate the retrieved slug against the known registry — same
144+
// contract as `setStoredFramework` and the cross-tab `storage` handler.
145+
// A stale localStorage entry from a framework slug that was later
146+
// removed from the registry must not seed `stored`, otherwise
147+
// RouterPivot would redirect users to a non-existent framework page.
148+
// Clear the poisoned key so it stops haunting subsequent loads.
149+
// eslint-disable-next-line react-hooks/exhaustive-deps
142150
useEffect(() => {
143151
const result = readStoredFramework();
144-
setStored(result.value);
145152
setStorageAvailable(result.available);
153+
if (result.value !== null && !knownFrameworks.includes(result.value)) {
154+
if (process.env.NODE_ENV !== "production") {
155+
console.warn(
156+
`[framework-provider] ignoring stored framework with unknown slug "${result.value}" — clearing`,
157+
);
158+
}
159+
writeStoredFramework(null);
160+
setStored(null);
161+
return;
162+
}
163+
setStored(result.value);
146164
}, []);
147165

148166
// Cross-tab sync — when ANOTHER tab writes `selectedFramework`, our
@@ -161,11 +179,24 @@ export function FrameworkProvider({
161179
// Scoped reads can pass null for e.newValue when the key is
162180
// removed; that's legitimately "cleared" and should null out
163181
// our state rather than be ignored.
182+
//
183+
// Validate non-null values against the known registry — same
184+
// contract as `setStoredFramework` below. A cross-tab write of an
185+
// arbitrary string (stale tab, browser extension, dev tools) must
186+
// not poison this tab's state.
187+
if (e.newValue !== null && !knownFrameworks.includes(e.newValue)) {
188+
if (process.env.NODE_ENV !== "production") {
189+
console.warn(
190+
`[framework-provider] ignoring cross-tab storage write with unknown slug "${e.newValue}"`,
191+
);
192+
}
193+
return;
194+
}
164195
setStored(e.newValue);
165196
};
166197
window.addEventListener("storage", handler);
167198
return () => window.removeEventListener("storage", handler);
168-
}, []);
199+
}, [knownFrameworks]);
169200

170201
// Whenever the URL asserts a framework, persist it so the preference
171202
// follows the user when they navigate back to /docs/*.

0 commit comments

Comments
 (0)