From ed045007673e97fa9ca701fbedfe19febd9e7fac Mon Sep 17 00:00:00 2001 From: Nico Kempe Date: Sun, 10 Aug 2025 10:40:48 +0200 Subject: [PATCH 1/5] docs(readme): add definePageMeta middleware guide Signed-off-by: Nico Kempe --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 1395088..fa43e0e 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,19 @@ export default defineEventHandler((event) => { }) ``` + +Or guard entire pages via `definePageMeta`: + +```html + +``` + ## Contribution
From 405c6274cc9ff7771e48f75668ccf0fb533bc5e3 Mon Sep 17 00:00:00 2001 From: Nico Kempe Date: Sun, 10 Aug 2025 10:41:12 +0200 Subject: [PATCH 2/5] types(middleware): add PageMeta interface to nuxt/schema module declaration Signed-off-by: Nico Kempe --- types/feature-flags.d.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/types/feature-flags.d.ts b/types/feature-flags.d.ts index 1cd9340..f6b6e5d 100644 --- a/types/feature-flags.d.ts +++ b/types/feature-flags.d.ts @@ -54,4 +54,10 @@ declare module 'nuxt/schema' { interface PublicRuntimeConfig { featureFlags: FeatureFlagsConfig } + + interface PageMeta { + featureFlag?: string + featureFallback?: string + featureNotifyOnBlock?: boolean + } } From e7e34277e931cb2c0a24a8177c9fd769e4b27daf Mon Sep 17 00:00:00 2001 From: Nico Kempe Date: Sun, 10 Aug 2025 10:41:54 +0200 Subject: [PATCH 3/5] feat(middleware): add global route middleware for feature flags through definePageMeta Signed-off-by: Nico Kempe --- src/module.ts | 6 ++++ src/runtime/middleware/featureFlag.ts | 51 +++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 src/runtime/middleware/featureFlag.ts diff --git a/src/module.ts b/src/module.ts index ea8c549..3f0bb25 100644 --- a/src/module.ts +++ b/src/module.ts @@ -3,6 +3,7 @@ import { createResolver, addPlugin, addImportsDir, + addRouteMiddleware, } from '@nuxt/kit' import type { Resolver } from '@nuxt/kit' @@ -37,6 +38,11 @@ export default defineNuxtModule({ addImportsDir(resolver.resolve('runtime/composables')) addPlugin(resolver.resolve('runtime/plugin')) addImportsDir(resolver.resolve('runtime/middleware')) + addRouteMiddleware({ + name: 'feature-flag', + path: resolver.resolve('runtime/middleware/featureFlag'), + global: true, + }) // Run validation at `ready` (after Nuxt merges config) nuxt.hook('ready', async (): Promise => { diff --git a/src/runtime/middleware/featureFlag.ts b/src/runtime/middleware/featureFlag.ts new file mode 100644 index 0000000..e705b8c --- /dev/null +++ b/src/runtime/middleware/featureFlag.ts @@ -0,0 +1,51 @@ +import { useFeatureFlag } from '../composables/useFeatureFlag' +import { defineNuxtRouteMiddleware, navigateTo, showError } from '#app' + +// Defines optional route metadata for feature-flag-based access control. +interface FeatureMeta { + // Name of the feature flag required to access the route. + featureFlag?: string + + // Path to navigate to when the feature flag is disabled. + featureFallback?: string + + // Whether to log a console warning when the route is blocked. + featureNotifyOnBlock?: boolean +} + +/** + * Nuxt route middleware that restricts access to routes based on feature flags. + * + * @param to - The target route object. + * @returns + * - `undefined` if access is allowed. + * - A `navigateTo()` redirect if a fallback route is defined. + * - A `showError()` result with a 404 status if blocked without fallback. + * + * @example + * ```ts + * definePageMeta({ + * featureFlag: 'beta-dashboard', + * featureFallback: '/dashboard', + * featureNotifyOnBlock: true + * }) + * ``` + */ +export default defineNuxtRouteMiddleware((to) => { + const meta: FeatureMeta = to.meta as FeatureMeta + const flag: string | undefined = meta.featureFlag + if (!flag) return + + const { isEnabled } = useFeatureFlag() + if (isEnabled(flag)) return + + if (meta.featureNotifyOnBlock) { + console.warn(`[nuxt-feature-flags] Feature "${flag}" is disabled`) + } + + if (meta.featureFallback) { + return navigateTo(meta.featureFallback) + } + + return showError({ statusCode: 404, statusMessage: 'Feature not available' }) +}) From 4074cb7dc1853e9fd72f0ba72dbd6772945061e4 Mon Sep 17 00:00:00 2001 From: Nico Kempe Date: Sun, 10 Aug 2025 10:42:12 +0200 Subject: [PATCH 4/5] test(middleware): add tests for new page protection Signed-off-by: Nico Kempe --- test/unit/featureFlagMiddleware.test.ts | 64 +++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 test/unit/featureFlagMiddleware.test.ts diff --git a/test/unit/featureFlagMiddleware.test.ts b/test/unit/featureFlagMiddleware.test.ts new file mode 100644 index 0000000..df30d21 --- /dev/null +++ b/test/unit/featureFlagMiddleware.test.ts @@ -0,0 +1,64 @@ +// test/unit/featureFlagMiddleware.test.ts +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest' +import middleware from '../../src/runtime/middleware/featureFlag' +import type { RouteLocationNormalized } from 'vue-router' + +const { navigateTo, showError } = vi.hoisted(() => ({ + navigateTo: vi.fn(), + showError: vi.fn(), +})) + +vi.mock('#app', () => ({ + defineNuxtRouteMiddleware: (fn: (to: RouteLocationNormalized, from: RouteLocationNormalized) => unknown) => fn, + navigateTo, + showError, +})) + +type IsEnabledFn = (flagName: string) => boolean +const isEnabled = vi.fn() +vi.mock('../../src/runtime/composables/useFeatureFlag', () => ({ + useFeatureFlag: () => ({ isEnabled }), +})) + +/** + * Helper to create a minimal, strictly typed RouteLocationNormalized for tests. + */ +const makeRoute = (meta: Partial): RouteLocationNormalized => + ({ meta } as unknown as RouteLocationNormalized) + +describe('featureFlag middleware', (): void => { + beforeEach((): void => { + isEnabled.mockReset() + navigateTo.mockReset() + showError.mockReset() + }) + + afterEach((): void => { + vi.restoreAllMocks() + }) + + it('redirects to fallback when flag disabled', (): void => { + isEnabled.mockReturnValue(false) + const to: RouteLocationNormalized = makeRoute({ featureFlag: 'test', featureFallback: '/404' } as Record) + const from: RouteLocationNormalized = makeRoute({}) + middleware(to, from) + expect(navigateTo).toHaveBeenCalledWith('/404') + }) + + it('shows error when flag disabled and no fallback', (): void => { + isEnabled.mockReturnValue(false) + const to: RouteLocationNormalized = makeRoute({ featureFlag: 'test' } as Record) + const from: RouteLocationNormalized = makeRoute({}) + middleware(to, from) + expect(showError).toHaveBeenCalledWith({ statusCode: 404, statusMessage: 'Feature not available' }) + }) + + it('does nothing when flag enabled', (): void => { + isEnabled.mockReturnValue(true) + const to: RouteLocationNormalized = makeRoute({ featureFlag: 'test' } as Record) + const from: RouteLocationNormalized = makeRoute({}) + middleware(to, from) + expect(navigateTo).not.toHaveBeenCalled() + expect(showError).not.toHaveBeenCalled() + }) +}) From dfdd15789f65d4bda952e84f7d4cfa5239c9c224 Mon Sep 17 00:00:00 2001 From: Nico Kempe Date: Sun, 10 Aug 2025 10:42:58 +0200 Subject: [PATCH 5/5] feat(playground): add playground example for new page protection Signed-off-by: Nico Kempe --- playground/pages/new-protected.vue | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 playground/pages/new-protected.vue diff --git a/playground/pages/new-protected.vue b/playground/pages/new-protected.vue new file mode 100644 index 0000000..473b143 --- /dev/null +++ b/playground/pages/new-protected.vue @@ -0,0 +1,19 @@ + + +