diff --git a/README.md b/README.md index 489893e..c227b14 100644 --- a/README.md +++ b/README.md @@ -15,17 +15,17 @@ Lightweight, environment-based feature flag system for Nuxt - made for developer - 🎯 Roll out features to internal QA teams without branching or releases - 📆 Schedule feature launches for specific environments or timeframes - 🕵️‍♀️ Detect undeclared feature flags at build time with configurable validation and precise file context +- 🌳 Group flags with hierarchical names and enable bundles via wildcard (`*`) patterns ## Planned Features +- 📊 A/B testing support for feature flags +- 💡 Flag descriptions / metadata for better documentation, DevTools tooltips, or internal usage notes - 🧩 Nuxt DevTools integration with a Feature Flag Explorer and Environment Switcher - 🔄 Dynamic feature flag updates without server restarts through a remote config service -- 📊 A/B testing support for feature flags -- 📈 Analytics for feature flag usage - 🧍‍♂️ Show features only for specific users (e.g., staff-only UIs, admin panels etc.) -- 🧬 Environment inheritance which lets environments inherit feature flags from others -- 💡 Flag descriptions / metadata for better documentation, DevTools tooltips, or internal usage notes - 🛠 Programmatic overrides to toggle or override feature flags dynamically at runtime (e.g., per user or session) +- 📈 Analytics for feature flag usage and user feedback collection ## Quick Setup @@ -53,6 +53,49 @@ export default defineNuxtConfig({ }) ``` +### Hierarchical & Wildcard Flags + +Feature flags can be organized with `/`-separated paths and enabled in bulk using `*`. + +```ts +export default defineNuxtConfig({ + modules: ['nuxt-feature-flags-module'], + featureFlags: { + environment: process.env.FEATURE_ENV || 'development', + flagSets: { + development: [ + 'solutions/*', + 'staging/*', + 'internal/experimental/ui' + ], + staging: [ + 'solutions/company-portal/addons/sales', + 'solutions/company-portal/addons/marketing', + 'internal/experimental/ui' + ], + production: [ + 'solutions/company-portal/addons/sales' + ] + } + } +}) +``` + +```ts +const { isEnabled } = useFeatureFlag() + +if (isEnabled('solutions/company-portal/addons/sales')) { + // sales addon enabled +} + +if (isEnabled('solutions/*')) { + // any solution-related flag is active +} +``` + +> [!CAUTION] +> Using `*` enables every flag and the validator will emit a warning. Reserve it for debugging scenarios. + Use in your app: ```html diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index 9750cc4..f8df5c9 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -33,9 +33,16 @@ export default defineNuxtConfig({ activeFrom: '2000-01-01T00:00:00Z', activeUntil: '2001-01-01T00:00:00Z', }, + 'solutions/*', + 'internal/experimental/ui', ], - staging: ['newSystem'], - production: [], + staging: [ + 'newSystem', + 'solutions/company-portal/addons/sales', + 'solutions/company-portal/addons/marketing', + 'internal/experimental/ui', + ], + production: ['solutions/company-portal/addons/sales'], }, validation: { mode: 'warn', diff --git a/playground/pages/hierarchical.vue b/playground/pages/hierarchical.vue new file mode 100644 index 0000000..9b97b8a --- /dev/null +++ b/playground/pages/hierarchical.vue @@ -0,0 +1,45 @@ + + + diff --git a/playground/pages/index.vue b/playground/pages/index.vue index 4c3a516..628c29e 100644 --- a/playground/pages/index.vue +++ b/playground/pages/index.vue @@ -75,6 +75,22 @@ Redirects to /404 unless 'newSystem' is active.

+ + +
+

+ 🌳 Hierarchical Flags +

+ +

+ Demonstrates grouped flags enabled via wildcard patterns. +

