From 1cdb54559d60bf0a98545d55a1f790a55605cbc5 Mon Sep 17 00:00:00 2001 From: hawkgs Date: Tue, 15 Jul 2025 15:02:00 +0300 Subject: [PATCH] refactor(devtools): implement settings store (#62429) Introduces a set of services tasked with saving user settings. PR Close #62429 --- .../src/lib/application-operations/index.ts | 3 + .../src/lib/application-providers/BUILD.bazel | 13 +- .../settings_provider.ts | 29 ++++ .../src/lib/application-services/BUILD.bazel | 23 ++- .../src/lib/application-services/settings.ts | 20 +++ .../application-services/settings_store.ts | 48 ++++++ .../settings_store_spec.ts | 146 ++++++++++++++++++ .../test-utils/BUILD.bazel | 27 ++++ .../test-utils/app_operations_mock.ts | 64 ++++++++ .../test-utils/settings_store_mock.ts | 25 +++ .../shell-browser/src/app/BUILD.bazel | 1 - .../src/app/chrome-application-operations.ts | 50 ++++-- .../src/manifest/manifest.chrome.json | 3 +- .../src/manifest/manifest.firefox.json | 1 + devtools/src/BUILD.bazel | 6 +- devtools/src/demo-application-operations.ts | 57 +++++++ devtools/src/local-storage.ts | 14 ++ package.json | 1 + pnpm-lock.yaml | 8 + 19 files changed, 519 insertions(+), 20 deletions(-) create mode 100644 devtools/projects/ng-devtools/src/lib/application-providers/settings_provider.ts create mode 100644 devtools/projects/ng-devtools/src/lib/application-services/settings.ts create mode 100644 devtools/projects/ng-devtools/src/lib/application-services/settings_store.ts create mode 100644 devtools/projects/ng-devtools/src/lib/application-services/settings_store_spec.ts create mode 100644 devtools/projects/ng-devtools/src/lib/application-services/test-utils/BUILD.bazel create mode 100644 devtools/projects/ng-devtools/src/lib/application-services/test-utils/app_operations_mock.ts create mode 100644 devtools/projects/ng-devtools/src/lib/application-services/test-utils/settings_store_mock.ts create mode 100644 devtools/src/local-storage.ts diff --git a/devtools/projects/ng-devtools/src/lib/application-operations/index.ts b/devtools/projects/ng-devtools/src/lib/application-operations/index.ts index 560257800fd..ae00f791819 100644 --- a/devtools/projects/ng-devtools/src/lib/application-operations/index.ts +++ b/devtools/projects/ng-devtools/src/lib/application-operations/index.ts @@ -15,4 +15,7 @@ export abstract class ApplicationOperations { abstract inspect(directivePosition: DirectivePosition, objectPath: string[], target: Frame): void; abstract inspectSignal(position: SignalNodePosition, target: Frame): void; abstract viewSourceFromRouter(name: string, type: string, target: Frame): void; + abstract setStorageItems(items: {[key: string]: unknown}): Promise; + abstract getStorageItems(items: string[]): Promise<{[key: string]: unknown}>; + abstract removeStorageItems(items: string[]): Promise; } diff --git a/devtools/projects/ng-devtools/src/lib/application-providers/BUILD.bazel b/devtools/projects/ng-devtools/src/lib/application-providers/BUILD.bazel index 0e3b97860d5..a2b55685d82 100644 --- a/devtools/projects/ng-devtools/src/lib/application-providers/BUILD.bazel +++ b/devtools/projects/ng-devtools/src/lib/application-providers/BUILD.bazel @@ -1,6 +1,6 @@ load("//devtools/tools:defaults.bzl", "ng_project") -package(default_visibility = ["//visibility:public"]) +package(default_visibility = ["//devtools:__subpackages__"]) ng_project( name = "window", @@ -9,3 +9,14 @@ ng_project( "//:node_modules/@angular/core", ], ) + +ng_project( + name = "settings", + srcs = ["settings_provider.ts"], + deps = [ + "//:node_modules/@angular/core", + "//devtools/projects/ng-devtools/src/lib/application-operations", + "//devtools/projects/ng-devtools/src/lib/application-services:settings", + "//devtools/projects/ng-devtools/src/lib/application-services:settings_store", + ], +) diff --git a/devtools/projects/ng-devtools/src/lib/application-providers/settings_provider.ts b/devtools/projects/ng-devtools/src/lib/application-providers/settings_provider.ts new file mode 100644 index 00000000000..2643d5c0ac5 --- /dev/null +++ b/devtools/projects/ng-devtools/src/lib/application-providers/settings_provider.ts @@ -0,0 +1,29 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {provideAppInitializer, inject, Provider, EnvironmentProviders} from '@angular/core'; +import {SETTINGS_STORE_KEY, SettingsStore} from '../application-services/settings_store'; +import {ApplicationOperations} from '../application-operations'; +import {Settings} from '../application-services/settings'; + +export function provideSettings(): (Provider | EnvironmentProviders)[] { + let savedSettings: {[key: string]: unknown}; + + return [ + provideAppInitializer(async () => { + const appOperations = inject(ApplicationOperations); + const keyedItem = await appOperations.getStorageItems([SETTINGS_STORE_KEY]); + savedSettings = (keyedItem[SETTINGS_STORE_KEY] ?? {}) as {[key: string]: unknown}; + }), + { + provide: SettingsStore, + useFactory: () => new SettingsStore(savedSettings), + }, + Settings, + ]; +} diff --git a/devtools/projects/ng-devtools/src/lib/application-services/BUILD.bazel b/devtools/projects/ng-devtools/src/lib/application-services/BUILD.bazel index eb668801fc6..156855e0f9d 100644 --- a/devtools/projects/ng-devtools/src/lib/application-services/BUILD.bazel +++ b/devtools/projects/ng-devtools/src/lib/application-services/BUILD.bazel @@ -1,6 +1,6 @@ load("//devtools/tools:defaults.bzl", "ng_project", "ng_web_test_suite", "ts_test_library") -package(default_visibility = ["//visibility:public"]) +package(default_visibility = ["//devtools:__subpackages__"]) ng_project( name = "browser_styles", @@ -34,18 +34,39 @@ ng_project( ], ) +ng_project( + name = "settings_store", + srcs = ["settings_store.ts"], + deps = [ + "//devtools/projects/ng-devtools/src/lib/application-operations", + "//packages/core", + ], +) + +ng_project( + name = "settings", + srcs = ["settings.ts"], + deps = [ + ":settings_store", + "//packages/core", + ], +) + ts_test_library( name = "test_application_services_lib", srcs = glob(["*_spec.ts"]), deps = [ ":browser_styles", ":frame_manager", + ":settings_store", ":theme", "//:node_modules/@angular/cdk", "//:node_modules/@angular/common", "//:node_modules/@angular/core", "//devtools/projects/ng-devtools/src/lib/application-environment", "//devtools/projects/ng-devtools/src/lib/application-providers:window", + "//devtools/projects/ng-devtools/src/lib/application-services/test-utils:app_operations_mock", + "//devtools/projects/ng-devtools/src/lib/application-services/test-utils:settings_store_mock", "//devtools/projects/protocol", ], ) diff --git a/devtools/projects/ng-devtools/src/lib/application-services/settings.ts b/devtools/projects/ng-devtools/src/lib/application-services/settings.ts new file mode 100644 index 00000000000..334226820c0 --- /dev/null +++ b/devtools/projects/ng-devtools/src/lib/application-services/settings.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {inject} from '@angular/core'; +import {SettingsStore} from './settings_store'; + +export class Settings { + private readonly settingsStore = inject(SettingsStore); + + readonly dummy = this.settingsStore.create({ + key: 'dummy', + category: 'general', + initialValue: true, + }); +} diff --git a/devtools/projects/ng-devtools/src/lib/application-services/settings_store.ts b/devtools/projects/ng-devtools/src/lib/application-services/settings_store.ts new file mode 100644 index 00000000000..90fda92222a --- /dev/null +++ b/devtools/projects/ng-devtools/src/lib/application-services/settings_store.ts @@ -0,0 +1,48 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {effect, inject, Injector, signal, WritableSignal} from '@angular/core'; +import {ApplicationOperations} from '../application-operations'; + +export const SETTINGS_STORE_KEY = 'ng-dt-settings-v1'; + +/** Provides an API for storing and preserving settings values. */ +export class SettingsStore { + private readonly appOperations = inject(ApplicationOperations); + private readonly injector = inject(Injector); + private readonly signals = new Map>(); + + constructor(private data: {[key: string]: unknown}) {} + + /** + * Create a settings value a provided key, as a writable signal. + * If the item doesn't exist, a new one will be created. + * Updates to the signal value are automatically stored in the storage. + */ + create(config: {key: string; category: string; initialValue: T}): WritableSignal { + const storeKey = `${config.key}@${config.category}`; + const existing = this.signals.get(storeKey); + if (existing) { + return existing as WritableSignal; + } + + const initialValue = storeKey in this.data ? (this.data[storeKey] as T) : config.initialValue; + const value = signal(initialValue); + this.signals.set(storeKey, value); + + effect( + () => { + this.data[storeKey] = value(); + this.appOperations.setStorageItems({[SETTINGS_STORE_KEY]: this.data}); + }, + {injector: this.injector}, + ); + + return value; + } +} diff --git a/devtools/projects/ng-devtools/src/lib/application-services/settings_store_spec.ts b/devtools/projects/ng-devtools/src/lib/application-services/settings_store_spec.ts new file mode 100644 index 00000000000..3d7e074a049 --- /dev/null +++ b/devtools/projects/ng-devtools/src/lib/application-services/settings_store_spec.ts @@ -0,0 +1,146 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {TestBed} from '@angular/core/testing'; +import {SettingsStore} from './settings_store'; +import {ApplicationOperations} from '../application-operations'; +import {AppOperationsMock} from './test-utils/app_operations_mock'; +import {ApplicationRef} from '@angular/core'; + +describe('SettingsStore', () => { + let settingsStore: SettingsStore; + let getStoredSettings: () => {[key: string]: unknown}; + + beforeEach(() => { + const appOperationsMock = new AppOperationsMock(); + + TestBed.configureTestingModule({ + providers: [ + {provide: ApplicationOperations, useValue: appOperationsMock}, + { + provide: SettingsStore, + useFactory: () => new SettingsStore({}), + }, + ], + }); + + settingsStore = TestBed.inject(SettingsStore); + getStoredSettings = () => appOperationsMock.getStoredSettings(); + }); + + it('should return a settings item with an initial value', async () => { + const item = settingsStore.create({ + key: 'item', + category: 'test', + initialValue: 'foo', + }); + expect(item()).toEqual('foo'); + }); + + it('should set a settings item value', async () => { + const value = settingsStore.create({ + key: 'item', + category: 'test', + initialValue: 'foo', + }); + expect(value()).toEqual('foo'); + + value.set('bar'); + expect(value()).toBe('bar'); + + await TestBed.inject(ApplicationRef).whenStable(); + TestBed.tick(); + + expect(getStoredSettings()['item@test']).toEqual('bar'); + }); + + it('should set multiple values to a single settings item', async () => { + const value = settingsStore.create({ + key: 'item', + category: 'test', + initialValue: 'foo', + }); + expect(value()).toEqual('foo'); + + value.set('bar'); + await TestBed.inject(ApplicationRef).whenStable(); + TestBed.tick(); + expect(getStoredSettings()['item@test']).toEqual('bar'); + + value.set('baz'); + await TestBed.inject(ApplicationRef).whenStable(); + TestBed.tick(); + expect(getStoredSettings()['item@test']).toEqual('baz'); + }); + + it('should set values to multiple settings items', async () => { + const first = settingsStore.create({ + key: 'first', + category: 'test', + initialValue: 'not_set', + }); + const second = settingsStore.create({ + key: 'second', + category: 'test', + initialValue: 'not_set', + }); + expect(first()).toEqual('not_set'); + expect(second()).toEqual('not_set'); + + first.set('1st'); + second.set('2nd'); + + await TestBed.inject(ApplicationRef).whenStable(); + TestBed.tick(); + + expect(getStoredSettings()['first@test']).toEqual('1st'); + expect(getStoredSettings()['second@test']).toEqual('2nd'); + }); + + it('should keep in sync multiple instances of the same settings item', async () => { + const foo = settingsStore.create({ + key: 'item', + category: 'test', + initialValue: 'foo', + }); + const bar = settingsStore.create({ + key: 'item', + category: 'test', + initialValue: 'bar', + }); + expect(foo()).toEqual('foo'); + expect(bar()).toEqual('foo'); + + bar.set('baz'); + expect(foo()).toEqual('baz'); + expect(bar()).toEqual('baz'); + + await TestBed.inject(ApplicationRef).whenStable(); + TestBed.tick(); + + expect(getStoredSettings()['item@test']).toEqual('baz'); + }); + + it('should keep the latest signal value', async () => { + const value = settingsStore.create({ + key: 'item', + category: 'test', + initialValue: 'foo', + }); + expect(value()).toEqual('foo'); + + value.set('bar'); + value.set('baz'); + expect(value()).toBe('baz'); + + await TestBed.inject(ApplicationRef).whenStable(); + TestBed.tick(); + + expect(getStoredSettings()['item@test']).toEqual('baz'); + }); +}); diff --git a/devtools/projects/ng-devtools/src/lib/application-services/test-utils/BUILD.bazel b/devtools/projects/ng-devtools/src/lib/application-services/test-utils/BUILD.bazel new file mode 100644 index 00000000000..5510dab0501 --- /dev/null +++ b/devtools/projects/ng-devtools/src/lib/application-services/test-utils/BUILD.bazel @@ -0,0 +1,27 @@ +load("//devtools/tools:defaults.bzl", "ts_project") + +package(default_visibility = ["//devtools:__subpackages__"]) + +ts_project( + name = "settings_store_mock", + srcs = [ + "settings_store_mock.ts", + ], + deps = [ + "//:node_modules/@angular/core", + "//devtools/projects/ng-devtools/src/lib/application-services:settings_store", + ], +) + +ts_project( + name = "app_operations_mock", + srcs = [ + "app_operations_mock.ts", + ], + deps = [ + "//devtools/projects/ng-devtools/src/lib/application-environment", + "//devtools/projects/ng-devtools/src/lib/application-operations", + "//devtools/projects/ng-devtools/src/lib/application-services:settings_store", + "//devtools/projects/protocol", + ], +) diff --git a/devtools/projects/ng-devtools/src/lib/application-services/test-utils/app_operations_mock.ts b/devtools/projects/ng-devtools/src/lib/application-services/test-utils/app_operations_mock.ts new file mode 100644 index 00000000000..7e3aff5a455 --- /dev/null +++ b/devtools/projects/ng-devtools/src/lib/application-services/test-utils/app_operations_mock.ts @@ -0,0 +1,64 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {DirectivePosition, ElementPosition, SignalNodePosition} from '../../../../../protocol'; +import {Frame} from '../../application-environment'; +import {ApplicationOperations} from '../../application-operations'; +import {SETTINGS_STORE_KEY} from '../settings_store'; + +export class AppOperationsMock extends ApplicationOperations { + private storage: {[key: string]: unknown} = {}; + + /** Helper method – gives access to stored settings */ + getStoredSettings() { + return this.storage[SETTINGS_STORE_KEY] as {[key: string]: unknown}; + } + + override async setStorageItems(items: {[key: string]: unknown}): Promise { + this.storage = { + ...this.storage, + ...items, + }; + } + + override async getStorageItems(items: string[]): Promise<{[key: string]: unknown}> { + const obj: {[key: string]: unknown} = {}; + for (const item of items) { + obj[item] = this.storage[item]; + } + return obj; + } + + override removeStorageItems(items: string[]): Promise { + throw new Error('Method not implemented.'); + } + + override viewSource(position: ElementPosition, target: Frame, directiveIndex?: number): void { + throw new Error('Method not implemented.'); + } + + override selectDomElement(position: ElementPosition, target: Frame): void { + throw new Error('Method not implemented.'); + } + + override inspect( + directivePosition: DirectivePosition, + objectPath: string[], + target: Frame, + ): void { + throw new Error('Method not implemented.'); + } + + override inspectSignal(position: SignalNodePosition, target: Frame): void { + throw new Error('Method not implemented.'); + } + + override viewSourceFromRouter(name: string, type: string, target: Frame): void { + throw new Error('Method not implemented.'); + } +} diff --git a/devtools/projects/ng-devtools/src/lib/application-services/test-utils/settings_store_mock.ts b/devtools/projects/ng-devtools/src/lib/application-services/test-utils/settings_store_mock.ts new file mode 100644 index 00000000000..eb2f9f59c2f --- /dev/null +++ b/devtools/projects/ng-devtools/src/lib/application-services/test-utils/settings_store_mock.ts @@ -0,0 +1,25 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {Provider, signal} from '@angular/core'; +import {SettingsStore} from '../settings_store'; + +export class SettingsStoreMock { + private readonly signals = new Map(); + + get(key: string, initialValue: unknown) { + const value = this.signals.get(key) ?? signal(initialValue); + this.signals.set(key, value); + return value; + } +} + +export const SETTINGS_STORE_MOCK: Provider = { + provide: SettingsStore, + useClass: SettingsStoreMock, +}; diff --git a/devtools/projects/shell-browser/src/app/BUILD.bazel b/devtools/projects/shell-browser/src/app/BUILD.bazel index b77c5ca942e..aca5e782f11 100644 --- a/devtools/projects/shell-browser/src/app/BUILD.bazel +++ b/devtools/projects/shell-browser/src/app/BUILD.bazel @@ -25,7 +25,6 @@ ng_project( "//:node_modules/@angular/core", "//:node_modules/@angular/material", "//:node_modules/@angular/platform-browser", - "//:node_modules/@types/chrome", "//:node_modules/rxjs", "//devtools/projects/ng-devtools", "//devtools/projects/ng-devtools-backend", diff --git a/devtools/projects/shell-browser/src/app/chrome-application-operations.ts b/devtools/projects/shell-browser/src/app/chrome-application-operations.ts index 0114fcce980..a6a5f113b0e 100644 --- a/devtools/projects/shell-browser/src/app/chrome-application-operations.ts +++ b/devtools/projects/shell-browser/src/app/chrome-application-operations.ts @@ -7,6 +7,7 @@ */ /// +/// import {Platform} from '@angular/cdk/platform'; import {inject} from '@angular/core'; @@ -16,21 +17,6 @@ import {DirectivePosition, ElementPosition, SignalNodePosition} from '../../../p export class ChromeApplicationOperations extends ApplicationOperations { platform = inject(Platform); - private runInInspectedWindow(script: string, target: Frame) { - if (this.platform.FIREFOX && target.id !== TOP_LEVEL_FRAME_ID) { - console.error( - '[Angular DevTools]: This browser does not support targeting a specific frame for eval by URL.', - ); - return; - } else if (this.platform.FIREFOX) { - chrome.devtools.inspectedWindow.eval(script); - return; - } - - const frameURL = target.url; - chrome.devtools.inspectedWindow.eval(script, {frameURL: frameURL?.toString?.()}); - } - override viewSource(position: ElementPosition, target: Frame, directiveIndex?: number): void { const viewSource = `inspect(inspectedApplication.findConstructorByPosition('${position}', ${directiveIndex}))`; this.runInInspectedWindow(viewSource, target); @@ -67,4 +53,38 @@ export class ChromeApplicationOperations extends ApplicationOperations { const viewSource = `inspect(inspectedApplication.findConstructorByNameForRouter('${name}', '${type}'))`; this.runInInspectedWindow(viewSource, target); } + + override setStorageItems(items: {[key: string]: unknown}): Promise { + return this.storage.set(items); + } + + override getStorageItems(items: string[]): Promise<{[key: string]: unknown}> { + return this.storage.get(items); + } + + override removeStorageItems(items: string[]): Promise { + return this.storage.remove(items); + } + + private runInInspectedWindow(script: string, target: Frame) { + if (this.platform.FIREFOX && target.id !== TOP_LEVEL_FRAME_ID) { + console.error( + '[Angular DevTools]: This browser does not support targeting a specific frame for eval by URL.', + ); + return; + } else if (this.platform.FIREFOX) { + chrome.devtools.inspectedWindow.eval(script); + return; + } + + const frameURL = target.url; + chrome.devtools.inspectedWindow.eval(script, {frameURL: frameURL?.toString?.()}); + } + + private get storage(): typeof browser.storage.local { + if (!this.platform.FIREFOX) { + return chrome.storage.local; + } + return browser.storage.local; + } } diff --git a/devtools/projects/shell-browser/src/manifest/manifest.chrome.json b/devtools/projects/shell-browser/src/manifest/manifest.chrome.json index 9e49b7fa050..5ce1fb91ecd 100644 --- a/devtools/projects/shell-browser/src/manifest/manifest.chrome.json +++ b/devtools/projects/shell-browser/src/manifest/manifest.chrome.json @@ -38,7 +38,8 @@ }, "permissions": [ "scripting", - "activeTab" + "activeTab", + "storage" ], "host_permissions": [ "" diff --git a/devtools/projects/shell-browser/src/manifest/manifest.firefox.json b/devtools/projects/shell-browser/src/manifest/manifest.firefox.json index 26d67225a0f..89a7f8bdf2a 100644 --- a/devtools/projects/shell-browser/src/manifest/manifest.firefox.json +++ b/devtools/projects/shell-browser/src/manifest/manifest.firefox.json @@ -25,6 +25,7 @@ }, "permissions": [ "activeTab", + "storage", "http://*/*", "https://*/*", "file:///*" diff --git a/devtools/src/BUILD.bazel b/devtools/src/BUILD.bazel index 8506fa647fb..4b8f52733ab 100644 --- a/devtools/src/BUILD.bazel +++ b/devtools/src/BUILD.bazel @@ -70,8 +70,12 @@ ng_project( ng_project( name = "demo_application_operations", - srcs = ["demo-application-operations.ts"], + srcs = [ + "demo-application-operations.ts", + "local-storage.ts", + ], deps = [ + "//:node_modules/@angular/core", "//devtools/projects/ng-devtools", "//devtools/projects/protocol", ], diff --git a/devtools/src/demo-application-operations.ts b/devtools/src/demo-application-operations.ts index ff4543680bb..9b70739b29e 100644 --- a/devtools/src/demo-application-operations.ts +++ b/devtools/src/demo-application-operations.ts @@ -6,30 +6,87 @@ * found in the LICENSE file at https://angular.dev/license */ +import {inject} from '@angular/core'; import {ApplicationOperations} from '../projects/ng-devtools'; import {DirectivePosition, ElementPosition, SignalNodePosition} from '../projects/protocol'; +import {LOCAL_STORAGE} from './local-storage'; + +const STORAGE_KEY = 'ng-dt-storage-sim'; export class DemoApplicationOperations extends ApplicationOperations { + private readonly localStorage = inject(LOCAL_STORAGE); + override viewSource(position: ElementPosition): void { console.warn('viewSource() is not implemented because the demo app runs in an Iframe'); throw new Error('Not implemented in demo app.'); } + override selectDomElement(position: ElementPosition): void { console.warn('selectDomElement() is not implemented because the demo app runs in an Iframe'); throw new Error('Not implemented in demo app.'); } + override inspect(directivePosition: DirectivePosition, keyPath: string[]): void { console.warn('inspect() is not implemented because the demo app runs in an Iframe'); return; } + override inspectSignal(position: SignalNodePosition): void { console.warn('inspectSignal() is not implemented because the demo app runs in an Iframe'); return; } + override viewSourceFromRouter(name: string, type: string): void { console.warn( 'viewSourceFromRouter() is not implemented because the demo app runs in an Iframe', ); throw new Error('Not implemented in demo app.'); } + + override async setStorageItems(items: {[key: string]: unknown}): Promise { + const currItems = this.getLsItems(); + this.setLsItems({...currItems, ...items}); + } + + override async getStorageItems(items: string[]): Promise<{[key: string]: unknown}> { + const currItems = this.getLsItems(); + const redundant = Object.keys(currItems).filter((prop) => !items.includes(prop)); + + for (const item of redundant) { + delete currItems[item]; + } + + return currItems; + } + + override async removeStorageItems(items: string[]): Promise { + const currItems = this.getLsItems(); + + for (const item of items) { + delete currItems[item]; + } + this.setLsItems(currItems); + } + + private getLsItems(): {[key: string]: unknown} { + const storage = this.localStorage.getItem(STORAGE_KEY); + + try { + return JSON.parse(storage ?? '{}'); + } catch { + this.localStorage.removeItem(STORAGE_KEY); + console.error( + 'Unable to parse the data from the simulated storage. Cleaning the item and returning a default object', + ); + return {}; + } + } + + private setLsItems(items: object) { + try { + this.localStorage.setItem(STORAGE_KEY, JSON.stringify(items)); + } catch { + console.error('Unable to set item in the simulated storage.'); + } + } } diff --git a/devtools/src/local-storage.ts b/devtools/src/local-storage.ts new file mode 100644 index 00000000000..41c8b49e363 --- /dev/null +++ b/devtools/src/local-storage.ts @@ -0,0 +1,14 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {InjectionToken} from '@angular/core'; + +export const LOCAL_STORAGE = new InjectionToken('LOCAL_STORAGE', { + providedIn: 'root', + factory: () => localStorage, +}); diff --git a/package.json b/package.json index 208693c84a3..48a7b85e1b8 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "@types/chrome": "^0.1.0", "@types/convert-source-map": "^2.0.0", "@types/dom-navigation": "^1.0.5", + "@types/firefox-webext-browser": "^120.0.4", "@types/hammerjs": "2.0.46", "@types/jasmine": "^5.0.0", "@types/jasminewd2": "^2.0.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 98d5ef191a5..d921abd8be6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -141,6 +141,9 @@ importers: '@types/dom-navigation': specifier: ^1.0.5 version: 1.0.6 + '@types/firefox-webext-browser': + specifier: ^120.0.4 + version: 120.0.4 '@types/hammerjs': specifier: 2.0.46 version: 2.0.46 @@ -3663,6 +3666,9 @@ packages: '@types/filewriter@0.0.33': resolution: {integrity: sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==} + '@types/firefox-webext-browser@120.0.4': + resolution: {integrity: sha512-lBrpf08xhiZBigrtdQfUaqX1UauwZ+skbFiL8u2Tdra/rklkKadYmIzTwkNZSWtuZ7OKpFqbE2HHfDoFqvZf6w==} + '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} @@ -14116,6 +14122,8 @@ snapshots: '@types/filewriter@0.0.33': {} + '@types/firefox-webext-browser@120.0.4': {} + '@types/geojson@7946.0.16': {} '@types/hammerjs@2.0.46': {}