feat(store): skin store setup (#298)

This commit is contained in:
rahim
2026-01-07 15:45:46 +11:00
committed by GitHub
parent 836c32952c
commit b2e2b88e19
17 changed files with 273 additions and 156 deletions
+36 -122
View File
@@ -36,7 +36,8 @@ Implement React and DOM bindings for Video.js 10's store, enabling:
| Primitives context | `useStoreContext()` internal hook for primitive UI components |
| displayName | For React DevTools component naming |
| Component types | Namespace pattern: `Skin.Props` via `namespace Skin { export type Props }` |
| Element define | `FrostedSkinElement.define(tagName, { mixins })` for declarative setup |
| Element define | `FrostedSkinElement.define(tagName?, mixin?)` - pass mixin directly |
| Slot handling | Default slot (`<slot></slot>`) - no `slot="media"` attribute needed |
| Config extension | `extendConfig()` uses `uniqBy` + `composeCallbacks` from utils |
| Provider resolution | Isolated by default; `inherit` prop to use parent store from context |
| Store instance | `create()` method for imperative store creation |
@@ -232,108 +233,18 @@ export { extendConfig, Provider } from './store';
---
## Phase 6: Frosted Skin (HTML)
## Phase 6: Frosted Skin [DONE]
### 6.1 Store config
> Refer to [PR #298](https://github.com/videojs/v10/pull/298) for implementation details.
**File:** `packages/html/src/skins/frosted/store.ts`
React and HTML frosted skin store setup:
```typescript
import type { AnySlice, StoreConfig } from '@videojs/store';
- React: `Provider`, `Skin`, `extendConfig`, `createStore` in `@videojs/react/skins/frosted`
- HTML: `FrostedSkinElement`, `StoreMixin`, `extendConfig` in `@videojs/html/skins/frosted`
- Auto-define entry: `@videojs/html/define/vjs-frosted-skin` for CDN usage
- `StoreAttachMixin` uses default slot (no `slot="media"` attribute needed)
import { extendConfig as extendBaseConfig } from '@videojs/store';
import { createStore } from '@videojs/store/lit';
import { media } from './slices'; // internal - re-exported from @videojs/html
/** Base config for frosted skin. */
const baseConfig = {
slices: [media.playback] as const,
};
/**
* Extends frosted skin config with additional slices/hooks.
* Composes lifecycle hooks (both called, base first).
*/
export function extendConfig<S extends readonly AnySlice[] = readonly []>(extension?: Partial<StoreConfig<any, S>>) {
return extendBaseConfig(baseConfig, extension);
}
export const { StoreMixin, StoreProviderMixin, StoreAttachMixin, context } = createStore(extendConfig());
```
### 6.2 Skin component
**File:** `packages/html/src/skins/frosted/skin.ts`
```typescript
import { StoreAttachMixin, StoreMixin, StoreProviderMixin } from './store';
type Mixin = <T extends Constructor<HTMLElement>>(Base: T) => T;
export interface DefineOptions {
/** Mixins to apply. Defaults to [StoreMixin] (combined provider + attach). */
mixins?: Mixin[];
}
/**
* Frosted skin element. Empty for now - controls will be added later.
* Uses shadow DOM with slot for video element.
*/
export class FrostedSkinElement extends HTMLElement {
/** Default tag name for this element. */
static tagName = 'vjs-frosted-skin';
/**
* Define this element with the custom elements registry.
*
* @example
* // Default: combined provider + attach
* FrostedSkinElement.define('vjs-frosted-skin');
*
* @example
* // Granular mixin control (e.g., attach only, inherit provider from parent)
* FrostedSkinElement.define('vjs-thumbnail', { mixins: [StoreAttachMixin] });
*
* @example
* // Custom store with extended slices
* const { StoreMixin } = createStore(extendConfig({ slices: [chaptersSlice] }));
* FrostedSkinElement.define('my-extended-player', { mixins: [StoreMixin] });
*/
static define(tagName: string, options: DefineOptions = {}) {
const { mixins = [StoreMixin] } = options;
// Apply mixins in order (right to left composition)
const Mixed = mixins.reduceRight((Base, mixin) => mixin(Base), this as typeof FrostedSkinElement);
customElements.define(tagName, Mixed);
}
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `<slot></slot>`;
}
}
```
### 6.3 Define export
**File:** `packages/html/src/define/vjs-frosted-skin.ts`
```typescript
import { FrostedSkinElement } from '../skins/frosted/skin';
FrostedSkinElement.define('vjs-frosted-skin');
```
### 6.4 Exports
**File:** `packages/html/src/skins/frosted/index.ts`
```typescript
export { FrostedSkinElement } from './skin';
export type { DefineOptions } from './skin';
export { context, extendConfig, StoreAttachMixin, StoreMixin, StoreProviderMixin } from './store';
```
**Note:** Don't export `context` from skins (causes unique symbol type issues). Users can import from `@videojs/store/lit` if needed.
---
@@ -501,17 +412,20 @@ function ChaptersPanel() {
Where `my-player.js` contains:
```typescript
import { createStore, media } from '@videojs/html';
import { ReactiveElement } from '@lit/reactive-element';
import { media } from '@videojs/core/dom';
import { createStore } from '@videojs/store/lit';
const { StoreMixin } = createStore({
slices: [media.playback],
slices: [...media.all],
});
// Create custom element with store provider and auto-attach
class MyPlayer extends StoreMixin(HTMLElement) {
connectedCallback() {
class MyPlayer extends StoreMixin(ReactiveElement) {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `<slot></slot>`;
shadow.innerHTML = '<slot></slot>';
}
}
@@ -539,7 +453,7 @@ import { chaptersSlice } from './slices/chapters.js';
// Extend frosted config with custom slice (merges with base slices)
const { StoreMixin } = createStore(extendConfig({ slices: [chaptersSlice] }));
FrostedSkinElement.define('my-extended-skin', { mixins: [StoreMixin] });
FrostedSkinElement.define('my-extended-skin', StoreMixin);
```
---
@@ -683,9 +597,9 @@ packages/html/src/
- Shared types: `OptimisticResult` discriminated union in `src/shared/types.ts`
- Tests: 13 new tests for React hook ✓
8. **Phase 6**: Skins
- React skin (Provider, Skin, extendConfig)
- HTML skin (FrostedSkinElement, extendConfig)
8. **Phase 6**: Skins **[DONE - PR #298]**
- React skin (Provider, Skin, extendConfig)
- HTML skin (FrostedSkinElement, extendConfig)
Each phase includes tests.
@@ -814,15 +728,15 @@ Use `types` + `default` format to match existing packages.
### Related Issues
| Issue | Title | Description | Status |
| ----- | ------------------- | --------------------------------------- | ------ |
| #218 | Store | Parent tracking issue | Open |
| #285 | Queue Task Refactor | Unified tasks map, status discriminator | Closed |
| #228 | Optimistic Updates | useMutation, useOptimistic | Open |
| #229 | React Bindings | createStore, hooks, context | Closed |
| #230 | Lit Bindings | Controllers, mixins, context | Closed |
| #239 | DOM Media Slices | media slices | Closed |
| #231 | Skin Stores | Skin store configuration | Open |
| Issue | Title | Description | Status |
| ----- | ------------------- | --------------------------------------- | ---------------- |
| #218 | Store | Parent tracking issue | Open |
| #285 | Queue Task Refactor | Unified tasks map, status discriminator | Closed |
| #228 | Optimistic Updates | useMutation, useOptimistic | Open |
| #229 | React Bindings | createStore, hooks, context | Closed |
| #230 | Lit Bindings | Controllers, mixins, context | Closed |
| #239 | DOM Media Slices | media slices | Closed |
| #231 | Skin Stores | Skin store configuration | Closed (PR #298) |
### PR Strategy
@@ -875,9 +789,9 @@ PR #291: Optimistic Hooks/Controllers [DONE]
├── Tests: 13 new tests for React hook ✓
└── Closes #228
PR F: Skins
├── React skin (Provider, Skin, extendConfig)
├── HTML skin (FrostedSkinElement, extendConfig)
PR #298: Skins [DONE]
├── React skin (Provider, Skin, extendConfig)
├── HTML skin (FrostedSkinElement, extendConfig)
├── References #218
└── Closes #231
```
@@ -885,12 +799,12 @@ PR F: Skins
### Dependency Graph
```
PR #283 ───> PR #287 ───> PR #288 ───> PR #290 ───> PR #291 ───> PR F (Skins)
PR #283 ───> PR #287 ───> PR #288 ───> PR #290 ───> PR #291 ───> PR #298 (Skins)
└──> PR #289 (done) ────────────────────────┘
└──> PR #292 (done) ────────────────────────┘
```
PRs are sequential. PR #288, #289, #292 can technically parallel after PR #287, but we'll do them sequentially for easier review.
All PRs merged. Store bindings implementation complete.
---
+9
View File
@@ -16,6 +16,14 @@
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./skins/frosted": {
"types": "./dist/skins/frosted.d.ts",
"default": "./dist/skins/frosted.js"
},
"./define/vjs-frosted-skin": {
"types": "./dist/define/vjs-frosted-skin.d.ts",
"default": "./dist/define/vjs-frosted-skin.js"
}
},
"main": "dist/index.js",
@@ -32,6 +40,7 @@
"clean": "rm -rf dist types"
},
"dependencies": {
"@lit/reactive-element": "^2.1.2",
"@videojs/core": "workspace:*",
"@videojs/store": "workspace:*",
"@videojs/utils": "workspace:*"
@@ -0,0 +1,3 @@
import { FrostedSkinElement } from '../skins/frosted';
FrostedSkinElement.define();
+12
View File
@@ -0,0 +1,12 @@
export { FrostedSkinElement } from './skin';
export {
create as createStore,
extendConfig,
RequestController,
SelectorController,
StoreAttachMixin,
StoreMixin,
StoreProviderMixin,
TasksController,
} from './store';
+57
View File
@@ -0,0 +1,57 @@
import type { Mixin } from '@videojs/utils/types';
import { ReactiveElement } from '@lit/reactive-element';
import { StoreMixin } from './store';
/**
* Frosted skin custom element.
*
* Uses shadow DOM with a slot for video elements. Controls will be added in future updates.
*
* @example Basic usage (after calling define)
* ```html
* <vjs-frosted-skin>
* <video src="video.mp4"></video>
* </vjs-frosted-skin>
* ```
*
* @example Define with default tag
* ```ts
* import { FrostedSkinElement } from '@videojs/html/skins/frosted';
* FrostedSkinElement.define();
* ```
*
* @example Define with custom tag
* ```ts
* FrostedSkinElement.define('my-player');
* ```
*
* @example Define with extended store
* ```ts
* import { createStore } from '@videojs/store/lit';
* import { extendConfig, FrostedSkinElement } from '@videojs/html/skins/frosted';
*
* const { StoreMixin } = createStore(extendConfig({ slices: [chaptersSlice] }));
* FrostedSkinElement.define('my-player', StoreMixin);
* ```
*/
export class FrostedSkinElement extends ReactiveElement {
static tagName = 'vjs-frosted-skin';
/**
* Registers this element with the custom elements registry.
*
* @param tagName - Custom element tag name (defaults to 'vjs-frosted-skin')
* @param mixin - Mixin to apply (defaults to StoreMixin)
*/
static define(tagName = this.tagName, mixin: Mixin<FrostedSkinElement, ReactiveElement> = StoreMixin): void {
customElements.define(tagName, mixin(this));
}
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = '<slot></slot>';
}
}
+41
View File
@@ -0,0 +1,41 @@
import type { AnySlice, StoreConfig } from '@videojs/store';
import { media } from '@videojs/core/dom';
import { extendConfig as extendBaseConfig } from '@videojs/store';
import { createStore } from '@videojs/store/lit';
const baseConfig = {
slices: [...media.all],
};
/**
* Extends frosted skin config.
*
* @example
* ```ts
* import { createStore } from '@videojs/store/lit';
* import { extendConfig, FrostedSkinElement } from '@videojs/html/skins/frosted';
* import { chaptersSlice } from './slices/chapters';
*
* const { StoreMixin } = createStore(
* extendConfig({ slices: [chaptersSlice] })
* );
*
* FrostedSkinElement.define('my-player', El => StoreMixin(El));
* ```
*/
export function extendConfig<Slices extends AnySlice<HTMLMediaElement>[] = []>(
extension?: Partial<StoreConfig<HTMLMediaElement, Slices>>,
) {
return extendBaseConfig(baseConfig, extension);
}
export const {
StoreMixin,
StoreProviderMixin,
StoreAttachMixin,
SelectorController,
RequestController,
TasksController,
create,
} = createStore(baseConfig);
+2 -6
View File
@@ -6,13 +6,9 @@ import { defineConfig } from 'tsdown';
export default defineConfig({
entry: {
index: 'src/index.ts',
// 'skins/frosted': 'src/skins/frosted/index.ts',
'skins/frosted': 'src/skins/frosted/index.ts',
// 'skins/minimal': 'src/skins/minimal/index.ts',
// ...defineFiles.reduce((entries, file) => {
// const name = file.replace(/\.ts$/, '');
// entries[`define/${name}`] = `src/define/${file}`;
// return entries;
// }, {} as Record<string, string>),
'define/vjs-frosted-skin': 'src/define/vjs-frosted-skin.ts',
},
platform: 'browser',
format: 'es',
+4
View File
@@ -16,6 +16,10 @@
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./skins/frosted": {
"types": "./dist/skins/frosted.d.ts",
"default": "./dist/skins/frosted.js"
}
},
"main": "dist/index.js",
+3
View File
@@ -3,5 +3,8 @@
// Media
export { Video, type VideoProps } from './media/video';
// Slices (re-export for convenience)
export { media } from '@videojs/core/dom';
// Store
export * from '@videojs/store/react';
+13
View File
@@ -0,0 +1,13 @@
'use client';
export { Skin, type SkinProps } from './skin';
export {
create as createStore,
extendConfig,
Provider,
useRequest,
useSelector,
useStore,
useTasks,
} from './store';
+33
View File
@@ -0,0 +1,33 @@
'use client';
import type { ReactNode } from 'react';
export interface SkinProps {
children?: ReactNode;
className?: string;
}
/**
* @example
* ```tsx
* import { Video } from '@videojs/react';
* import { Provider, Skin } from '@videojs/react/skins/frosted';
*
* function App() {
* return (
* <Provider>
* <Skin>
* <Video src="video.mp4" />
* </Skin>
* </Provider>
* );
* }
* ```
*/
export function Skin({ children, className }: SkinProps): React.JSX.Element {
return <div className={`vjs-frosted-skin ${className ?? ''}`.trim()}>{children}</div>;
}
export namespace Skin {
export type Props = SkinProps;
}
+41
View File
@@ -0,0 +1,41 @@
'use client';
import type { AnySlice, StoreConfig } from '@videojs/store';
import { media } from '@videojs/core/dom';
import { extendConfig as extendBaseConfig } from '@videojs/store';
import { createStore } from '@videojs/store/react';
const baseConfig = {
slices: [...media.all],
displayName: 'FrostedSkin',
};
/**
* Extends frosted skin config.
*
* @example
* ```ts
* import { createStore } from '@videojs/store/react';
* import { extendConfig } from '@videojs/react/skins/frosted';
* import { chaptersSlice } from './slices/chapters';
*
* const { Provider, useSelector } = createStore(
* extendConfig({ slices: [chaptersSlice] })
* );
* ```
*/
export function extendConfig<S extends AnySlice<HTMLMediaElement>[] = []>(
extension?: Partial<StoreConfig<HTMLMediaElement, S>>,
) {
return extendBaseConfig(baseConfig, extension);
}
export const {
Provider,
create,
useStore,
useSelector,
useRequest,
useTasks,
} = createStore(baseConfig);
+1 -2
View File
@@ -3,8 +3,7 @@ import { defineConfig } from 'tsdown';
export default defineConfig({
entry: {
index: './src/index.ts',
// 'skins/frosted': './src/skins/frosted/index.ts',
// 'skins/minimal': './src/skins/minimal/index.ts',
'skins/frosted': './src/skins/frosted/index.ts',
},
platform: 'browser',
format: 'es',
-1
View File
@@ -208,7 +208,6 @@ export function createStore<Slices extends AnySlice[]>(config: CreateStoreConfig
const StoreAttachMixin = createStoreAttachMixin<Slices>(context);
const StoreMixin = createStoreMixin<Slices>(context, create);
// Bound controllers - context is pre-bound
class SelectorController<Value> extends SelectorControllerBase<ProvidedStore, Value> {
constructor(host: CreateStoreHost, selector: (state: State) => Value) {
super(host, context, selector);
@@ -14,7 +14,7 @@ import { isNull } from '@videojs/utils/predicate';
* Creates a mixin that consumes a store from context and auto-attaches media elements.
*
* - Requests store from context (must have a provider ancestor)
* - Observes slotted elements for `<video slot="media">` or `<audio slot="media">`
* - Observes slotted elements for `<video>` or `<audio>` in the default slot
* - Falls back to light DOM children if no shadow root
* - Calls `store.attach(mediaElement)` when found
* - Cleans up on disconnect
@@ -51,7 +51,7 @@ export function createStoreAttachMixin<Slices extends AnySlice[]>(
const shadow = this.shadowRoot;
if (shadow) {
const slot = querySlot(shadow, 'media');
const slot = querySlot(shadow, '');
if (slot) this.#disposer.add(listen(slot, 'slotchange', () => this.#attachMedia()));
}
@@ -74,7 +74,7 @@ export function createStoreAttachMixin<Slices extends AnySlice[]>(
isHTMLMediaElement(el) ? el : el.querySelector('video, audio');
const media = this.shadowRoot
? getSlottedElement(this.shadowRoot, 'media', findMedia)
? getSlottedElement(this.shadowRoot, '', findMedia)
: this.querySelector('video, audio');
if (store.target !== media) {
@@ -4,10 +4,10 @@ import { createLitTestStore, setupDomCleanup, TestBaseElement, uniqueTag } from
setupDomCleanup();
// Helper to create shadow root with named media slot
function createShadowWithSlot(el: HTMLElement): void {
function createShadowWithSlot(el: HTMLElement): ShadowRoot {
const shadow = el.attachShadow({ mode: 'open' });
shadow.innerHTML = '<slot name="media"></slot>';
shadow.innerHTML = '<slot></slot>';
return shadow;
}
describe('createStoreMixin', () => {
@@ -17,8 +17,7 @@ describe('createStoreMixin', () => {
class TestElement extends StoreMixin(TestBaseElement) {
override createRenderRoot() {
createShadowWithSlot(this);
return this.shadowRoot!;
return createShadowWithSlot(this);
}
}
customElements.define(tagName, TestElement);
@@ -37,15 +36,13 @@ describe('createStoreMixin', () => {
class TestElement extends StoreMixin(TestBaseElement) {
override createRenderRoot() {
createShadowWithSlot(this);
return this.shadowRoot!;
return createShadowWithSlot(this);
}
}
customElements.define(tagName, TestElement);
const el = document.createElement(tagName) as TestElement;
const video = document.createElement('video');
video.slot = 'media';
el.appendChild(video);
document.body.appendChild(el);
await el.updateComplete;
@@ -85,15 +82,13 @@ describe('createStoreMixin', () => {
class TestElement extends StoreMixin(TestBaseElement) {
override createRenderRoot() {
createShadowWithSlot(this);
return this.shadowRoot!;
return createShadowWithSlot(this);
}
}
customElements.define(tagName, TestElement);
const el = document.createElement(tagName) as TestElement;
const wrapper = document.createElement('div');
wrapper.slot = 'media';
const video = document.createElement('video');
wrapper.appendChild(video);
el.appendChild(wrapper);
@@ -112,15 +107,13 @@ describe('createStoreMixin', () => {
class TestElement extends StoreMixin(TestBaseElement) {
override createRenderRoot() {
createShadowWithSlot(this);
return this.shadowRoot!;
return createShadowWithSlot(this);
}
}
customElements.define(tagName, TestElement);
const el = document.createElement(tagName) as TestElement;
const audio = document.createElement('audio');
audio.slot = 'media';
el.appendChild(audio);
document.body.appendChild(el);
await el.updateComplete;
@@ -137,17 +130,14 @@ describe('createStoreMixin', () => {
class TestElement extends StoreMixin(TestBaseElement) {
override createRenderRoot() {
createShadowWithSlot(this);
return this.shadowRoot!;
return createShadowWithSlot(this);
}
}
customElements.define(tagName, TestElement);
const el = document.createElement(tagName) as TestElement;
const video1 = document.createElement('video');
video1.slot = 'media';
const video2 = document.createElement('video');
video2.slot = 'media';
el.appendChild(video1);
el.appendChild(video2);
document.body.appendChild(el);
+7 -4
View File
@@ -330,6 +330,9 @@ importers:
packages/html:
dependencies:
'@lit/reactive-element':
specifier: ^2.1.2
version: 2.1.2
'@videojs/core':
specifier: workspace:*
version: link:../core
@@ -8947,7 +8950,7 @@ snapshots:
sirv: 3.0.2
tinyglobby: 0.2.15
tinyrainbow: 2.0.0
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.3)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.3)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
'@vitest/utils@3.2.4':
dependencies:
@@ -10016,7 +10019,7 @@ snapshots:
eslint: 9.39.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1))
@@ -10058,7 +10061,7 @@ snapshots:
tinyglobby: 0.2.15
unrs-resolver: 1.11.1
optionalDependencies:
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
transitivePeerDependencies:
- supports-color
@@ -10134,7 +10137,7 @@ snapshots:
optionalDependencies:
typescript: 5.9.3
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9