+
@@ -129,6 +145,10 @@ function navigateToProtected() { } } +function navigateToHierarchical() { + navigateTo('/hierarchical') +} + useSeoMeta({ title: 'Nuxt Feature Flags Playground', description: 'A playground for the Nuxt Feature Flags Module. Test and explore feature flags in a Nuxt application.', diff --git a/playground/server/api/solutions.ts b/playground/server/api/solutions.ts new file mode 100644 index 0000000..e3844c1 --- /dev/null +++ b/playground/server/api/solutions.ts @@ -0,0 +1,9 @@ +import { isFeatureEnabled } from '../../../src/runtime/server/isFeatureEnabled' + +export default defineEventHandler((event) => { + if (!isFeatureEnabled('solutions/company-portal/addons/sales', event)) { + return sendError(event, createError({ statusCode: 403, message: 'Sales addon disabled' })) + } + + return { message: 'Sales addon feature unlocked 🎉' } +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 95f28d2..8968e56 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,10 +17,10 @@ importers: version: 2.6.3(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.19(typescript@5.9.2)) '@nuxt/eslint': specifier: 1.9.0 - version: 1.9.0(@typescript-eslint/utils@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(@vue/compiler-sfc@3.5.19)(eslint@9.33.0(jiti@2.5.1))(magicast@0.3.5)(typescript@5.9.2)(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)) + version: 1.9.0(@typescript-eslint/utils@8.40.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(@vue/compiler-sfc@3.5.19)(eslint@9.34.0(jiti@2.5.1))(magicast@0.3.5)(typescript@5.9.2)(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)) '@nuxt/eslint-config': specifier: ^1.3.1 - version: 1.9.0(@typescript-eslint/utils@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(@vue/compiler-sfc@3.5.19)(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) + version: 1.9.0(@typescript-eslint/utils@8.40.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(@vue/compiler-sfc@3.5.19)(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) '@nuxt/module-builder': specifier: ^1.0.1 version: 1.0.2(@nuxt/cli@3.28.0(magicast@0.3.5))(@vue/compiler-core@3.5.19)(esbuild@0.25.9)(typescript@5.9.2)(vue-tsc@2.2.12(typescript@5.9.2))(vue@3.5.19(typescript@5.9.2)) @@ -38,13 +38,13 @@ importers: version: 0.6.2(magicast@0.3.5) eslint: specifier: ^9.26.0 - version: 9.33.0(jiti@2.5.1) + version: 9.34.0(jiti@2.5.1) globby: specifier: ^14.1.0 version: 14.1.0 nuxt: specifier: ^4.0.3 - version: 4.0.3(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@24.3.0)(@vue/compiler-sfc@3.5.19)(db0@0.3.2)(eslint@9.33.0(jiti@2.5.1))(ioredis@5.7.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.47.1)(terser@5.43.1)(typescript@5.9.2)(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@2.2.12(typescript@5.9.2))(yaml@2.8.1) + version: 4.0.3(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@24.3.0)(@vue/compiler-sfc@3.5.19)(db0@0.3.2)(eslint@9.34.0(jiti@2.5.1))(ioredis@5.7.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.47.1)(terser@5.43.1)(typescript@5.9.2)(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@2.2.12(typescript@5.9.2))(yaml@2.8.1) typescript: specifier: ~5.9.2 version: 5.9.2 @@ -572,8 +572,8 @@ packages: resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.33.0': - resolution: {integrity: sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==} + '@eslint/js@9.34.0': + resolution: {integrity: sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.6': @@ -1974,8 +1974,8 @@ packages: caniuse-lite@1.0.30001737: resolution: {integrity: sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==} - chai@5.3.2: - resolution: {integrity: sha512-kx7GHSOBiiIQ+DDgMP6YMtYkb/3Usm2nUYblNEM9P+/OfkuP7OjfoDlq/DCe1OU0GsREUa0rNAxZmzxgO6+jWg==} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} chalk@4.1.2: @@ -2174,8 +2174,8 @@ packages: engines: {node: '>=4'} hasBin: true - cssnano-preset-default@7.0.8: - resolution: {integrity: sha512-d+3R2qwrUV3g4LEMOjnndognKirBZISylDZAF/TPeCWVjEwlXS2e4eN4ICkoobRe7pD3H6lltinKVyS1AJhdjQ==} + cssnano-preset-default@7.0.9: + resolution: {integrity: sha512-tCD6AAFgYBOVpMBX41KjbvRh9c2uUjLXRyV7KHSIrwHiq5Z9o0TFfUCoM3TwVrRsRteN3sVXGNvjVNxYzkpTsA==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.32 @@ -2186,8 +2186,8 @@ packages: peerDependencies: postcss: ^8.4.32 - cssnano@7.1.0: - resolution: {integrity: sha512-Pu3rlKkd0ZtlCUzBrKL1Z4YmhKppjC1H9jo7u1o4qaKqyhvixFgu5qLyNIAOjSTg9DjVPtUqdROq2EfpVMEe+w==} + cssnano@7.1.1: + resolution: {integrity: sha512-fm4D8ti0dQmFPeF8DXSAA//btEmqCOgAc/9Oa3C1LW94h5usNrJEfrON7b4FkPZgnDEn6OUs5NdxiJZmAtGOpQ==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.32 @@ -2558,8 +2558,8 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.33.0: - resolution: {integrity: sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==} + eslint@9.34.0: + resolution: {integrity: sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -3318,8 +3318,8 @@ packages: vue-tsc: optional: true - mlly@1.7.4: - resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} mocked-exports@0.1.1: resolution: {integrity: sha512-aF7yRQr/Q0O2/4pIXm6PZ5G+jAd7QS4Yu8m+WEeEHGnbo+7mE36CbLSDQiXYV8bVL3NfmdeqPJct0tUlnjVSnA==} @@ -3673,8 +3673,8 @@ packages: peerDependencies: postcss: ^8.4.32 - postcss-convert-values@7.0.6: - resolution: {integrity: sha512-MD/eb39Mr60hvgrqpXsgbiqluawYg/8K4nKsqRsuDX9f+xN1j6awZCUv/5tLH8ak3vYp/EMXwdcnXvfZYiejCQ==} + postcss-convert-values@7.0.7: + resolution: {integrity: sha512-HR9DZLN04Xbe6xugRH6lS4ZQH2zm/bFh/ZyRkpedZozhvh+awAfbA0P36InO4fZfDhvYfNJeNvlTf1sjwGbw/A==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.32 @@ -5239,16 +5239,16 @@ snapshots: '@esbuild/win32-x64@0.25.9': optional: true - '@eslint-community/eslint-utils@4.7.0(eslint@9.33.0(jiti@2.5.1))': + '@eslint-community/eslint-utils@4.7.0(eslint@9.34.0(jiti@2.5.1))': dependencies: - eslint: 9.33.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.5.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} - '@eslint/compat@1.3.2(eslint@9.33.0(jiti@2.5.1))': + '@eslint/compat@1.3.2(eslint@9.34.0(jiti@2.5.1))': optionalDependencies: - eslint: 9.33.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.5.1) '@eslint/config-array@0.21.0': dependencies: @@ -5260,7 +5260,7 @@ snapshots: '@eslint/config-helpers@0.3.1': {} - '@eslint/config-inspector@1.2.0(eslint@9.33.0(jiti@2.5.1))': + '@eslint/config-inspector@1.2.0(eslint@9.34.0(jiti@2.5.1))': dependencies: '@nodelib/fs.walk': 3.0.1 ansis: 4.1.0 @@ -5269,11 +5269,11 @@ snapshots: chokidar: 4.0.3 debug: 4.4.1 esbuild: 0.25.9 - eslint: 9.33.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.5.1) find-up: 7.0.0 get-port-please: 3.2.0 h3: 1.15.4 - mlly: 1.7.4 + mlly: 1.8.0 mrmime: 2.0.1 open: 10.2.0 tinyglobby: 0.2.14 @@ -5301,7 +5301,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.33.0': {} + '@eslint/js@9.34.0': {} '@eslint/object-schema@2.1.6': {} @@ -5610,30 +5610,30 @@ snapshots: - utf-8-validate - vue - '@nuxt/eslint-config@1.9.0(@typescript-eslint/utils@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(@vue/compiler-sfc@3.5.19)(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)': + '@nuxt/eslint-config@1.9.0(@typescript-eslint/utils@8.40.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(@vue/compiler-sfc@3.5.19)(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2)': dependencies: '@antfu/install-pkg': 1.1.0 '@clack/prompts': 0.11.0 - '@eslint/js': 9.33.0 - '@nuxt/eslint-plugin': 1.9.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) - '@stylistic/eslint-plugin': 5.2.3(eslint@9.33.0(jiti@2.5.1)) - '@typescript-eslint/eslint-plugin': 8.40.0(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) - '@typescript-eslint/parser': 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) - eslint: 9.33.0(jiti@2.5.1) - eslint-config-flat-gitignore: 2.1.0(eslint@9.33.0(jiti@2.5.1)) + '@eslint/js': 9.34.0 + '@nuxt/eslint-plugin': 1.9.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + '@stylistic/eslint-plugin': 5.2.3(eslint@9.34.0(jiti@2.5.1)) + '@typescript-eslint/eslint-plugin': 8.40.0(@typescript-eslint/parser@8.40.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/parser': 8.40.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + eslint: 9.34.0(jiti@2.5.1) + eslint-config-flat-gitignore: 2.1.0(eslint@9.34.0(jiti@2.5.1)) eslint-flat-config-utils: 2.1.1 - eslint-merge-processors: 2.0.0(eslint@9.33.0(jiti@2.5.1)) - eslint-plugin-import-lite: 0.3.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) - eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.5.1)) - eslint-plugin-jsdoc: 54.1.1(eslint@9.33.0(jiti@2.5.1)) - eslint-plugin-regexp: 2.10.0(eslint@9.33.0(jiti@2.5.1)) - eslint-plugin-unicorn: 60.0.0(eslint@9.33.0(jiti@2.5.1)) - eslint-plugin-vue: 10.4.0(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.5.1))(vue-eslint-parser@10.2.0(eslint@9.33.0(jiti@2.5.1))) - eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.19)(eslint@9.33.0(jiti@2.5.1)) + eslint-merge-processors: 2.0.0(eslint@9.34.0(jiti@2.5.1)) + eslint-plugin-import-lite: 0.3.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.40.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)) + eslint-plugin-jsdoc: 54.1.1(eslint@9.34.0(jiti@2.5.1)) + eslint-plugin-regexp: 2.10.0(eslint@9.34.0(jiti@2.5.1)) + eslint-plugin-unicorn: 60.0.0(eslint@9.34.0(jiti@2.5.1)) + eslint-plugin-vue: 10.4.0(@typescript-eslint/parser@8.40.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1))(vue-eslint-parser@10.2.0(eslint@9.34.0(jiti@2.5.1))) + eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.19)(eslint@9.34.0(jiti@2.5.1)) globals: 16.3.0 local-pkg: 1.1.2 pathe: 2.0.3 - vue-eslint-parser: 10.2.0(eslint@9.33.0(jiti@2.5.1)) + vue-eslint-parser: 10.2.0(eslint@9.34.0(jiti@2.5.1)) transitivePeerDependencies: - '@typescript-eslint/utils' - '@vue/compiler-sfc' @@ -5641,29 +5641,29 @@ snapshots: - supports-color - typescript - '@nuxt/eslint-plugin@1.9.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)': + '@nuxt/eslint-plugin@1.9.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2)': dependencies: '@typescript-eslint/types': 8.40.0 - '@typescript-eslint/utils': 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) - eslint: 9.33.0(jiti@2.5.1) + '@typescript-eslint/utils': 8.40.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + eslint: 9.34.0(jiti@2.5.1) transitivePeerDependencies: - supports-color - typescript - '@nuxt/eslint@1.9.0(@typescript-eslint/utils@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(@vue/compiler-sfc@3.5.19)(eslint@9.33.0(jiti@2.5.1))(magicast@0.3.5)(typescript@5.9.2)(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))': + '@nuxt/eslint@1.9.0(@typescript-eslint/utils@8.40.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(@vue/compiler-sfc@3.5.19)(eslint@9.34.0(jiti@2.5.1))(magicast@0.3.5)(typescript@5.9.2)(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))': dependencies: - '@eslint/config-inspector': 1.2.0(eslint@9.33.0(jiti@2.5.1)) + '@eslint/config-inspector': 1.2.0(eslint@9.34.0(jiti@2.5.1)) '@nuxt/devtools-kit': 2.6.3(magicast@0.3.5)(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)) - '@nuxt/eslint-config': 1.9.0(@typescript-eslint/utils@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(@vue/compiler-sfc@3.5.19)(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) - '@nuxt/eslint-plugin': 1.9.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) + '@nuxt/eslint-config': 1.9.0(@typescript-eslint/utils@8.40.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(@vue/compiler-sfc@3.5.19)(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + '@nuxt/eslint-plugin': 1.9.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) '@nuxt/kit': 4.0.3(magicast@0.3.5) chokidar: 4.0.3 - eslint: 9.33.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.5.1) eslint-flat-config-utils: 2.1.1 - eslint-typegen: 2.3.0(eslint@9.33.0(jiti@2.5.1)) + eslint-typegen: 2.3.0(eslint@9.34.0(jiti@2.5.1)) find-up: 7.0.0 get-port-please: 3.2.0 - mlly: 1.7.4 + mlly: 1.8.0 pathe: 2.0.3 unimport: 5.2.0 transitivePeerDependencies: @@ -5690,7 +5690,7 @@ snapshots: jiti: 2.5.1 klona: 2.0.6 knitwork: 1.2.0 - mlly: 1.7.4 + mlly: 1.8.0 ohash: 2.0.11 pathe: 2.0.3 pkg-types: 2.3.0 @@ -5716,7 +5716,7 @@ snapshots: ignore: 7.0.5 jiti: 2.5.1 klona: 2.0.6 - mlly: 1.7.4 + mlly: 1.8.0 ohash: 2.0.11 pathe: 2.0.3 pkg-types: 2.3.0 @@ -5740,7 +5740,7 @@ snapshots: jiti: 2.5.1 magic-regexp: 0.10.0 mkdist: 2.3.0(typescript@5.9.2)(vue-sfc-transformer@0.1.16(@vue/compiler-core@3.5.19)(esbuild@0.25.9)(vue@3.5.19(typescript@5.9.2)))(vue-tsc@2.2.12(typescript@5.9.2))(vue@3.5.19(typescript@5.9.2)) - mlly: 1.7.4 + mlly: 1.8.0 pathe: 2.0.3 pkg-types: 2.3.0 tsconfck: 3.1.6(typescript@5.9.2) @@ -5812,7 +5812,7 @@ snapshots: - magicast - typescript - '@nuxt/vite-builder@4.0.3(@types/node@24.3.0)(eslint@9.33.0(jiti@2.5.1))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.47.1)(terser@5.43.1)(typescript@5.9.2)(vue-tsc@2.2.12(typescript@5.9.2))(vue@3.5.19(typescript@5.9.2))(yaml@2.8.1)': + '@nuxt/vite-builder@4.0.3(@types/node@24.3.0)(eslint@9.34.0(jiti@2.5.1))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.47.1)(terser@5.43.1)(typescript@5.9.2)(vue-tsc@2.2.12(typescript@5.9.2))(vue@3.5.19(typescript@5.9.2))(yaml@2.8.1)': dependencies: '@nuxt/kit': 4.0.3(magicast@0.3.5) '@rollup/plugin-replace': 6.0.2(rollup@4.47.1) @@ -5820,7 +5820,7 @@ snapshots: '@vitejs/plugin-vue-jsx': 5.0.1(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.19(typescript@5.9.2)) autoprefixer: 10.4.21(postcss@8.5.6) consola: 3.4.2 - cssnano: 7.1.0(postcss@8.5.6) + cssnano: 7.1.1(postcss@8.5.6) defu: 6.1.4 esbuild: 0.25.9 escape-string-regexp: 5.0.0 @@ -5830,7 +5830,7 @@ snapshots: jiti: 2.5.1 knitwork: 1.2.0 magic-string: 0.30.18 - mlly: 1.7.4 + mlly: 1.8.0 mocked-exports: 0.1.1 pathe: 2.0.3 pkg-types: 2.3.0 @@ -5841,7 +5841,7 @@ snapshots: unenv: 2.0.0-rc.19 vite: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1) vite-node: 3.2.4(@types/node@24.3.0)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1) - vite-plugin-checker: 0.10.2(eslint@9.33.0(jiti@2.5.1))(optionator@0.9.4)(typescript@5.9.2)(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@2.2.12(typescript@5.9.2)) + vite-plugin-checker: 0.10.2(eslint@9.34.0(jiti@2.5.1))(optionator@0.9.4)(typescript@5.9.2)(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@2.2.12(typescript@5.9.2)) vue: 3.5.19(typescript@5.9.2) vue-bundle-renderer: 2.1.2 transitivePeerDependencies: @@ -6227,11 +6227,11 @@ snapshots: '@speed-highlight/core@1.2.7': {} - '@stylistic/eslint-plugin@5.2.3(eslint@9.33.0(jiti@2.5.1))': + '@stylistic/eslint-plugin@5.2.3(eslint@9.34.0(jiti@2.5.1))': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.33.0(jiti@2.5.1)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.34.0(jiti@2.5.1)) '@typescript-eslint/types': 8.40.0 - eslint: 9.33.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.5.1) eslint-visitor-keys: 4.2.1 espree: 10.4.0 estraverse: 5.3.0 @@ -6271,15 +6271,15 @@ snapshots: '@types/node': 24.3.0 optional: true - '@typescript-eslint/eslint-plugin@8.40.0(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)': + '@typescript-eslint/eslint-plugin@8.40.0(@typescript-eslint/parser@8.40.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/parser': 8.40.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) '@typescript-eslint/scope-manager': 8.40.0 - '@typescript-eslint/type-utils': 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) - '@typescript-eslint/utils': 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/type-utils': 8.40.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/utils': 8.40.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) '@typescript-eslint/visitor-keys': 8.40.0 - eslint: 9.33.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.5.1) graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -6288,14 +6288,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)': + '@typescript-eslint/parser@8.40.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2)': dependencies: '@typescript-eslint/scope-manager': 8.40.0 '@typescript-eslint/types': 8.40.0 '@typescript-eslint/typescript-estree': 8.40.0(typescript@5.9.2) '@typescript-eslint/visitor-keys': 8.40.0 debug: 4.4.1 - eslint: 9.33.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.5.1) typescript: 5.9.2 transitivePeerDependencies: - supports-color @@ -6318,13 +6318,13 @@ snapshots: dependencies: typescript: 5.9.2 - '@typescript-eslint/type-utils@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)': + '@typescript-eslint/type-utils@8.40.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2)': dependencies: '@typescript-eslint/types': 8.40.0 '@typescript-eslint/typescript-estree': 8.40.0(typescript@5.9.2) - '@typescript-eslint/utils': 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/utils': 8.40.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) debug: 4.4.1 - eslint: 9.33.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.5.1) ts-api-utils: 2.1.0(typescript@5.9.2) typescript: 5.9.2 transitivePeerDependencies: @@ -6348,13 +6348,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)': + '@typescript-eslint/utils@8.40.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.33.0(jiti@2.5.1)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.34.0(jiti@2.5.1)) '@typescript-eslint/scope-manager': 8.40.0 '@typescript-eslint/types': 8.40.0 '@typescript-eslint/typescript-estree': 8.40.0(typescript@5.9.2) - eslint: 9.33.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.5.1) typescript: 5.9.2 transitivePeerDependencies: - supports-color @@ -6470,7 +6470,7 @@ snapshots: '@types/chai': 5.2.2 '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 - chai: 5.3.2 + chai: 5.3.3 tinyrainbow: 2.0.0 '@vitest/mocker@3.2.4(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))': @@ -6906,7 +6906,7 @@ snapshots: caniuse-lite@1.0.30001737: {} - chai@5.3.2: + chai@5.3.3: dependencies: assertion-error: 2.0.1 check-error: 2.1.1 @@ -7106,7 +7106,7 @@ snapshots: cssesc@3.0.0: {} - cssnano-preset-default@7.0.8(postcss@8.5.6): + cssnano-preset-default@7.0.9(postcss@8.5.6): dependencies: browserslist: 4.25.3 css-declaration-sorter: 7.2.0(postcss@8.5.6) @@ -7114,7 +7114,7 @@ snapshots: postcss: 8.5.6 postcss-calc: 10.1.1(postcss@8.5.6) postcss-colormin: 7.0.4(postcss@8.5.6) - postcss-convert-values: 7.0.6(postcss@8.5.6) + postcss-convert-values: 7.0.7(postcss@8.5.6) postcss-discard-comments: 7.0.4(postcss@8.5.6) postcss-discard-duplicates: 7.0.2(postcss@8.5.6) postcss-discard-empty: 7.0.1(postcss@8.5.6) @@ -7144,9 +7144,9 @@ snapshots: dependencies: postcss: 8.5.6 - cssnano@7.1.0(postcss@8.5.6): + cssnano@7.1.1(postcss@8.5.6): dependencies: - cssnano-preset-default: 7.0.8(postcss@8.5.6) + cssnano-preset-default: 7.0.9(postcss@8.5.6) lilconfig: 3.1.3 postcss: 8.5.6 @@ -7404,10 +7404,10 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-config-flat-gitignore@2.1.0(eslint@9.33.0(jiti@2.5.1)): + eslint-config-flat-gitignore@2.1.0(eslint@9.34.0(jiti@2.5.1)): dependencies: - '@eslint/compat': 1.3.2(eslint@9.33.0(jiti@2.5.1)) - eslint: 9.33.0(jiti@2.5.1) + '@eslint/compat': 1.3.2(eslint@9.34.0(jiti@2.5.1)) + eslint: 9.34.0(jiti@2.5.1) eslint-flat-config-utils@2.1.1: dependencies: @@ -7420,24 +7420,24 @@ snapshots: optionalDependencies: unrs-resolver: 1.11.1 - eslint-merge-processors@2.0.0(eslint@9.33.0(jiti@2.5.1)): + eslint-merge-processors@2.0.0(eslint@9.34.0(jiti@2.5.1)): dependencies: - eslint: 9.33.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.5.1) - eslint-plugin-import-lite@0.3.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2): + eslint-plugin-import-lite@0.3.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.33.0(jiti@2.5.1)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.34.0(jiti@2.5.1)) '@typescript-eslint/types': 8.40.0 - eslint: 9.33.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.5.1) optionalDependencies: typescript: 5.9.2 - eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.5.1)): + eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.40.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)): dependencies: '@typescript-eslint/types': 8.40.0 comment-parser: 1.4.1 debug: 4.4.1 - eslint: 9.33.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.5.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) is-glob: 4.0.3 minimatch: 10.0.3 @@ -7445,18 +7445,18 @@ snapshots: stable-hash-x: 0.2.0 unrs-resolver: 1.11.1 optionalDependencies: - '@typescript-eslint/utils': 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/utils': 8.40.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) transitivePeerDependencies: - supports-color - eslint-plugin-jsdoc@54.1.1(eslint@9.33.0(jiti@2.5.1)): + eslint-plugin-jsdoc@54.1.1(eslint@9.34.0(jiti@2.5.1)): dependencies: '@es-joy/jsdoccomment': 0.53.0 are-docs-informative: 0.0.2 comment-parser: 1.4.1 debug: 4.4.1 escape-string-regexp: 4.0.0 - eslint: 9.33.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.5.1) espree: 10.4.0 esquery: 1.6.0 parse-imports-exports: 0.2.4 @@ -7465,27 +7465,27 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-regexp@2.10.0(eslint@9.33.0(jiti@2.5.1)): + eslint-plugin-regexp@2.10.0(eslint@9.34.0(jiti@2.5.1)): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.33.0(jiti@2.5.1)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.34.0(jiti@2.5.1)) '@eslint-community/regexpp': 4.12.1 comment-parser: 1.4.1 - eslint: 9.33.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.5.1) jsdoc-type-pratt-parser: 4.8.0 refa: 0.12.1 regexp-ast-analysis: 0.7.1 scslre: 0.3.0 - eslint-plugin-unicorn@60.0.0(eslint@9.33.0(jiti@2.5.1)): + eslint-plugin-unicorn@60.0.0(eslint@9.34.0(jiti@2.5.1)): dependencies: '@babel/helper-validator-identifier': 7.27.1 - '@eslint-community/eslint-utils': 4.7.0(eslint@9.33.0(jiti@2.5.1)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.34.0(jiti@2.5.1)) '@eslint/plugin-kit': 0.3.5 change-case: 5.4.4 ci-info: 4.3.0 clean-regexp: 1.0.0 core-js-compat: 3.45.1 - eslint: 9.33.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.5.1) esquery: 1.6.0 find-up-simple: 1.0.1 globals: 16.3.0 @@ -7498,32 +7498,32 @@ snapshots: semver: 7.7.2 strip-indent: 4.0.0 - eslint-plugin-vue@10.4.0(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.5.1))(vue-eslint-parser@10.2.0(eslint@9.33.0(jiti@2.5.1))): + eslint-plugin-vue@10.4.0(@typescript-eslint/parser@8.40.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1))(vue-eslint-parser@10.2.0(eslint@9.34.0(jiti@2.5.1))): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.33.0(jiti@2.5.1)) - eslint: 9.33.0(jiti@2.5.1) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.34.0(jiti@2.5.1)) + eslint: 9.34.0(jiti@2.5.1) natural-compare: 1.4.0 nth-check: 2.1.1 postcss-selector-parser: 6.1.2 semver: 7.7.2 - vue-eslint-parser: 10.2.0(eslint@9.33.0(jiti@2.5.1)) + vue-eslint-parser: 10.2.0(eslint@9.34.0(jiti@2.5.1)) xml-name-validator: 4.0.0 optionalDependencies: - '@typescript-eslint/parser': 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/parser': 8.40.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) - eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.19)(eslint@9.33.0(jiti@2.5.1)): + eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.19)(eslint@9.34.0(jiti@2.5.1)): dependencies: '@vue/compiler-sfc': 3.5.19 - eslint: 9.33.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.5.1) eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 - eslint-typegen@2.3.0(eslint@9.33.0(jiti@2.5.1)): + eslint-typegen@2.3.0(eslint@9.34.0(jiti@2.5.1)): dependencies: - eslint: 9.33.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.5.1) json-schema-to-typescript-lite: 15.0.0 ohash: 2.0.11 @@ -7531,15 +7531,15 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.33.0(jiti@2.5.1): + eslint@9.34.0(jiti@2.5.1): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.33.0(jiti@2.5.1)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.34.0(jiti@2.5.1)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.21.0 '@eslint/config-helpers': 0.3.1 '@eslint/core': 0.15.2 '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.33.0 + '@eslint/js': 9.34.0 '@eslint/plugin-kit': 0.3.5 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 @@ -7698,7 +7698,7 @@ snapshots: fix-dts-default-cjs-exports@1.0.1: dependencies: magic-string: 0.30.18 - mlly: 1.7.4 + mlly: 1.8.0 rollup: 4.47.1 flat-cache@4.0.1: @@ -8102,7 +8102,7 @@ snapshots: h3: 1.15.4 http-shutdown: 1.2.2 jiti: 2.5.1 - mlly: 1.7.4 + mlly: 1.8.0 node-forge: 1.3.1 pathe: 1.1.2 std-env: 3.9.0 @@ -8114,7 +8114,7 @@ snapshots: local-pkg@1.1.2: dependencies: - mlly: 1.7.4 + mlly: 1.8.0 pkg-types: 2.3.0 quansync: 0.2.11 @@ -8165,7 +8165,7 @@ snapshots: dependencies: estree-walker: 3.0.3 magic-string: 0.30.18 - mlly: 1.7.4 + mlly: 1.8.0 regexp-tree: 0.1.27 type-level-regexp: 0.1.17 ufo: 1.6.1 @@ -8252,11 +8252,11 @@ snapshots: dependencies: autoprefixer: 10.4.21(postcss@8.5.6) citty: 0.1.6 - cssnano: 7.1.0(postcss@8.5.6) + cssnano: 7.1.1(postcss@8.5.6) defu: 6.1.4 esbuild: 0.25.9 jiti: 1.21.7 - mlly: 1.7.4 + mlly: 1.8.0 pathe: 2.0.3 pkg-types: 2.3.0 postcss: 8.5.6 @@ -8269,7 +8269,7 @@ snapshots: vue-sfc-transformer: 0.1.16(@vue/compiler-core@3.5.19)(esbuild@0.25.9)(vue@3.5.19(typescript@5.9.2)) vue-tsc: 2.2.12(typescript@5.9.2) - mlly@1.7.4: + mlly@1.8.0: dependencies: acorn: 8.15.0 pathe: 2.0.3 @@ -8353,7 +8353,7 @@ snapshots: magic-string: 0.30.18 magicast: 0.3.5 mime: 4.0.7 - mlly: 1.7.4 + mlly: 1.8.0 node-fetch-native: 1.6.7 node-mock-http: 1.0.2 ofetch: 1.4.1 @@ -8469,7 +8469,7 @@ snapshots: dependencies: boolbase: 1.0.0 - nuxt@4.0.3(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@24.3.0)(@vue/compiler-sfc@3.5.19)(db0@0.3.2)(eslint@9.33.0(jiti@2.5.1))(ioredis@5.7.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.47.1)(terser@5.43.1)(typescript@5.9.2)(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@2.2.12(typescript@5.9.2))(yaml@2.8.1): + nuxt@4.0.3(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@24.3.0)(@vue/compiler-sfc@3.5.19)(db0@0.3.2)(eslint@9.34.0(jiti@2.5.1))(ioredis@5.7.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.47.1)(terser@5.43.1)(typescript@5.9.2)(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@2.2.12(typescript@5.9.2))(yaml@2.8.1): dependencies: '@nuxt/cli': 3.28.0(magicast@0.3.5) '@nuxt/devalue': 2.0.2 @@ -8477,7 +8477,7 @@ snapshots: '@nuxt/kit': 4.0.3(magicast@0.3.5) '@nuxt/schema': 4.0.3 '@nuxt/telemetry': 2.6.6(magicast@0.3.5) - '@nuxt/vite-builder': 4.0.3(@types/node@24.3.0)(eslint@9.33.0(jiti@2.5.1))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.47.1)(terser@5.43.1)(typescript@5.9.2)(vue-tsc@2.2.12(typescript@5.9.2))(vue@3.5.19(typescript@5.9.2))(yaml@2.8.1) + '@nuxt/vite-builder': 4.0.3(@types/node@24.3.0)(eslint@9.34.0(jiti@2.5.1))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.47.1)(terser@5.43.1)(typescript@5.9.2)(vue-tsc@2.2.12(typescript@5.9.2))(vue@3.5.19(typescript@5.9.2))(yaml@2.8.1) '@unhead/vue': 2.0.14(vue@3.5.19(typescript@5.9.2)) '@vue/shared': 3.5.19 c12: 3.2.0(magicast@0.3.5) @@ -8501,7 +8501,7 @@ snapshots: klona: 2.0.6 knitwork: 1.2.0 magic-string: 0.30.18 - mlly: 1.7.4 + mlly: 1.8.0 mocked-exports: 0.1.1 nanotar: 0.2.0 nitropack: 2.12.4(@netlify/blobs@9.1.2) @@ -8811,7 +8811,7 @@ snapshots: pkg-types@1.3.1: dependencies: confbox: 0.1.8 - mlly: 1.7.4 + mlly: 1.8.0 pathe: 2.0.3 pkg-types@2.3.0: @@ -8836,7 +8836,7 @@ snapshots: postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-convert-values@7.0.6(postcss@8.5.6): + postcss-convert-values@7.0.7(postcss@8.5.6): dependencies: browserslist: 4.25.3 postcss: 8.5.6 @@ -9562,7 +9562,7 @@ snapshots: jiti: 2.5.1 magic-string: 0.30.18 mkdist: 2.3.0(typescript@5.9.2)(vue-sfc-transformer@0.1.16(@vue/compiler-core@3.5.19)(esbuild@0.25.9)(vue@3.5.19(typescript@5.9.2)))(vue-tsc@2.2.12(typescript@5.9.2))(vue@3.5.19(typescript@5.9.2)) - mlly: 1.7.4 + mlly: 1.8.0 pathe: 2.0.3 pkg-types: 2.3.0 pretty-bytes: 7.0.1 @@ -9613,7 +9613,7 @@ snapshots: estree-walker: 3.0.3 local-pkg: 1.1.2 magic-string: 0.30.18 - mlly: 1.7.4 + mlly: 1.8.0 pathe: 2.0.3 picomatch: 4.0.3 pkg-types: 2.3.0 @@ -9642,7 +9642,7 @@ snapshots: json5: 2.2.3 local-pkg: 1.1.2 magic-string: 0.30.18 - mlly: 1.7.4 + mlly: 1.8.0 muggle-string: 0.4.1 pathe: 2.0.3 picomatch: 4.0.3 @@ -9721,7 +9721,7 @@ snapshots: dependencies: knitwork: 1.2.0 magic-string: 0.30.18 - mlly: 1.7.4 + mlly: 1.8.0 pathe: 2.0.3 pkg-types: 2.3.0 unplugin: 2.3.8 @@ -9782,7 +9782,7 @@ snapshots: - tsx - yaml - vite-plugin-checker@0.10.2(eslint@9.33.0(jiti@2.5.1))(optionator@0.9.4)(typescript@5.9.2)(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@2.2.12(typescript@5.9.2)): + vite-plugin-checker@0.10.2(eslint@9.34.0(jiti@2.5.1))(optionator@0.9.4)(typescript@5.9.2)(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@2.2.12(typescript@5.9.2)): dependencies: '@babel/code-frame': 7.27.1 chokidar: 4.0.3 @@ -9795,7 +9795,7 @@ snapshots: vite: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1) vscode-uri: 3.1.0 optionalDependencies: - eslint: 9.33.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.5.1) optionator: 0.9.4 typescript: 5.9.2 vue-tsc: 2.2.12(typescript@5.9.2) @@ -9869,7 +9869,7 @@ snapshots: '@vitest/snapshot': 3.2.4 '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 - chai: 5.3.2 + chai: 5.3.3 debug: 4.4.1 expect-type: 1.2.2 magic-string: 0.30.18 @@ -9908,10 +9908,10 @@ snapshots: vue-devtools-stub@0.1.0: {} - vue-eslint-parser@10.2.0(eslint@9.33.0(jiti@2.5.1)): + vue-eslint-parser@10.2.0(eslint@9.34.0(jiti@2.5.1)): dependencies: debug: 4.4.1 - eslint: 9.33.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.5.1) eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 espree: 10.4.0 diff --git a/src/runtime/composables/useFeatureFlag.ts b/src/runtime/composables/useFeatureFlag.ts index e500d40..d5065e9 100644 --- a/src/runtime/composables/useFeatureFlag.ts +++ b/src/runtime/composables/useFeatureFlag.ts @@ -1,10 +1,11 @@ import type { FeatureFlagsConfig, FeatureFlag, FeatureFlagInput } from '../../../types/feature-flags' import { isFlagActiveNow } from '../utils/isFlagActiveNow' +import { matchFlag } from '../utils/matchFlag' import { useRuntimeConfig } from '#imports' /** * Provides runtime access to feature flag utilities within the client app. - * Supports both static and scheduled flags. + * Supports static flags, scheduled flags, and hierarchical paths with `*` wildcards for grouped checks. * * @returns An object with utilities: * - `isEnabled(flagName)` — checks if a feature is currently active @@ -12,7 +13,7 @@ import { useRuntimeConfig } from '#imports' */ export const useFeatureFlag = () => { const config: FeatureFlagsConfig = useRuntimeConfig().public.featureFlags - const env = config.environment + const env: string = config.environment const flags: FeatureFlagInput[] = config.flagSets?.[env] || [] /** @@ -20,14 +21,25 @@ export const useFeatureFlag = () => { * Supports: * - Static string flags * - Scheduled flags (with `activeFrom` and/or `activeUntil`) + * - Hierarchical paths and `*` wildcards declared in the flag set + * + * Wildcard queries only return `true` if the wildcard itself is enabled + * (e.g. checking `solutions/*` requires that `solutions/*` exists in the + * current flag set). * * @param flagName - The name of the feature flag to check. * @returns `true` if the feature is currently enabled, otherwise `false`. */ const isEnabled = (flagName: string): boolean => { for (const flag of flags) { - if (typeof flag === 'string' && flag === flagName) return true - if (typeof flag === 'object' && flag.name === flagName && isFlagActiveNow(flag)) return true + const name: string = typeof flag === 'string' ? flag : flag.name + if (!matchFlag(name, flagName)) continue + if (typeof flag === 'object') { + if (isFlagActiveNow(flag)) return true + } + else { + return true + } } return false } diff --git a/src/runtime/server/isFeatureEnabled.ts b/src/runtime/server/isFeatureEnabled.ts index 901022e..ac899ee 100644 --- a/src/runtime/server/isFeatureEnabled.ts +++ b/src/runtime/server/isFeatureEnabled.ts @@ -1,14 +1,19 @@ import type { H3Event } from 'h3' import { isFlagActiveNow } from '../utils/isFlagActiveNow' +import { matchFlag } from '../utils/matchFlag' import { useRuntimeConfig } from '#imports' import type { FeatureFlagInput, FeatureFlagsConfig } from '~/types/feature-flags' /** * Server-side utility to check if a feature flag is currently enabled. - * Supports both string and scheduled flags. + * Supports string flags, scheduled flags, and wildcard groups declared in the + * flag set (e.g. `solutions/*`). * * Intended for use in server routes (`server/api/**`) or middleware. * + * Wildcard queries only return `true` if the wildcard itself is enabled in the + * active flag set. + * * @param feature - The name of the feature flag to check. * @param event - Optional H3 event context (used to access runtime config). * @returns `true` if the feature is currently enabled in the active environment. @@ -26,8 +31,15 @@ export const isFeatureEnabled = (feature: string, event?: H3Event): boolean => { const flags: FeatureFlagInput[] = config.flagSets?.[env] || [] for (const flag of flags) { - if (typeof flag === 'string' && flag === feature) return true - if (typeof flag === 'object' && flag.name === feature && isFlagActiveNow(flag)) return true + const name: string = typeof flag === 'string' ? flag : flag.name + if (!matchFlag(name, feature)) continue + + if (typeof flag === 'object') { + if (isFlagActiveNow(flag)) return true + } + else { + return true + } } return false diff --git a/src/runtime/utils/flagValidator.ts b/src/runtime/utils/flagValidator.ts index 5d267b9..1be6726 100644 --- a/src/runtime/utils/flagValidator.ts +++ b/src/runtime/utils/flagValidator.ts @@ -2,10 +2,12 @@ import { readFile } from 'node:fs/promises' import { globby } from 'globby' import { resolve as resolvePath } from 'pathe' import type { FeatureFlagsConfig } from '../../../types/feature-flags' +import { matchFlag } from './matchFlag' /** * Validates that all feature flags used in the source code are declared * in at least one environment of the FeatureFlagsConfig. + * Handles hierarchical flag paths and `*` wildcards, allowing patterns such as `solutions/*` in both declarations and usage. * * Supports customizing: * - `validation.mode` (disabled | warn | error) @@ -56,20 +58,20 @@ export async function validateFeatureFlags( // 4. Prepare regex patterns (allow letters, digits, underscores, hyphens, dots) const regexes: RegExp[] = [ // Matches: v-feature="flagName", v-feature='flagName', v-feature="'flagName'" - /v-feature\s*=\s*["']\s*'?([\w.-]+)'?\s*["']/g, + /v-feature\s*=\s*["']\s*'?([\w./*-]+)'?\s*["']/g, // Matches: isEnabled('flagName') or isEnabled("flagName") - /\bisEnabled\(\s*['"]([\w.-]+)['"]\s*\)/g, + /\bisEnabled\(\s*['"]([\w./*-]+)['"]\s*\)/g, // Matches: defineFeatureFlagMiddleware('flagName') or defineFeatureFlagMiddleware("flagName") - /\bdefineFeatureFlagMiddleware\(\s*['"]([\w.-]+)['"]\s*\)/g, + /\bdefineFeatureFlagMiddleware\(\s*['"]([\w./*-]+)['"]\s*\)/g, ] // 5. Read & scan each file for literal flag usage interface FileContext { path: string, content: string } const contexts: FileContext[] = [] - await Promise.all(allFiles.map(async (relativePath) => { + await Promise.all(allFiles.map(async (relativePath): Promise => { const absolutePath: string = resolvePath(rootDir, relativePath) try { const content: string = await readFile(absolutePath, 'utf-8') @@ -91,7 +93,11 @@ export async function validateFeatureFlags( for (const regex of regexes) { let match: RegExpExecArray | null while ((match = regex.exec(content)) !== null) { - const flagName: string = match[1] // capture group 1 is always the flag + const captured: string | undefined = match[1] + if (typeof captured !== 'string' || captured.length === 0) { + continue + } + const flagName: string = captured // capture group 1 is always the flag // Calculate line/column from match.index const beforeMatch: string = content.slice(0, match.index) const lineNumber: number = beforeMatch.split('\n').length @@ -111,17 +117,21 @@ export async function validateFeatureFlags( } // 7. Build Set of all declared flags (across all flags in all flagSets) - const declaredFlags: Set = new Set() - for (const envFlags of Object.values(options.flagSets)) { + const declaredFlags: string[] = [] + for (const [env, envFlags] of Object.entries(options.flagSets)) { for (const item of envFlags || []) { const name: string = typeof item === 'string' ? item : item.name - declaredFlags.add(name) + declaredFlags.push(name) + if (name === '*') { + console.warn(`[nuxt-feature-flags] Environment "${env}" uses "*" which enables all flags and should only be used for debugging.`) + } } } // 8. Compare used vs. declared, emit warnings/errors with context for (const info of usedFlagsInfo) { - if (!declaredFlags.has(info.name)) { + const declared: boolean = declaredFlags.some(pattern => matchFlag(pattern, info.name)) + if (!declared) { const location: string = `${info.file}:${info.line}:${info.column}` const message: string = `[nuxt-feature-flags] ${location} → Flag "${info.name}" ` diff --git a/src/runtime/utils/matchFlag.ts b/src/runtime/utils/matchFlag.ts new file mode 100644 index 0000000..49dc50e --- /dev/null +++ b/src/runtime/utils/matchFlag.ts @@ -0,0 +1,24 @@ +/** + * Checks whether a feature flag name matches a given pattern. + * + * Both `pattern` and `value` support hierarchical segments separated by `/`. + * Only the `pattern` may end in a trailing `*` to match any descendant nodes + * (e.g. `solutions/*`). The `value` is always treated as a concrete flag name. + * + * @param pattern - The flag or wildcard pattern to test against. + * @param value - The concrete flag name being evaluated. + * @returns `true` if the pattern matches the value, otherwise `false`. + */ +export const matchFlag = (pattern: string, value: string): boolean => { + if (pattern === '*' || value === '*') return true + + const patternIsWildcard: boolean = pattern.endsWith('/*') + + const patternBase: string = patternIsWildcard ? pattern.slice(0, -1) : pattern + + if (patternIsWildcard) { + return value.startsWith(patternBase) + } + + return pattern === value +} diff --git a/test/unit/flagValidator.test.ts b/test/unit/flagValidator.test.ts index 1660833..b515d8f 100644 --- a/test/unit/flagValidator.test.ts +++ b/test/unit/flagValidator.test.ts @@ -6,17 +6,17 @@ import { validateFeatureFlags } from '../../src/runtime/utils/flagValidator' let dir: string -beforeEach(async () => { +beforeEach(async (): Promise => { dir = await mkdtemp(join(tmpdir(), 'ff-')) }) -afterEach(async () => { +afterEach(async (): Promise => { // cleanup automatically by tmpdir removal on container end }) -describe('flagValidator', () => { - it('warns on missing flags in warn mode', async () => { - const file = join(dir, 'test.ts') +describe('flagValidator', (): void => { + it('warns on missing flags in warn mode', async (): Promise => { + const file: string = join(dir, 'test.ts') await writeFile(file, 'isEnabled(\'missing\')') const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) await validateFeatureFlags({ @@ -28,8 +28,8 @@ describe('flagValidator', () => { warnSpy.mockRestore() }) - it('throws on missing flags in error mode', async () => { - const file = join(dir, 'test2.ts') + it('throws on missing flags in error mode', async (): Promise => { + const file: string = join(dir, 'test2.ts') await writeFile(file, 'isEnabled(\'missing\')') await expect(validateFeatureFlags({ environment: 'prod', @@ -37,4 +37,28 @@ describe('flagValidator', () => { validation: { mode: 'error', includeGlobs: ['**/*.ts'], excludeGlobs: [] }, }, dir)).rejects.toThrow() }) + + it('supports wildcard declarations', async (): Promise => { + const file: string = join(dir, 'wild.ts') + await writeFile(file, 'isEnabled(\'solutions/company-portal/addons/sales\')') + const warnSpy = vi.spyOn(console, 'warn').mockImplementation((): void => {}) + await validateFeatureFlags({ + environment: 'prod', + flagSets: { prod: ['solutions/*'] }, + validation: { mode: 'warn', includeGlobs: ['**/*.ts'], excludeGlobs: [] }, + }, dir) + expect(warnSpy).not.toHaveBeenCalled() + warnSpy.mockRestore() + }) + + it('warns when root wildcard is used', async (): Promise => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation((): void => {}) + await validateFeatureFlags({ + environment: 'prod', + flagSets: { prod: ['*'] }, + validation: { mode: 'warn', includeGlobs: ['**/*.ts'], excludeGlobs: [] }, + }, dir) + expect(warnSpy).toHaveBeenCalled() + warnSpy.mockRestore() + }) }) diff --git a/test/unit/isFeatureEnabled.test.ts b/test/unit/isFeatureEnabled.test.ts index 443be0c..89f7bd3 100644 --- a/test/unit/isFeatureEnabled.test.ts +++ b/test/unit/isFeatureEnabled.test.ts @@ -12,7 +12,7 @@ vi.mock('#imports', () => ({ useRuntimeConfig: () => runtimeConfig, })) -beforeEach(() => { +beforeEach((): void => { runtimeConfig = { featureFlags: { environment: 'prod', @@ -23,15 +23,15 @@ beforeEach(() => { } }) -describe('isFeatureEnabled', () => { - it('checks simple string flags', () => { +describe('isFeatureEnabled', (): void => { + it('checks simple string flags', (): void => { runtimeConfig.featureFlags.flagSets.prod = ['flagA'] expect(isFeatureEnabled('flagA')).toBe(true) expect(isFeatureEnabled('unknown')).toBe(false) }) - it('evaluates scheduled flags', () => { - const now = new Date('2024-06-01T12:00:00Z') + it('evaluates scheduled flags', (): void => { + const now: Date = new Date('2024-06-01T12:00:00Z') vi.useFakeTimers() vi.setSystemTime(now) @@ -43,8 +43,8 @@ describe('isFeatureEnabled', () => { vi.useRealTimers() }) - it('returns false when flag is inactive', () => { - const now = new Date('2024-08-01T12:00:00Z') + it('returns false when flag is inactive', (): void => { + const now: Date = new Date('2024-08-01T12:00:00Z') vi.useFakeTimers() vi.setSystemTime(now) @@ -55,4 +55,16 @@ describe('isFeatureEnabled', () => { expect(isFeatureEnabled('scheduled')).toBe(false) vi.useRealTimers() }) + + it('supports hierarchical wildcard patterns', (): void => { + runtimeConfig.featureFlags.flagSets.prod = ['solutions/*'] + expect(isFeatureEnabled('solutions/company-portal/addons/sales')).toBe(true) + expect(isFeatureEnabled('solutions/*')).toBe(true) + + runtimeConfig.featureFlags.flagSets.prod = ['solutions/company-portal/addons/sales'] + expect(isFeatureEnabled('solutions/*')).toBe(false) + + runtimeConfig.featureFlags.flagSets.prod = ['*'] + expect(isFeatureEnabled('any/flag')).toBe(true) + }) }) diff --git a/test/unit/useFeatureFlag.test.ts b/test/unit/useFeatureFlag.test.ts index a8d04e0..2214ac1 100644 --- a/test/unit/useFeatureFlag.test.ts +++ b/test/unit/useFeatureFlag.test.ts @@ -14,7 +14,7 @@ vi.mock('#imports', () => ({ useRuntimeConfig: () => runtimeConfig, })) -beforeEach(() => { +beforeEach((): void => { runtimeConfig = { public: { featureFlags: { @@ -27,16 +27,16 @@ beforeEach(() => { } }) -describe('useFeatureFlag', () => { - it('detects static flags', () => { +describe('useFeatureFlag', (): void => { + it('detects static flags', (): void => { runtimeConfig.public.featureFlags.flagSets.prod = ['flagA'] const { isEnabled } = useFeatureFlag() expect(isEnabled('flagA')).toBe(true) expect(isEnabled('unknown')).toBe(false) }) - it('handles scheduled flags', () => { - const now = new Date('2024-06-01T12:00:00Z') + it('handles scheduled flags', (): void => { + const now: Date = new Date('2024-06-01T12:00:00Z') vi.useFakeTimers() vi.setSystemTime(now) @@ -51,8 +51,8 @@ describe('useFeatureFlag', () => { vi.useRealTimers() }) - it('filters out inactive scheduled flags', () => { - const now = new Date('2024-04-01T12:00:00Z') + it('filters out inactive scheduled flags', (): void => { + const now: Date = new Date('2024-04-01T12:00:00Z') vi.useFakeTimers() vi.setSystemTime(now) @@ -66,4 +66,19 @@ describe('useFeatureFlag', () => { vi.useRealTimers() }) + + it('supports hierarchical wildcard patterns', (): void => { + runtimeConfig.public.featureFlags.flagSets.prod = ['solutions/*'] + let utils = useFeatureFlag() + expect(utils.isEnabled('solutions/company-portal/addons/sales')).toBe(true) + expect(utils.isEnabled('solutions/*')).toBe(true) + + runtimeConfig.public.featureFlags.flagSets.prod = ['solutions/company-portal/addons/sales'] + utils = useFeatureFlag() + expect(utils.isEnabled('solutions/*')).toBe(false) + + runtimeConfig.public.featureFlags.flagSets.prod = ['*'] + utils = useFeatureFlag() + expect(utils.isEnabled('whatever')).toBe(true) + }) }) diff --git a/types/feature-flags.d.ts b/types/feature-flags.d.ts index f6b6e5d..bf5107c 100644 --- a/types/feature-flags.d.ts +++ b/types/feature-flags.d.ts @@ -1,6 +1,15 @@ +/** + * A feature flag definition which may be a simple string or an object with + * scheduling metadata. Flag names support hierarchical segments separated by + * `/` and may include `*` wildcards for pattern matching (e.g. `section/*`). + */ export type FeatureFlagInput = string | FeatureFlag export interface FeatureFlag { + /** + * Unique name of the feature flag. May include `/` segments and end with + * `/*` to represent a wildcard group. + */ name: string activeFrom?: string activeUntil?: string @@ -28,7 +37,8 @@ export interface FeatureFlagsConfig { environment: string /** - * A record mapping each environment to an array of flags (string or { name, activeFrom, activeUntil }). + * A record mapping each environment to an array of flags (string or `{ name, activeFrom, activeUntil }`). + * Flag names may leverage hierarchical paths and `*` wildcards for grouped enablement. */ flagSets: Record