Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/flags/.attw.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"excludeEntrypoints": ["flags/nuxt", "flags/nuxt/runtime"]
}
4 changes: 2 additions & 2 deletions packages/flags/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

# Flags SDK

The feature flags toolkit for Next.js and SvelteKit.
The feature flags toolkit for Next.js, Nuxt, and SvelteKit.

From the creators of Next.js, the Flags SDK is a free open-source library that gives you the tools you need to use feature flags in Next.js and SvelteKit applications.
From the creators of Next.js, the Flags SDK is a free open-source library that gives you the tools you need to use feature flags in Next.js, Nuxt, and SvelteKit applications.

- Works with any flag provider, custom setups or no flag provider at all
- Compatible with App Router, Pages Router, and Edge Middleware
Expand Down
23 changes: 21 additions & 2 deletions packages/flags/package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
{
"name": "flags",
"version": "4.0.1",
"description": "Flags SDK by Vercel - The feature flags toolkit for Next.js and SvelteKit",
"description": "Flags SDK by Vercel - The feature flags toolkit for Next.js, Nuxt, and SvelteKit",
"keywords": [
"feature flags",
"Next.js",
"Nuxt",
"react",
"vue",
"toolbar",
"overrides",
"SvelteKit"
Expand All @@ -30,6 +32,8 @@
"import": "./dist/next.js",
"require": "./dist/next.cjs"
},
"./nuxt": "./dist/nuxt.js",
"./nuxt/runtime": "./dist/nuxt/runtime/index.js",
"./analytics": {
"import": "./dist/analytics.js",
"require": "./dist/analytics.cjs"
Expand All @@ -54,6 +58,12 @@
"dist/next.d.ts",
"dist/next.d.cts"
],
"nuxt": [
"dist/nuxt.d.ts"
],
"nuxt/runtime": [
"dist/nuxt/runtime.d.ts"
],
"react": [
"dist/react.d.ts",
"dist/react.d.cts"
Expand All @@ -80,16 +90,21 @@
},
"dependencies": {
"@edge-runtime/cookies": "^5.0.2",
"jose": "^5.2.1"
"exsolve": "^1.0.7",
"jose": "^5.2.1",
"std-env": "^3.9.0"
},
"devDependencies": {
"@arethetypeswrong/cli": "0.17.3",
"@nuxt/test-utils": "3.19.2",
"@types/node": "20.11.17",
"@types/react": "18.2.55",
"@vitejs/plugin-react": "4.2.1",
"eslint-config-custom": "workspace:*",
"h3": "1.15.4",
"msw": "2.6.4",
"next": "15.1.4",
"nuxt": "4.0.3",
"react": "canary",
"tsconfig": "workspace:*",
"tsup": "8.0.1",
Expand All @@ -101,6 +116,7 @@
"@opentelemetry/api": "^1.7.0",
"@sveltejs/kit": "*",
"next": "*",
"nuxt": ">=3.0.0",
"react": "*",
"react-dom": "*"
},
Expand All @@ -114,6 +130,9 @@
"next": {
"optional": true
},
"nuxt": {
"optional": true
},
"react": {
"optional": true
},
Expand Down
18 changes: 18 additions & 0 deletions packages/flags/src/nuxt/implementation.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
declare module '#flags-implementation' {
import type { H3Event } from 'h3';
import type { Identify, JsonValue } from 'flags';

export function getState<ValueType>(
key: string,
event?: H3Event,
): { value: ValueType };
export function getStore<T>(event?: H3Event): T;

export interface FlagStore {
event: H3Event;
secret: string;
params: Record<string, string | string[]>;
usedFlags: Record<string, Promise<JsonValue>>;
identifiers: Map<Identify<unknown>, ReturnType<Identify<unknown>>>;
}
}
167 changes: 167 additions & 0 deletions packages/flags/src/nuxt/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { readdirSync, statSync } from 'node:fs';
import { join } from 'node:path';
import { provider } from 'std-env';
import { resolveModulePath } from 'exsolve';
import {
addImports,
addImportsDir,
addPluginTemplate,
addServerHandler,
addServerImports,
addServerImportsDir,
addServerTemplate,
addTemplate,
addTypeTemplate,
createResolver,
defineNuxtModule,
resolveAlias,
} from 'nuxt/kit';

interface ModuleOptions {
/** The directory to scan for exported feature flags */
dir: string | false;
/** Whether to enable support for the Vercel Toolbar */
toolbar: {
enabled: boolean;
};
}

export default defineNuxtModule<ModuleOptions>().with({
meta: {
name: 'flags',
configKey: 'flags',
},
defaults: (nuxt) => ({
dir: '#shared/flags',
toolbar: {
enabled:
provider === 'vercel' ||
nuxt.options.nitro.preset?.includes('vercel') ||
!!resolveModulePath('@vercel/toolbar', {
from: nuxt.options.modulesDir,
try: true,
}),
},
}),
setup(options, nuxt) {
const resolver = createResolver(import.meta.url);

nuxt.options.vite.optimizeDeps ||= {};
nuxt.options.vite.optimizeDeps.include ||= [];
nuxt.options.vite.optimizeDeps.include.push('flags/nuxt/runtime');

nuxt.options.vite.optimizeDeps.exclude ||= [];
nuxt.options.vite.optimizeDeps.exclude.push('#flags-implementation');

addServerTemplate({
filename: '#flags-implementation',
getContents() {
return `
import { getRouterParams } from 'h3'
export function getStore(event) {
return event.context.flags ||= {
event,
secret: process.env.FLAGS_SECRET,
params: getRouterParams(event),
usedFlags: {},
identifiers: new Map(),
};
}
export function getState(key) {
return { value: undefined }
}
`;
},
});

addTemplate({
filename: 'flags/implementation.mjs',
getContents: () => `
import { useNuxtApp, useState } from "#imports"

export function getStore() {
return useNuxtApp().$flagStore
}

export function getState(key) {
return useState(\`flag:$\{key}\`);
}
`,
});

nuxt.options.alias['#flags-implementation'] =
'#build/flags/implementation.mjs';

addImports({
name: 'defineFlag',
from: 'flags/nuxt/runtime',
});

addServerImports({
name: 'defineFlag',
from: 'flags/nuxt/runtime',
});

addTypeTemplate(
{
filename: 'flags-declaration.d.ts',
getContents() {
return `
declare global {
const defineFlag: typeof import('flags/nuxt/runtime')['defineFlag']
}
export {}
`;
},
},
{ shared: true },
);

if (options.dir) {
const path = resolveAlias(options.dir, nuxt.options.alias);

addImportsDir(path);
addServerImportsDir(path);
}

if (options.toolbar?.enabled) {
addServerHandler({
route: '/.well-known/vercel/flags',
handler: resolver.resolve('./nuxt/runtime/server/flags.js'),
});

addServerTemplate({
filename: '#flags/defined-flags',
getContents() {
if (!options.dir) {
return 'export const flags = {}';
}
const path = resolveAlias(options.dir, nuxt.options.alias);
try {
const isDir = statSync(path).isDirectory();
if (isDir) {
const files = readdirSync(path);
const lines = files.map(
(f, index) =>
`import * as n${index} from ${JSON.stringify(join(path, f))}`,
);
return (
lines.join('\n') +
`\nexport const flags = {${files.map((f, index) => `...n${index}`).join(', ')}}`
);
}
} catch {}
return 'export const flags = {}';
},
});
}

addTemplate({
filename: 'flags/config.mjs',
getContents: () =>
`export const toolbarEnabled = ${!!options.toolbar.enabled}`,
});

addPluginTemplate(resolver.resolve('./nuxt/runtime/app/plugin.server.js'));
},
});
63 changes: 63 additions & 0 deletions packages/flags/src/nuxt/runtime/app/plugin.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { encryptFlagValues } from '../../../lib/crypto';
import { safeJsonStringify } from '../../../lib/safe-json-stringify';
import { resolveObjectPromises } from '../../../shared';

import type { FlagStore } from '#flags-implementation';
// @ts-ignore
import { useHead } from '#imports';
// @ts-ignore
import { toolbarEnabled } from '#build/flags/config.mjs';
import { defineNuxtPlugin, useRoute } from 'nuxt/app';

export default defineNuxtPlugin(async (nuxtApp) => {
const flagStore: FlagStore = {
secret: process.env.FLAGS_SECRET!,
usedFlags: {},
identifiers: new Map(),
event: nuxtApp.ssrContext!.event,
params: useRoute().params || {},
};

// we are not directly returning the store as we don't want to expose the types
// to the end user
nuxtApp.provide('flagStore', flagStore);

if (!toolbarEnabled) {
return;
}

// This is for reporting which flags were used when this page was generated,
// so the value shows up in Vercel Toolbar, without the client ever being
// aware of this feature flag.
nuxtApp.hook('app:rendered', async () => {
const entries = [
...Object.entries(flagStore.usedFlags),
...Object.entries(
nuxtApp.ssrContext!.event.context.flags?.usedFlags || {},
),
];

if (entries.length === 0) return;

const encryptedFlagValues = await encryptFlagValues(
await resolveObjectPromises({
...nuxtApp.ssrContext!.event.context.flags?.usedFlags,
...flagStore.usedFlags,
}),
process.env.FLAGS_SECRET,
);

nuxtApp.runWithContext(() =>
useHead({
script: [
() => ({
tagPosition: 'bodyClose',
type: 'application/json',
'data-flag-values': '',
innerHTML: safeJsonStringify(encryptedFlagValues),
}),
],
}),
);
});
});
Loading
Loading