-
-
Notifications
You must be signed in to change notification settings - Fork 10.1k
Manager API: Namespace localStorage keys by project ID #34352
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,19 +9,23 @@ storeSetup(store._); | |
|
|
||
| export const STORAGE_KEY = '@storybook/manager/store'; | ||
|
|
||
| function get(storage: StoreAPI) { | ||
| const data = storage.get(STORAGE_KEY); | ||
| function getStorageKey(projectId?: string) { | ||
| return projectId ? `${STORAGE_KEY}:${projectId}` : STORAGE_KEY; | ||
| } | ||
|
|
||
| function get(storage: StoreAPI, projectId?: string) { | ||
| const data = storage.get(getStorageKey(projectId)); | ||
| return data || {}; | ||
| } | ||
|
|
||
| function set(storage: StoreAPI, value: Patch) { | ||
| return storage.set(STORAGE_KEY, value); | ||
| function set(storage: StoreAPI, value: Patch, projectId?: string) { | ||
| return storage.set(getStorageKey(projectId), value); | ||
| } | ||
|
|
||
| function update(storage: StoreAPI, patch: Patch) { | ||
| const previous = get(storage); | ||
| function update(storage: StoreAPI, patch: Patch, projectId?: string) { | ||
| const previous = get(storage, projectId); | ||
| // Apply the same behaviour as react here | ||
| return set(storage, { ...previous, ...patch }); | ||
| return set(storage, { ...previous, ...patch }, projectId); | ||
| } | ||
|
|
||
| type GetState = () => State; | ||
|
|
@@ -33,6 +37,11 @@ export interface Upstream { | |
| * persistence in Storybook's own tests. True by default. | ||
| */ | ||
| allowPersistence?: boolean; | ||
| /** | ||
| * Unique project identifier used to namespace localStorage/sessionStorage keys. | ||
| * Prevents state from being shared across different Storybook instances. | ||
| */ | ||
| projectId?: string; | ||
| getState: GetState; | ||
| setState: SetState; | ||
| } | ||
|
|
@@ -60,10 +69,12 @@ export default class Store { | |
| upstreamPersistence: boolean; | ||
| upstreamGetState: GetState; | ||
| upstreamSetState: SetState; | ||
| projectId: string | undefined; | ||
| private persistenceHandlers: Map<string, PersistenceHandler> = new Map(); | ||
|
|
||
| constructor({ allowPersistence, setState, getState }: Upstream) { | ||
| constructor({ allowPersistence, projectId, setState, getState }: Upstream) { | ||
| this.upstreamPersistence = allowPersistence ?? true; | ||
| this.projectId = projectId; | ||
| this.upstreamSetState = setState; | ||
| this.upstreamGetState = getState; | ||
| } | ||
|
|
@@ -79,18 +90,18 @@ export default class Store { | |
| // One-time migration: tag filter state moved from localStorage to URL persistence. | ||
| // Remove the old keys so they no longer interfere with URL-derived initial state. | ||
| for (const storage of [store.local, store.session] as const) { | ||
| const persisted = get(storage); | ||
| const persisted = get(storage, this.projectId); | ||
| if ('includedTagFilters' in persisted || 'excludedTagFilters' in persisted) { | ||
| const { includedTagFilters: _i, excludedTagFilters: _e, ...rest } = persisted; | ||
| set(storage, rest); | ||
| set(storage, rest, this.projectId); | ||
| } | ||
| } | ||
|
|
||
| // We don't only merge at the very top level (the same way as React setState) | ||
| // when you set keys, so it makes sense to do the same in combining the two storage modes | ||
| // Really, you shouldn't store the same key in both places | ||
| const local = get(store.local); | ||
| const session = get(store.session); | ||
| const local = get(store.local, this.projectId); | ||
| const session = get(store.session, this.projectId); | ||
|
|
||
| return { ...base, ...local, ...session }; | ||
| } | ||
|
|
@@ -147,7 +158,7 @@ export default class Store { | |
| } | ||
| } else { | ||
| const storage = persistence === 'session' ? store.session : store.local; | ||
| await update(storage, delta); | ||
| await update(storage, delta, this.projectId); | ||
| } | ||
|
Comment on lines
160
to
162
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify test coverage for projectId-namespaced storage paths
echo "=== Checking Store instantiations in tests ==="
rg -n "new Store\(" --type js --type ts -g '*test*' -A 5
echo ""
echo "=== Checking if any tests pass projectId ==="
rg -n "projectId" --type js --type ts -g '*test*' code/core/src/manager-api/Repository: storybookjs/storybook Length of output: 5292 🏁 Script executed: #!/bin/bash
# Check Store class definition and projectId usage
echo "=== Store class constructor signature ==="
head -50 code/core/src/manager-api/store.ts | cat -n
echo ""
echo "=== Lines around 160-162 in store.ts ==="
sed -n '150,170p' code/core/src/manager-api/store.ts | cat -n
echo ""
echo "=== Check if projectId is a constructor parameter ==="
rg -n "constructor\|projectId" code/core/src/manager-api/store.ts -A 2 -B 1Repository: storybookjs/storybook Length of output: 2681 🏁 Script executed: #!/bin/bash
# Find the Store class constructor and projectId assignment
echo "=== Full Store class with constructor ==="
sed -n '50,130p' code/core/src/manager-api/store.ts | cat -n
echo ""
echo "=== Check root.tsx for Store instantiation ==="
rg -n "new Store" code/core/src/root.tsx -B 2 -A 5Repository: storybookjs/storybook Length of output: 3769 🏁 Script executed: #!/bin/bash
# Find root.tsx
echo "=== Searching for root.tsx ==="
fd -name "root.tsx" code/
echo ""
echo "=== Searching for Store instantiation in production ==="
rg -n "new Store" code/core/src/ -t ts -t tsx --max-count 5Repository: storybookjs/storybook Length of output: 428 🏁 Script executed: #!/bin/bash
# Search for root.tsx and Store instantiation with correct syntax
echo "=== Searching for root.tsx in entire repo ==="
fd root.tsx
echo ""
echo "=== Searching for Store instantiation in manager-api ==="
rg "new Store" code/core/src/manager-api/ -A 3
echo ""
echo "=== Check how Store is instantiated in production ==="
rg "new Store" code/ -A 3 --type ts --type js | head -50Repository: storybookjs/storybook Length of output: 6829 Add tests with projectId to cover namespaced storage paths. The Consider adding test cases that pass a 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: storybookjs/storybook
Length of output: 5171
🏁 Script executed:
Repository: storybookjs/storybook
Length of output: 5025
🏁 Script executed:
Repository: storybookjs/storybook
Length of output: 606
Remove Node.js-dependent code from browser context, or defer
projectIdresolution.getAnonymousProjectId()callsprocess.cwd()andexecuteCommandSync()which are not available in the browser. The function's try-catch silently fails, returningundefined, causing the Store to use an un-namespaced storage key and defeating the project isolation purpose of this PR.Solution options:
projectIdas a propprojectIdas an optional prop toManagerProvider(from server-side setup)typeof process !== 'undefined'checks if SSR-compatible behavior is intended🤖 Prompt for AI Agents