From 06ffcd3294544a7525e60ab2053985679f242441 Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Fri, 15 Aug 2025 18:48:02 +0200 Subject: [PATCH 1/2] feat: support disabling router integrations --- .../1.getting-started/2.installation/2.vue.md | 23 ++ src/defaults.ts | 1 + src/module.ts | 6 + src/plugins/components.ts | 9 + src/plugins/nuxt-environment.ts | 13 +- src/runtime/minimal/components/Link.vue | 212 ++++++++++++++++++ src/runtime/minimal/stubs.ts | 96 ++++++++ src/unplugin.ts | 10 + 8 files changed, 369 insertions(+), 1 deletion(-) create mode 100644 src/runtime/minimal/components/Link.vue create mode 100644 src/runtime/minimal/stubs.ts diff --git a/docs/content/1.getting-started/2.installation/2.vue.md b/docs/content/1.getting-started/2.installation/2.vue.md index 44bdf1d1c1..2894d8a028 100644 --- a/docs/content/1.getting-started/2.installation/2.vue.md +++ b/docs/content/1.getting-started/2.installation/2.vue.md @@ -403,6 +403,29 @@ export default defineConfig({ When using this option, `vue-router` is not required as Inertia.js provides its own routing system. The components that would normally use `RouterLink` will automatically use Inertia's `InertiaLink` component instead. :: +### `router` + +Use the `router` option to disable router integration entirely. + +```ts [vite.config.ts] +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import ui from '@nuxt/ui/vite' + +export default defineConfig({ + plugins: [ + vue(), + ui({ + router: false + }) + ] +}) +``` + +::note +When this option is disabled, components that would normally integrate with routing (like `ULink`) will behave as regular anchor tags for external links or plain elements for internal navigation. This is useful for single-page applications without routing or when using custom routing solutions. +:: + ## Continuous Releases Nuxt UI uses [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new) for continuous preview releases, providing developers with instant access to the latest features and bug fixes without waiting for official releases. diff --git a/src/defaults.ts b/src/defaults.ts index 0a88d6a680..024b85e70a 100644 --- a/src/defaults.ts +++ b/src/defaults.ts @@ -19,6 +19,7 @@ export const defaultOptions = { prefix: 'U', fonts: true, colorMode: true, + router: true, theme: { colors: undefined, transitions: true diff --git a/src/module.ts b/src/module.ts index 76cd729f83..5322f5a80b 100644 --- a/src/module.ts +++ b/src/module.ts @@ -31,6 +31,12 @@ export interface ModuleOptions { */ colorMode?: boolean + /** + * Enable or disable router integration + * @defaultValue `true` + */ + router?: boolean + /** * Customize how the theme is generated * @link https://ui3.nuxt.com/getting-started/theme diff --git a/src/plugins/components.ts b/src/plugins/components.ts index 7902635a75..3aa1e10988 100644 --- a/src/plugins/components.ts +++ b/src/plugins/components.ts @@ -21,6 +21,9 @@ export default function ComponentImportPlugin(options: NuxtUIOptions & { prefix: const inertiaOverrides = globSync('**/*.vue', { cwd: join(runtimeDir, 'inertia/components') }) const inertiaOverrideNames = new Set(inertiaOverrides.map(c => `${options.prefix}${c.replace(/\.vue$/, '')}`)) + const minimalOverrides = globSync('**/*.vue', { cwd: join(runtimeDir, 'minimal/components') }) + const minimalOverrideNames = new Set(minimalOverrides.map(c => `${options.prefix}${c.replace(/\.vue$/, '')}`)) + const pluginOptions = defu(options.components, { dts: options.dts ?? true, exclude: [ @@ -30,6 +33,9 @@ export default function ComponentImportPlugin(options: NuxtUIOptions & { prefix: ], resolvers: [ (componentName) => { + if (options.router === false && minimalOverrideNames.has(componentName)) { + return { name: 'default', from: join(runtimeDir, 'minimal/components', `${componentName.slice(options.prefix.length)}.vue`) } + } if (options.inertia && inertiaOverrideNames.has(componentName)) { return { name: 'default', from: join(runtimeDir, 'inertia/components', `${componentName.slice(options.prefix.length)}.vue`) } } @@ -63,6 +69,9 @@ export default function ComponentImportPlugin(options: NuxtUIOptions & { prefix: } const filename = id.match(/([^/]+)\.vue$/)?.[1] + if (filename && options.router === false && minimalOverrideNames.has(`${options.prefix}${filename}`)) { + return join(runtimeDir, 'minimal/components', `${filename}.vue`) + } if (filename && options.inertia && inertiaOverrideNames.has(`${options.prefix}${filename}`)) { return join(runtimeDir, 'inertia/components', `${filename}.vue`) } diff --git a/src/plugins/nuxt-environment.ts b/src/plugins/nuxt-environment.ts index 5bd3084152..aee63a9ef7 100644 --- a/src/plugins/nuxt-environment.ts +++ b/src/plugins/nuxt-environment.ts @@ -10,7 +10,18 @@ import type { NuxtUIOptions } from '../unplugin' * This plugin normalises Nuxt environment (#imports) and `import.meta.client` within the Nuxt UI components. */ export default function NuxtEnvironmentPlugin(options: NuxtUIOptions) { - const stubPath = resolvePathSync(options.inertia ? '../runtime/inertia/stubs' : '../runtime/vue/stubs', { extensions: ['.ts', '.mjs', '.js'], url: import.meta.url }) + let stubPath: string + + if (options.router === false) { + // Router disabled - use minimal stubs + stubPath = resolvePathSync('../runtime/minimal/stubs', { extensions: ['.ts', '.mjs', '.js'], url: import.meta.url }) + } else if (options.inertia) { + // Inertia mode + stubPath = resolvePathSync('../runtime/inertia/stubs', { extensions: ['.ts', '.mjs', '.js'], url: import.meta.url }) + } else { + // Default Vue mode + stubPath = resolvePathSync('../runtime/vue/stubs', { extensions: ['.ts', '.mjs', '.js'], url: import.meta.url }) + } return { name: 'nuxt:ui', diff --git a/src/runtime/minimal/components/Link.vue b/src/runtime/minimal/components/Link.vue new file mode 100644 index 0000000000..844ef3c08c --- /dev/null +++ b/src/runtime/minimal/components/Link.vue @@ -0,0 +1,212 @@ + + + + + diff --git a/src/runtime/minimal/stubs.ts b/src/runtime/minimal/stubs.ts new file mode 100644 index 0000000000..3bedb63829 --- /dev/null +++ b/src/runtime/minimal/stubs.ts @@ -0,0 +1,96 @@ +import { ref, onScopeDispose } from 'vue' +import type { Ref, Plugin as VuePlugin } from 'vue' +import { createHooks } from 'hookable' +import { useColorMode as useColorModeVueUse } from '@vueuse/core' +import appConfig from '#build/app.config' +import type { NuxtApp } from '#app' + +export { useHead } from '@unhead/vue' + +export { useAppConfig } from '../vue/composables/useAppConfig' +export { defineShortcuts } from '../composables/defineShortcuts' +export { defineLocale } from '../composables/defineLocale' +export { useLocale } from '../composables/useLocale' + +export const useRoute = () => { + return {} +} + +export const useRouter = () => { + return {} +} + +export const clearError = () => { + +} + +export const useColorMode = () => { + if (!appConfig.colorMode) { + return { + forced: true + } + } + + const { store, system } = useColorModeVueUse() + + return { + get preference() { return store.value === 'auto' ? 'system' : store.value }, + set preference(value) { store.value = value === 'system' ? 'auto' : value }, + get value() { return store.value === 'auto' ? system.value : store.value }, + forced: false + } +} + +export const useCookie = ( + _name: string, + _options: Record = {} +) => { + const value = ref(null) as Ref + + return { + value, + get: () => value.value, + set: () => {}, + update: () => {}, + refresh: () => Promise.resolve(value.value), + remove: () => {} + } +} + +const state: Record = {} + +export const useState = (key: string, init: () => T): Ref => { + if (state[key]) { + return state[key] as Ref + } + const value = ref(init()) + state[key] = value + return value as Ref +} + +const hooks = createHooks() + +export function useNuxtApp() { + return { + isHydrating: true, + payload: { serverRendered: false }, + hooks, + hook: hooks.hook + } +} + +export function useRuntimeHook(name: string, fn: (...args: any[]) => void): void { + const nuxtApp = useNuxtApp() + + const unregister = nuxtApp.hook(name, fn) + + onScopeDispose(unregister) +} + +export function defineNuxtPlugin(plugin: (nuxtApp: NuxtApp) => void) { + return { + install(app) { + app.runWithContext(() => plugin({ vueApp: app } as NuxtApp)) + } + } satisfies VuePlugin +} diff --git a/src/unplugin.ts b/src/unplugin.ts index 1e26ef5a0e..ee123f285d 100644 --- a/src/unplugin.ts +++ b/src/unplugin.ts @@ -54,6 +54,11 @@ export interface NuxtUIOptions extends Omit((_options = {}, meta) => { const options = defu(_options, { fonts: false }, defaultOptions) + // Validation: router and inertia options + if (options.router === false && options.inertia === true) { + throw new Error('[Nuxt UI] Cannot enable Inertia integration when router is disabled. Inertia requires router functionality.') + } + options.theme = options.theme || {} options.theme.colors = resolveColors(options.theme.colors) From 061dfde3daa845bbd6fc462e3c4258e5b74ee321 Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Fri, 15 Aug 2025 19:26:00 +0200 Subject: [PATCH 2/2] style: apply fixes from eslint --- src/plugins/nuxt-environment.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/plugins/nuxt-environment.ts b/src/plugins/nuxt-environment.ts index aee63a9ef7..26966c0fa5 100644 --- a/src/plugins/nuxt-environment.ts +++ b/src/plugins/nuxt-environment.ts @@ -11,15 +11,12 @@ import type { NuxtUIOptions } from '../unplugin' */ export default function NuxtEnvironmentPlugin(options: NuxtUIOptions) { let stubPath: string - + if (options.router === false) { - // Router disabled - use minimal stubs stubPath = resolvePathSync('../runtime/minimal/stubs', { extensions: ['.ts', '.mjs', '.js'], url: import.meta.url }) } else if (options.inertia) { - // Inertia mode stubPath = resolvePathSync('../runtime/inertia/stubs', { extensions: ['.ts', '.mjs', '.js'], url: import.meta.url }) } else { - // Default Vue mode stubPath = resolvePathSync('../runtime/vue/stubs', { extensions: ['.ts', '.mjs', '.js'], url: import.meta.url }) }