From 58fee0cf02b21156f2fcb68932053a38dff34397 Mon Sep 17 00:00:00 2001 From: chrizy Date: Sat, 15 Mar 2025 23:48:27 +0000 Subject: [PATCH 1/3] add devunus package --- packages/adapter-devunus/.eslintrc.cjs | 4 + packages/adapter-devunus/CHANGELOG.md | 7 + packages/adapter-devunus/README.md | 33 +++ packages/adapter-devunus/package.json | 57 +++++ packages/adapter-devunus/src/index.test.ts | 83 +++++++ packages/adapter-devunus/src/index.ts | 17 ++ .../src/provider/index.test.ts | 74 ++++++ .../adapter-devunus/src/provider/index.ts | 98 ++++++++ packages/adapter-devunus/tsconfig.json | 9 + packages/adapter-devunus/tsup.config.js | 12 + packages/adapter-devunus/vitest.config.ts | 8 + pnpm-lock.yaml | 214 ++++++++++++++++-- 12 files changed, 603 insertions(+), 13 deletions(-) create mode 100644 packages/adapter-devunus/.eslintrc.cjs create mode 100644 packages/adapter-devunus/CHANGELOG.md create mode 100644 packages/adapter-devunus/README.md create mode 100644 packages/adapter-devunus/package.json create mode 100644 packages/adapter-devunus/src/index.test.ts create mode 100644 packages/adapter-devunus/src/index.ts create mode 100644 packages/adapter-devunus/src/provider/index.test.ts create mode 100644 packages/adapter-devunus/src/provider/index.ts create mode 100644 packages/adapter-devunus/tsconfig.json create mode 100644 packages/adapter-devunus/tsup.config.js create mode 100644 packages/adapter-devunus/vitest.config.ts diff --git a/packages/adapter-devunus/.eslintrc.cjs b/packages/adapter-devunus/.eslintrc.cjs new file mode 100644 index 00000000..d45517e4 --- /dev/null +++ b/packages/adapter-devunus/.eslintrc.cjs @@ -0,0 +1,4 @@ +module.exports = { + extends: ['custom'], + root: true, +}; diff --git a/packages/adapter-devunus/CHANGELOG.md b/packages/adapter-devunus/CHANGELOG.md new file mode 100644 index 00000000..8f9f5180 --- /dev/null +++ b/packages/adapter-devunus/CHANGELOG.md @@ -0,0 +1,7 @@ +# @flags-sdk/devunus + +## 0.1.0 + +### Initial Release + +- Initial release of the Devunus adapter for flags-sdk diff --git a/packages/adapter-devunus/README.md b/packages/adapter-devunus/README.md new file mode 100644 index 00000000..7bd4ca2b --- /dev/null +++ b/packages/adapter-devunus/README.md @@ -0,0 +1,33 @@ +# @flags-sdk/Devunus + +Devunus adapter for [flags-sdk](https://github.com/vercel/flags). + +- An adapter for loading feature flags from devunus (coming soon). +- A getProviderData function for use with the Flags Explorer (available today). + +## Installation + +```bash +npm install @flags-sdk/Devunus +``` + +## Usage getProviderData + +`app/.well-known/vercel/flags/route.ts`: + +```tsx +import { verifyAccess, type ApiData } from 'flags'; +import { getProviderData } from '@flags-sdk/devunus'; +import { NextResponse, type NextRequest } from 'next/server'; + +export async function GET(request: NextRequest) { + const access = await verifyAccess(request.headers.get('Authorization')); + if (!access) return NextResponse.json(null, { status: 401 }); + + const flagData = await getProviderData({ + envKey: process.env.DEVUNUS_ENV_KEY, + }); + + return NextResponse.json(flagData); +} +``` diff --git a/packages/adapter-devunus/package.json b/packages/adapter-devunus/package.json new file mode 100644 index 00000000..a89002ee --- /dev/null +++ b/packages/adapter-devunus/package.json @@ -0,0 +1,57 @@ +{ + "name": "@flags-sdk/devunus", + "version": "0.1.0", + "description": "Devunus adapter for flags-sdk", + "keywords": [ + "feature-flags", + "devunus", + "flags-sdk" + ], + "license": "MIT", + "author": "", + "sideEffects": false, + "type": "module", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "main": "./dist/index.js", + "typesVersions": { + "*": { + ".": [ + "dist/*.d.ts", + "dist/*.d.cts" + ] + } + }, + "files": [ + "dist", + "CHANGELOG.md" + ], + "scripts": { + "build": "rimraf dist && tsup", + "dev": "tsup --watch --clean=false", + "eslint": "eslint-runner", + "eslint:fix": "eslint-runner --fix", + "test": "vitest --run", + "test:watch": "vitest", + "type-check": "tsc --noEmit" + }, + "devDependencies": { + "@types/node": "20.11.17", + "eslint-config-custom": "workspace:*", + "eslint-plugin-vitest": "0.5.4", + "flags": "workspace:*", + "msw": "2.6.4", + "rimraf": "6.0.1", + "tsconfig": "workspace:*", + "tsup": "8.0.1", + "typescript": "5.6.3", + "vitest": "1.4.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/adapter-devunus/src/index.test.ts b/packages/adapter-devunus/src/index.test.ts new file mode 100644 index 00000000..86e9908d --- /dev/null +++ b/packages/adapter-devunus/src/index.test.ts @@ -0,0 +1,83 @@ +import { + describe, + it, + expect, + vi, + beforeAll, + afterAll, + afterEach, + beforeEach, +} from 'vitest'; +import { devunusAdapter } from './index'; +import { setupServer } from 'msw/node'; +import { http, HttpResponse } from 'msw'; + +const server = setupServer( + http.get('https://api.devunus.com/api/flags', ({ request }) => { + const authHeader = request.headers.get('Authorization'); + return HttpResponse.json({ + flags: [ + { + id: 'flag1', + name: 'enableFeatureX', + description: 'Enable feature X', + value: true, + type: 'boolean', + createdAt: 1615000000000, + updatedAt: 1620000000000, + }, + ], + baseUrl: 'https://app.devunus.com/admin/333/project/1', + }); + }), +); + +beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); +afterAll(() => server.close()); +afterEach(() => server.resetHandlers()); + +describe('Devunus default adapter', () => { + const originalEnv = process.env; + + beforeEach(() => { + vi.resetModules(); + process.env = { ...originalEnv }; + process.env.DEVUNUS_ENV_KEY = 'test-key'; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it('should create an adapter with correct interface', () => { + const adapter = devunusAdapter.getFeature(); + expect(adapter.provider).toBe('devunus'); + expect(typeof adapter.getProviderData).toBe('function'); + }); + + it('should pass environment variables to provider', async () => { + const customKey = 'custom-key'; + process.env.DEVUNUS_ENV_KEY = customKey; + const adapter = devunusAdapter.getFeature(); + await adapter.getProviderData(); + // We only test that the adapter properly passes the env var + // The actual API response handling is tested in provider tests + }); + + it('should construct correct origin URL with project ID', async () => { + process.env.DEVUNUS_PROJECT_ID = 'test-project'; + const adapter = devunusAdapter.getFeature(); + const result = await adapter.getProviderData(); + + const featureX = result.definitions.enableFeatureX; + expect(featureX?.origin).toEqual( + 'https://app.devunus.com/admin/333/project/1/flag/flag1', + ); + }); + + it('should properly initialize with default configuration', () => { + const adapter = devunusAdapter.getFeature(); + expect(adapter).toBeDefined(); + expect(adapter.provider).toBe('devunus'); + }); +}); diff --git a/packages/adapter-devunus/src/index.ts b/packages/adapter-devunus/src/index.ts new file mode 100644 index 00000000..6cbcbe2d --- /dev/null +++ b/packages/adapter-devunus/src/index.ts @@ -0,0 +1,17 @@ +import { getProviderData } from './provider'; +export * from './provider'; + +/** + * Default adapter that uses environment variables for configuration + */ +export const devunusAdapter = { + getFeature: () => ({ + provider: 'devunus', + getProviderData: () => + getProviderData({ + envKey: process.env.DEVUNUS_ENV_KEY || '', + }), + }), +}; + +export default devunusAdapter; diff --git a/packages/adapter-devunus/src/provider/index.test.ts b/packages/adapter-devunus/src/provider/index.test.ts new file mode 100644 index 00000000..3e537e54 --- /dev/null +++ b/packages/adapter-devunus/src/provider/index.test.ts @@ -0,0 +1,74 @@ +import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest'; +import { getProviderData } from './index'; +import { setupServer } from 'msw/node'; +import { http, HttpResponse } from 'msw'; + +const server = setupServer( + http.get('https://api.devunus.com/api/flags', ({ request }) => { + const authHeader = request.headers.get('Authorization'); + + if (!authHeader || authHeader !== 'valid-key') { + return new HttpResponse(null, { status: 401 }); + } + + return HttpResponse.json({ + flags: [ + { + id: 'flag1', + name: 'enableFeatureX', + description: 'Enable feature X', + value: true, + type: 'boolean', + createdAt: 1615000000000, + updatedAt: 1620000000000, + }, + { + id: 'flag2', + name: 'userTheme', + description: 'User theme preference', + value: 'dark', + type: 'string', + createdAt: 1615000000000, + updatedAt: 1620000000000, + }, + ], + baseUrl: 'https://app.devunus.com/admin/333/project/1', + }); + }), +); + +describe('Devunus provider', () => { + beforeAll(() => server.listen()); + afterAll(() => server.close()); + + it('should return empty definitions and a hint when no env key is provided', async () => { + const result = await getProviderData({ envKey: '' }); + + expect(result.definitions).toEqual({}); + expect(result.hints).toBeDefined(); + expect(result.hints?.length).toBe(1); + expect(result.hints?.[0]?.key).toBe('devunus/missing-env-key'); + }); + + it('should return empty definitions and a hint when the API returns an error', async () => { + const result = await getProviderData({ envKey: 'invalid-key' }); + + expect(result.definitions).toEqual({}); + expect(result.hints).toBeDefined(); + expect(result.hints?.length).toBe(1); + expect(result.hints?.[0]?.key).toBe('devunus/response-not-ok'); + }); + + it('should return flag definitions when the API returns valid data', async () => { + const result = await getProviderData({ envKey: 'valid-key' }); + + expect(Object.keys(result.definitions)).toHaveLength(2); + expect(result.definitions.enableFeatureX).toBeDefined(); + expect(result.definitions.userTheme).toBeDefined(); + const enableFeatureX = result.definitions.enableFeatureX; + const userTheme = result.definitions.userTheme; + expect(enableFeatureX?.description).toBe('Enable feature X'); + expect(userTheme?.description).toBe('User theme preference'); + expect(result.hints?.length).toBe(0); + }); +}); diff --git a/packages/adapter-devunus/src/provider/index.ts b/packages/adapter-devunus/src/provider/index.ts new file mode 100644 index 00000000..bdd23923 --- /dev/null +++ b/packages/adapter-devunus/src/provider/index.ts @@ -0,0 +1,98 @@ +import type { ProviderData, JsonValue } from 'flags'; + +interface DevunusFlag { + id: string; + name: string; + description: string; + value: JsonValue; + type: 'boolean' | 'string' | 'number' | 'json'; + createdAt: number; + updatedAt: number; +} + +interface DevunusResponse { + flags: DevunusFlag[]; + baseUrl: string; +} + +export async function getProviderData(options: { + /** + * The Devunus environment key. + */ + envKey: string; +}): Promise { + if (!options.envKey) { + return { + definitions: {}, + hints: [ + { + key: 'devunus/missing-env-key', + text: 'Missing DevUnus environment key', + }, + ], + }; + } + + try { + // Get from edge API + const response = await fetch(`https://api.devunus.com/api/flags`, { + headers: { Authorization: `${options.envKey}` }, + cache: 'no-store', + }); + + if (response.status !== 200) { + return { + definitions: {}, + hints: [ + { + key: 'devunus/response-not-ok', + text: `Failed to fetch DevUnus flag definitions (received ${response.status} response)`, + }, + ], + }; + } + + const data = (await response.json()) as DevunusResponse; + const { flags, baseUrl } = data; + + const definitions: ProviderData['definitions'] = {}; + + for (const flag of flags) { + const flagOptions = []; + + // For boolean flags, provide true/false options + if (flag.type === 'boolean') { + flagOptions.push({ value: true, label: 'On' }); + flagOptions.push({ value: false, label: 'Off' }); + } + + // For string flags, include the current value as an option + if (flag.type === 'string') { + flagOptions.push({ value: flag.value }); + } + + definitions[flag.name] = { + description: flag.description, + options: flagOptions, + origin: `${baseUrl}/flag/${flag.id}`, + updatedAt: flag.updatedAt, + createdAt: flag.createdAt, + defaultValue: flag.value, + }; + } + + return { definitions, hints: [] }; + } catch (error) { + return { + definitions: {}, + hints: [ + { + key: 'devunus/unexpected-error', + text: `Unexpected error fetching DevUnus flag definitions: ${ + error instanceof Error ? error.message : String(error) + }`, + }, + ], + }; + } +} diff --git a/packages/adapter-devunus/tsconfig.json b/packages/adapter-devunus/tsconfig.json new file mode 100644 index 00000000..f56eab73 --- /dev/null +++ b/packages/adapter-devunus/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "tsconfig/base.json", + "include": ["src"], + "exclude": ["node_modules"], + "compilerOptions": { + "outDir": "dist", + "types": ["node", "vitest/globals"] + } +} diff --git a/packages/adapter-devunus/tsup.config.js b/packages/adapter-devunus/tsup.config.js new file mode 100644 index 00000000..b6a89d32 --- /dev/null +++ b/packages/adapter-devunus/tsup.config.js @@ -0,0 +1,12 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm', 'cjs'], + dts: true, + splitting: false, + sourcemap: true, + clean: true, + treeshake: true, + minify: true, +}); diff --git a/packages/adapter-devunus/vitest.config.ts b/packages/adapter-devunus/vitest.config.ts new file mode 100644 index 00000000..014f97ef --- /dev/null +++ b/packages/adapter-devunus/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + globals: true, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7a6ff942..cf156626 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -304,6 +304,39 @@ importers: specifier: ^5.0.3 version: 5.1.1(@types/node@22.9.0) + packages/adapter-devunus: + devDependencies: + '@types/node': + specifier: 20.11.17 + version: 20.11.17 + eslint-config-custom: + specifier: workspace:* + version: link:../../tooling/eslint-config-custom + eslint-plugin-vitest: + specifier: 0.5.4 + version: 0.5.4(eslint@8.57.1)(typescript@5.6.3)(vitest@1.4.0) + flags: + specifier: workspace:* + version: link:../flags + msw: + specifier: 2.6.4 + version: 2.6.4(@types/node@20.11.17)(typescript@5.6.3) + rimraf: + specifier: 6.0.1 + version: 6.0.1 + tsconfig: + specifier: workspace:* + version: link:../../tooling/tsconfig + tsup: + specifier: 8.0.1 + version: 8.0.1(typescript@5.6.3) + typescript: + specifier: 5.6.3 + version: 5.6.3 + vitest: + specifier: 1.4.0 + version: 1.4.0(@types/node@20.11.17) + packages/adapter-edge-config: dependencies: '@vercel/edge-config': @@ -579,7 +612,7 @@ importers: version: 5.10.0 react-dom: specifier: '*' - version: 18.3.1(react@19.1.0-canary-f9d78089-20250306) + version: 18.3.1(react@19.1.0-canary-5398b711-20250314) devDependencies: '@arethetypeswrong/cli': specifier: 0.17.3 @@ -601,10 +634,10 @@ importers: version: 2.6.4(@types/node@20.11.17)(typescript@5.6.3) next: specifier: 15.1.4 - version: 15.1.4(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(react-dom@18.3.1)(react@19.1.0-canary-f9d78089-20250306) + version: 15.1.4(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(react-dom@18.3.1)(react@19.1.0-canary-5398b711-20250314) react: specifier: canary - version: 19.1.0-canary-f9d78089-20250306 + version: 19.1.0-canary-5398b711-20250314 tsconfig: specifier: workspace:* version: link:../../tooling/tsconfig @@ -1610,6 +1643,16 @@ packages: eslint-visitor-keys: 3.4.3 dev: true + /@eslint-community/eslint-utils@4.4.1(eslint@8.57.1): + resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + dev: true + /@eslint-community/eslint-utils@4.4.1(eslint@9.21.0): resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1684,6 +1727,11 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@eslint/js@8.57.1: + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + /@eslint/js@9.21.0: resolution: {integrity: sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1812,6 +1860,18 @@ packages: transitivePeerDependencies: - supports-color + /@humanwhocodes/config-array@0.13.0: + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.0 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + /@humanwhocodes/module-importer@1.0.1: resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} @@ -4345,6 +4405,14 @@ packages: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 + /@typescript-eslint/scope-manager@7.18.0: + resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} + engines: {node: ^18.18.0 || >=20.0.0} + dependencies: + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/visitor-keys': 7.18.0 + dev: true + /@typescript-eslint/scope-manager@8.25.0: resolution: {integrity: sha512-6PPeiKIGbgStEyt4NNXa2ru5pMzQ8OYKO1hX1z53HMomrmiSB+R5FmChgQAP1ro8jMtNawz+TRQo/cSXrauTpg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4454,6 +4522,11 @@ packages: resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} engines: {node: ^16.0.0 || >=18.0.0} + /@typescript-eslint/types@7.18.0: + resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} + engines: {node: ^18.18.0 || >=20.0.0} + dev: true + /@typescript-eslint/types@8.25.0: resolution: {integrity: sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4543,6 +4616,28 @@ packages: transitivePeerDependencies: - supports-color + /@typescript-eslint/typescript-estree@7.18.0(typescript@5.6.3): + resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/visitor-keys': 7.18.0 + debug: 4.4.0 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.1 + ts-api-utils: 1.4.3(typescript@5.6.3) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/typescript-estree@8.25.0(typescript@5.6.3): resolution: {integrity: sha512-ZPaiAKEZ6Blt/TPAx5Ot0EIB/yGtLI2EsGoY6F7XKklfMxYQyvtL+gT/UCqkMzO0BVFHLDlzvFqQzurYahxv9Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4696,6 +4791,22 @@ packages: - typescript dev: true + /@typescript-eslint/utils@7.18.0(eslint@8.57.1)(typescript@5.6.3): + resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.3) + eslint: 8.57.1 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /@typescript-eslint/utils@8.25.0(eslint@8.56.0)(typescript@5.6.3): resolution: {integrity: sha512-syqRbrEv0J1wywiLsK60XzHnQe/kRViI3zwFALrNEgnntn1l24Ra2KvOAWwWbWZ1lBZxZljPDGOq967dsl6fkA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4744,6 +4855,14 @@ packages: '@typescript-eslint/types': 6.21.0 eslint-visitor-keys: 3.4.3 + /@typescript-eslint/visitor-keys@7.18.0: + resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} + engines: {node: ^18.18.0 || >=20.0.0} + dependencies: + '@typescript-eslint/types': 7.18.0 + eslint-visitor-keys: 3.4.3 + dev: true + /@typescript-eslint/visitor-keys@8.25.0: resolution: {integrity: sha512-kCYXKAum9CecGVHGij7muybDfTS2sD3t0L4bJsEZLkyrXUImiCTq1M3LG2SRtOhiHFwMR9wAFplpT6XHYjTkwQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -7247,6 +7366,27 @@ packages: strip-indent: 3.0.0 dev: true + /eslint-plugin-vitest@0.5.4(eslint@8.57.1)(typescript@5.6.3)(vitest@1.4.0): + resolution: {integrity: sha512-um+odCkccAHU53WdKAw39MY61+1x990uXjSPguUCq3VcEHdqJrOb8OTMrbYlY6f9jAKx7x98kLVlIe3RJeJqoQ==} + engines: {node: ^18.0.0 || >= 20.0.0} + peerDependencies: + '@typescript-eslint/eslint-plugin': '*' + eslint: ^8.57.0 || ^9.0.0 + vitest: '*' + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + vitest: + optional: true + dependencies: + '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.6.3) + eslint: 8.57.1 + vitest: 1.4.0(@types/node@20.11.17) + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -7376,6 +7516,54 @@ packages: - supports-color dev: true + /eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.1 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.0 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + /eslint@9.21.0: resolution: {integrity: sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -9705,7 +9893,7 @@ packages: - babel-plugin-macros dev: false - /next@15.1.4(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(react-dom@18.3.1)(react@19.1.0-canary-f9d78089-20250306): + /next@15.1.4(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(react-dom@18.3.1)(react@19.1.0-canary-5398b711-20250314): resolution: {integrity: sha512-mTaq9dwaSuwwOrcu3ebjDYObekkxRnXpuVL21zotM8qE2W0HBOdVIdg2Li9QjMEZrj73LN96LcWcz62V19FjAg==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true @@ -9733,9 +9921,9 @@ packages: busboy: 1.6.0 caniuse-lite: 1.0.30001701 postcss: 8.4.31 - react: 19.1.0-canary-f9d78089-20250306 - react-dom: 18.3.1(react@19.1.0-canary-f9d78089-20250306) - styled-jsx: 5.1.6(@babel/core@7.26.9)(react@19.1.0-canary-f9d78089-20250306) + react: 19.1.0-canary-5398b711-20250314 + react-dom: 18.3.1(react@19.1.0-canary-5398b711-20250314) + styled-jsx: 5.1.6(@babel/core@7.26.9)(react@19.1.0-canary-5398b711-20250314) optionalDependencies: '@next/swc-darwin-arm64': 15.1.4 '@next/swc-darwin-x64': 15.1.4 @@ -10555,13 +10743,13 @@ packages: scheduler: 0.23.2 dev: false - /react-dom@18.3.1(react@19.1.0-canary-f9d78089-20250306): + /react-dom@18.3.1(react@19.1.0-canary-5398b711-20250314): resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: react: ^18.3.1 dependencies: loose-envify: 1.4.0 - react: 19.1.0-canary-f9d78089-20250306 + react: 19.1.0-canary-5398b711-20250314 scheduler: 0.23.2 /react-dom@19.0.0(react@19.0.0): @@ -10675,8 +10863,8 @@ packages: engines: {node: '>=0.10.0'} dev: false - /react@19.1.0-canary-f9d78089-20250306: - resolution: {integrity: sha512-PpeBxA/Ldt6Fu4cPrOxCxnzs9SllZVwNPBu71w6pVViLAa4x4zYlzNv+h+3eXFJi4eL/cbN91ySLZAII/fL+0w==} + /react@19.1.0-canary-5398b711-20250314: + resolution: {integrity: sha512-9gpKdBbdUgEumcFhTmd85L53mOxRG85lcZatD6pButFP1ZWwK8y/1rioY99enx7fkhLo8lR6gV8qFv90+T4VAA==} engines: {node: '>=0.10.0'} /read-cache@1.0.0: @@ -11577,7 +11765,7 @@ packages: react: 19.0.0-rc.1 dev: false - /styled-jsx@5.1.6(@babel/core@7.26.9)(react@19.1.0-canary-f9d78089-20250306): + /styled-jsx@5.1.6(@babel/core@7.26.9)(react@19.1.0-canary-5398b711-20250314): resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} peerDependencies: @@ -11592,7 +11780,7 @@ packages: dependencies: '@babel/core': 7.26.9 client-only: 0.0.1 - react: 19.1.0-canary-f9d78089-20250306 + react: 19.1.0-canary-5398b711-20250314 dev: true /sucrase@3.35.0: From aba143e48b4bd27b305904553226be2015bfde19 Mon Sep 17 00:00:00 2001 From: chrizy Date: Mon, 17 Mar 2025 16:49:45 +0000 Subject: [PATCH 2/3] fix: docs update --- packages/adapter-devunus/README.md | 6 ++++-- packages/adapter-devunus/src/provider/index.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/adapter-devunus/README.md b/packages/adapter-devunus/README.md index 7bd4ca2b..7629171f 100644 --- a/packages/adapter-devunus/README.md +++ b/packages/adapter-devunus/README.md @@ -1,4 +1,4 @@ -# @flags-sdk/Devunus +# @flags-sdk/devunus Devunus adapter for [flags-sdk](https://github.com/vercel/flags). @@ -8,11 +8,13 @@ Devunus adapter for [flags-sdk](https://github.com/vercel/flags). ## Installation ```bash -npm install @flags-sdk/Devunus +npm install @flags-sdk/devunus ``` ## Usage getProviderData +Use a server env key for DEVUNUS_ENV_KEY. You can find your environment key in the [Devunus Admin Console](https://app.devunus.com/admin/def/project/1/get-started/e0-0/keys). + `app/.well-known/vercel/flags/route.ts`: ```tsx diff --git a/packages/adapter-devunus/src/provider/index.ts b/packages/adapter-devunus/src/provider/index.ts index bdd23923..8616f795 100644 --- a/packages/adapter-devunus/src/provider/index.ts +++ b/packages/adapter-devunus/src/provider/index.ts @@ -4,7 +4,7 @@ interface DevunusFlag { id: string; name: string; description: string; - value: JsonValue; + value: string; type: 'boolean' | 'string' | 'number' | 'json'; createdAt: number; updatedAt: number; From e4d2089530aea99fdb2c90d8d1f7451993a0cb78 Mon Sep 17 00:00:00 2001 From: chrizy Date: Thu, 20 Mar 2025 20:35:42 +0000 Subject: [PATCH 3/3] fix: remove devunusAdapter until complete --- packages/adapter-devunus/src/index.test.ts | 83 ---------------------- packages/adapter-devunus/src/index.ts | 18 +---- 2 files changed, 1 insertion(+), 100 deletions(-) delete mode 100644 packages/adapter-devunus/src/index.test.ts diff --git a/packages/adapter-devunus/src/index.test.ts b/packages/adapter-devunus/src/index.test.ts deleted file mode 100644 index 86e9908d..00000000 --- a/packages/adapter-devunus/src/index.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { - describe, - it, - expect, - vi, - beforeAll, - afterAll, - afterEach, - beforeEach, -} from 'vitest'; -import { devunusAdapter } from './index'; -import { setupServer } from 'msw/node'; -import { http, HttpResponse } from 'msw'; - -const server = setupServer( - http.get('https://api.devunus.com/api/flags', ({ request }) => { - const authHeader = request.headers.get('Authorization'); - return HttpResponse.json({ - flags: [ - { - id: 'flag1', - name: 'enableFeatureX', - description: 'Enable feature X', - value: true, - type: 'boolean', - createdAt: 1615000000000, - updatedAt: 1620000000000, - }, - ], - baseUrl: 'https://app.devunus.com/admin/333/project/1', - }); - }), -); - -beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); -afterAll(() => server.close()); -afterEach(() => server.resetHandlers()); - -describe('Devunus default adapter', () => { - const originalEnv = process.env; - - beforeEach(() => { - vi.resetModules(); - process.env = { ...originalEnv }; - process.env.DEVUNUS_ENV_KEY = 'test-key'; - }); - - afterEach(() => { - process.env = originalEnv; - }); - - it('should create an adapter with correct interface', () => { - const adapter = devunusAdapter.getFeature(); - expect(adapter.provider).toBe('devunus'); - expect(typeof adapter.getProviderData).toBe('function'); - }); - - it('should pass environment variables to provider', async () => { - const customKey = 'custom-key'; - process.env.DEVUNUS_ENV_KEY = customKey; - const adapter = devunusAdapter.getFeature(); - await adapter.getProviderData(); - // We only test that the adapter properly passes the env var - // The actual API response handling is tested in provider tests - }); - - it('should construct correct origin URL with project ID', async () => { - process.env.DEVUNUS_PROJECT_ID = 'test-project'; - const adapter = devunusAdapter.getFeature(); - const result = await adapter.getProviderData(); - - const featureX = result.definitions.enableFeatureX; - expect(featureX?.origin).toEqual( - 'https://app.devunus.com/admin/333/project/1/flag/flag1', - ); - }); - - it('should properly initialize with default configuration', () => { - const adapter = devunusAdapter.getFeature(); - expect(adapter).toBeDefined(); - expect(adapter.provider).toBe('devunus'); - }); -}); diff --git a/packages/adapter-devunus/src/index.ts b/packages/adapter-devunus/src/index.ts index 6cbcbe2d..a1be2962 100644 --- a/packages/adapter-devunus/src/index.ts +++ b/packages/adapter-devunus/src/index.ts @@ -1,17 +1 @@ -import { getProviderData } from './provider'; -export * from './provider'; - -/** - * Default adapter that uses environment variables for configuration - */ -export const devunusAdapter = { - getFeature: () => ({ - provider: 'devunus', - getProviderData: () => - getProviderData({ - envKey: process.env.DEVUNUS_ENV_KEY || '', - }), - }), -}; - -export default devunusAdapter; +export { getProviderData } from './provider';