Skip to content

Commit d95b7ea

Browse files
committed
refactor(0): extract createPluginContext factory to eliminate trinity boilerplate
Introduces createPluginContext inside createPlugin — a factory that generates the standard context/plugin/consumer triple for plugin composables from a namespace, factory function, and optional setup/fallback config. The setup callback receives (context, app, options) so adapter-wired plugins can access their options without a manual plugin wrapper. Migrates all 8 plugin composables to the factory: - usePermissions, useStorage: simple (no setup/fallback) - useLogger: setup (dev window export) + fallback (safe outside instances) - useHydration: setup (root mixin) + fallback (SSR-safe) - useBreakpoints: setup (resize listener via mixin) - useLocale: fallback (safe outside instances) - useFeatures: setup (adapter wiring) - useTheme: setup (adapter + target)
1 parent 1d99e5b commit d95b7ea

File tree

10 files changed

+222
-879
lines changed

10 files changed

+222
-879
lines changed

packages/0/src/composables/createPlugin/index.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,20 @@
99
*
1010
* Wraps the provide function in app.runWithContext() to ensure proper execution context,
1111
* allowing plugins to safely provide dependency injection contexts at the application level.
12+
*
13+
* Also exports `createPluginContext` — a higher-level factory that generates the standard
14+
* context/plugin/consumer triple for plugin composables, eliminating boilerplate.
1215
*/
1316

17+
// Foundational
18+
import { createContext, useContext } from '#v0/composables/createContext'
19+
import { createTrinity } from '#v0/composables/createTrinity'
20+
21+
// Utilities
22+
import { instanceExists } from '#v0/utilities/instance'
23+
1424
// Types
25+
import type { ContextTrinity } from '#v0/composables/createTrinity'
1526
import type { App } from 'vue'
1627

1728
export interface PluginOptions {
@@ -70,3 +81,100 @@ export function createPlugin<Z extends Plugin = Plugin> (options: PluginOptions)
7081
},
7182
} as Z
7283
}
84+
85+
export interface PluginContextConfig<O, E> {
86+
/**
87+
* Optional plugin setup callback, called once per Vue app after context provision.
88+
* Use for adapter initialization, Vue app mixins, global side effects, etc.
89+
* Receives the plugin options (minus namespace) so adapters and targets are accessible.
90+
*/
91+
setup?: (context: E, app: App, options: O) => void
92+
/**
93+
* Optional fallback factory. When provided, the generated `useX` consumer uses the
94+
* defensive pattern: returns the fallback when called outside a component instance or
95+
* when the context is not found. Required for composables that may be consumed outside
96+
* component setup (e.g. useLogger, useLocale, useHydration).
97+
*
98+
* Receives the requested namespace so error messages can include it.
99+
*/
100+
fallback?: (namespace: string) => E
101+
}
102+
103+
/**
104+
* Creates the three standard functions for a plugin composable.
105+
*
106+
* @param defaultNamespace The default DI namespace string (e.g. `'v0:logger'`).
107+
* @param factory Function that creates the composable context instance from options.
108+
* @param config Optional setup callback and fallback factory.
109+
* @returns A readonly tuple: `[createXContext, createXPlugin, useX]`.
110+
*
111+
* @example
112+
* ```ts
113+
* // Simple — no setup or fallback
114+
* export const [createStorageContext, createStoragePlugin, useStorage] =
115+
* createPluginContext('v0:storage', options => createStorage(options))
116+
*
117+
* // With fallback — safe outside component instances
118+
* export const [createLoggerContext, createLoggerPlugin, useLogger] =
119+
* createPluginContext('v0:logger', options => createLogger(options), {
120+
* fallback: ns => createFallbackLogger(ns),
121+
* setup: (context) => {
122+
* if (__DEV__ && IN_BROWSER) (window as any).__v0Logger__ = context
123+
* },
124+
* })
125+
* ```
126+
*/
127+
export function createPluginContext<
128+
O extends { namespace?: string } = Record<never, never>,
129+
E = unknown,
130+
> (
131+
defaultNamespace: string,
132+
factory: (options: Omit<O, 'namespace'>) => E,
133+
config?: PluginContextConfig<Omit<O, 'namespace'>, E>,
134+
): readonly [
135+
<_E extends E = E>(_options?: O) => ContextTrinity<_E>,
136+
(_options?: O) => Plugin,
137+
<_E extends E = E>(namespace?: string) => _E,
138+
] {
139+
function createXContext<_E extends E = E> (_options: O = {} as O): ContextTrinity<_E> {
140+
const { namespace = defaultNamespace, ...options } = _options as O & { namespace?: string }
141+
const [_use, _provide] = createContext<_E>(namespace)
142+
const context = factory(options as Omit<O, 'namespace'>) as _E
143+
144+
function provide (_context: _E = context, app?: App): _E {
145+
return _provide(_context, app)
146+
}
147+
148+
return createTrinity<_E>(_use, provide, context)
149+
}
150+
151+
function createXPlugin (_options: O = {} as O): Plugin {
152+
const { namespace = defaultNamespace, ...options } = _options as O & { namespace?: string }
153+
const [, provide, context] = createXContext({ ...options, namespace } as O)
154+
155+
return createPlugin({
156+
namespace,
157+
provide: app => {
158+
provide(context, app)
159+
},
160+
setup: config?.setup
161+
? app => config.setup!(context, app, options as Omit<O, 'namespace'>)
162+
: undefined,
163+
})
164+
}
165+
166+
function useX<_E extends E = E> (namespace = defaultNamespace): _E {
167+
if (config?.fallback) {
168+
const instance = config.fallback(namespace) as _E
169+
if (!instanceExists()) return instance
170+
try {
171+
return useContext<_E>(namespace, instance)
172+
} catch {
173+
return instance
174+
}
175+
}
176+
return useContext<_E>(namespace)
177+
}
178+
179+
return [createXContext, createXPlugin, useX] as const
180+
}

packages/0/src/composables/useBreakpoints/index.ts

Lines changed: 31 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@
2121
import { IN_BROWSER } from '#v0/constants/globals'
2222

2323
// Foundational
24-
import { createContext, useContext } from '#v0/composables/createContext'
25-
import { createPlugin } from '#v0/composables/createPlugin'
26-
import { createTrinity } from '#v0/composables/createTrinity'
24+
import { createPluginContext } from '#v0/composables/createPlugin'
2725

2826
// Composables
2927
import { useWindowEventListener } from '#v0/composables/useEventListener'
@@ -34,8 +32,7 @@ import { isNull, isNumber, mergeDeep } from '#v0/utilities'
3432
import { onScopeDispose, readonly, shallowRef, watch } from 'vue'
3533

3634
// Types
37-
import type { ContextTrinity } from '#v0/composables/createTrinity'
38-
import type { App, ShallowRef } from 'vue'
35+
import type { ShallowRef } from 'vue'
3936

4037
export type BreakpointName = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'
4138

@@ -213,144 +210,33 @@ export function createBreakpoints<
213210
} as E
214211
}
215212

216-
/**
217-
* Creates a new breakpoints context.
218-
*
219-
* @param options The options for the breakpoints context.
220-
* @template E The type of the breakpoints context.
221-
* @returns A new breakpoints context.
222-
*
223-
* @see https://0.vuetifyjs.com/composables/plugins/use-breakpoints
224-
*
225-
* @example
226-
* ```ts
227-
* import { createBreakpointsContext } from '@vuetify/v0'
228-
*
229-
* export const [useBreakpoints, provideBreakpoints, context] = createBreakpointsContext({
230-
* namespace: 'v0:breakpoints',
231-
* mobileBreakpoint: 'sm',
232-
* })
233-
* ```
234-
*/
235-
export function createBreakpointsContext<
236-
E extends BreakpointsContext = BreakpointsContext,
237-
> (_options: BreakpointsContextOptions = {}): ContextTrinity<E> {
238-
const { namespace = 'v0:breakpoints', ...options } = _options
239-
const [useBreakpointsContext, _provideBreakpointsContext] = createContext<E>(namespace)
240-
const context = createBreakpoints<E>(options)
241-
242-
function provideBreakpointsContext (_context: E = context, app?: App): E {
243-
return _provideBreakpointsContext(_context, app)
244-
}
245-
246-
return createTrinity<E>(useBreakpointsContext, provideBreakpointsContext, context)
247-
}
248-
249-
/**
250-
* Creates a new breakpoints plugin.
251-
*
252-
* @param options The options for the breakpoints plugin.
253-
* @template E The type of the breakpoints context.
254-
* @returns A new breakpoints plugin.
255-
*
256-
* @see https://0.vuetifyjs.com/composables/plugins/use-breakpoints
257-
*
258-
* @example
259-
* ```ts
260-
* import { createApp } from 'vue'
261-
* import { createBreakpointsPlugin } from '@vuetify/v0'
262-
* import App from './App.vue'
263-
*
264-
* const app = createApp(App)
265-
*
266-
* app.use(
267-
* createBreakpointsPlugin({
268-
* namespace: 'v0:breakpoints',
269-
* mobileBreakpoint: 'md',
270-
* breakpoints: {
271-
* xs: 0,
272-
* sm: 600,
273-
* md: 840,
274-
* lg: 1145,
275-
* xl: 1545,
276-
* xxl: 2138,
277-
* },
278-
* })
279-
* )
280-
*
281-
* app.mount('#app')
282-
* ```
283-
*/
284-
export function createBreakpointsPlugin<
285-
E extends BreakpointsContext = BreakpointsContext,
286-
> (_options: BreakpointsPluginOptions = {}) {
287-
const { namespace = 'v0:breakpoints', ...options } = _options
288-
const [, provideBreakpointsContext, context] = createBreakpointsContext<E>({ ...options, namespace })
289-
290-
return createPlugin({
291-
namespace,
292-
provide: (app: App) => {
293-
provideBreakpointsContext(context, app)
213+
export const [createBreakpointsContext, createBreakpointsPlugin, useBreakpoints] =
214+
createPluginContext<BreakpointsContextOptions, BreakpointsContext>(
215+
'v0:breakpoints',
216+
options => createBreakpoints(options),
217+
{
218+
setup: (context, app, _options) => {
219+
app.mixin({
220+
mounted () {
221+
if (!isNull(this.$parent)) return
222+
223+
const hydration = useHydration()
224+
225+
function listener () {
226+
context.update()
227+
}
228+
229+
const unwatch = watch(hydration.isHydrated, hydrated => {
230+
if (hydrated) listener()
231+
}, { immediate: true })
232+
233+
const cleanup = useWindowEventListener('resize', listener, { passive: true })
234+
onScopeDispose(() => {
235+
cleanup()
236+
unwatch()
237+
}, true)
238+
},
239+
})
240+
},
294241
},
295-
setup: (app: App) => {
296-
app.mixin({
297-
mounted () {
298-
if (!isNull(this.$parent)) return
299-
300-
const hydration = useHydration()
301-
302-
function listener () {
303-
context.update()
304-
}
305-
306-
const unwatch = watch(hydration.isHydrated, hydrated => {
307-
if (hydrated) listener()
308-
}, { immediate: true })
309-
310-
const cleanup = useWindowEventListener('resize', listener, { passive: true })
311-
onScopeDispose(() => {
312-
cleanup()
313-
unwatch()
314-
}, true)
315-
},
316-
})
317-
},
318-
})
319-
}
320-
321-
/**
322-
* Returns the current breakpoints instance.
323-
*
324-
* @param namespace The namespace for the breakpoints context. Defaults to `v0:breakpoints`.
325-
* @returns The current breakpoints instance.
326-
*
327-
* @see https://0.vuetifyjs.com/composables/plugins/use-breakpoints
328-
*
329-
* @example
330-
* ```vue
331-
* <script setup lang="ts">
332-
* import { useBreakpoints } from '@vuetify/v0'
333-
*
334-
* // Destructure for template auto-unwrapping
335-
* const { isMobile, mdAndUp } = useBreakpoints()
336-
*
337-
* // In script, use .value
338-
* if (isMobile.value) {
339-
* console.log('Mobile detected')
340-
* }
341-
* </script>
342-
*
343-
* <template>
344-
* <div class="pa-4">
345-
* <!-- Destructured refs auto-unwrap in templates -->
346-
* <p v-if="isMobile">Mobile layout active</p>
347-
* <p v-else-if="mdAndUp">Medium and up layout active</p>
348-
* </div>
349-
* </template>
350-
* ```
351-
*/
352-
export function useBreakpoints<
353-
E extends BreakpointsContext = BreakpointsContext,
354-
> (namespace = 'v0:breakpoints'): E {
355-
return useContext<E>(namespace)
356-
}
242+
)

0 commit comments

Comments
 (0)