diff --git a/.agents/skills/vitest/GENERATION.md b/.agents/skills/vitest/GENERATION.md new file mode 100644 index 0000000000000..9bc76640e49d4 --- /dev/null +++ b/.agents/skills/vitest/GENERATION.md @@ -0,0 +1,5 @@ +# Generation Info + +- **Source:** `sources/vitest` +- **Git SHA:** `4a7321e10672f00f0bb698823a381c2cc245b8f7` +- **Generated:** 2026-01-28 diff --git a/.agents/skills/vitest/SKILL.md b/.agents/skills/vitest/SKILL.md new file mode 100644 index 0000000000000..0578bdcf3a870 --- /dev/null +++ b/.agents/skills/vitest/SKILL.md @@ -0,0 +1,52 @@ +--- +name: vitest +description: Vitest fast unit testing framework powered by Vite with Jest-compatible API. Use when writing tests, mocking, configuring coverage, or working with test filtering and fixtures. +metadata: + author: Anthony Fu + version: "2026.1.28" + source: Generated from https://github.com/vitest-dev/vitest, scripts located at https://github.com/antfu/skills +--- + +Vitest is a next-generation testing framework powered by Vite. It provides a Jest-compatible API with native ESM, TypeScript, and JSX support out of the box. Vitest shares the same config, transformers, resolvers, and plugins with your Vite app. + +**Key Features:** +- Vite-native: Uses Vite's transformation pipeline for fast HMR-like test updates +- Jest-compatible: Drop-in replacement for most Jest test suites +- Smart watch mode: Only reruns affected tests based on module graph +- Native ESM, TypeScript, JSX support without configuration +- Multi-threaded workers for parallel test execution +- Built-in coverage via V8 or Istanbul +- Snapshot testing, mocking, and spy utilities + +> The skill is based on Vitest 3.x, generated at 2026-01-28. + +## Core + +| Topic | Description | Reference | +|-------|-------------|-----------| +| Configuration | Vitest and Vite config integration, defineConfig usage | [core-config](references/core-config.md) | +| CLI | Command line interface, commands and options | [core-cli](references/core-cli.md) | +| Test API | test/it function, modifiers like skip, only, concurrent | [core-test-api](references/core-test-api.md) | +| Describe API | describe/suite for grouping tests and nested suites | [core-describe](references/core-describe.md) | +| Expect API | Assertions with toBe, toEqual, matchers and asymmetric matchers | [core-expect](references/core-expect.md) | +| Hooks | beforeEach, afterEach, beforeAll, afterAll, aroundEach | [core-hooks](references/core-hooks.md) | + +## Features + +| Topic | Description | Reference | +|-------|-------------|-----------| +| Mocking | Mock functions, modules, timers, dates with vi utilities | [features-mocking](references/features-mocking.md) | +| Snapshots | Snapshot testing with toMatchSnapshot and inline snapshots | [features-snapshots](references/features-snapshots.md) | +| Coverage | Code coverage with V8 or Istanbul providers | [features-coverage](references/features-coverage.md) | +| Test Context | Test fixtures, context.expect, test.extend for custom fixtures | [features-context](references/features-context.md) | +| Concurrency | Concurrent tests, parallel execution, sharding | [features-concurrency](references/features-concurrency.md) | +| Filtering | Filter tests by name, file patterns, tags | [features-filtering](references/features-filtering.md) | + +## Advanced + +| Topic | Description | Reference | +|-------|-------------|-----------| +| Vi Utilities | vi helper: mock, spyOn, fake timers, hoisted, waitFor | [advanced-vi](references/advanced-vi.md) | +| Environments | Test environments: node, jsdom, happy-dom, custom | [advanced-environments](references/advanced-environments.md) | +| Type Testing | Type-level testing with expectTypeOf and assertType | [advanced-type-testing](references/advanced-type-testing.md) | +| Projects | Multi-project workspaces, different configs per project | [advanced-projects](references/advanced-projects.md) | diff --git a/.agents/skills/vitest/references/advanced-environments.md b/.agents/skills/vitest/references/advanced-environments.md new file mode 100644 index 0000000000000..25a1d5b07880d --- /dev/null +++ b/.agents/skills/vitest/references/advanced-environments.md @@ -0,0 +1,264 @@ +--- +name: test-environments +description: Configure environments like jsdom, happy-dom for browser APIs +--- + +# Test Environments + +## Available Environments + +- `node` (default) - Node.js environment +- `jsdom` - Browser-like with DOM APIs +- `happy-dom` - Faster alternative to jsdom +- `edge-runtime` - Vercel Edge Runtime + +## Configuration + +```ts +// vitest.config.ts +defineConfig({ + test: { + environment: 'jsdom', + + // Environment-specific options + environmentOptions: { + jsdom: { + url: 'http://localhost', + }, + }, + }, +}) +``` + +## Installing Environment Packages + +```bash +# jsdom +npm i -D jsdom + +# happy-dom (faster, fewer APIs) +npm i -D happy-dom +``` + +## Per-File Environment + +Use magic comment at top of file: + +```ts +// @vitest-environment jsdom + +import { expect, test } from 'vitest' + +test('DOM test', () => { + const div = document.createElement('div') + expect(div).toBeInstanceOf(HTMLDivElement) +}) +``` + +## jsdom Environment + +Full browser environment simulation: + +```ts +// @vitest-environment jsdom + +test('DOM manipulation', () => { + document.body.innerHTML = '
' + + const app = document.getElementById('app') + app.textContent = 'Hello' + + expect(app.textContent).toBe('Hello') +}) + +test('window APIs', () => { + expect(window.location.href).toBeDefined() + expect(localStorage).toBeDefined() +}) +``` + +### jsdom Options + +```ts +defineConfig({ + test: { + environmentOptions: { + jsdom: { + url: 'http://localhost:3000', + html: '', + userAgent: 'custom-agent', + resources: 'usable', + }, + }, + }, +}) +``` + +## happy-dom Environment + +Faster but fewer APIs: + +```ts +// @vitest-environment happy-dom + +test('basic DOM', () => { + const el = document.createElement('div') + el.className = 'test' + expect(el.className).toBe('test') +}) +``` + +## Multiple Environments per Project + +Use projects for different environments: + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'unit', + include: ['tests/unit/**/*.test.ts'], + environment: 'node', + }, + }, + { + test: { + name: 'dom', + include: ['tests/dom/**/*.test.ts'], + environment: 'jsdom', + }, + }, + ], + }, +}) +``` + +## Custom Environment + +Create custom environment package: + +```ts +// vitest-environment-custom/index.ts +import type { Environment } from 'vitest/runtime' + +export default { + name: 'custom', + viteEnvironment: 'ssr', // or 'client' + + setup() { + // Setup global state + globalThis.myGlobal = 'value' + + return { + teardown() { + delete globalThis.myGlobal + }, + } + }, +} +``` + +Use with: + +```ts +defineConfig({ + test: { + environment: 'custom', + }, +}) +``` + +## Environment with VM + +For full isolation: + +```ts +export default { + name: 'isolated', + viteEnvironment: 'ssr', + + async setupVM() { + const vm = await import('node:vm') + const context = vm.createContext() + + return { + getVmContext() { + return context + }, + teardown() {}, + } + }, + + setup() { + return { teardown() {} } + }, +} +``` + +## Browser Mode (Separate from Environments) + +For real browser testing, use Vitest Browser Mode: + +```ts +defineConfig({ + test: { + browser: { + enabled: true, + name: 'chromium', // or 'firefox', 'webkit' + provider: 'playwright', + }, + }, +}) +``` + +## CSS and Assets + +In jsdom/happy-dom, configure CSS handling: + +```ts +defineConfig({ + test: { + css: true, // Process CSS + + // Or with options + css: { + include: /\.module\.css$/, + modules: { + classNameStrategy: 'non-scoped', + }, + }, + }, +}) +``` + +## Fixing External Dependencies + +If external deps fail with CSS/asset errors: + +```ts +defineConfig({ + test: { + server: { + deps: { + inline: ['problematic-package'], + }, + }, + }, +}) +``` + +## Key Points + +- Default is `node` - no browser APIs +- Use `jsdom` for full browser simulation +- Use `happy-dom` for faster tests with basic DOM +- Per-file environment via `// @vitest-environment` comment +- Use projects for multiple environment configurations +- Browser Mode is for real browser testing, not environment + + diff --git a/.agents/skills/vitest/references/advanced-projects.md b/.agents/skills/vitest/references/advanced-projects.md new file mode 100644 index 0000000000000..57b9a7356419d --- /dev/null +++ b/.agents/skills/vitest/references/advanced-projects.md @@ -0,0 +1,300 @@ +--- +name: projects-workspaces +description: Multi-project configuration for monorepos and different test types +--- + +# Projects + +Run different test configurations in the same Vitest process. + +## Basic Projects Setup + +```ts +// vitest.config.ts +defineConfig({ + test: { + projects: [ + // Glob patterns for config files + 'packages/*', + + // Inline config + { + test: { + name: 'unit', + include: ['tests/unit/**/*.test.ts'], + environment: 'node', + }, + }, + { + test: { + name: 'integration', + include: ['tests/integration/**/*.test.ts'], + environment: 'jsdom', + }, + }, + ], + }, +}) +``` + +## Monorepo Pattern + +```ts +defineConfig({ + test: { + projects: [ + // Each package has its own vitest.config.ts + 'packages/core', + 'packages/cli', + 'packages/utils', + ], + }, +}) +``` + +Package config: + +```ts +// packages/core/vitest.config.ts +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + name: 'core', + include: ['src/**/*.test.ts'], + environment: 'node', + }, +}) +``` + +## Different Environments + +Run same tests in different environments: + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'happy-dom', + root: './shared-tests', + environment: 'happy-dom', + setupFiles: ['./setup.happy-dom.ts'], + }, + }, + { + test: { + name: 'node', + root: './shared-tests', + environment: 'node', + setupFiles: ['./setup.node.ts'], + }, + }, + ], + }, +}) +``` + +## Browser + Node Projects + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'unit', + include: ['tests/unit/**/*.test.ts'], + environment: 'node', + }, + }, + { + test: { + name: 'browser', + include: ['tests/browser/**/*.test.ts'], + browser: { + enabled: true, + name: 'chromium', + provider: 'playwright', + }, + }, + }, + ], + }, +}) +``` + +## Shared Configuration + +```ts +// vitest.shared.ts +export const sharedConfig = { + testTimeout: 10000, + setupFiles: ['./tests/setup.ts'], +} + +// vitest.config.ts +import { sharedConfig } from './vitest.shared' + +defineConfig({ + test: { + projects: [ + { + test: { + ...sharedConfig, + name: 'unit', + include: ['tests/unit/**/*.test.ts'], + }, + }, + { + test: { + ...sharedConfig, + name: 'e2e', + include: ['tests/e2e/**/*.test.ts'], + }, + }, + ], + }, +}) +``` + +## Project-Specific Dependencies + +Each project can have different dependencies inlined: + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'project-a', + server: { + deps: { + inline: ['package-a'], + }, + }, + }, + }, + ], + }, +}) +``` + +## Running Specific Projects + +```bash +# Run specific project +vitest --project unit +vitest --project integration + +# Multiple projects +vitest --project unit --project e2e + +# Exclude project +vitest --project.ignore browser +``` + +## Providing Values to Projects + +Share values from config to tests: + +```ts +// vitest.config.ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'staging', + provide: { + apiUrl: 'https://staging.api.com', + debug: true, + }, + }, + }, + { + test: { + name: 'production', + provide: { + apiUrl: 'https://api.com', + debug: false, + }, + }, + }, + ], + }, +}) + +// In tests, use inject +import { inject } from 'vitest' + +test('uses correct api', () => { + const url = inject('apiUrl') + expect(url).toContain('api.com') +}) +``` + +## With Fixtures + +```ts +const test = base.extend({ + apiUrl: ['/default', { injected: true }], +}) + +test('uses injected url', ({ apiUrl }) => { + // apiUrl comes from project's provide config +}) +``` + +## Project Isolation + +Each project runs in its own thread pool by default: + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'isolated', + isolate: true, // Full isolation + pool: 'forks', + }, + }, + ], + }, +}) +``` + +## Global Setup per Project + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'with-db', + globalSetup: ['./tests/db-setup.ts'], + }, + }, + ], + }, +}) +``` + +## Key Points + +- Projects run in same Vitest process +- Each project can have different environment, config +- Use glob patterns for monorepo packages +- Run specific projects with `--project` flag +- Use `provide` to inject config values into tests +- Projects inherit from root config unless overridden + + diff --git a/.agents/skills/vitest/references/advanced-type-testing.md b/.agents/skills/vitest/references/advanced-type-testing.md new file mode 100644 index 0000000000000..f67a034e30e4e --- /dev/null +++ b/.agents/skills/vitest/references/advanced-type-testing.md @@ -0,0 +1,237 @@ +--- +name: type-testing +description: Test TypeScript types with expectTypeOf and assertType +--- + +# Type Testing + +Test TypeScript types without runtime execution. + +## Setup + +Type tests use `.test-d.ts` extension: + +```ts +// math.test-d.ts +import { expectTypeOf } from 'vitest' +import { add } from './math' + +test('add returns number', () => { + expectTypeOf(add).returns.toBeNumber() +}) +``` + +## Configuration + +```ts +defineConfig({ + test: { + typecheck: { + enabled: true, + + // Only type check + only: false, + + // Checker: 'tsc' or 'vue-tsc' + checker: 'tsc', + + // Include patterns + include: ['**/*.test-d.ts'], + + // tsconfig to use + tsconfig: './tsconfig.json', + }, + }, +}) +``` + +## expectTypeOf API + +```ts +import { expectTypeOf } from 'vitest' + +// Basic type checks +expectTypeOf().toBeString() +expectTypeOf().toBeNumber() +expectTypeOf().toBeBoolean() +expectTypeOf().toBeNull() +expectTypeOf().toBeUndefined() +expectTypeOf().toBeVoid() +expectTypeOf().toBeNever() +expectTypeOf().toBeAny() +expectTypeOf().toBeUnknown() +expectTypeOf().toBeObject() +expectTypeOf().toBeFunction() +expectTypeOf<[]>().toBeArray() +expectTypeOf().toBeSymbol() +``` + +## Value Type Checking + +```ts +const value = 'hello' +expectTypeOf(value).toBeString() + +const obj = { name: 'test', count: 42 } +expectTypeOf(obj).toMatchTypeOf<{ name: string }>() +expectTypeOf(obj).toHaveProperty('name') +``` + +## Function Types + +```ts +function greet(name: string): string { + return `Hello, ${name}` +} + +expectTypeOf(greet).toBeFunction() +expectTypeOf(greet).parameters.toEqualTypeOf<[string]>() +expectTypeOf(greet).returns.toBeString() + +// Parameter checking +expectTypeOf(greet).parameter(0).toBeString() +``` + +## Object Types + +```ts +interface User { + id: number + name: string + email?: string +} + +expectTypeOf().toHaveProperty('id') +expectTypeOf().toHaveProperty('name').toBeString() + +// Check shape +expectTypeOf({ id: 1, name: 'test' }).toMatchTypeOf() +``` + +## Equality vs Matching + +```ts +interface A { x: number } +interface B { x: number; y: string } + +// toMatchTypeOf - subset matching +expectTypeOf().toMatchTypeOf() // B extends A + +// toEqualTypeOf - exact match +expectTypeOf().not.toEqualTypeOf() // Not exact match +expectTypeOf().toEqualTypeOf<{ x: number }>() // Exact match +``` + +## Branded Types + +```ts +type UserId = number & { __brand: 'UserId' } +type PostId = number & { __brand: 'PostId' } + +expectTypeOf().not.toEqualTypeOf() +expectTypeOf().not.toEqualTypeOf() +``` + +## Generic Types + +```ts +function identity(value: T): T { + return value +} + +expectTypeOf(identity).returns.toBeString() +expectTypeOf(identity).returns.toBeNumber() +``` + +## Nullable Types + +```ts +type MaybeString = string | null | undefined + +expectTypeOf().toBeNullable() +expectTypeOf().not.toBeNullable() +``` + +## assertType + +Assert a value matches a type (no assertion at runtime): + +```ts +import { assertType } from 'vitest' + +function getUser(): User | null { + return { id: 1, name: 'test' } +} + +test('returns user', () => { + const result = getUser() + + // @ts-expect-error - should fail type check + assertType(result) + + // Correct type + assertType(result) +}) +``` + +## Using @ts-expect-error + +Test that code produces type error: + +```ts +test('rejects wrong types', () => { + function requireString(s: string) {} + + // @ts-expect-error - number not assignable to string + requireString(123) +}) +``` + +## Running Type Tests + +```bash +# Run type tests +vitest typecheck + +# Run alongside unit tests +vitest --typecheck + +# Type tests only +vitest --typecheck.only +``` + +## Mixed Test Files + +Combine runtime and type tests: + +```ts +// user.test.ts +import { describe, expect, expectTypeOf, test } from 'vitest' +import { createUser } from './user' + +describe('createUser', () => { + test('runtime: creates user', () => { + const user = createUser('John') + expect(user.name).toBe('John') + }) + + test('types: returns User type', () => { + expectTypeOf(createUser).returns.toMatchTypeOf<{ name: string }>() + }) +}) +``` + +## Key Points + +- Use `.test-d.ts` for type-only tests +- `expectTypeOf` for type assertions +- `toMatchTypeOf` for subset matching +- `toEqualTypeOf` for exact type matching +- Use `@ts-expect-error` to test type errors +- Run with `vitest typecheck` or `--typecheck` + + diff --git a/.agents/skills/vitest/references/advanced-vi.md b/.agents/skills/vitest/references/advanced-vi.md new file mode 100644 index 0000000000000..57a4784252413 --- /dev/null +++ b/.agents/skills/vitest/references/advanced-vi.md @@ -0,0 +1,249 @@ +--- +name: vi-utilities +description: vi helper for mocking, timers, utilities +--- + +# Vi Utilities + +The `vi` helper provides mocking and utility functions. + +```ts +import { vi } from 'vitest' +``` + +## Mock Functions + +```ts +// Create mock +const fn = vi.fn() +const fnWithImpl = vi.fn((x) => x * 2) + +// Check if mock +vi.isMockFunction(fn) // true + +// Mock methods +fn.mockReturnValue(42) +fn.mockReturnValueOnce(1) +fn.mockResolvedValue(data) +fn.mockRejectedValue(error) +fn.mockImplementation(() => 'result') +fn.mockImplementationOnce(() => 'once') + +// Clear/reset +fn.mockClear() // Clear call history +fn.mockReset() // Clear history + implementation +fn.mockRestore() // Restore original (for spies) +``` + +## Spying + +```ts +const obj = { method: () => 'original' } + +const spy = vi.spyOn(obj, 'method') +obj.method() + +expect(spy).toHaveBeenCalled() + +// Mock implementation +spy.mockReturnValue('mocked') + +// Spy on getter/setter +vi.spyOn(obj, 'prop', 'get').mockReturnValue('value') +``` + +## Module Mocking + +```ts +// Hoisted to top of file +vi.mock('./module', () => ({ + fn: vi.fn(), +})) + +// Partial mock +vi.mock('./module', async (importOriginal) => ({ + ...(await importOriginal()), + specificFn: vi.fn(), +})) + +// Spy mode - keep implementation +vi.mock('./module', { spy: true }) + +// Import actual module inside mock +const actual = await vi.importActual('./module') + +// Import as mock +const mocked = await vi.importMock('./module') +``` + +## Dynamic Mocking + +```ts +// Not hoisted - use with dynamic imports +vi.doMock('./config', () => ({ key: 'value' })) +const config = await import('./config') + +// Unmock +vi.doUnmock('./config') +vi.unmock('./module') // Hoisted +``` + +## Reset Modules + +```ts +// Clear module cache +vi.resetModules() + +// Wait for dynamic imports +await vi.dynamicImportSettled() +``` + +## Fake Timers + +```ts +vi.useFakeTimers() + +setTimeout(() => console.log('done'), 1000) + +// Advance time +vi.advanceTimersByTime(1000) +vi.advanceTimersByTimeAsync(1000) // For async callbacks +vi.advanceTimersToNextTimer() +vi.advanceTimersToNextFrame() // requestAnimationFrame + +// Run all timers +vi.runAllTimers() +vi.runAllTimersAsync() +vi.runOnlyPendingTimers() + +// Clear timers +vi.clearAllTimers() + +// Check state +vi.getTimerCount() +vi.isFakeTimers() + +// Restore +vi.useRealTimers() +``` + +## Mock Date/Time + +```ts +vi.setSystemTime(new Date('2024-01-01')) +expect(new Date().getFullYear()).toBe(2024) + +vi.getMockedSystemTime() // Get mocked date +vi.getRealSystemTime() // Get real time (ms) +``` + +## Global/Env Mocking + +```ts +// Stub global +vi.stubGlobal('fetch', vi.fn()) +vi.unstubAllGlobals() + +// Stub environment +vi.stubEnv('API_KEY', 'test') +vi.stubEnv('NODE_ENV', 'test') +vi.unstubAllEnvs() +``` + +## Hoisted Code + +Run code before imports: + +```ts +const mock = vi.hoisted(() => vi.fn()) + +vi.mock('./module', () => ({ + fn: mock, // Can reference hoisted variable +})) +``` + +## Waiting Utilities + +```ts +// Wait for callback to succeed +await vi.waitFor(async () => { + const el = document.querySelector('.loaded') + expect(el).toBeTruthy() +}, { timeout: 5000, interval: 100 }) + +// Wait for truthy value +const element = await vi.waitUntil( + () => document.querySelector('.loaded'), + { timeout: 5000 } +) +``` + +## Mock Object + +Mock all methods of an object: + +```ts +const original = { + method: () => 'real', + nested: { fn: () => 'nested' }, +} + +const mocked = vi.mockObject(original) +mocked.method() // undefined (mocked) +mocked.method.mockReturnValue('mocked') + +// Spy mode +const spied = vi.mockObject(original, { spy: true }) +spied.method() // 'real' +expect(spied.method).toHaveBeenCalled() +``` + +## Test Configuration + +```ts +vi.setConfig({ + testTimeout: 10_000, + hookTimeout: 10_000, +}) + +vi.resetConfig() +``` + +## Global Mock Management + +```ts +vi.clearAllMocks() // Clear all mock call history +vi.resetAllMocks() // Reset + clear implementation +vi.restoreAllMocks() // Restore originals (spies) +``` + +## vi.mocked Type Helper + +TypeScript helper for mocked values: + +```ts +import { myFn } from './module' +vi.mock('./module') + +// Type as mock +vi.mocked(myFn).mockReturnValue('typed') + +// Deep mocking +vi.mocked(myModule, { deep: true }) + +// Partial mock typing +vi.mocked(fn, { partial: true }).mockResolvedValue({ ok: true }) +``` + +## Key Points + +- `vi.mock` is hoisted - use `vi.doMock` for dynamic mocking +- `vi.hoisted` lets you reference variables in mock factories +- Use `vi.spyOn` to spy on existing methods +- Fake timers require explicit setup and teardown +- `vi.waitFor` retries until assertion passes + + diff --git a/.agents/skills/vitest/references/core-cli.md b/.agents/skills/vitest/references/core-cli.md new file mode 100644 index 0000000000000..7a05c049bee73 --- /dev/null +++ b/.agents/skills/vitest/references/core-cli.md @@ -0,0 +1,166 @@ +--- +name: vitest-cli +description: Command line interface commands and options +--- + +# Command Line Interface + +## Commands + +### `vitest` + +Start Vitest in watch mode (dev) or run mode (CI): + +```bash +vitest # Watch mode in dev, run mode in CI +vitest foobar # Run tests containing "foobar" in path +vitest basic/foo.test.ts:10 # Run specific test by file and line number +``` + +### `vitest run` + +Run tests once without watch mode: + +```bash +vitest run +vitest run --coverage +``` + +### `vitest watch` + +Explicitly start watch mode: + +```bash +vitest watch +``` + +### `vitest related` + +Run tests that import specific files (useful with lint-staged): + +```bash +vitest related src/index.ts src/utils.ts --run +``` + +### `vitest bench` + +Run only benchmark tests: + +```bash +vitest bench +``` + +### `vitest list` + +List all matching tests without running them: + +```bash +vitest list # List test names +vitest list --json # Output as JSON +vitest list --filesOnly # List only test files +``` + +### `vitest init` + +Initialize project setup: + +```bash +vitest init browser # Set up browser testing +``` + +## Common Options + +```bash +# Configuration +--config # Path to config file +--project # Run specific project + +# Filtering +--testNamePattern, -t # Run tests matching pattern +--changed # Run tests for changed files +--changed HEAD~1 # Tests for last commit changes + +# Reporters +--reporter # default, verbose, dot, json, html +--reporter=html --outputFile=report.html + +# Coverage +--coverage # Enable coverage +--coverage.provider v8 # Use v8 provider +--coverage.reporter text,html + +# Execution +--shard / # Split tests across machines +--bail # Stop after n failures +--retry # Retry failed tests n times +--sequence.shuffle # Randomize test order + +# Watch mode +--no-watch # Disable watch mode +--standalone # Start without running tests + +# Environment +--environment # jsdom, happy-dom, node +--globals # Enable global APIs + +# Debugging +--inspect # Enable Node inspector +--inspect-brk # Break on start + +# Output +--silent # Suppress console output +--no-color # Disable colors +``` + +## Package.json Scripts + +```json +{ + "scripts": { + "test": "vitest", + "test:run": "vitest run", + "test:ui": "vitest --ui", + "coverage": "vitest run --coverage" + } +} +``` + +## Sharding for CI + +Split tests across multiple machines: + +```bash +# Machine 1 +vitest run --shard=1/3 --reporter=blob + +# Machine 2 +vitest run --shard=2/3 --reporter=blob + +# Machine 3 +vitest run --shard=3/3 --reporter=blob + +# Merge reports +vitest --merge-reports --reporter=junit +``` + +## Watch Mode Keyboard Shortcuts + +In watch mode, press: +- `a` - Run all tests +- `f` - Run only failed tests +- `u` - Update snapshots +- `p` - Filter by filename pattern +- `t` - Filter by test name pattern +- `q` - Quit + +## Key Points + +- Watch mode is default in dev, run mode in CI (when `process.env.CI` is set) +- Use `--run` flag to ensure single run (important for lint-staged) +- Both camelCase (`--testTimeout`) and kebab-case (`--test-timeout`) work +- Boolean options can be negated with `--no-` prefix + + diff --git a/.agents/skills/vitest/references/core-config.md b/.agents/skills/vitest/references/core-config.md new file mode 100644 index 0000000000000..76002a58fbc4a --- /dev/null +++ b/.agents/skills/vitest/references/core-config.md @@ -0,0 +1,174 @@ +--- +name: vitest-configuration +description: Configure Vitest with vite.config.ts or vitest.config.ts +--- + +# Configuration + +Vitest reads configuration from `vitest.config.ts` or `vite.config.ts`. It shares the same config format as Vite. + +## Basic Setup + +```ts +// vitest.config.ts +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + // test options + }, +}) +``` + +## Using with Existing Vite Config + +Add Vitest types reference and use the `test` property: + +```ts +// vite.config.ts +/// +import { defineConfig } from 'vite' + +export default defineConfig({ + test: { + globals: true, + environment: 'jsdom', + }, +}) +``` + +## Merging Configs + +If you have separate config files, use `mergeConfig`: + +```ts +// vitest.config.ts +import { defineConfig, mergeConfig } from 'vitest/config' +import viteConfig from './vite.config' + +export default mergeConfig(viteConfig, defineConfig({ + test: { + environment: 'jsdom', + }, +})) +``` + +## Common Options + +```ts +defineConfig({ + test: { + // Enable global APIs (describe, it, expect) without imports + globals: true, + + // Test environment: 'node', 'jsdom', 'happy-dom' + environment: 'node', + + // Setup files to run before each test file + setupFiles: ['./tests/setup.ts'], + + // Include patterns for test files + include: ['**/*.{test,spec}.{js,ts,jsx,tsx}'], + + // Exclude patterns + exclude: ['**/node_modules/**', '**/dist/**'], + + // Test timeout in ms + testTimeout: 5000, + + // Hook timeout in ms + hookTimeout: 10000, + + // Enable watch mode by default + watch: true, + + // Coverage configuration + coverage: { + provider: 'v8', // or 'istanbul' + reporter: ['text', 'html'], + include: ['src/**/*.ts'], + }, + + // Run tests in isolation (each file in separate process) + isolate: true, + + // Pool for running tests: 'threads', 'forks', 'vmThreads' + pool: 'threads', + + // Number of threads/processes + poolOptions: { + threads: { + maxThreads: 4, + minThreads: 1, + }, + }, + + // Automatically clear mocks between tests + clearMocks: true, + + // Restore mocks between tests + restoreMocks: true, + + // Retry failed tests + retry: 0, + + // Stop after first failure + bail: 0, + }, +}) +``` + +## Conditional Configuration + +Use `mode` or `process.env.VITEST` for test-specific config: + +```ts +export default defineConfig(({ mode }) => ({ + plugins: mode === 'test' ? [] : [myPlugin()], + test: { + // test options + }, +})) +``` + +## Projects (Monorepos) + +Run different configurations in the same Vitest process: + +```ts +defineConfig({ + test: { + projects: [ + 'packages/*', + { + test: { + name: 'unit', + include: ['tests/unit/**/*.test.ts'], + environment: 'node', + }, + }, + { + test: { + name: 'integration', + include: ['tests/integration/**/*.test.ts'], + environment: 'jsdom', + }, + }, + ], + }, +}) +``` + +## Key Points + +- Vitest uses Vite's transformation pipeline - same `resolve.alias`, plugins work +- `vitest.config.ts` takes priority over `vite.config.ts` +- Use `--config` flag to specify a custom config path +- `process.env.VITEST` is set to `true` when running tests +- Test config uses `test` property, rest is Vite config + + diff --git a/.agents/skills/vitest/references/core-describe.md b/.agents/skills/vitest/references/core-describe.md new file mode 100644 index 0000000000000..3f7f3fe1cb67f --- /dev/null +++ b/.agents/skills/vitest/references/core-describe.md @@ -0,0 +1,193 @@ +--- +name: describe-api +description: describe/suite for grouping tests into logical blocks +--- + +# Describe API + +Group related tests into suites for organization and shared setup. + +## Basic Usage + +```ts +import { describe, expect, test } from 'vitest' + +describe('Math', () => { + test('adds numbers', () => { + expect(1 + 1).toBe(2) + }) + + test('subtracts numbers', () => { + expect(3 - 1).toBe(2) + }) +}) + +// Alias: suite +import { suite } from 'vitest' +suite('equivalent to describe', () => {}) +``` + +## Nested Suites + +```ts +describe('User', () => { + describe('when logged in', () => { + test('shows dashboard', () => {}) + test('can update profile', () => {}) + }) + + describe('when logged out', () => { + test('shows login page', () => {}) + }) +}) +``` + +## Suite Options + +```ts +// All tests inherit options +describe('slow tests', { timeout: 30_000 }, () => { + test('test 1', () => {}) // 30s timeout + test('test 2', () => {}) // 30s timeout +}) +``` + +## Suite Modifiers + +### Skip Suites + +```ts +describe.skip('skipped suite', () => { + test('wont run', () => {}) +}) + +// Conditional +describe.skipIf(process.env.CI)('not in CI', () => {}) +describe.runIf(!process.env.CI)('only local', () => {}) +``` + +### Focus Suites + +```ts +describe.only('only this suite runs', () => { + test('runs', () => {}) +}) +``` + +### Todo Suites + +```ts +describe.todo('implement later') +``` + +### Concurrent Suites + +```ts +// All tests run in parallel +describe.concurrent('parallel tests', () => { + test('test 1', async ({ expect }) => {}) + test('test 2', async ({ expect }) => {}) +}) +``` + +### Sequential in Concurrent + +```ts +describe.concurrent('parallel', () => { + test('concurrent 1', async () => {}) + + describe.sequential('must be sequential', () => { + test('step 1', async () => {}) + test('step 2', async () => {}) + }) +}) +``` + +### Shuffle Tests + +```ts +describe.shuffle('random order', () => { + test('test 1', () => {}) + test('test 2', () => {}) + test('test 3', () => {}) +}) + +// Or with option +describe('random', { shuffle: true }, () => {}) +``` + +## Parameterized Suites + +### describe.each + +```ts +describe.each([ + { name: 'Chrome', version: 100 }, + { name: 'Firefox', version: 90 }, +])('$name browser', ({ name, version }) => { + test('has version', () => { + expect(version).toBeGreaterThan(0) + }) +}) +``` + +### describe.for + +```ts +describe.for([ + ['Chrome', 100], + ['Firefox', 90], +])('%s browser', ([name, version]) => { + test('has version', () => { + expect(version).toBeGreaterThan(0) + }) +}) +``` + +## Hooks in Suites + +```ts +describe('Database', () => { + let db + + beforeAll(async () => { + db = await createDb() + }) + + afterAll(async () => { + await db.close() + }) + + beforeEach(async () => { + await db.clear() + }) + + test('insert works', async () => { + await db.insert({ name: 'test' }) + expect(await db.count()).toBe(1) + }) +}) +``` + +## Modifier Combinations + +All modifiers can be chained: + +```ts +describe.skip.concurrent('skipped concurrent', () => {}) +describe.only.shuffle('only and shuffled', () => {}) +describe.concurrent.skip('equivalent', () => {}) +``` + +## Key Points + +- Top-level tests belong to an implicit file suite +- Nested suites inherit parent's options (timeout, retry, etc.) +- Hooks are scoped to their suite and nested suites +- Use `describe.concurrent` with context's `expect` for snapshots +- Shuffle order depends on `sequence.seed` config + + diff --git a/.agents/skills/vitest/references/core-expect.md b/.agents/skills/vitest/references/core-expect.md new file mode 100644 index 0000000000000..91de00a64ed4a --- /dev/null +++ b/.agents/skills/vitest/references/core-expect.md @@ -0,0 +1,219 @@ +--- +name: expect-api +description: Assertions with matchers, asymmetric matchers, and custom matchers +--- + +# Expect API + +Vitest uses Chai assertions with Jest-compatible API. + +## Basic Assertions + +```ts +import { expect, test } from 'vitest' + +test('assertions', () => { + // Equality + expect(1 + 1).toBe(2) // Strict equality (===) + expect({ a: 1 }).toEqual({ a: 1 }) // Deep equality + + // Truthiness + expect(true).toBeTruthy() + expect(false).toBeFalsy() + expect(null).toBeNull() + expect(undefined).toBeUndefined() + expect('value').toBeDefined() + + // Numbers + expect(10).toBeGreaterThan(5) + expect(10).toBeGreaterThanOrEqual(10) + expect(5).toBeLessThan(10) + expect(0.1 + 0.2).toBeCloseTo(0.3, 5) + + // Strings + expect('hello world').toMatch(/world/) + expect('hello').toContain('ell') + + // Arrays + expect([1, 2, 3]).toContain(2) + expect([{ a: 1 }]).toContainEqual({ a: 1 }) + expect([1, 2, 3]).toHaveLength(3) + + // Objects + expect({ a: 1, b: 2 }).toHaveProperty('a') + expect({ a: 1, b: 2 }).toHaveProperty('a', 1) + expect({ a: { b: 1 } }).toHaveProperty('a.b', 1) + expect({ a: 1 }).toMatchObject({ a: 1 }) + + // Types + expect('string').toBeTypeOf('string') + expect(new Date()).toBeInstanceOf(Date) +}) +``` + +## Negation + +```ts +expect(1).not.toBe(2) +expect({ a: 1 }).not.toEqual({ a: 2 }) +``` + +## Error Assertions + +```ts +// Sync errors - wrap in function +expect(() => throwError()).toThrow() +expect(() => throwError()).toThrow('message') +expect(() => throwError()).toThrow(/pattern/) +expect(() => throwError()).toThrow(CustomError) + +// Async errors - use rejects +await expect(asyncThrow()).rejects.toThrow('error') +``` + +## Promise Assertions + +```ts +// Resolves +await expect(Promise.resolve(1)).resolves.toBe(1) +await expect(fetchData()).resolves.toEqual({ data: true }) + +// Rejects +await expect(Promise.reject('error')).rejects.toBe('error') +await expect(failingFetch()).rejects.toThrow() +``` + +## Spy/Mock Assertions + +```ts +const fn = vi.fn() +fn('arg1', 'arg2') +fn('arg3') + +expect(fn).toHaveBeenCalled() +expect(fn).toHaveBeenCalledTimes(2) +expect(fn).toHaveBeenCalledWith('arg1', 'arg2') +expect(fn).toHaveBeenLastCalledWith('arg3') +expect(fn).toHaveBeenNthCalledWith(1, 'arg1', 'arg2') + +expect(fn).toHaveReturned() +expect(fn).toHaveReturnedWith(value) +``` + +## Asymmetric Matchers + +Use inside `toEqual`, `toHaveBeenCalledWith`, etc: + +```ts +expect({ id: 1, name: 'test' }).toEqual({ + id: expect.any(Number), + name: expect.any(String), +}) + +expect({ a: 1, b: 2, c: 3 }).toEqual( + expect.objectContaining({ a: 1 }) +) + +expect([1, 2, 3, 4]).toEqual( + expect.arrayContaining([1, 3]) +) + +expect('hello world').toEqual( + expect.stringContaining('world') +) + +expect('hello world').toEqual( + expect.stringMatching(/world$/) +) + +expect({ value: null }).toEqual({ + value: expect.anything() // Matches anything except null/undefined +}) + +// Negate with expect.not +expect([1, 2]).toEqual( + expect.not.arrayContaining([3]) +) +``` + +## Soft Assertions + +Continue test after failure: + +```ts +expect.soft(1).toBe(2) // Marks test failed but continues +expect.soft(2).toBe(3) // Also runs +// All failures reported at end +``` + +## Poll Assertions + +Retry until passes: + +```ts +await expect.poll(() => fetchStatus()).toBe('ready') + +await expect.poll( + () => document.querySelector('.element'), + { interval: 100, timeout: 5000 } +).toBeTruthy() +``` + +## Assertion Count + +```ts +test('async assertions', async () => { + expect.assertions(2) // Exactly 2 assertions must run + + await doAsync((data) => { + expect(data).toBeDefined() + expect(data.id).toBe(1) + }) +}) + +test('at least one', () => { + expect.hasAssertions() // At least 1 assertion must run +}) +``` + +## Extending Matchers + +```ts +expect.extend({ + toBeWithinRange(received, floor, ceiling) { + const pass = received >= floor && received <= ceiling + return { + pass, + message: () => + `expected ${received} to be within range ${floor} - ${ceiling}`, + } + }, +}) + +test('custom matcher', () => { + expect(100).toBeWithinRange(90, 110) +}) +``` + +## Snapshot Assertions + +```ts +expect(data).toMatchSnapshot() +expect(data).toMatchInlineSnapshot(`{ "id": 1 }`) +await expect(result).toMatchFileSnapshot('./expected.json') + +expect(() => throw new Error('fail')).toThrowErrorMatchingSnapshot() +``` + +## Key Points + +- Use `toBe` for primitives, `toEqual` for objects/arrays +- `toStrictEqual` checks undefined properties and array sparseness +- Always `await` async assertions (`resolves`, `rejects`, `poll`) +- Use context's `expect` in concurrent tests for correct tracking +- `toThrow` requires wrapping sync code in a function + + diff --git a/.agents/skills/vitest/references/core-hooks.md b/.agents/skills/vitest/references/core-hooks.md new file mode 100644 index 0000000000000..d0c2bfa098d83 --- /dev/null +++ b/.agents/skills/vitest/references/core-hooks.md @@ -0,0 +1,244 @@ +--- +name: lifecycle-hooks +description: beforeEach, afterEach, beforeAll, afterAll, and around hooks +--- + +# Lifecycle Hooks + +## Basic Hooks + +```ts +import { afterAll, afterEach, beforeAll, beforeEach, test } from 'vitest' + +beforeAll(async () => { + // Runs once before all tests in file/suite + await setupDatabase() +}) + +afterAll(async () => { + // Runs once after all tests in file/suite + await teardownDatabase() +}) + +beforeEach(async () => { + // Runs before each test + await clearTestData() +}) + +afterEach(async () => { + // Runs after each test + await cleanupMocks() +}) +``` + +## Cleanup Return Pattern + +Return cleanup function from `before*` hooks: + +```ts +beforeAll(async () => { + const server = await startServer() + + // Returned function runs as afterAll + return async () => { + await server.close() + } +}) + +beforeEach(async () => { + const connection = await connect() + + // Runs as afterEach + return () => connection.close() +}) +``` + +## Scoped Hooks + +Hooks apply to current suite and nested suites: + +```ts +describe('outer', () => { + beforeEach(() => console.log('outer before')) + + test('test 1', () => {}) // outer before → test + + describe('inner', () => { + beforeEach(() => console.log('inner before')) + + test('test 2', () => {}) // outer before → inner before → test + }) +}) +``` + +## Hook Timeout + +```ts +beforeAll(async () => { + await slowSetup() +}, 30_000) // 30 second timeout +``` + +## Around Hooks + +Wrap tests with setup/teardown context: + +```ts +import { aroundEach, test } from 'vitest' + +// Wrap each test in database transaction +aroundEach(async (runTest) => { + await db.beginTransaction() + await runTest() // Must be called! + await db.rollback() +}) + +test('insert user', async () => { + await db.insert({ name: 'Alice' }) + // Automatically rolled back after test +}) +``` + +### aroundAll + +Wrap entire suite: + +```ts +import { aroundAll, test } from 'vitest' + +aroundAll(async (runSuite) => { + console.log('before all tests') + await runSuite() // Must be called! + console.log('after all tests') +}) +``` + +### Multiple Around Hooks + +Nested like onion layers: + +```ts +aroundEach(async (runTest) => { + console.log('outer before') + await runTest() + console.log('outer after') +}) + +aroundEach(async (runTest) => { + console.log('inner before') + await runTest() + console.log('inner after') +}) + +// Order: outer before → inner before → test → inner after → outer after +``` + +## Test Hooks + +Inside test body: + +```ts +import { onTestFailed, onTestFinished, test } from 'vitest' + +test('with cleanup', () => { + const db = connect() + + // Runs after test finishes (pass or fail) + onTestFinished(() => db.close()) + + // Only runs if test fails + onTestFailed(({ task }) => { + console.log('Failed:', task.result?.errors) + }) + + db.query('SELECT * FROM users') +}) +``` + +### Reusable Cleanup Pattern + +```ts +function useTestDb() { + const db = connect() + onTestFinished(() => db.close()) + return db +} + +test('query users', () => { + const db = useTestDb() + expect(db.query('SELECT * FROM users')).toBeDefined() +}) + +test('query orders', () => { + const db = useTestDb() // Fresh connection, auto-closed + expect(db.query('SELECT * FROM orders')).toBeDefined() +}) +``` + +## Concurrent Test Hooks + +For concurrent tests, use context's hooks: + +```ts +test.concurrent('concurrent', ({ onTestFinished }) => { + const resource = allocate() + onTestFinished(() => resource.release()) +}) +``` + +## Extended Test Hooks + +With `test.extend`, hooks are type-aware: + +```ts +const test = base.extend<{ db: Database }>({ + db: async ({}, use) => { + const db = await createDb() + await use(db) + await db.close() + }, +}) + +// These hooks know about `db` fixture +test.beforeEach(({ db }) => { + db.seed() +}) + +test.afterEach(({ db }) => { + db.clear() +}) +``` + +## Hook Execution Order + +Default order (stack): +1. `beforeAll` (in order) +2. `beforeEach` (in order) +3. Test +4. `afterEach` (reverse order) +5. `afterAll` (reverse order) + +Configure with `sequence.hooks`: + +```ts +defineConfig({ + test: { + sequence: { + hooks: 'list', // 'stack' (default), 'list', 'parallel' + }, + }, +}) +``` + +## Key Points + +- Hooks are not called during type checking +- Return cleanup function from `before*` to avoid `after*` duplication +- `aroundEach`/`aroundAll` must call `runTest()`/`runSuite()` +- `onTestFinished` always runs, even if test fails +- Use context hooks for concurrent tests + + diff --git a/.agents/skills/vitest/references/core-test-api.md b/.agents/skills/vitest/references/core-test-api.md new file mode 100644 index 0000000000000..1f3c93238a386 --- /dev/null +++ b/.agents/skills/vitest/references/core-test-api.md @@ -0,0 +1,233 @@ +--- +name: test-api +description: test/it function for defining tests with modifiers +--- + +# Test API + +## Basic Test + +```ts +import { expect, test } from 'vitest' + +test('adds numbers', () => { + expect(1 + 1).toBe(2) +}) + +// Alias: it +import { it } from 'vitest' + +it('works the same', () => { + expect(true).toBe(true) +}) +``` + +## Async Tests + +```ts +test('async test', async () => { + const result = await fetchData() + expect(result).toBeDefined() +}) + +// Promises are automatically awaited +test('returns promise', () => { + return fetchData().then(result => { + expect(result).toBeDefined() + }) +}) +``` + +## Test Options + +```ts +// Timeout (default: 5000ms) +test('slow test', async () => { + // ... +}, 10_000) + +// Or with options object +test('with options', { timeout: 10_000, retry: 2 }, async () => { + // ... +}) +``` + +## Test Modifiers + +### Skip Tests + +```ts +test.skip('skipped test', () => { + // Won't run +}) + +// Conditional skip +test.skipIf(process.env.CI)('not in CI', () => {}) +test.runIf(process.env.CI)('only in CI', () => {}) + +// Dynamic skip via context +test('dynamic skip', ({ skip }) => { + skip(someCondition, 'reason') + // ... +}) +``` + +### Focus Tests + +```ts +test.only('only this runs', () => { + // Other tests in file are skipped +}) +``` + +### Todo Tests + +```ts +test.todo('implement later') + +test.todo('with body', () => { + // Not run, shows in report +}) +``` + +### Failing Tests + +```ts +test.fails('expected to fail', () => { + expect(1).toBe(2) // Test passes because assertion fails +}) +``` + +### Concurrent Tests + +```ts +// Run tests in parallel +test.concurrent('test 1', async ({ expect }) => { + // Use context.expect for concurrent tests + expect(await fetch1()).toBe('result') +}) + +test.concurrent('test 2', async ({ expect }) => { + expect(await fetch2()).toBe('result') +}) +``` + +### Sequential Tests + +```ts +// Force sequential in concurrent context +test.sequential('must run alone', async () => {}) +``` + +## Parameterized Tests + +### test.each + +```ts +test.each([ + [1, 1, 2], + [1, 2, 3], + [2, 1, 3], +])('add(%i, %i) = %i', (a, b, expected) => { + expect(a + b).toBe(expected) +}) + +// With objects +test.each([ + { a: 1, b: 1, expected: 2 }, + { a: 1, b: 2, expected: 3 }, +])('add($a, $b) = $expected', ({ a, b, expected }) => { + expect(a + b).toBe(expected) +}) + +// Template literal +test.each` + a | b | expected + ${1} | ${1} | ${2} + ${1} | ${2} | ${3} +`('add($a, $b) = $expected', ({ a, b, expected }) => { + expect(a + b).toBe(expected) +}) +``` + +### test.for + +Preferred over `.each` - doesn't spread arrays: + +```ts +test.for([ + [1, 1, 2], + [1, 2, 3], +])('add(%i, %i) = %i', ([a, b, expected], { expect }) => { + // Second arg is TestContext + expect(a + b).toBe(expected) +}) +``` + +## Test Context + +First argument provides context utilities: + +```ts +test('with context', ({ expect, skip, task }) => { + console.log(task.name) // Test name + skip(someCondition) // Skip dynamically + expect(1).toBe(1) // Context-bound expect +}) +``` + +## Custom Test with Fixtures + +```ts +import { test as base } from 'vitest' + +const test = base.extend({ + db: async ({}, use) => { + const db = await createDb() + await use(db) + await db.close() + }, +}) + +test('query', async ({ db }) => { + const users = await db.query('SELECT * FROM users') + expect(users).toBeDefined() +}) +``` + +## Retry Configuration + +```ts +test('flaky test', { retry: 3 }, async () => { + // Retries up to 3 times on failure +}) + +// Advanced retry options +test('with delay', { + retry: { + count: 3, + delay: 1000, + condition: /timeout/i, // Only retry on timeout errors + }, +}, async () => {}) +``` + +## Tags + +```ts +test('database test', { tags: ['db', 'slow'] }, async () => {}) + +// Run with: vitest --tags db +``` + +## Key Points + +- Tests with no body are marked as `todo` +- `test.only` throws in CI unless `allowOnly: true` +- Use context's `expect` for concurrent tests and snapshots +- Function name is used as test name if passed as first arg + + diff --git a/.agents/skills/vitest/references/features-concurrency.md b/.agents/skills/vitest/references/features-concurrency.md new file mode 100644 index 0000000000000..412f60d8214be --- /dev/null +++ b/.agents/skills/vitest/references/features-concurrency.md @@ -0,0 +1,250 @@ +--- +name: concurrency-parallelism +description: Concurrent tests, parallel execution, and sharding +--- + +# Concurrency & Parallelism + +## File Parallelism + +By default, Vitest runs test files in parallel across workers: + +```ts +defineConfig({ + test: { + // Run files in parallel (default: true) + fileParallelism: true, + + // Number of worker threads + maxWorkers: 4, + minWorkers: 1, + + // Pool type: 'threads', 'forks', 'vmThreads' + pool: 'threads', + }, +}) +``` + +## Concurrent Tests + +Run tests within a file in parallel: + +```ts +// Individual concurrent tests +test.concurrent('test 1', async ({ expect }) => { + expect(await fetch1()).toBe('result') +}) + +test.concurrent('test 2', async ({ expect }) => { + expect(await fetch2()).toBe('result') +}) + +// All tests in suite concurrent +describe.concurrent('parallel suite', () => { + test('test 1', async ({ expect }) => {}) + test('test 2', async ({ expect }) => {}) +}) +``` + +**Important:** Use `{ expect }` from context for concurrent tests. + +## Sequential in Concurrent Context + +Force sequential execution: + +```ts +describe.concurrent('mostly parallel', () => { + test('parallel 1', async () => {}) + test('parallel 2', async () => {}) + + test.sequential('must run alone 1', async () => {}) + test.sequential('must run alone 2', async () => {}) +}) + +// Or entire suite +describe.sequential('sequential suite', () => { + test('first', () => {}) + test('second', () => {}) +}) +``` + +## Max Concurrency + +Limit concurrent tests: + +```ts +defineConfig({ + test: { + maxConcurrency: 5, // Max concurrent tests per file + }, +}) +``` + +## Isolation + +Each file runs in isolated environment by default: + +```ts +defineConfig({ + test: { + // Disable isolation for faster runs (less safe) + isolate: false, + }, +}) +``` + +## Sharding + +Split tests across machines: + +```bash +# Machine 1 +vitest run --shard=1/3 + +# Machine 2 +vitest run --shard=2/3 + +# Machine 3 +vitest run --shard=3/3 +``` + +### CI Example (GitHub Actions) + +```yaml +jobs: + test: + strategy: + matrix: + shard: [1, 2, 3] + steps: + - run: vitest run --shard=${{ matrix.shard }}/3 --reporter=blob + + merge: + needs: test + steps: + - run: vitest --merge-reports --reporter=junit +``` + +### Merge Reports + +```bash +# Each shard outputs blob +vitest run --shard=1/3 --reporter=blob --coverage +vitest run --shard=2/3 --reporter=blob --coverage + +# Merge all blobs +vitest --merge-reports --reporter=json --coverage +``` + +## Test Sequence + +Control test order: + +```ts +defineConfig({ + test: { + sequence: { + // Run tests in random order + shuffle: true, + + // Seed for reproducible shuffle + seed: 12345, + + // Hook execution order + hooks: 'stack', // 'stack', 'list', 'parallel' + + // All tests concurrent by default + concurrent: true, + }, + }, +}) +``` + +## Shuffle Tests + +Randomize to catch hidden dependencies: + +```ts +// Via CLI +vitest --sequence.shuffle + +// Per suite +describe.shuffle('random order', () => { + test('test 1', () => {}) + test('test 2', () => {}) + test('test 3', () => {}) +}) +``` + +## Pool Options + +### Threads (Default) + +```ts +defineConfig({ + test: { + pool: 'threads', + poolOptions: { + threads: { + maxThreads: 8, + minThreads: 2, + isolate: true, + }, + }, + }, +}) +``` + +### Forks + +Better isolation, slower: + +```ts +defineConfig({ + test: { + pool: 'forks', + poolOptions: { + forks: { + maxForks: 4, + isolate: true, + }, + }, + }, +}) +``` + +### VM Threads + +Full VM isolation per file: + +```ts +defineConfig({ + test: { + pool: 'vmThreads', + }, +}) +``` + +## Bail on Failure + +Stop after first failure: + +```bash +vitest --bail 1 # Stop after 1 failure +vitest --bail # Stop on first failure (same as --bail 1) +``` + +## Key Points + +- Files run in parallel by default +- Use `.concurrent` for parallel tests within file +- Always use context's `expect` in concurrent tests +- Sharding splits tests across CI machines +- Use `--merge-reports` to combine sharded results +- Shuffle tests to find hidden dependencies + + diff --git a/.agents/skills/vitest/references/features-context.md b/.agents/skills/vitest/references/features-context.md new file mode 100644 index 0000000000000..a9db0a1f61b0b --- /dev/null +++ b/.agents/skills/vitest/references/features-context.md @@ -0,0 +1,238 @@ +--- +name: test-context-fixtures +description: Test context, custom fixtures with test.extend +--- + +# Test Context & Fixtures + +## Built-in Context + +Every test receives context as first argument: + +```ts +test('context', ({ task, expect, skip }) => { + console.log(task.name) // Test name + expect(1).toBe(1) // Context-bound expect + skip() // Skip test dynamically +}) +``` + +### Context Properties + +- `task` - Test metadata (name, file, etc.) +- `expect` - Expect bound to this test (important for concurrent tests) +- `skip(condition?, message?)` - Skip the test +- `onTestFinished(fn)` - Cleanup after test +- `onTestFailed(fn)` - Run on failure only + +## Custom Fixtures with test.extend + +Create reusable test utilities: + +```ts +import { test as base } from 'vitest' + +// Define fixture types +interface Fixtures { + db: Database + user: User +} + +// Create extended test +export const test = base.extend({ + // Fixture with setup/teardown + db: async ({}, use) => { + const db = await createDatabase() + await use(db) // Provide to test + await db.close() // Cleanup + }, + + // Fixture depending on another fixture + user: async ({ db }, use) => { + const user = await db.createUser({ name: 'Test' }) + await use(user) + await db.deleteUser(user.id) + }, +}) +``` + +Using fixtures: + +```ts +test('query user', async ({ db, user }) => { + const found = await db.findUser(user.id) + expect(found).toEqual(user) +}) +``` + +## Fixture Initialization + +Fixtures only initialize when accessed: + +```ts +const test = base.extend({ + expensive: async ({}, use) => { + console.log('initializing') // Only runs if test uses it + await use('value') + }, +}) + +test('no fixture', () => {}) // expensive not called +test('uses fixture', ({ expensive }) => {}) // expensive called +``` + +## Auto Fixtures + +Run fixture for every test: + +```ts +const test = base.extend({ + setup: [ + async ({}, use) => { + await globalSetup() + await use() + await globalTeardown() + }, + { auto: true } // Always run + ], +}) +``` + +## Scoped Fixtures + +### File Scope + +Initialize once per file: + +```ts +const test = base.extend({ + connection: [ + async ({}, use) => { + const conn = await connect() + await use(conn) + await conn.close() + }, + { scope: 'file' } + ], +}) +``` + +### Worker Scope + +Initialize once per worker: + +```ts +const test = base.extend({ + sharedResource: [ + async ({}, use) => { + await use(globalResource) + }, + { scope: 'worker' } + ], +}) +``` + +## Injected Fixtures (from Config) + +Override fixtures per project: + +```ts +// test file +const test = base.extend({ + apiUrl: ['/default', { injected: true }], +}) + +// vitest.config.ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'prod', + provide: { apiUrl: 'https://api.prod.com' }, + }, + }, + ], + }, +}) +``` + +## Scoped Values per Suite + +Override fixture for specific suite: + +```ts +const test = base.extend({ + environment: 'development', +}) + +describe('production tests', () => { + test.scoped({ environment: 'production' }) + + test('uses production', ({ environment }) => { + expect(environment).toBe('production') + }) +}) + +test('uses default', ({ environment }) => { + expect(environment).toBe('development') +}) +``` + +## Extended Test Hooks + +Type-aware hooks with fixtures: + +```ts +const test = base.extend<{ db: Database }>({ + db: async ({}, use) => { + const db = await createDb() + await use(db) + await db.close() + }, +}) + +// Hooks know about fixtures +test.beforeEach(({ db }) => { + db.seed() +}) + +test.afterEach(({ db }) => { + db.clear() +}) +``` + +## Composing Fixtures + +Extend from another extended test: + +```ts +// base-test.ts +export const test = base.extend<{ db: Database }>({ + db: async ({}, use) => { /* ... */ }, +}) + +// admin-test.ts +import { test as dbTest } from './base-test' + +export const test = dbTest.extend<{ admin: User }>({ + admin: async ({ db }, use) => { + const admin = await db.createAdmin() + await use(admin) + }, +}) +``` + +## Key Points + +- Use `{ }` destructuring to access fixtures +- Fixtures are lazy - only initialize when accessed +- Return cleanup function from fixtures +- Use `{ auto: true }` for setup fixtures +- Use `{ scope: 'file' }` for expensive shared resources +- Fixtures compose - extend from extended tests + + diff --git a/.agents/skills/vitest/references/features-coverage.md b/.agents/skills/vitest/references/features-coverage.md new file mode 100644 index 0000000000000..aaf44cfb2bf07 --- /dev/null +++ b/.agents/skills/vitest/references/features-coverage.md @@ -0,0 +1,207 @@ +--- +name: code-coverage +description: Code coverage with V8 or Istanbul providers +--- + +# Code Coverage + +## Setup + +```bash +# Run tests with coverage +vitest run --coverage +``` + +## Configuration + +```ts +// vitest.config.ts +defineConfig({ + test: { + coverage: { + // Provider: 'v8' (default, faster) or 'istanbul' (more compatible) + provider: 'v8', + + // Enable coverage + enabled: true, + + // Reporters + reporter: ['text', 'json', 'html'], + + // Files to include + include: ['src/**/*.{ts,tsx}'], + + // Files to exclude + exclude: [ + 'node_modules/', + 'tests/', + '**/*.d.ts', + '**/*.test.ts', + ], + + // Report uncovered files + all: true, + + // Thresholds + thresholds: { + lines: 80, + functions: 80, + branches: 80, + statements: 80, + }, + }, + }, +}) +``` + +## Providers + +### V8 (Default) + +```bash +npm i -D @vitest/coverage-v8 +``` + +- Faster, no pre-instrumentation +- Uses V8's native coverage +- Recommended for most projects + +### Istanbul + +```bash +npm i -D @vitest/coverage-istanbul +``` + +- Pre-instruments code +- Works in any JS runtime +- More overhead but widely compatible + +## Reporters + +```ts +coverage: { + reporter: [ + 'text', // Terminal output + 'text-summary', // Summary only + 'json', // JSON file + 'html', // HTML report + 'lcov', // For CI tools + 'cobertura', // XML format + ], + reportsDirectory: './coverage', +} +``` + +## Thresholds + +Fail tests if coverage is below threshold: + +```ts +coverage: { + thresholds: { + // Global thresholds + lines: 80, + functions: 75, + branches: 70, + statements: 80, + + // Per-file thresholds + perFile: true, + + // Auto-update thresholds (for gradual improvement) + autoUpdate: true, + }, +} +``` + +## Ignoring Code + +### V8 + +```ts +/* v8 ignore next -- @preserve */ +function ignored() { + return 'not covered' +} + +/* v8 ignore start -- @preserve */ +// All code here ignored +/* v8 ignore stop -- @preserve */ +``` + +### Istanbul + +```ts +/* istanbul ignore next -- @preserve */ +function ignored() {} + +/* istanbul ignore if -- @preserve */ +if (condition) { + // ignored +} +``` + +Note: `@preserve` keeps comments through esbuild. + +## Package.json Scripts + +```json +{ + "scripts": { + "test": "vitest", + "test:coverage": "vitest run --coverage", + "test:coverage:watch": "vitest --coverage" + } +} +``` + +## Vitest UI Coverage + +Enable HTML coverage in Vitest UI: + +```ts +coverage: { + enabled: true, + reporter: ['text', 'html'], +} +``` + +Run with `vitest --ui` to view coverage visually. + +## CI Integration + +```yaml +# GitHub Actions +- name: Run tests with coverage + run: npm run test:coverage + +- name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + files: ./coverage/lcov.info +``` + +## Coverage with Sharding + +Merge coverage from sharded runs: + +```bash +vitest run --shard=1/3 --coverage --reporter=blob +vitest run --shard=2/3 --coverage --reporter=blob +vitest run --shard=3/3 --coverage --reporter=blob + +vitest --merge-reports --coverage --reporter=json +``` + +## Key Points + +- V8 is faster, Istanbul is more compatible +- Use `--coverage` flag or `coverage.enabled: true` +- Include `all: true` to see uncovered files +- Set thresholds to enforce minimum coverage +- Use `@preserve` comment to keep ignore hints + + diff --git a/.agents/skills/vitest/references/features-filtering.md b/.agents/skills/vitest/references/features-filtering.md new file mode 100644 index 0000000000000..24a41cb521d1a --- /dev/null +++ b/.agents/skills/vitest/references/features-filtering.md @@ -0,0 +1,211 @@ +--- +name: test-filtering +description: Filter tests by name, file patterns, and tags +--- + +# Test Filtering + +## CLI Filtering + +### By File Path + +```bash +# Run files containing "user" +vitest user + +# Multiple patterns +vitest user auth + +# Specific file +vitest src/user.test.ts + +# By line number +vitest src/user.test.ts:25 +``` + +### By Test Name + +```bash +# Tests matching pattern +vitest -t "login" +vitest --testNamePattern "should.*work" + +# Regex patterns +vitest -t "/user|auth/" +``` + +## Changed Files + +```bash +# Uncommitted changes +vitest --changed + +# Since specific commit +vitest --changed HEAD~1 +vitest --changed abc123 + +# Since branch +vitest --changed origin/main +``` + +## Related Files + +Run tests that import specific files: + +```bash +vitest related src/utils.ts src/api.ts --run +``` + +Useful with lint-staged: + +```js +// .lintstagedrc.js +export default { + '*.{ts,tsx}': 'vitest related --run', +} +``` + +## Focus Tests (.only) + +```ts +test.only('only this runs', () => {}) + +describe.only('only this suite', () => { + test('runs', () => {}) +}) +``` + +In CI, `.only` throws error unless configured: + +```ts +defineConfig({ + test: { + allowOnly: true, // Allow .only in CI + }, +}) +``` + +## Skip Tests + +```ts +test.skip('skipped', () => {}) + +// Conditional +test.skipIf(process.env.CI)('not in CI', () => {}) +test.runIf(!process.env.CI)('local only', () => {}) + +// Dynamic skip +test('dynamic', ({ skip }) => { + skip(someCondition, 'reason') +}) +``` + +## Tags + +Filter by custom tags: + +```ts +test('database test', { tags: ['db'] }, () => {}) +test('slow test', { tags: ['slow', 'integration'] }, () => {}) +``` + +Run tagged tests: + +```bash +vitest --tags db +vitest --tags "db,slow" # OR +vitest --tags db --tags slow # OR +``` + +Configure allowed tags: + +```ts +defineConfig({ + test: { + tags: ['db', 'slow', 'integration'], + strictTags: true, // Fail on unknown tags + }, +}) +``` + +## Include/Exclude Patterns + +```ts +defineConfig({ + test: { + // Test file patterns + include: ['**/*.{test,spec}.{ts,tsx}'], + + // Exclude patterns + exclude: [ + '**/node_modules/**', + '**/e2e/**', + '**/*.skip.test.ts', + ], + + // Include source for in-source testing + includeSource: ['src/**/*.ts'], + }, +}) +``` + +## Watch Mode Filtering + +In watch mode, press: +- `p` - Filter by filename pattern +- `t` - Filter by test name pattern +- `a` - Run all tests +- `f` - Run only failed tests + +## Projects Filtering + +Run specific project: + +```bash +vitest --project unit +vitest --project integration --project e2e +``` + +## Environment-based Filtering + +```ts +const isDev = process.env.NODE_ENV === 'development' +const isCI = process.env.CI + +describe.skipIf(isCI)('local only tests', () => {}) +describe.runIf(isDev)('dev tests', () => {}) +``` + +## Combining Filters + +```bash +# File pattern + test name + changed +vitest user -t "login" --changed + +# Related files + run mode +vitest related src/auth.ts --run +``` + +## List Tests Without Running + +```bash +vitest list # Show all test names +vitest list -t "user" # Filter by name +vitest list --filesOnly # Show only file paths +vitest list --json # JSON output +``` + +## Key Points + +- Use `-t` for test name pattern filtering +- `--changed` runs only tests affected by changes +- `--related` runs tests importing specific files +- Tags provide semantic test grouping +- Use `.only` for debugging, but configure CI to reject it +- Watch mode has interactive filtering + + diff --git a/.agents/skills/vitest/references/features-mocking.md b/.agents/skills/vitest/references/features-mocking.md new file mode 100644 index 0000000000000..e351efef62a6a --- /dev/null +++ b/.agents/skills/vitest/references/features-mocking.md @@ -0,0 +1,265 @@ +--- +name: mocking +description: Mock functions, modules, timers, and dates with vi utilities +--- + +# Mocking + +## Mock Functions + +```ts +import { expect, vi } from 'vitest' + +// Create mock function +const fn = vi.fn() +fn('hello') + +expect(fn).toHaveBeenCalled() +expect(fn).toHaveBeenCalledWith('hello') + +// With implementation +const add = vi.fn((a, b) => a + b) +expect(add(1, 2)).toBe(3) + +// Mock return values +fn.mockReturnValue(42) +fn.mockReturnValueOnce(1).mockReturnValueOnce(2) +fn.mockResolvedValue({ data: true }) +fn.mockRejectedValue(new Error('fail')) + +// Mock implementation +fn.mockImplementation((x) => x * 2) +fn.mockImplementationOnce(() => 'first call') +``` + +## Spying on Objects + +```ts +const cart = { + getTotal: () => 100, +} + +const spy = vi.spyOn(cart, 'getTotal') +cart.getTotal() + +expect(spy).toHaveBeenCalled() + +// Mock implementation +spy.mockReturnValue(200) +expect(cart.getTotal()).toBe(200) + +// Restore original +spy.mockRestore() +``` + +## Module Mocking + +```ts +// vi.mock is hoisted to top of file +vi.mock('./api', () => ({ + fetchUser: vi.fn(() => ({ id: 1, name: 'Mock' })), +})) + +import { fetchUser } from './api' + +test('mocked module', () => { + expect(fetchUser()).toEqual({ id: 1, name: 'Mock' }) +}) +``` + +### Partial Mock + +```ts +vi.mock('./utils', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + specificFunction: vi.fn(), + } +}) +``` + +### Auto-mock with Spy + +```ts +// Keep implementation but spy on calls +vi.mock('./calculator', { spy: true }) + +import { add } from './calculator' + +test('spy on module', () => { + const result = add(1, 2) // Real implementation + expect(result).toBe(3) + expect(add).toHaveBeenCalledWith(1, 2) +}) +``` + +### Manual Mocks (__mocks__) + +``` +src/ + __mocks__/ + axios.ts # Mocks 'axios' + api/ + __mocks__/ + client.ts # Mocks './client' + client.ts +``` + +```ts +// Just call vi.mock with no factory +vi.mock('axios') +vi.mock('./api/client') +``` + +## Dynamic Mocking (vi.doMock) + +Not hoisted - use for dynamic imports: + +```ts +test('dynamic mock', async () => { + vi.doMock('./config', () => ({ + apiUrl: 'http://test.local', + })) + + const { apiUrl } = await import('./config') + expect(apiUrl).toBe('http://test.local') + + vi.doUnmock('./config') +}) +``` + +## Mock Timers + +```ts +import { afterEach, beforeEach, vi } from 'vitest' + +beforeEach(() => { + vi.useFakeTimers() +}) + +afterEach(() => { + vi.useRealTimers() +}) + +test('timers', () => { + const fn = vi.fn() + setTimeout(fn, 1000) + + expect(fn).not.toHaveBeenCalled() + + vi.advanceTimersByTime(1000) + expect(fn).toHaveBeenCalled() +}) + +// Other timer methods +vi.runAllTimers() // Run all pending timers +vi.runOnlyPendingTimers() // Run only currently pending +vi.advanceTimersToNextTimer() // Advance to next timer +``` + +### Async Timer Methods + +```ts +test('async timers', async () => { + vi.useFakeTimers() + + let resolved = false + setTimeout(() => Promise.resolve().then(() => { resolved = true }), 100) + + await vi.advanceTimersByTimeAsync(100) + expect(resolved).toBe(true) +}) +``` + +## Mock Dates + +```ts +vi.setSystemTime(new Date('2024-01-01')) +expect(new Date().getFullYear()).toBe(2024) + +vi.useRealTimers() // Restore +``` + +## Mock Globals + +```ts +vi.stubGlobal('fetch', vi.fn(() => + Promise.resolve({ json: () => ({ data: 'mock' }) }) +)) + +// Restore +vi.unstubAllGlobals() +``` + +## Mock Environment Variables + +```ts +vi.stubEnv('API_KEY', 'test-key') +expect(import.meta.env.API_KEY).toBe('test-key') + +// Restore +vi.unstubAllEnvs() +``` + +## Clearing Mocks + +```ts +const fn = vi.fn() +fn() + +fn.mockClear() // Clear call history +fn.mockReset() // Clear history + implementation +fn.mockRestore() // Restore original (for spies) + +// Global +vi.clearAllMocks() +vi.resetAllMocks() +vi.restoreAllMocks() +``` + +## Config Auto-Reset + +```ts +// vitest.config.ts +defineConfig({ + test: { + clearMocks: true, // Clear before each test + mockReset: true, // Reset before each test + restoreMocks: true, // Restore after each test + unstubEnvs: true, // Restore env vars + unstubGlobals: true, // Restore globals + }, +}) +``` + +## Hoisted Variables for Mocks + +```ts +const mockFn = vi.hoisted(() => vi.fn()) + +vi.mock('./module', () => ({ + getData: mockFn, +})) + +import { getData } from './module' + +test('hoisted mock', () => { + mockFn.mockReturnValue('test') + expect(getData()).toBe('test') +}) +``` + +## Key Points + +- `vi.mock` is hoisted - called before imports +- Use `vi.doMock` for dynamic, non-hoisted mocking +- Always restore mocks to avoid test pollution +- Use `{ spy: true }` to keep implementation but track calls +- `vi.hoisted` lets you reference variables in mock factories + + diff --git a/.agents/skills/vitest/references/features-snapshots.md b/.agents/skills/vitest/references/features-snapshots.md new file mode 100644 index 0000000000000..6868fb13b7a6a --- /dev/null +++ b/.agents/skills/vitest/references/features-snapshots.md @@ -0,0 +1,207 @@ +--- +name: snapshot-testing +description: Snapshot testing with file, inline, and file snapshots +--- + +# Snapshot Testing + +Snapshot tests capture output and compare against stored references. + +## Basic Snapshot + +```ts +import { expect, test } from 'vitest' + +test('snapshot', () => { + const result = generateOutput() + expect(result).toMatchSnapshot() +}) +``` + +First run creates `.snap` file: + +```js +// __snapshots__/test.spec.ts.snap +exports['snapshot 1'] = ` +{ + "id": 1, + "name": "test" +} +` +``` + +## Inline Snapshots + +Stored directly in test file: + +```ts +test('inline snapshot', () => { + const data = { foo: 'bar' } + expect(data).toMatchInlineSnapshot() +}) +``` + +Vitest updates the test file: + +```ts +test('inline snapshot', () => { + const data = { foo: 'bar' } + expect(data).toMatchInlineSnapshot(` + { + "foo": "bar", + } + `) +}) +``` + +## File Snapshots + +Compare against explicit file: + +```ts +test('render html', async () => { + const html = renderComponent() + await expect(html).toMatchFileSnapshot('./expected/component.html') +}) +``` + +## Snapshot Hints + +Add descriptive hints: + +```ts +test('multiple snapshots', () => { + expect(header).toMatchSnapshot('header') + expect(body).toMatchSnapshot('body content') + expect(footer).toMatchSnapshot('footer') +}) +``` + +## Object Shape Matching + +Match partial structure: + +```ts +test('shape snapshot', () => { + const data = { + id: Math.random(), + created: new Date(), + name: 'test' + } + + expect(data).toMatchSnapshot({ + id: expect.any(Number), + created: expect.any(Date), + }) +}) +``` + +## Error Snapshots + +```ts +test('error message', () => { + expect(() => { + throw new Error('Something went wrong') + }).toThrowErrorMatchingSnapshot() +}) + +test('inline error', () => { + expect(() => { + throw new Error('Bad input') + }).toThrowErrorMatchingInlineSnapshot(`[Error: Bad input]`) +}) +``` + +## Updating Snapshots + +```bash +# Update all snapshots +vitest -u +vitest --update + +# In watch mode, press 'u' to update failed snapshots +``` + +## Custom Serializers + +Add custom snapshot formatting: + +```ts +expect.addSnapshotSerializer({ + test(val) { + return val && typeof val.toJSON === 'function' + }, + serialize(val, config, indentation, depth, refs, printer) { + return printer(val.toJSON(), config, indentation, depth, refs) + }, +}) +``` + +Or via config: + +```ts +// vitest.config.ts +defineConfig({ + test: { + snapshotSerializers: ['./my-serializer.ts'], + }, +}) +``` + +## Snapshot Format Options + +```ts +defineConfig({ + test: { + snapshotFormat: { + printBasicPrototype: false, // Don't print Array/Object prototypes + escapeString: false, + }, + }, +}) +``` + +## Concurrent Test Snapshots + +Use context's expect: + +```ts +test.concurrent('concurrent 1', async ({ expect }) => { + expect(await getData()).toMatchSnapshot() +}) + +test.concurrent('concurrent 2', async ({ expect }) => { + expect(await getOther()).toMatchSnapshot() +}) +``` + +## Snapshot File Location + +Default: `__snapshots__/.snap` + +Customize: + +```ts +defineConfig({ + test: { + resolveSnapshotPath: (testPath, snapExtension) => { + return testPath.replace('__tests__', '__snapshots__') + snapExtension + }, + }, +}) +``` + +## Key Points + +- Commit snapshot files to version control +- Review snapshot changes in code review +- Use hints for multiple snapshots in one test +- Use `toMatchFileSnapshot` for large outputs (HTML, JSON) +- Inline snapshots auto-update in test file +- Use context's `expect` for concurrent tests + + diff --git a/.claude/skills/vitest b/.claude/skills/vitest new file mode 120000 index 0000000000000..766153642772c --- /dev/null +++ b/.claude/skills/vitest @@ -0,0 +1 @@ +../../.agents/skills/vitest \ No newline at end of file diff --git a/.cursor/skills/vitest b/.cursor/skills/vitest new file mode 120000 index 0000000000000..766153642772c --- /dev/null +++ b/.cursor/skills/vitest @@ -0,0 +1 @@ +../../.agents/skills/vitest \ No newline at end of file diff --git a/apps/design-system/content/docs/fragments/text-confirm-dialog.mdx b/apps/design-system/content/docs/fragments/text-confirm-dialog.mdx index 4b812ead978fc..12f90b69c7a80 100644 --- a/apps/design-system/content/docs/fragments/text-confirm-dialog.mdx +++ b/apps/design-system/content/docs/fragments/text-confirm-dialog.mdx @@ -53,7 +53,7 @@ export default function TextConfirmDialogDemo() { ## Props -- `confirmString`: The exact string the user must type to enable the confirm action +- `confirmString`: The exact string the user must type to enable the confirm action (leading/trailing whitespace is trimmed) - `confirmPlaceholder`: Placeholder text shown in the confirmation input - `variant`: Visual intent of the dialog (`default`, `destructive`, or `warning`) - Other standard modal props inherited from the underlying [Dialog](../components/dialog) component diff --git a/apps/design-system/content/docs/ui-patterns/modality.mdx b/apps/design-system/content/docs/ui-patterns/modality.mdx index 3da04913adf5e..3f593a0eb8e09 100644 --- a/apps/design-system/content/docs/ui-patterns/modality.mdx +++ b/apps/design-system/content/docs/ui-patterns/modality.mdx @@ -43,7 +43,7 @@ There are quite a few dialog components, each suited to a different task or cont #### Text Confirm Dialog -[Text Confirm Dialog](../fragments/text-confirm-dialog) adds a deliberate speed bump for highly destructive actions by requiring the user to type an exact confirmation string before proceeding. +[Text Confirm Dialog](../fragments/text-confirm-dialog) adds a deliberate speed bump for highly destructive actions by requiring the user to type an exact confirmation string before proceeding. The confirm action remains disabled until the input matches. diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index 5c59d50f4180a..af7e845bb06d0 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -195,6 +195,7 @@ Szymon Mentel Sugu Sougoumarane Supun Sudaraka Kalidasa Taha Le Bras +Tanun Chalermsinsuwan Taryn King Terry Sutton Thomas E diff --git a/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx b/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx index 0f7db86b186aa..8907cca134b3c 100644 --- a/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx +++ b/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx @@ -18,33 +18,33 @@ import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' import { Badge, Button, + cn, CodeBlock, + Collapsible_Shadcn_, CollapsibleContent_Shadcn_, CollapsibleTrigger_Shadcn_, - Collapsible_Shadcn_, DIALOG_PADDING_X, + Select_Shadcn_, SelectContent_Shadcn_, SelectItem_Shadcn_, SelectTrigger_Shadcn_, SelectValue_Shadcn_, - Select_Shadcn_, Separator, - cn, } from 'ui' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { CONNECTION_PARAMETERS, - type ConnectionStringMethod, + connectionStringMethodOptions, DATABASE_CONNECTION_TYPES, DatabaseConnectionType, IPV4_ADDON_TEXT, PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT, - connectionStringMethodOptions, + type ConnectionStringMethod, } from './Connect.constants' import { CodeBlockFileHeader, ConnectionPanel } from './ConnectionPanel' import { getConnectionStrings } from './DatabaseSettings.utils' -import examples, { Example } from './DirectConnectionExamples' +import { examples, type Example } from './DirectConnectionExamples' const StepLabel = ({ number, diff --git a/apps/studio/components/interfaces/Connect/DirectConnectionExamples.tsx b/apps/studio/components/interfaces/Connect/DirectConnectionExamples.tsx index 80084d1337d57..fb3b6c06c6c50 100644 --- a/apps/studio/components/interfaces/Connect/DirectConnectionExamples.tsx +++ b/apps/studio/components/interfaces/Connect/DirectConnectionExamples.tsx @@ -7,7 +7,7 @@ export type Example = { }[] } -const examples = { +export const examples = { nodejs: { installCommands: ['npm install postgres'], files: [ @@ -149,5 +149,3 @@ except Exception as e: ], }, } - -export default examples diff --git a/apps/studio/components/interfaces/Connect/content/nextjs/pages/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/nextjs/pages/supabasejs/content.tsx index 92a3ea50e6406..29d4bea895b13 100644 --- a/apps/studio/components/interfaces/Connect/content/nextjs/pages/supabasejs/content.tsx +++ b/apps/studio/components/interfaces/Connect/content/nextjs/pages/supabasejs/content.tsx @@ -1,12 +1,11 @@ import type { ContentFileProps } from 'components/interfaces/Connect/Connect.types' - -import { SimpleCodeBlock } from 'ui' import { ConnectTabContent, ConnectTabs, ConnectTabTrigger, ConnectTabTriggers, } from 'components/interfaces/Connect/ConnectTabs' +import { SimpleCodeBlock } from 'ui' const ContentFile = ({ projectKeys }: ContentFileProps) => { return ( @@ -53,7 +52,7 @@ function Page() { const [todos, setTodos] = useState([]) useEffect(() => { - function getTodos() { + async function getTodos() { const { data: todos } = await supabase.from('todos').select() if (todos.length > 1) { diff --git a/apps/studio/components/interfaces/ConnectSheet/Connect.constants.ts b/apps/studio/components/interfaces/ConnectSheet/Connect.constants.ts new file mode 100644 index 0000000000000..ed6c00599a787 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/Connect.constants.ts @@ -0,0 +1,421 @@ +import { DOCS_URL } from 'lib/constants' +import { CodeBlockLang } from 'ui' + +export type DatabaseConnectionType = + | 'uri' + | 'psql' + | 'golang' + | 'jdbc' + | 'dotnet' + | 'nodejs' + | 'php' + | 'python' + | 'sqlalchemy' + +export const INSTALL_COMMANDS: Record = { + supabasejs: 'npm install @supabase/supabase-js', + supabasepy: 'pip install supabase', + supabaseflutter: 'flutter pub add supabase_flutter', + supabaseswift: + 'swift package add-dependency https://github.com/supabase-community/supabase-swift', + supabasekt: 'implementation("io.github.jan-tennert.supabase:supabase-kt:VERSION")', +} + +export const DATABASE_CONNECTION_TYPES: { + id: DatabaseConnectionType + label: string + contentType: 'input' | 'code' + lang: CodeBlockLang + fileTitle: string | undefined +}[] = [ + { id: 'uri', label: 'URI', contentType: 'input', lang: 'bash', fileTitle: undefined }, + { id: 'psql', label: 'PSQL', contentType: 'code', lang: 'bash', fileTitle: undefined }, + { id: 'golang', label: 'Golang', contentType: 'code', lang: 'go', fileTitle: '.env' }, + { id: 'jdbc', label: 'JDBC', contentType: 'input', lang: 'bash', fileTitle: undefined }, + { + id: 'dotnet', + label: '.NET', + contentType: 'code', + lang: 'csharp', + fileTitle: 'appsettings.json', + }, + { id: 'nodejs', label: 'Node.js', contentType: 'code', lang: 'js', fileTitle: '.env' }, + { id: 'php', label: 'PHP', contentType: 'code', lang: 'php', fileTitle: '.env' }, + { id: 'python', label: 'Python', contentType: 'code', lang: 'python', fileTitle: '.env' }, + { id: 'sqlalchemy', label: 'SQLAlchemy', contentType: 'code', lang: 'python', fileTitle: '.env' }, +] + +export const CONNECTION_PARAMETERS = { + host: { + key: 'host', + description: 'The hostname of your database', + }, + port: { + key: 'port', + description: 'Port number for the connection', + }, + database: { + key: 'database', + description: 'Default database name', + }, + user: { + key: 'user', + description: 'Database user', + }, + pool_mode: { + key: 'pool_mode', + description: 'Connection pooling behavior', + }, +} as const + +export type ConnectionType = { + key: string + icon: string + label: string + guideLink?: string + children: ConnectionType[] + files?: { + name: string + content: string + }[] +} + +export const FRAMEWORKS: ConnectionType[] = [ + { + key: 'nextjs', + label: 'Next.js', + icon: 'nextjs', + guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/nextjs`, + children: [ + { + key: 'app', + label: 'App Router', + icon: '', + children: [ + { + key: 'supabasejs', + label: 'supabase-js', + icon: 'supabase', + children: [], + }, + ], + }, + { + key: 'pages', + label: 'Pages Router', + icon: '', + children: [ + { + key: 'supabasejs', + label: 'Supabase-js', + children: [], + icon: 'supabase', + }, + ], + }, + ], + }, + { + key: 'remix', + label: 'Remix', + icon: 'remix', + guideLink: `${DOCS_URL}/guides/auth/server-side/creating-a-client?framework=remix&environment=remix-loader`, + children: [ + { + key: 'supabasejs', + label: 'Supabase-js', + children: [], + icon: 'supabase', + }, + ], + }, + { + key: 'react', + label: 'React', + icon: 'react', + guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/reactjs`, + children: [ + { + key: 'vite', + label: 'Vite', + icon: 'vite', + children: [ + { + key: 'supabasejs', + label: 'Supabase-js', + children: [], + icon: 'supabase', + }, + ], + }, + { + key: 'create-react-app', + label: 'Create React App', + icon: 'react', + children: [ + { + key: 'supabasejs', + label: 'supabase-js', + icon: 'supabase', + children: [], + }, + ], + }, + ], + }, + { + key: 'nuxt', + label: 'Nuxt', + icon: 'nuxt', + guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/nuxtjs`, + children: [ + { + key: 'supabasejs', + label: 'Supabase-js', + children: [], + icon: 'supabase', + }, + ], + }, + { + key: 'vuejs', + label: 'Vue.JS', + icon: 'vuejs', + guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/vue`, + children: [ + { + key: 'supabasejs', + label: 'Supabase-js', + children: [], + icon: 'supabase', + }, + ], + }, + + { + key: 'sveltekit', + label: 'SvelteKit', + icon: 'sveltekit', + guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/sveltekit`, + children: [ + { + key: 'supabasejs', + label: 'Supabase-js', + children: [], + icon: 'supabase', + }, + ], + }, + { + key: 'solidjs', + label: 'Solid.js', + icon: 'solidjs', + guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/solidjs`, + children: [ + { + key: 'supabasejs', + label: 'Supabase-js', + children: [], + icon: 'supabase', + }, + ], + }, + { + key: 'astro', + label: 'Astro', + icon: 'astro', + guideLink: 'https://docs.astro.build/en/guides/backend/supabase/', + children: [ + { + key: 'supabasejs', + label: 'Supabase-js', + children: [], + icon: 'supabase', + }, + ], + }, + { + key: 'refine', + label: 'Refine', + icon: 'refine', + guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/refine`, + children: [ + { + key: 'supabasejs', + label: 'Supabase-js', + children: [], + icon: 'supabase', + }, + ], + }, + { + key: 'tanstack', + label: 'TanStack Start', + icon: 'tanstack', + guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/tanstack`, + children: [ + { + key: 'supabasejs', + label: 'Supabase-js', + children: [], + icon: 'supabase', + }, + ], + }, + { + key: 'flask', + label: 'Flask (Python)', + icon: 'python', + guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/flask`, + children: [ + { + key: 'supabasepy', + label: 'supabase-py', + children: [], + icon: 'supabase', + }, + ], + }, +] + +export const MOBILES: ConnectionType[] = [ + { + key: 'exporeactnative', + label: 'Expo React Native', + icon: 'expo', + guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/expo-react-native`, + children: [ + { + key: 'supabasejs', + label: 'Supabase-js', + children: [], + icon: 'supabase', + }, + ], + }, + { + key: 'flutter', + label: 'Flutter', + icon: 'flutter', + guideLink: `${DOCS_URL}/guides/getting-started/tutorials/with-flutter`, + children: [ + { + key: 'supabaseflutter', + label: 'supabase-flutter', + children: [], + icon: 'supabase', + }, + ], + }, + { + key: 'ionicreact', + label: 'Ionic React', + icon: 'react', + guideLink: `${DOCS_URL}/guides/getting-started/tutorials/with-ionic-react`, + children: [ + { + key: 'supabasejs', + label: 'Supabase-js', + children: [], + icon: 'supabase', + }, + ], + }, + { + key: 'swift', + label: 'Swift', + icon: 'swift', + guideLink: `${DOCS_URL}/guides/getting-started/tutorials/with-swift`, + children: [ + { + key: 'supabaseswift', + label: 'supabase-swift', + children: [], + icon: 'supabase', + }, + ], + }, + { + key: 'androidkotlin', + label: 'Android Kotlin', + icon: 'kotlin', + guideLink: `${DOCS_URL}/guides/getting-started/tutorials/with-kotlin`, + children: [ + { + key: 'supabasekt', + label: 'supabase-kt', + children: [], + icon: 'supabase', + }, + ], + }, + { + key: 'ionicangular', + label: 'Ionic Angular', + icon: 'ionic-angular', + guideLink: `${DOCS_URL}/guides/getting-started/tutorials/with-ionic-angular`, + children: [ + { + key: 'supabasejs', + label: 'Supabase-js', + children: [], + icon: 'supabase', + }, + ], + }, +] + +export const ORMS: ConnectionType[] = [ + { + key: 'prisma', + label: 'Prisma', + icon: 'prisma', + guideLink: 'https://supabase.com/partners/integrations/prisma', + children: [], + }, + { + key: 'drizzle', + label: 'Drizzle', + icon: 'drizzle', + guideLink: `${DOCS_URL}/guides/database/connecting-to-postgres#connecting-with-drizzle`, + children: [], + }, +] + +export const CONNECTION_TYPES = [ + { key: 'direct', label: 'Connection String', obj: [] }, + { key: 'frameworks', label: 'App Frameworks', obj: FRAMEWORKS }, + { key: 'mobiles', label: 'Mobile Frameworks', obj: MOBILES }, + { key: 'orms', label: 'ORMs', obj: ORMS }, + { key: 'mcp', label: 'MCP', obj: [] }, +] + +export const PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT = + 'Purchase IPv4 add-on or use Shared Pooler if on a IPv4 network' +export const IPV4_ADDON_TEXT = 'Connections are IPv4 proxied with IPv4 add-on' + +export type ConnectionStringMethod = 'direct' | 'transaction' | 'session' + +export const connectionStringMethodOptions: Record< + ConnectionStringMethod, + { value: string; label: string; description: string } +> = { + direct: { + value: 'direct', + label: 'Direct connection', + description: + 'Ideal for applications with persistent and long-lived connections, such as those running on virtual machines or long-standing containers.', + }, + transaction: { + value: 'transaction', + label: 'Transaction pooler', + description: + 'Ideal for stateless applications like serverless functions where each interaction with Postgres is brief and isolated.', + }, + session: { + value: 'session', + label: 'Session pooler', + description: + 'Only recommended as an alternative to Direct Connection, when connecting via an IPv4 network.', + }, +} diff --git a/apps/studio/components/interfaces/ConnectSheet/Connect.types.ts b/apps/studio/components/interfaces/ConnectSheet/Connect.types.ts new file mode 100644 index 0000000000000..929208032ddba --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/Connect.types.ts @@ -0,0 +1,149 @@ +// ============================================================================ +// Project Keys (existing) +// ============================================================================ + +export type ProjectKeys = { + apiUrl: string | null + anonKey: string | null + publishableKey: string | null +} + +// ============================================================================ +// Connection Strings +// ============================================================================ + +export interface ConnectionStringPooler { + transactionShared: string + sessionShared: string + transactionDedicated?: string + sessionDedicated?: string + ipv4SupportedForDedicatedPooler: boolean + direct?: string +} + +// ============================================================================ +// Schema Types - Conditional Resolution +// ============================================================================ + +/** + * A value that can be resolved conditionally based on state. + * Resolution walks the tree using state values, falling back to DEFAULT. + * + * Example: + * { + * framework: { + * nextjs: 'NextJsContent', + * react: 'ReactContent', + * DEFAULT: 'GenericFrameworkContent' + * }, + * direct: 'DirectContent', + * DEFAULT: null + * } + */ +export type ConditionalValue = + | T + | { + [stateValue: string]: ConditionalValue | undefined + DEFAULT?: T + } + +// ============================================================================ +// Schema Types - Modes +// ============================================================================ + +type ConnectMode = string + +// ============================================================================ +// Schema Types - Fields +// ============================================================================ + +type FieldType = 'select' | 'radio-grid' | 'radio-list' | 'switch' | 'multi-select' + +export interface FieldOption { + value: string + label: string + icon?: string + description?: string +} + +type FieldOptionsResolver = (state: ConnectState) => FieldOption[] + +interface FieldDefinition { + id: string + type: FieldType + label: string + description?: string + // Options can be static, conditional, or resolved from state + options?: FieldOption[] | ConditionalValue | FieldOptionsResolver + // Only show this field when these state conditions are met + dependsOn?: Record + // Default value for this field + defaultValue?: string | boolean | string[] +} + +// ============================================================================ +// Schema Types - Steps +// ============================================================================ + +export interface StepDefinition { + id: string + title: string + description: string + // Component identifier or content file path, can be conditional + content: ConditionalValue +} + +export type StepTree = + | StepDefinition[] + | { + [fieldId: string]: StepFieldValueMap + } + +export type StepFieldValueMap = { + [fieldValue: string]: StepTree | undefined + DEFAULT?: StepTree +} + +// ============================================================================ +// Schema Types - Main Schema +// ============================================================================ + +export interface ConnectSchema { + fields: Record + // Steps are fully conditional based on state + steps: StepTree +} + +// ============================================================================ +// State Types +// ============================================================================ + +export interface ConnectState { + mode: ConnectMode + [fieldId: string]: string | boolean | string[] +} + +export interface ResolvedStep { + id: string + title: string + description: string + content: string // Resolved component identifier +} + +export interface ResolvedField extends FieldDefinition { + resolvedOptions: FieldOption[] +} + +// ============================================================================ +// Step Content Props - Unified props for all step content components +// ============================================================================ + +/** + * Props passed to all step content components. + * Components receive full state and can conditionally render whatever they need. + */ +export interface StepContentProps { + state: ConnectState + projectKeys: ProjectKeys + connectionStringPooler: ConnectionStringPooler +} diff --git a/apps/studio/components/interfaces/ConnectSheet/ConnectConfigSection.tsx b/apps/studio/components/interfaces/ConnectSheet/ConnectConfigSection.tsx new file mode 100644 index 0000000000000..5219d1474bb14 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/ConnectConfigSection.tsx @@ -0,0 +1,216 @@ +import { + RadioGroupStacked, + RadioGroupStackedItem, + Select_Shadcn_, + SelectContent_Shadcn_, + SelectItem_Shadcn_, + SelectTrigger_Shadcn_, + SelectValue_Shadcn_, + Switch, +} from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { + MultiSelector, + MultiSelectorContent, + MultiSelectorItem, + MultiSelectorList, + MultiSelectorTrigger, +} from 'ui-patterns/multi-select' + +import type { FieldOption, ResolvedField } from './Connect.types' +import { ConnectionIcon } from './ConnectionIcon' + +interface ConnectConfigSectionProps { + activeFields: ResolvedField[] + state: Record + onFieldChange: (fieldId: string, value: string | boolean | string[]) => void + getFieldOptions: (fieldId: string) => FieldOption[] +} + +export function ConnectConfigSection({ + activeFields, + state, + onFieldChange, + getFieldOptions, +}: ConnectConfigSectionProps) { + if (activeFields.length === 0) return null + + const formLayoutClassName = 'md:[&>div:first-child]:!w-1/3 xl:[&>div:first-child]:!w-2/5' + + return ( +
+ {activeFields.map((field) => { + const options = getFieldOptions(field.id) + const value = state[field.id] + + // Skip fields with no options (or single option that's auto-selected) + // Exception: switch and multi-select fields don't require options + if (field.type !== 'switch' && field.type !== 'multi-select') { + if (options.length === 0) return null + if (options.length === 1) return null + } + + switch (field.type) { + case 'radio-grid': + return ( + + onFieldChange(field.id, v)} + className="flex-row gap-3 space-y-0" + > + {options.map((option) => ( + +
+ {option.icon && } + {option.label} +
+
+ ))} +
+
+ ) + + case 'radio-list': + return ( + + onFieldChange(field.id, v)} + > + {options.map((option) => ( + +
+
+ {option.icon && } + {option.label} +
+ {option.description && ( + + {option.description} + + )} +
+
+ ))} +
+
+ ) + + case 'select': + return ( + + onFieldChange(field.id, v)} + > + + + + + {options.map((option) => ( + + {option.label} + + ))} + + + + ) + + case 'switch': + return ( + + onFieldChange(field.id, v)} + /> + + ) + + case 'multi-select': + return ( + + onFieldChange(field.id, v)} + > + + + + {options.map((option) => ( + +
+ {option.label} + {option.description && ( + + {option.description} + + )} +
+
+ ))} +
+
+
+
+ ) + + default: + return null + } + })} +
+ ) +} diff --git a/apps/studio/components/interfaces/ConnectSheet/ConnectSheet.tsx b/apps/studio/components/interfaces/ConnectSheet/ConnectSheet.tsx index 737be71a55de6..20905f90f4233 100644 --- a/apps/studio/components/interfaces/ConnectSheet/ConnectSheet.tsx +++ b/apps/studio/components/interfaces/ConnectSheet/ConnectSheet.tsx @@ -1,5 +1,16 @@ +import { PermissionAction } from '@supabase/shared-types/out/constants' +import { useParams } from 'common' +import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' +import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs' -import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, cn } from 'ui' +import { useMemo } from 'react' +import { cn, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from 'ui' + +import type { ProjectKeys } from './Connect.types' +import { ConnectConfigSection } from './ConnectConfigSection' +import { ConnectStepsSection } from './ConnectStepsSection' +import { useConnectState } from './useConnectState' export const ConnectSheet = () => { const [showConnect, setShowConnect] = useQueryState( @@ -15,21 +26,57 @@ export const ConnectSheet = () => { setShowConnect(sheetOpen) } + const { state, updateField, activeFields, resolvedSteps, getFieldOptions } = useConnectState() + + // Project keys for step components + const { ref: projectRef } = useParams() + const { data: settings } = useProjectSettingsV2Query({ projectRef }, { enabled: showConnect }) + const { can: canReadAPIKeys } = useAsyncCheckPermissions( + PermissionAction.READ, + 'service_api_keys' + ) + const { data: apiKeys } = useAPIKeysQuery({ projectRef }, { enabled: canReadAPIKeys }) + const { anonKey, publishableKey } = canReadAPIKeys + ? getKeys(apiKeys) + : { anonKey: null, publishableKey: null } + + const projectKeys: ProjectKeys = useMemo(() => { + const protocol = settings?.app_config?.protocol ?? 'https' + const endpoint = settings?.app_config?.endpoint ?? '' + const apiHost = canReadAPIKeys ? `${protocol}://${endpoint ?? '-'}` : '' + + return { + apiUrl: apiHost ?? null, + anonKey: anonKey?.api_key ?? null, + publishableKey: publishableKey?.api_key ?? null, + } + }, [ + settings?.app_config?.protocol, + settings?.app_config?.endpoint, + canReadAPIKeys, + anonKey?.api_key, + publishableKey?.api_key, + ]) + return ( -
-
- Connect to your project - Choose how you want to use Supabase -
-
+ Connect to your project + Choose how you want to use Supabase
- {/* Configuration Section */} -
+
+ +
+ +
diff --git a/apps/studio/components/interfaces/ConnectSheet/ConnectSheetStep.tsx b/apps/studio/components/interfaces/ConnectSheet/ConnectSheetStep.tsx new file mode 100644 index 0000000000000..77f969c6cc5ea --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/ConnectSheetStep.tsx @@ -0,0 +1,53 @@ +import { PropsWithChildren } from 'react' +import { cn } from 'ui' + +interface ConnectSheetStepProps { + number: number + title: string + description: string + className?: string +} + +export const ConnectSheetStep = ({ + number, + title, + description, + className, + children, +}: PropsWithChildren) => { + return ( +
+
+
+
+ +
+ +
+
+

{title}

+

{description}

+
+
+ {children} +
+
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/ConnectSheet/ConnectStepsSection.tsx b/apps/studio/components/interfaces/ConnectSheet/ConnectStepsSection.tsx new file mode 100644 index 0000000000000..55288932b698a --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/ConnectStepsSection.tsx @@ -0,0 +1,181 @@ +import { useParams } from 'common' +import { getAddons } from 'components/interfaces/Billing/Subscription/Subscription.utils' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' +import { usePgbouncerConfigQuery } from 'data/database/pgbouncer-config-query' +import { useSupavisorConfigurationQuery } from 'data/database/supavisor-configuration-query' +import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' +import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' +import { pluckObjectFields } from 'lib/helpers' +import dynamic from 'next/dynamic' +import { useMemo, useRef } from 'react' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' + +import type { + ConnectionStringPooler, + ConnectState, + ProjectKeys, + ResolvedStep, + StepContentProps, +} from './Connect.types' +import { ConnectSheetStep } from './ConnectSheetStep' +import { CopyPromptAdmonition } from './CopyPromptAdmonition' +import { getConnectionStringPooler } from './DatabaseSettings.utils' + +interface ConnectStepsSectionProps { + steps: ResolvedStep[] + state: ConnectState + projectKeys: ProjectKeys +} + +/** + * Resolves a content path template by replacing {{key}} placeholders with state values. + * Empty segments are filtered out to handle optional state values like frameworkVariant. + * + * Examples: + * - '{{framework}}/{{frameworkVariant}}/{{library}}' with state {framework: 'nextjs', frameworkVariant: 'app', library: 'supabasejs'} + * → 'nextjs/app/supabasejs' + * - '{{orm}}' with state {orm: 'prisma'} + * → 'prisma' + * - 'steps/install' (no templates) + * → 'steps/install' + */ +function resolveContentPath(template: string, state: ConnectState): string { + return template + .replace(/\{\{(\w+)\}\}/g, (_, key) => String(state[key] ?? '')) + .split('/') + .filter(Boolean) + .join('/') +} + +/** + * Hook to fetch and prepare connection strings for step content. + */ +function useConnectionStringPooler(): ConnectionStringPooler { + const { ref: projectRef } = useParams() + const { data: selectedOrg } = useSelectedOrganizationQuery() + const allowPgBouncerSelection = useMemo(() => selectedOrg?.plan.id !== 'free', [selectedOrg]) + + const { data: settings } = useProjectSettingsV2Query({ projectRef }) + const { data: pgbouncerConfig } = usePgbouncerConfigQuery({ projectRef }) + const { data: supavisorConfig } = useSupavisorConfigurationQuery({ projectRef }) + const { data: addons } = useProjectAddonsQuery({ projectRef }) + const { ipv4: ipv4Addon } = getAddons(addons?.selected_addons ?? []) + + const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user', 'inserted_at'] + const emptyState = { db_user: '', db_host: '', db_port: '', db_name: '' } + const connectionInfo = pluckObjectFields(settings || emptyState, DB_FIELDS) + const poolingConfigurationShared = supavisorConfig?.find((x) => x.database_type === 'PRIMARY') + const poolingConfigurationDedicated = allowPgBouncerSelection ? pgbouncerConfig : undefined + + const ConnectionStringPoolerShared = getConnectionStringPooler({ + connectionInfo, + poolingInfo: { + connectionString: poolingConfigurationShared?.connection_string ?? '', + db_host: poolingConfigurationShared?.db_host ?? '', + db_name: poolingConfigurationShared?.db_name ?? '', + db_port: poolingConfigurationShared?.db_port ?? 0, + db_user: poolingConfigurationShared?.db_user ?? '', + }, + metadata: { projectRef }, + }) + + const ConnectionStringPoolerDedicated = + poolingConfigurationDedicated !== undefined + ? getConnectionStringPooler({ + connectionInfo, + poolingInfo: { + connectionString: poolingConfigurationDedicated.connection_string, + db_host: poolingConfigurationDedicated.db_host, + db_name: poolingConfigurationDedicated.db_name, + db_port: poolingConfigurationDedicated.db_port, + db_user: poolingConfigurationDedicated.db_user, + }, + metadata: { projectRef }, + }) + : undefined + + return useMemo( + () => ({ + transactionShared: ConnectionStringPoolerShared.pooler.uri, + sessionShared: ConnectionStringPoolerShared.pooler.uri.replace('6543', '5432'), + transactionDedicated: ConnectionStringPoolerDedicated?.pooler.uri, + sessionDedicated: ConnectionStringPoolerDedicated?.pooler.uri.replace('6543', '5432'), + ipv4SupportedForDedicatedPooler: !!ipv4Addon, + direct: ConnectionStringPoolerShared.direct.uri, + }), + [ConnectionStringPoolerShared, ConnectionStringPoolerDedicated, ipv4Addon] + ) +} + +/** + * Dynamically loads and renders a content component from the content directory. + * All step content uses this unified loader - no built-in component registry needed. + */ +function StepContent({ + contentId, + state, + projectKeys, + connectionStringPooler, +}: { + contentId: string + state: ConnectState + projectKeys: ProjectKeys + connectionStringPooler: ConnectionStringPooler +}) { + // Resolve any template placeholders in the content path + const filePath = useMemo(() => resolveContentPath(contentId, state), [contentId, state]) + + // Dynamically import the content component + const ContentComponent = useMemo(() => { + return dynamic(() => import(`./content/${filePath}/content`), { + loading: () => ( +
+ +
+ ), + }) + }, [filePath]) + + return ( + + ) +} + +export function ConnectStepsSection({ steps, state, projectKeys }: ConnectStepsSectionProps) { + const stepsContainerRef = useRef(null) + const connectionStringPooler = useConnectionStringPooler() + + if (steps.length === 0) return null + + return ( +
+
+

Connect your app

+ + + +
+ {steps.map((step, index) => ( + + + + ))} +
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/ConnectSheet/ConnectionIcon.tsx b/apps/studio/components/interfaces/ConnectSheet/ConnectionIcon.tsx new file mode 100644 index 0000000000000..b2de0e2079571 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/ConnectionIcon.tsx @@ -0,0 +1,48 @@ +import { useTheme } from 'next-themes' +import Image from 'next/image' + +import { BASE_PATH } from 'lib/constants' +import { cn } from 'ui' + +interface ConnectionIconProps { + icon: string + iconFolder?: string + supportsDarkMode?: boolean + size?: number + className?: string +} + +export const ConnectionIcon = ({ + icon, + iconFolder, + supportsDarkMode, + size = 14, + className, +}: ConnectionIconProps) => { + const { resolvedTheme } = useTheme() + + const imageFolder = + iconFolder || (['ionic-angular'].includes(icon) ? 'icons/frameworks' : 'libraries') + + const imageExtension = imageFolder === 'icons/frameworks' ? '' : '-icon' + + const shouldUseDarkMode = + supportsDarkMode || + ['expo', 'nextjs', 'prisma', 'drizzle', 'astro', 'remix', 'refine'].includes(icon.toLowerCase()) + + const iconImgSrc = icon.startsWith('http') + ? icon + : `${BASE_PATH}/img/${imageFolder}/${icon.toLowerCase()}${ + shouldUseDarkMode && resolvedTheme?.includes('dark') ? '-dark' : '' + }${imageExtension}.svg` + + return ( + {`${icon} + ) +} diff --git a/apps/studio/components/interfaces/ConnectSheet/ConnectionParameters.tsx b/apps/studio/components/interfaces/ConnectSheet/ConnectionParameters.tsx new file mode 100644 index 0000000000000..4fe7579855576 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/ConnectionParameters.tsx @@ -0,0 +1,52 @@ +import { Check, Copy } from 'lucide-react' +import { useState } from 'react' +import { cn, copyToClipboard } from 'ui' + +interface Parameter { + key: string + value: string +} + +interface ConnectionParametersProps { + parameters: Parameter[] +} + +export const ConnectionParameters = ({ parameters }: ConnectionParametersProps) => { + const [copiedMap, setCopiedMap] = useState>({}) + + return ( +
+ {parameters.map((param) => ( +
+
+ {param.key}: + {param.value} + +
+
+ ))} +
+ ) +} diff --git a/apps/studio/components/interfaces/ConnectSheet/ConnectionString.utils.ts b/apps/studio/components/interfaces/ConnectSheet/ConnectionString.utils.ts new file mode 100644 index 0000000000000..a681ca002c45c --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/ConnectionString.utils.ts @@ -0,0 +1,88 @@ +import type { ConnectionStringPooler } from './Connect.types' +import type { ConnectionStringMethod } from './Connect.constants' + +export const DEFAULT_PORT = '5432' +export const PASSWORD_PLACEHOLDER = '[YOUR-PASSWORD]' + +export type ConnectionParams = { + host: string + port: string + user: string + database: string +} + +export const resolveConnectionString = ({ + connectionMethod, + useSharedPooler, + connectionStringPooler, +}: { + connectionMethod: ConnectionStringMethod + useSharedPooler: boolean + connectionStringPooler: ConnectionStringPooler +}) => { + if (connectionMethod === 'direct') { + return connectionStringPooler.direct ?? '' + } + + if (connectionMethod === 'session') { + return connectionStringPooler.sessionShared ?? '' + } + + if (useSharedPooler || !connectionStringPooler.transactionDedicated) { + return connectionStringPooler.transactionShared ?? '' + } + + return connectionStringPooler.transactionDedicated ?? '' +} + +export const parseConnectionParams = (connectionString: string): ConnectionParams => { + if (!connectionString) { + return { + host: 'hidden', + port: DEFAULT_PORT, + user: 'hidden', + database: 'hidden', + } + } + + try { + const parsed = new URL(connectionString) + return { + host: parsed.hostname || 'hidden', + port: parsed.port || DEFAULT_PORT, + user: parsed.username || 'hidden', + database: parsed.pathname?.replace(/^\//, '') || 'hidden', + } + } catch (error) { + return { + host: 'hidden', + port: DEFAULT_PORT, + user: 'hidden', + database: 'hidden', + } + } +} + +export const buildSafeConnectionString = ( + connectionString: string, + params: ConnectionParams +): string => { + if (!connectionString) return '' + + const search = (() => { + try { + return new URL(connectionString).search + } catch (error) { + return '' + } + })() + + return `postgresql://${params.user}:${PASSWORD_PLACEHOLDER}@${params.host}:${params.port}/${params.database}${search}` +} + +export const buildConnectionParameters = (params: ConnectionParams) => [ + { key: 'host', value: params.host }, + { key: 'port', value: params.port }, + { key: 'database', value: params.database }, + { key: 'user', value: params.user }, +] diff --git a/apps/studio/components/interfaces/ConnectSheet/CopyPromptAdmonition.tsx b/apps/studio/components/interfaces/ConnectSheet/CopyPromptAdmonition.tsx new file mode 100644 index 0000000000000..1ced61a03bb18 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/CopyPromptAdmonition.tsx @@ -0,0 +1,157 @@ +import { BASE_PATH } from 'lib/constants' +import { type RefObject } from 'react' +import { Badge } from 'ui' +import { Admonition } from 'ui-patterns/admonition' + +import CopyButton from '@/components/ui/CopyButton' + +interface CopyPromptAdmonitionProps { + stepsContainerRef: RefObject +} + +export function CopyPromptAdmonition({ stepsContainerRef }: CopyPromptAdmonitionProps) { + const normalizeTextLines = (value: string) => { + return value + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .join('\n') + } + + const getStepTextContent = (contentElement: HTMLElement) => { + const clone = contentElement.cloneNode(true) as HTMLElement + clone + .querySelectorAll('pre, button, svg, input, textarea, select, [aria-hidden="true"]') + .forEach((element) => { + element.remove() + }) + + const text = clone.textContent ?? '' + return normalizeTextLines(text) + } + + const getStepCodeSnippets = (contentElement: HTMLElement) => { + const snippets: Array<{ label: string; snippet: string }> = [] + const seen = new Set() + + const addSnippet = (label: string, snippet: string) => { + if (!snippet || seen.has(snippet)) return + seen.add(snippet) + snippets.push({ label, snippet }) + } + + const tabContents = Array.from( + contentElement.querySelectorAll('[data-connect-tab-content]') + ) as HTMLElement[] + + tabContents.forEach((tabContent) => { + const label = tabContent.getAttribute('data-tab-label') || 'Code' + const tabSnippets = Array.from(tabContent.querySelectorAll('pre')) + .map((pre) => pre.textContent?.trim()) + .filter((snippet): snippet is string => Boolean(snippet)) + + if (tabSnippets.length === 0) { + const inlineSnippets = Array.from(tabContent.querySelectorAll('code')) + .filter((code) => !code.closest('pre') && code.closest('.font-mono')) + .map((code) => code.textContent?.trim()) + .filter((snippet): snippet is string => Boolean(snippet)) + inlineSnippets.forEach((snippet, index) => { + const inlineLabel = inlineSnippets.length > 1 ? `${label} (part ${index + 1})` : label + addSnippet(inlineLabel, snippet) + }) + return + } + + tabSnippets.forEach((snippet, index) => { + const tabLabel = tabSnippets.length > 1 ? `${label} (part ${index + 1})` : label + addSnippet(tabLabel, snippet) + }) + }) + + contentElement.querySelectorAll('pre').forEach((pre) => { + if (pre.closest('[data-connect-tab-content]')) return + const snippet = pre.textContent?.trim() + if (snippet) addSnippet('Code', snippet) + }) + + contentElement.querySelectorAll('code').forEach((code) => { + if (code.closest('pre')) return + if (code.closest('[data-connect-tab-content]')) return + if (!code.closest('.font-mono')) return + const snippet = code.textContent?.trim() + if (snippet) addSnippet('Code', snippet) + }) + + return snippets + } + + const handleCopyPrompt = () => { + const stepElements = stepsContainerRef.current?.querySelectorAll('[data-connect-step]') + if (!stepElements?.length) return '' + + const promptContent = Array.from(stepElements) + .map((stepElement, index) => { + const title = stepElement.getAttribute('data-step-title') ?? `Step ${index + 1}` + const description = stepElement.getAttribute('data-step-description') ?? '' + const contentElement = stepElement.querySelector( + '[data-step-content]' + ) as HTMLElement | null + + const details = contentElement ? getStepTextContent(contentElement) : '' + const codeSnippets = contentElement ? getStepCodeSnippets(contentElement) : [] + + const sections = [ + `${index + 1}. ${title}`, + description, + details ? `Details:\n${details}` : null, + codeSnippets.length + ? `Code:\n${codeSnippets + .map(({ label, snippet }) => `File: ${label}\n\`\`\`\n${snippet}\n\`\`\``) + .join('\n\n')}` + : null, + ].filter(Boolean) + + return sections.join('\n') + }) + .join('\n\n') + + return promptContent + } + + return ( + } + > +
+ Supabase Grafana + Supabase Grafana +
+
+ +
+
+
+ + Skip the steps + +

Prompt your agent

+
+

+ Copy a prompt with everything your agent needs to connect your app for you. +

+
+
+ + ) +} diff --git a/apps/studio/components/interfaces/ConnectSheet/DatabaseSettings.utils.ts b/apps/studio/components/interfaces/ConnectSheet/DatabaseSettings.utils.ts new file mode 100644 index 0000000000000..402be85432336 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/DatabaseSettings.utils.ts @@ -0,0 +1,135 @@ +type ConnectionStringPooler = { + psql: string + uri: string + golang: string + jdbc: string + dotnet: string + nodejs: string + php: string + python: string + sqlalchemy: string +} + +export const getConnectionStringPooler = ({ + connectionInfo, + poolingInfo, + metadata, +}: { + connectionInfo: { + db_user: string + db_port: number + db_host: string + db_name: string + } + poolingInfo?: { + connectionString: string + db_user: string + db_port: number + db_host: string + db_name: string + } + metadata: { + projectRef?: string + pgVersion?: string + } +}): { + direct: ConnectionStringPooler + pooler: ConnectionStringPooler +} => { + const isMd5 = poolingInfo?.connectionString?.includes('options=reference') + const { projectRef } = metadata + const password = '[YOUR-PASSWORD]' + + // Direct connection variables + const directUser = connectionInfo.db_user + const directPort = connectionInfo.db_port + const directHost = connectionInfo.db_host + const directName = connectionInfo.db_name + + // Pooler connection variables + const poolerUser = poolingInfo?.db_user + const poolerPort = poolingInfo?.db_port + const poolerHost = poolingInfo?.db_host + const poolerName = poolingInfo?.db_name + + // Direct connection strings + const directPsqlString = isMd5 + ? `psql "postgresql://${directUser}:${password}@${directHost}:${directPort}/${directName}"` + : `psql -h ${directHost} -p ${directPort} -d ${directName} -U ${directUser}` + + const directUriString = `postgresql://${directUser}:${password}@${directHost}:${directPort}/${directName}` + + const directGolangString = `DATABASE_URL=${directUriString}` + + const directJdbcString = `jdbc:postgresql://${directHost}:${directPort}/${directName}?user=${directUser}&password=${password}` + + // User Id=${directUser};Password=${password};Server=${directHost};Port=${directPort};Database=${directName}` + const directDotNetString = `{ + "ConnectionStrings": { + "DefaultConnection": "Host=${directHost};Database=${directName};Username=${directUser};Password=${password};SSL Mode=Require;Trust Server Certificate=true" + } +}` + + // `User Id=${poolerUser};Password=${password};Server=${poolerHost};Port=${poolerPort};Database=${poolerName}${isMd5 ? `;Options='reference=${projectRef}'` : ''}` + const poolerDotNetString = `{ + "ConnectionStrings": { + "DefaultConnection": "User Id=${poolerUser};Password=${password};Server=${poolerHost};Port=${poolerPort};Database=${poolerName}${isMd5 ? `;Options='reference=${projectRef}'` : ''}" + } +}` + + const directNodejsString = `DATABASE_URL=${directUriString}` + + // Pooler connection strings + const poolerPsqlString = isMd5 + ? `psql "postgresql://${poolerUser}:${password}@${poolerHost}:${poolerPort}/${poolerName}?options=reference%3D${projectRef}"` + : `psql -h ${poolerHost} -p ${poolerPort} -d ${poolerName} -U ${poolerUser}` + + const poolerUriString = poolingInfo?.connectionString ?? '' + + const nodejsPoolerUriString = `DATABASE_URL=${poolingInfo?.connectionString ?? ''}` + + const poolerGolangString = `user=${poolerUser} +password=${password} +host=${poolerHost} +port=${poolerPort} +dbname=${poolerName}${isMd5 ? `options=reference=${projectRef}` : ''}` + + const poolerJdbcString = `jdbc:postgresql://${poolerHost}:${poolerPort}/${poolerName}?user=${poolerUser}${isMd5 ? `&options=reference%3D${projectRef}` : ''}&password=${password}` + + const sqlalchemyString = `user=${directUser} +password=${password} +host=${directHost} +port=${directPort} +dbname=${directName}` + + const poolerSqlalchemyString = `user=${poolerUser} +password=${password} +host=${poolerHost} +port=${poolerPort} +dbname=${poolerName}` + + return { + direct: { + psql: directPsqlString, + uri: directUriString, + golang: directGolangString, + jdbc: directJdbcString, + dotnet: directDotNetString, + nodejs: directNodejsString, + php: directGolangString, + python: directGolangString, + sqlalchemy: sqlalchemyString, + }, + pooler: { + psql: poolerPsqlString, + uri: poolerUriString, + golang: poolerGolangString, + jdbc: poolerJdbcString, + dotnet: poolerDotNetString, + nodejs: nodejsPoolerUriString, + php: poolerGolangString, + python: poolerGolangString, + sqlalchemy: poolerSqlalchemyString, + }, + } +} diff --git a/apps/studio/components/interfaces/ConnectSheet/DirectConnectionExamples.tsx b/apps/studio/components/interfaces/ConnectSheet/DirectConnectionExamples.tsx new file mode 100644 index 0000000000000..80084d1337d57 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/DirectConnectionExamples.tsx @@ -0,0 +1,153 @@ +export type Example = { + installCommands?: string[] + postInstallCommands?: string[] + files?: { + name: string + content: string + }[] +} + +const examples = { + nodejs: { + installCommands: ['npm install postgres'], + files: [ + { + name: 'db.js', + content: `import postgres from 'postgres' + +const connectionString = process.env.DATABASE_URL +const sql = postgres(connectionString) + +export default sql`, + }, + ], + }, + golang: { + installCommands: ['go get github.com/jackc/pgx/v5'], + files: [ + { + name: 'main.go', + content: `package main + +import ( + "context" + "log" + "os" + "github.com/jackc/pgx/v5" +) + +func main() { + conn, err := pgx.Connect(context.Background(), os.Getenv("DATABASE_URL")) + if err != nil { + log.Fatalf("Failed to connect to the database: %v", err) + } + defer conn.Close(context.Background()) + + // Example query to test connection + var version string + if err := conn.QueryRow(context.Background(), "SELECT version()").Scan(&version); err != nil { + log.Fatalf("Query failed: %v", err) + } + + log.Println("Connected to:", version) +}`, + }, + ], + }, + dotnet: { + installCommands: [ + 'dotnet add package Microsoft.Extensions.Configuration.Json --version YOUR_DOTNET_VERSION', + ], + postInstallCommands: [ + 'dotnet add package Microsoft.Extensions.Configuration.Json --version YOUR_DOTNET_VERSION', + ], + }, + python: { + installCommands: ['pip install python-dotenv psycopg2'], + files: [ + { + name: 'main.py', + content: `import psycopg2 +from dotenv import load_dotenv +import os + +# Load environment variables from .env +load_dotenv() + +# Fetch variables +USER = os.getenv("user") +PASSWORD = os.getenv("password") +HOST = os.getenv("host") +PORT = os.getenv("port") +DBNAME = os.getenv("dbname") + +# Connect to the database +try: + connection = psycopg2.connect( + user=USER, + password=PASSWORD, + host=HOST, + port=PORT, + dbname=DBNAME + ) + print("Connection successful!") + + # Create a cursor to execute SQL queries + cursor = connection.cursor() + + # Example query + cursor.execute("SELECT NOW();") + result = cursor.fetchone() + print("Current Time:", result) + + # Close the cursor and connection + cursor.close() + connection.close() + print("Connection closed.") + +except Exception as e: + print(f"Failed to connect: {e}")`, + }, + ], + }, + sqlalchemy: { + installCommands: ['pip install python-dotenv sqlalchemy psycopg2'], + files: [ + { + name: 'main.py', + content: `from sqlalchemy import create_engine +# from sqlalchemy.pool import NullPool +from dotenv import load_dotenv +import os + +# Load environment variables from .env +load_dotenv() + +# Fetch variables +USER = os.getenv("user") +PASSWORD = os.getenv("password") +HOST = os.getenv("host") +PORT = os.getenv("port") +DBNAME = os.getenv("dbname") + +# Construct the SQLAlchemy connection string +DATABASE_URL = f"postgresql+psycopg2://{USER}:{PASSWORD}@{HOST}:{PORT}/{DBNAME}?sslmode=require" + +# Create the SQLAlchemy engine +engine = create_engine(DATABASE_URL) +# If using Transaction Pooler or Session Pooler, we want to ensure we disable SQLAlchemy client side pooling - +# https://docs.sqlalchemy.org/en/20/core/pooling.html#switching-pool-implementations +# engine = create_engine(DATABASE_URL, poolclass=NullPool) + +# Test the connection +try: + with engine.connect() as connection: + print("Connection successful!") +except Exception as e: + print(f"Failed to connect: {e}")`, + }, + ], + }, +} + +export default examples diff --git a/apps/studio/components/interfaces/ConnectSheet/connect.resolver.test.ts b/apps/studio/components/interfaces/ConnectSheet/connect.resolver.test.ts new file mode 100644 index 0000000000000..30f264907c476 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/connect.resolver.test.ts @@ -0,0 +1,614 @@ +import { describe, expect, test } from 'vitest' + +import type { ConditionalValue, ConnectSchema, StepTree } from './Connect.types' +import { + getActiveFields, + getDefaultState, + resolveConditional, + resolveState, + resolveSteps, +} from './connect.resolver' + +// ============================================================================ +// resolveConditional Tests +// ============================================================================ + +describe('connect.resolver:resolveConditional', () => { + test('should return primitive values directly', () => { + expect(resolveConditional('hello', { mode: 'framework' })).toBe('hello') + expect(resolveConditional(42, { mode: 'framework' })).toBe(42) + expect(resolveConditional(null, { mode: 'framework' })).toBe(null) + expect(resolveConditional(true, { mode: 'framework' })).toBe(true) + }) + + test('should return arrays directly', () => { + const arr = ['a', 'b', 'c'] + expect(resolveConditional(arr, { mode: 'framework' })).toBe(arr) + }) + + test('should resolve single-level conditional based on mode', () => { + const conditional: ConditionalValue = { + framework: 'Framework Content', + direct: 'Direct Content', + DEFAULT: 'Default Content', + } + + expect(resolveConditional(conditional, { mode: 'framework' })).toBe('Framework Content') + expect(resolveConditional(conditional, { mode: 'direct' })).toBe('Direct Content') + expect(resolveConditional(conditional, { mode: 'orm' })).toBe('Default Content') + }) + + test('should resolve nested conditionals (mode -> framework)', () => { + const conditional: ConditionalValue = { + framework: { + nextjs: 'Next.js Content', + react: 'React Content', + DEFAULT: 'Generic Framework', + }, + direct: 'Direct Content', + } + + expect(resolveConditional(conditional, { mode: 'framework', framework: 'nextjs' })).toBe( + 'Next.js Content' + ) + expect(resolveConditional(conditional, { mode: 'framework', framework: 'react' })).toBe( + 'React Content' + ) + expect(resolveConditional(conditional, { mode: 'framework', framework: 'vue' })).toBe( + 'Generic Framework' + ) + expect(resolveConditional(conditional, { mode: 'direct' })).toBe('Direct Content') + }) + + test('should resolve deeply nested conditionals (mode -> framework -> variant)', () => { + const conditional: ConditionalValue = { + framework: { + nextjs: { + app: 'App Router Content', + pages: 'Pages Router Content', + DEFAULT: 'Generic Next.js', + }, + DEFAULT: 'Generic Framework', + }, + } + + expect( + resolveConditional(conditional, { + mode: 'framework', + framework: 'nextjs', + frameworkVariant: 'app', + }) + ).toBe('App Router Content') + expect( + resolveConditional(conditional, { + mode: 'framework', + framework: 'nextjs', + frameworkVariant: 'pages', + }) + ).toBe('Pages Router Content') + expect( + resolveConditional(conditional, { + mode: 'framework', + framework: 'nextjs', + frameworkVariant: 'unknown', + }) + ).toBe('Generic Next.js') + }) + + test('should resolve MCP client-specific content', () => { + const conditional: ConditionalValue = { + mcp: { + cursor: 'Cursor Config', + codex: 'Codex Config', + 'claude-code': 'Claude Code Config', + DEFAULT: 'Generic MCP', + }, + } + + expect(resolveConditional(conditional, { mode: 'mcp', mcpClient: 'cursor' })).toBe( + 'Cursor Config' + ) + expect(resolveConditional(conditional, { mode: 'mcp', mcpClient: 'codex' })).toBe( + 'Codex Config' + ) + expect(resolveConditional(conditional, { mode: 'mcp', mcpClient: 'claude-code' })).toBe( + 'Claude Code Config' + ) + }) + + test('should return undefined when no match and no DEFAULT', () => { + const conditional: ConditionalValue = { + framework: 'Framework', + direct: 'Direct', + } + + expect(resolveConditional(conditional, { mode: 'orm' })).toBe(undefined) + }) + + test('should handle frameworkUi boolean state (as string "true"/"false")', () => { + const conditional: ConditionalValue = { + framework: { + nextjs: { + true: 'Shadcn Steps', + DEFAULT: 'Regular Steps', + }, + }, + } + + // When frameworkUi is true (boolean), it gets converted to string "true" + expect( + resolveConditional(conditional, { mode: 'framework', framework: 'nextjs', frameworkUi: true }) + ).toBe('Shadcn Steps') + expect( + resolveConditional(conditional, { + mode: 'framework', + framework: 'nextjs', + frameworkUi: false, + }) + ).toBe('Regular Steps') + }) +}) + +// ============================================================================ +// resolveSteps Tests +// ============================================================================ + +describe('connect.resolver:resolveSteps', () => { + const createMockSchema = (steps: StepTree): ConnectSchema => ({ + fields: {}, + steps, + }) + + test('should resolve steps array for framework mode', () => { + const schema = createMockSchema({ + mode: { + framework: [ + { id: 'step1', title: 'Install', description: 'Install pkg', content: 'install-content' }, + { id: 'step2', title: 'Configure', description: 'Configure', content: 'config-content' }, + ], + direct: [ + { + id: 'connection', + title: 'Connection', + description: 'Connect', + content: 'direct-content', + }, + ], + }, + }) + + const steps = resolveSteps(schema, { mode: 'framework' }) + expect(steps).toHaveLength(2) + expect(steps[0].id).toBe('step1') + expect(steps[0].title).toBe('Install') + expect(steps[1].id).toBe('step2') + }) + + test('should resolve steps for direct mode', () => { + const schema = createMockSchema({ + mode: { + framework: [{ id: 'step1', title: 'Install', description: 'Install', content: 'install' }], + direct: [ + { id: 'connection', title: 'Connection', description: 'Connect', content: 'direct' }, + ], + }, + }) + + const steps = resolveSteps(schema, { mode: 'direct' }) + expect(steps).toHaveLength(1) + expect(steps[0].id).toBe('connection') + }) + + test('should filter out steps with empty content', () => { + const schema = createMockSchema({ + mode: { + framework: [ + { id: 'step1', title: 'Valid', description: 'Valid step', content: 'valid-content' }, + { id: 'step2', title: 'Empty', description: 'Empty step', content: '' }, + { id: 'step3', title: 'Null', description: 'Null step', content: null }, + ], + }, + }) + + const steps = resolveSteps(schema, { mode: 'framework' }) + expect(steps).toHaveLength(1) + expect(steps[0].id).toBe('step1') + }) + + test('should resolve conditional step content based on state', () => { + const schema = createMockSchema({ + mode: { + framework: [ + { + id: 'configure', + title: 'Configure', + description: 'Configure', + content: { + nextjs: 'nextjs-content', + react: 'react-content', + DEFAULT: 'generic-content', + }, + }, + ], + }, + }) + + const nextjsSteps = resolveSteps(schema, { mode: 'framework', framework: 'nextjs' }) + expect(nextjsSteps[0].content).toBe('nextjs-content') + + const reactSteps = resolveSteps(schema, { mode: 'framework', framework: 'react' }) + expect(reactSteps[0].content).toBe('react-content') + + const vueSteps = resolveSteps(schema, { mode: 'framework', framework: 'vue' }) + expect(vueSteps[0].content).toBe('generic-content') + }) + + test('should return empty array when no steps resolve', () => { + const schema = createMockSchema({ + mode: { + framework: [{ id: 'step1', title: 'Step', description: 'Desc', content: '' }], + }, + }) + + const steps = resolveSteps(schema, { mode: 'framework' }) + expect(steps).toEqual([]) + }) + + test('should return empty array when steps is not an array', () => { + const schema = createMockSchema({ + mode: { + framework: { + framework: {}, + }, + }, + } as any) + + const steps = resolveSteps(schema, { mode: 'framework' }) + expect(steps).toEqual([]) + }) + + test('should append steps from multiple field conditions in order', () => { + const schema = createMockSchema({ + mode: { + framework: { + framework: { + nextjs: [{ id: 'base', title: 'Base', description: 'Base step', content: 'base' }], + }, + frameworkUi: { + true: [{ id: 'ui', title: 'UI', description: 'UI step', content: 'ui' }], + }, + }, + }, + }) + + const steps = resolveSteps(schema, { + mode: 'framework', + framework: 'nextjs', + frameworkUi: true, + }) + expect(steps.map((step) => step.id)).toEqual(['base', 'ui']) + }) +}) + +// ============================================================================ +// getActiveFields Tests +// ============================================================================ + +describe('connect.resolver:getActiveFields', () => { + const createSchemaWithFields = (fields: ConnectSchema['fields']): ConnectSchema => ({ + fields, + steps: [], + }) + + test('should return fields for the current mode', () => { + const schema = createSchemaWithFields({ + mode: { + id: 'mode', + type: 'select', + label: 'Mode', + defaultValue: 'framework', + options: () => [ + { value: 'framework', label: 'Framework' }, + { value: 'direct', label: 'Direct' }, + ], + }, + framework: { + id: 'framework', + type: 'radio-grid', + label: 'Framework', + defaultValue: 'nextjs', + dependsOn: { mode: ['framework'] }, + }, + library: { + id: 'library', + type: 'select', + label: 'Library', + defaultValue: 'supabasejs', + dependsOn: { mode: ['framework'] }, + }, + connectionType: { + id: 'connectionType', + type: 'select', + label: 'Type', + defaultValue: 'uri', + dependsOn: { mode: ['direct'] }, + }, + }) + + const frameworkFields = getActiveFields(schema, { mode: 'framework' }) + expect(frameworkFields.map((f) => f.id)).toEqual(['mode', 'framework', 'library']) + + const directFields = getActiveFields(schema, { mode: 'direct' }) + expect(directFields.map((f) => f.id)).toEqual(['mode', 'connectionType']) + }) + + test('should filter fields by dependsOn conditions', () => { + const schema = createSchemaWithFields({ + mode: { + id: 'mode', + type: 'select', + label: 'Mode', + defaultValue: 'framework', + options: () => [{ value: 'framework', label: 'Framework' }], + }, + framework: { + id: 'framework', + type: 'radio-grid', + label: 'Framework', + defaultValue: 'nextjs', + dependsOn: { mode: ['framework'] }, + }, + frameworkVariant: { + id: 'frameworkVariant', + type: 'select', + label: 'Variant', + dependsOn: { mode: ['framework'], framework: ['nextjs', 'react'] }, + }, + frameworkUi: { + id: 'frameworkUi', + type: 'switch', + label: 'Shadcn', + dependsOn: { mode: ['framework'], framework: ['nextjs', 'react'] }, + }, + }) + + // With nextjs - should show all fields + const nextjsFields = getActiveFields(schema, { mode: 'framework', framework: 'nextjs' }) + expect(nextjsFields).toHaveLength(4) + + // With vue - should hide frameworkVariant and frameworkUi + const vueFields = getActiveFields(schema, { mode: 'framework', framework: 'vue' }) + expect(vueFields.map((field) => field.id)).toEqual(['mode', 'framework']) + }) + + test('should handle multiple dependsOn conditions', () => { + const schema = createSchemaWithFields({ + mode: { + id: 'mode', + type: 'select', + label: 'Mode', + defaultValue: 'direct', + options: () => [{ value: 'direct', label: 'Direct' }], + }, + connectionMethod: { + id: 'connectionMethod', + type: 'radio-list', + label: 'Method', + defaultValue: 'direct', + dependsOn: { mode: ['direct'] }, + }, + useSharedPooler: { + id: 'useSharedPooler', + type: 'switch', + label: 'Use Shared Pooler', + dependsOn: { mode: ['direct'], connectionMethod: ['transaction'] }, + }, + }) + + // Transaction mode - show shared pooler option + const transactionFields = getActiveFields(schema, { + mode: 'direct', + connectionMethod: 'transaction', + }) + expect(transactionFields.map((field) => field.id)).toEqual([ + 'mode', + 'connectionMethod', + 'useSharedPooler', + ]) + + // Direct mode - hide shared pooler option + const directFields = getActiveFields(schema, { mode: 'direct', connectionMethod: 'direct' }) + expect(directFields.map((field) => field.id)).toEqual(['mode', 'connectionMethod']) + }) + + test('should return only mode field when dependsOn does not match', () => { + const schema = createSchemaWithFields({ + mode: { + id: 'mode', + type: 'select', + label: 'Mode', + defaultValue: 'framework', + options: () => [{ value: 'framework', label: 'Framework' }], + }, + framework: { + id: 'framework', + type: 'radio-grid', + label: 'Framework', + dependsOn: { mode: ['framework'] }, + }, + }) + + const fields = getActiveFields(schema, { mode: 'invalid' as any }) + expect(fields.map((field) => field.id)).toEqual(['mode']) + }) + + test('should include resolvedOptions for each field', () => { + const schema = createSchemaWithFields({ + framework: { + id: 'framework', + type: 'radio-grid', + label: 'Framework', + options: () => [{ value: 'nextjs', label: 'Next.js' }], + }, + }) + + const fields = getActiveFields(schema, { mode: 'framework' }) + expect(fields[0]).toHaveProperty('resolvedOptions') + expect(fields[0].resolvedOptions).toEqual([{ value: 'nextjs', label: 'Next.js' }]) + }) +}) + +// ============================================================================ +// getDefaultState Tests +// ============================================================================ + +describe('connect.resolver:getDefaultState', () => { + test('should include default values from fields', () => { + const schema: ConnectSchema = { + fields: { + mode: { + id: 'mode', + type: 'select', + label: 'Mode', + defaultValue: 'framework', + options: () => [{ value: 'framework', label: 'Framework' }], + }, + framework: { + id: 'framework', + type: 'radio-grid', + label: 'Framework', + defaultValue: 'nextjs', + dependsOn: { mode: ['framework'] }, + }, + library: { + id: 'library', + type: 'select', + label: 'Library', + defaultValue: 'supabasejs', + dependsOn: { mode: ['framework'] }, + }, + mcpReadonly: { + id: 'mcpReadonly', + type: 'switch', + label: 'Readonly', + defaultValue: false, + }, + }, + steps: [], + } + + const state = getDefaultState(schema) + expect(state.framework).toBe('nextjs') + expect(state.library).toBe('supabasejs') + expect(state.mcpReadonly).toBe(false) + }) +}) + +// ============================================================================ +// resolveState Tests +// ============================================================================ + +describe('connect.resolver:resolveState', () => { + test('should apply defaults from options when valid', () => { + const schema: ConnectSchema = { + fields: { + mode: { + id: 'mode', + type: 'select', + label: 'Mode', + defaultValue: 'framework', + options: () => [{ value: 'framework', label: 'Framework' }], + }, + framework: { + id: 'framework', + type: 'select', + label: 'Framework', + defaultValue: 'react', + options: () => [ + { value: 'nextjs', label: 'Next.js' }, + { value: 'react', label: 'React' }, + ], + dependsOn: { mode: ['framework'] }, + }, + }, + steps: [], + } + + const state = resolveState(schema, {}) + expect(state.mode).toBe('framework') + expect(state.framework).toBe('react') + }) + + test('should fall back to first option when default is invalid', () => { + const schema: ConnectSchema = { + fields: { + mode: { + id: 'mode', + type: 'select', + label: 'Mode', + defaultValue: 'framework', + options: () => [{ value: 'framework', label: 'Framework' }], + }, + framework: { + id: 'framework', + type: 'select', + label: 'Framework', + defaultValue: 'angular', + options: () => [ + { value: 'nextjs', label: 'Next.js' }, + { value: 'react', label: 'React' }, + ], + dependsOn: { mode: ['framework'] }, + }, + }, + steps: [], + } + + const state = resolveState(schema, {}) + expect(state.framework).toBe('nextjs') + }) + + test('should refresh dependent values when options change', () => { + const schema: ConnectSchema = { + fields: { + mode: { + id: 'mode', + type: 'select', + label: 'Mode', + defaultValue: 'framework', + options: () => [{ value: 'framework', label: 'Framework' }], + }, + framework: { + id: 'framework', + type: 'select', + label: 'Framework', + defaultValue: 'nextjs', + options: () => [ + { value: 'nextjs', label: 'Next.js' }, + { value: 'react', label: 'React' }, + ], + dependsOn: { mode: ['framework'] }, + }, + variant: { + id: 'variant', + type: 'select', + label: 'Variant', + options: (state) => + state.framework === 'react' + ? [{ value: 'vite', label: 'Vite' }] + : [{ value: 'app', label: 'App' }], + dependsOn: { mode: ['framework'], framework: ['nextjs', 'react'] }, + }, + }, + steps: [], + } + + const state = resolveState(schema, { + mode: 'framework', + framework: 'react', + variant: 'app', + }) + + expect(state.variant).toBe('vite') + }) +}) diff --git a/apps/studio/components/interfaces/ConnectSheet/connect.resolver.ts b/apps/studio/components/interfaces/ConnectSheet/connect.resolver.ts new file mode 100644 index 0000000000000..019224ffe1468 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/connect.resolver.ts @@ -0,0 +1,281 @@ +import type { + ConditionalValue, + ConnectSchema, + ConnectState, + FieldOption, + ResolvedField, + ResolvedStep, + StepDefinition, + StepFieldValueMap, + StepTree, +} from './Connect.types' + +/** + * Check if a value is a conditional object (has nested state keys or DEFAULT) + */ +function isConditionalObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +/** + * Resolves a conditional value based on current state. + * Walks the tree using stateKeys in order, falling back to DEFAULT at each level. + * + * Example: Given state { mode: 'mcp', mcpClient: 'codex' } + * and stateKeys derived from schema field order + * + * 1. Look up 'mcp' (state.mode value) in tree -> found, continue + * 2. At mcp subtree { codex: [...], DEFAULT: [...] }, skip irrelevant keys + * until we find a key whose state value matches an entry in the object + * 3. Look up 'codex' (state.mcpClient value) in that subtree -> found, return value + * 4. If no state key matches, try DEFAULT at that level + */ +export function resolveConditional( + value: ConditionalValue, + state: ConnectState, + stateKeys: readonly string[] = Object.keys(state) +): T | undefined { + // Base case: we've reached a leaf value (string, array, null, boolean, etc.) + if (!isConditionalObject(value)) { + return value as T + } + + const conditionalObj = value as Record> + const objectKeys = Object.keys(conditionalObj).filter((k) => k !== 'DEFAULT') + + // Try each state key in order to find one that matches an entry in the object + for (let i = 0; i < stateKeys.length; i++) { + const currentKey = stateKeys[i] + const stateValue = String(state[currentKey] ?? '') + + // If this state value matches a key in the conditional object, use it + if (stateValue && objectKeys.includes(stateValue)) { + const nextValue = conditionalObj[stateValue] + // Continue resolving with remaining keys (after this one) + return resolveConditional(nextValue, state, stateKeys.slice(i + 1)) + } + } + + // No state key matched - use DEFAULT if available + if (conditionalObj.DEFAULT !== undefined) { + return resolveConditional(conditionalObj.DEFAULT, state, stateKeys) + } + + return undefined +} + +/** + * Resolves the steps array based on current state. + * Returns only steps that have non-null content. + */ +export function resolveSteps(schema: ConnectSchema, state: ConnectState): ResolvedStep[] { + const steps = resolveStepTree(schema.steps, state) + if (steps.length === 0) return [] + const stateKeys = Object.keys(schema.fields) + const resolutionKeys = stateKeys.length > 0 ? stateKeys : Object.keys(state) + + return steps + .map((step) => { + const content = resolveConditional(step.content, state, resolutionKeys) + return { + id: step.id, + title: step.title, + description: step.description, + content: content ?? '', + } + }) + .filter((step) => step.content !== '' && step.content !== null) +} + +/** + * Resolves a step tree by evaluating field-specific branches in insertion order. + * Each matching branch appends its steps to the final list. + */ +function resolveStepTree(tree: StepTree, state: ConnectState): StepDefinition[] { + if (Array.isArray(tree)) return tree + if (!isConditionalObject(tree)) return [] + + const resolved: StepDefinition[] = [] + + for (const [fieldId, valueMap] of Object.entries(tree)) { + if (fieldId === 'DEFAULT') continue + if (!isConditionalObject(valueMap)) continue + + const branch = resolveStepBranch(valueMap as StepFieldValueMap, state[fieldId]) + if (!branch) continue + + resolved.push(...resolveStepTree(branch, state)) + } + + return resolved +} + +function resolveStepBranch( + valueMap: StepFieldValueMap, + stateValue: ConnectState[keyof ConnectState] | undefined +): StepTree | undefined { + const key = String(stateValue ?? '') + if (key && Object.prototype.hasOwnProperty.call(valueMap, key)) { + return valueMap[key] + } + + if (valueMap.DEFAULT !== undefined) { + return valueMap.DEFAULT + } + + return undefined +} + +/** + * Gets the active fields for the current mode, filtering by dependsOn conditions. + */ +export function getActiveFields(schema: ConnectSchema, state: ConnectState): ResolvedField[] { + const stateKeys = Object.keys(schema.fields) + + return stateKeys + .map((fieldId) => schema.fields[fieldId]) + .filter((field): field is NonNullable => !!field) + .filter((field) => { + // Check dependsOn conditions + if (!field.dependsOn) return true + return Object.entries(field.dependsOn).every(([key, values]) => { + const stateValue = String(state[key] ?? '') + return values.includes(stateValue) + }) + }) + .map((field) => ({ + ...field, + resolvedOptions: resolveFieldOptions(field, state, stateKeys), + })) +} + +/** + * Resolves field options based on current state. + */ +function resolveFieldOptions( + field: { options?: unknown }, + state: ConnectState, + stateKeys: readonly string[] +): FieldOption[] { + if (!field.options) return [] + + // Static options array + if (Array.isArray(field.options)) { + return field.options + } + + if (typeof field.options === 'function') { + return (field.options as (state: ConnectState) => FieldOption[])(state) + } + + // Conditional options + if (typeof field.options === 'object') { + const resolved = resolveConditional( + field.options as ConditionalValue, + state, + stateKeys + ) + return resolved ?? [] + } + + return [] +} + +/** + * Normalizes state values based on schema defaults, options, and dependencies. + */ +export function resolveState( + schema: ConnectSchema, + inputState: Partial +): ConnectState { + const next: ConnectState = { ...(inputState as ConnectState) } + + const maxIterations = Math.max(1, Object.keys(schema.fields).length + 1) + + for (let iteration = 0; iteration < maxIterations; iteration++) { + let changed = false + const activeFields = getActiveFields(schema, next) + + for (const field of activeFields) { + const currentValue = next[field.id] + const optionValues = field.resolvedOptions.map((option) => option.value) + const hasOptions = optionValues.length > 0 + + if (field.type === 'switch') { + if (typeof currentValue !== 'boolean' && typeof field.defaultValue === 'boolean') { + next[field.id] = field.defaultValue + changed = true + } + continue + } + + if (field.type === 'multi-select') { + if (Array.isArray(currentValue)) { + if (hasOptions) { + const filtered = currentValue.filter((value) => optionValues.includes(String(value))) + if (filtered.length !== currentValue.length) { + next[field.id] = filtered + changed = true + } + } + } else if (Array.isArray(field.defaultValue)) { + next[field.id] = field.defaultValue + changed = true + } + continue + } + + if (typeof currentValue !== 'string') { + let nextValue: string | undefined + + if ( + typeof field.defaultValue === 'string' && + (!hasOptions || optionValues.includes(field.defaultValue)) + ) { + nextValue = field.defaultValue + } else if (hasOptions) { + nextValue = optionValues[0] + } + + if (nextValue !== undefined) { + next[field.id] = nextValue + changed = true + } + continue + } + + if (hasOptions && !optionValues.includes(currentValue)) { + let nextValue: string | undefined + + if (typeof field.defaultValue === 'string' && optionValues.includes(field.defaultValue)) { + nextValue = field.defaultValue + } else { + nextValue = optionValues[0] + } + + if (nextValue !== currentValue) { + next[field.id] = nextValue + changed = true + } + } + } + + if (!changed) break + } + + const activeIds = new Set(getActiveFields(schema, next).map((field) => field.id)) + Object.keys(schema.fields).forEach((fieldId) => { + if (!activeIds.has(fieldId)) { + delete next[fieldId] + } + }) + + return next +} + +/** + * Gets default state for the schema, using first mode and default field values. + */ +export function getDefaultState(schema: ConnectSchema): ConnectState { + return resolveState(schema, {}) +} diff --git a/apps/studio/components/interfaces/ConnectSheet/connect.schema.test.ts b/apps/studio/components/interfaces/ConnectSheet/connect.schema.test.ts new file mode 100644 index 0000000000000..750d08f6976c1 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/connect.schema.test.ts @@ -0,0 +1,266 @@ +import { describe, expect, test } from 'vitest' + +import { INSTALL_COMMANDS } from './Connect.constants' +import type { ConnectState } from './Connect.types' +import { resolveSteps } from './connect.resolver' +import { connectSchema } from './connect.schema' + +// ============================================================================ +// Schema Structure Tests +// ============================================================================ + +describe('connect.schema:structure', () => { + test('should define a mode field', () => { + const field = connectSchema.fields.mode + expect(field).toBeDefined() + expect(field.type).toBe('radio-list') + expect(field.defaultValue).toBe('framework') + const options = Array.isArray(field.options) ? field.options : [] + expect(options.some((option) => option.value === 'framework')).toBe(true) + }) +}) + +// ============================================================================ +// Field Definition Tests +// ============================================================================ + +describe('connect.schema:fields', () => { + test('framework field should have correct type', () => { + const field = connectSchema.fields.framework + expect(field.type).toBe('select') + expect(Array.isArray(field.options)).toBe(true) + const options = Array.isArray(field.options) ? field.options : [] + expect(options.some((option) => option.value === 'nextjs')).toBe(true) + expect(field.defaultValue).toBe('nextjs') + expect(field.dependsOn).toEqual({ mode: ['framework'] }) + }) + + test('frameworkVariant field should depend on framework', () => { + const field = connectSchema.fields.frameworkVariant + expect(field.dependsOn).toEqual({ mode: ['framework'], framework: ['nextjs', 'react'] }) + expect(typeof field.options).toBe('function') + }) + + test('frameworkUi field should be a switch type', () => { + const field = connectSchema.fields.frameworkUi + expect(field.type).toBe('switch') + expect(field.defaultValue).toBe(false) + expect(field.dependsOn).toEqual({ mode: ['framework'], framework: ['nextjs', 'react'] }) + }) + + test('connectionMethod field should be removed', () => { + const field = connectSchema.fields.connectionMethod + expect(field).toBeUndefined() + }) + + test('useSharedPooler field should be removed', () => { + const field = connectSchema.fields.useSharedPooler + expect(field).toBeUndefined() + }) + + test('orm field should be removed', () => { + const field = connectSchema.fields.orm + expect(field).toBeUndefined() + }) + + test('mcpClient field should be removed', () => { + const field = connectSchema.fields.mcpClient + expect(field).toBeUndefined() + }) + + test('mcpFeatures field should be removed', () => { + const field = connectSchema.fields.mcpFeatures + expect(field).toBeUndefined() + }) +}) + +// ============================================================================ +// Install Commands Tests +// ============================================================================ + +describe('connect.schema:INSTALL_COMMANDS', () => { + test('should have install command for supabase-js', () => { + expect(INSTALL_COMMANDS.supabasejs).toBe('npm install @supabase/supabase-js') + }) + + test('should have install command for supabase-py', () => { + expect(INSTALL_COMMANDS.supabasepy).toBe('pip install supabase') + }) + + test('should have install command for supabase-flutter', () => { + expect(INSTALL_COMMANDS.supabaseflutter).toBe('flutter pub add supabase_flutter') + }) + + test('should have install command for supabase-swift', () => { + expect(INSTALL_COMMANDS.supabaseswift).toContain('swift package add-dependency') + }) + + test('should have install command for supabase-kt', () => { + expect(INSTALL_COMMANDS.supabasekt).toContain('io.github.jan-tennert.supabase') + }) +}) + +// ============================================================================ +// Steps Resolution Integration Tests +// ============================================================================ + +describe('connect.schema:steps resolution', () => { + describe('framework mode steps', () => { + test('should resolve steps for nextjs without shadcn', () => { + const state: ConnectState = { mode: 'framework', framework: 'nextjs', frameworkUi: false } + const steps = resolveSteps(connectSchema, state) + + expect(steps.length).toBeGreaterThan(0) + expect(steps.find((s) => s.id === 'install')).toBeDefined() + expect(steps.find((s) => s.id === 'install-skills')).toBeDefined() + }) + + test('should resolve shadcn steps for nextjs with frameworkUi true', () => { + const state: ConnectState = { mode: 'framework', framework: 'nextjs', frameworkUi: true } + const steps = resolveSteps(connectSchema, state) + + expect(steps.find((s) => s.id === 'shadcn-add')).toBeDefined() + expect(steps.find((s) => s.id === 'shadcn-explore')).toBeDefined() + }) + + test('should resolve steps for react without shadcn', () => { + const state: ConnectState = { mode: 'framework', framework: 'react', frameworkUi: false } + const steps = resolveSteps(connectSchema, state) + + expect(steps.length).toBeGreaterThan(0) + expect(steps.find((s) => s.id === 'install')).toBeDefined() + }) + + test('should resolve shadcn steps for react with frameworkUi true', () => { + const state: ConnectState = { mode: 'framework', framework: 'react', frameworkUi: true } + const steps = resolveSteps(connectSchema, state) + + expect(steps.find((s) => s.id === 'shadcn-add')).toBeDefined() + }) + + test('should resolve default steps for other frameworks', () => { + const state: ConnectState = { mode: 'framework', framework: 'remix' } + const steps = resolveSteps(connectSchema, state) + + expect(steps.length).toBeGreaterThan(0) + expect(steps.find((s) => s.id === 'install')).toBeDefined() + expect(steps.find((s) => s.id === 'configure')).toBeDefined() + }) + }) + + describe('direct mode steps', () => { + test('should not resolve steps for direct mode', () => { + const state: ConnectState = { mode: 'direct' } + const steps = resolveSteps(connectSchema, state) + + expect(steps.length).toBe(0) + }) + }) + + describe('orm mode steps', () => { + test('should not resolve steps for orm mode', () => { + const state: ConnectState = { mode: 'orm', orm: 'prisma' } + const steps = resolveSteps(connectSchema, state) + + expect(steps.length).toBe(0) + }) + }) + + describe('mcp mode steps', () => { + test('should not resolve steps for mcp mode', () => { + const state: ConnectState = { mode: 'mcp', mcpClient: 'cursor' } + const steps = resolveSteps(connectSchema, state) + + expect(steps.length).toBe(0) + }) + }) + + describe('skills install step', () => { + test('should include skills install step in all modes', () => { + const modes: ConnectState['mode'][] = ['framework'] + + modes.forEach((mode) => { + const state: ConnectState = { mode } + const steps = resolveSteps(connectSchema, state) + + expect( + steps.find((s) => s.id === 'install-skills'), + `Mode "${mode}" should have skills install step` + ).toBeDefined() + }) + }) + }) +}) + +// ============================================================================ +// Step Content Path Tests +// ============================================================================ + +describe('connect.schema:step content paths', () => { + test('install step should have valid content path', () => { + const state: ConnectState = { mode: 'framework', framework: 'nextjs' } + const steps = resolveSteps(connectSchema, state) + const installStep = steps.find((s) => s.id === 'install') + + expect(installStep?.content).toBe('steps/install') + }) + + test('shadcn command step should have valid content path', () => { + const state: ConnectState = { mode: 'framework', framework: 'nextjs', frameworkUi: true } + const steps = resolveSteps(connectSchema, state) + const shadcnStep = steps.find((s) => s.id === 'shadcn-add') + + expect(shadcnStep?.content).toBe('steps/shadcn/command') + }) + + test('shadcn explore step should have valid content path', () => { + const state: ConnectState = { mode: 'framework', framework: 'nextjs', frameworkUi: true } + const steps = resolveSteps(connectSchema, state) + const exploreStep = steps.find((s) => s.id === 'shadcn-explore') + + expect(exploreStep?.content).toBe('steps/shadcn/explore') + }) + + test('direct connection step should not be resolved', () => { + const state: ConnectState = { mode: 'direct' } + const steps = resolveSteps(connectSchema, state) + + expect(steps.length).toBe(0) + }) + + test('skills install step should have valid content path', () => { + const state: ConnectState = { mode: 'framework' } + const steps = resolveSteps(connectSchema, state) + const skillsStep = steps.find((s) => s.id === 'install-skills') + + expect(skillsStep?.content).toBe('steps/skills-install') + }) + + test('orm configure step should not be resolved', () => { + const state: ConnectState = { mode: 'orm', orm: 'prisma' } + const steps = resolveSteps(connectSchema, state) + + expect(steps.length).toBe(0) + }) + + test('mcp cursor configure step should not be resolved', () => { + const state: ConnectState = { mode: 'mcp', mcpClient: 'cursor' } + const steps = resolveSteps(connectSchema, state) + + expect(steps.length).toBe(0) + }) + + test('codex steps should not be resolved', () => { + const state: ConnectState = { mode: 'mcp', mcpClient: 'codex' } + const steps = resolveSteps(connectSchema, state) + + expect(steps.length).toBe(0) + }) + + test('claude-code steps should not be resolved', () => { + const state: ConnectState = { mode: 'mcp', mcpClient: 'claude-code' } + const steps = resolveSteps(connectSchema, state) + + expect(steps.length).toBe(0) + }) +}) diff --git a/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts b/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts new file mode 100644 index 0000000000000..ed4f9d83ee9c8 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts @@ -0,0 +1,205 @@ +import { FRAMEWORKS, MOBILES } from './Connect.constants' +import type { ConnectSchema, ConnectState, FieldOption, StepDefinition } from './Connect.types' + +const frameworkOptions: FieldOption[] = [...FRAMEWORKS, ...MOBILES].map((framework) => ({ + value: framework.key, + label: framework.label, + icon: framework.icon, +})) + +const modeOptions: FieldOption[] = [ + { + value: 'framework', + label: 'Framework', + description: 'Use a client library', + }, +] + +const getFrameworkVariantOptions = (state: ConnectState): FieldOption[] => { + const allFrameworks = [...FRAMEWORKS, ...MOBILES] + const selected = allFrameworks.find((framework) => framework.key === state.framework) + if (!selected?.children?.length) return [] + if (selected.children.length <= 1) return [] + + return selected.children.map((variant) => ({ + value: variant.key, + label: variant.label, + icon: variant.icon, + })) +} + +const getLibraryOptions = (state: ConnectState): FieldOption[] => { + const allFrameworks = [...FRAMEWORKS, ...MOBILES] + const selectedFramework = allFrameworks.find((framework) => framework.key === state.framework) + if (!selectedFramework) return [] + + if (selectedFramework.children?.length > 1 && state.frameworkVariant) { + const variant = selectedFramework.children.find((child) => child.key === state.frameworkVariant) + if (variant?.children?.length) { + return variant.children.map((child) => ({ + value: child.key, + label: child.label, + icon: child.icon, + })) + } + } + + if (selectedFramework.children?.length === 1) { + const child = selectedFramework.children[0] + if (child.children?.length) { + return child.children.map((library) => ({ + value: library.key, + label: library.label, + icon: library.icon, + })) + } + + return [{ value: child.key, label: child.label, icon: child.icon }] + } + + return [] +} + +// ============================================================================ +// Step Definitions (reusable) +// All content paths use template syntax: {{stateKey}} is replaced with state values +// ============================================================================ + +const frameworkInstallStep: StepDefinition = { + id: 'install', + title: 'Install package', + description: 'Run this command to install the required dependencies.', + content: 'steps/install', +} + +const frameworkConfigureStep: StepDefinition = { + id: 'configure', + title: 'Add files', + description: 'Copy the following code into your project.', + content: '{{framework}}/{{frameworkVariant}}/{{library}}', +} + +const frameworkNextJsFilesStep: StepDefinition = { + id: 'configure-nextjs', + title: 'Add files', + description: + 'Add env variables, create Supabase client helpers, and set up middleware to keep sessions refreshed.', + content: '{{framework}}/{{frameworkVariant}}/{{library}}', +} + +const frameworkReactFilesStep: StepDefinition = { + id: 'configure-react', + title: 'Add files', + description: 'Add env variables, create a Supabase client, and use it in your app to query data.', + content: '{{framework}}/{{frameworkVariant}}/{{library}}', +} + +const frameworkShadcnStep: StepDefinition = { + id: 'shadcn-add', + title: 'Add Supabase UI components', + description: 'Run this command to install the Supabase shadcn components.', + content: 'steps/shadcn/command', +} + +const frameworkShadcnExploreStep: StepDefinition = { + id: 'shadcn-explore', + title: 'Check out more UI components', + description: 'Add auth, realtime and storage functionality to your project', + content: 'steps/shadcn/explore', +} + +const skillsInstallStep: StepDefinition = { + id: 'install-skills', + title: 'Install Agent Skills (Optional)', + description: + 'Agent Skills give AI coding tools ready-made instructions, scripts, and resources for working with Supabase more accurately and efficiently.', + content: 'steps/skills-install', +} + +// ============================================================================ +// Main Schema +// ============================================================================ + +export const connectSchema: ConnectSchema = { + // ------------------------------------------------------------------------- + // Field Definitions + // ------------------------------------------------------------------------- + fields: { + mode: { + id: 'mode', + type: 'radio-list', + label: 'Mode', + options: modeOptions, + defaultValue: 'framework', + }, + // Framework fields + framework: { + id: 'framework', + type: 'select', + label: 'Framework', + options: frameworkOptions, + defaultValue: 'nextjs', + dependsOn: { mode: ['framework'] }, + }, + frameworkVariant: { + id: 'frameworkVariant', + type: 'select', + label: 'Variant', + options: getFrameworkVariantOptions, + defaultValue: 'vite', + dependsOn: { mode: ['framework'], framework: ['nextjs', 'react'] }, // Only show for frameworks with multiple variants + }, + library: { + id: 'library', + type: 'select', + label: 'Library', + options: getLibraryOptions, + defaultValue: 'supabasejs', + dependsOn: { mode: ['framework'] }, + }, + frameworkUi: { + id: 'frameworkUi', + type: 'switch', + label: 'Shadcn', + description: 'Install components via the Supabase shadcn registry.', + defaultValue: false, + dependsOn: { mode: ['framework'], framework: ['nextjs', 'react'] }, + }, + }, + + // ------------------------------------------------------------------------- + // Steps - Conditional based on mode and nested selections + // ------------------------------------------------------------------------- + steps: { + // Keys are field IDs; each field maps state values to step trees. + mode: { + framework: { + framework: { + nextjs: { + frameworkUi: { + true: [ + frameworkInstallStep, + frameworkShadcnStep, + frameworkShadcnExploreStep, + skillsInstallStep, + ], + DEFAULT: [frameworkInstallStep, frameworkNextJsFilesStep, skillsInstallStep], + }, + }, + react: { + frameworkUi: { + true: [ + frameworkInstallStep, + frameworkShadcnStep, + frameworkShadcnExploreStep, + skillsInstallStep, + ], + DEFAULT: [frameworkInstallStep, frameworkReactFilesStep, skillsInstallStep], + }, + }, + DEFAULT: [frameworkInstallStep, frameworkConfigureStep, skillsInstallStep], + }, + }, + }, + }, +} diff --git a/apps/studio/components/interfaces/ConnectSheet/content/androidkotlin/supabasekt/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/androidkotlin/supabasekt/content.tsx new file mode 100644 index 0000000000000..455506fce0ff6 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/content/androidkotlin/supabasekt/content.tsx @@ -0,0 +1,71 @@ +import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock' + +import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types' + +const ContentFile = ({ projectKeys }: StepContentProps) => { + const files = [ + { + name: 'MainActivity.kt', + language: 'kotlin', + code: ` +val supabase = createSupabaseClient( + supabaseUrl = "${projectKeys.apiUrl ?? 'your-project-url'}", + supabaseKey = "${projectKeys.publishableKey ?? ''}" + ) { + install(Postgrest) +} + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + MaterialTheme { + // A surface container using the 'background' color from the theme + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + TodoList() + } + } + } + } +} + +@Composable +fun TodoList() { + var items by remember { mutableStateOf>(listOf()) } + LaunchedEffect(Unit) { + withContext(Dispatchers.IO) { + items = supabase.from("todos") + .select().decodeList() + } + } + LazyColumn { + items( + items, + key = { item -> item.id }, + ) { item -> + Text( + item.name, + modifier = Modifier.padding(8.dp), + ) + } + } +} +`, + }, + { + name: 'TodoItem.kt', + language: 'kotlin', + code: ` +@Serializable +data class TodoItem(val id: Int, val name: String) + `, + }, + ] + + return +} + +export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/astro/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/astro/supabasejs/content.tsx new file mode 100644 index 0000000000000..f30bcf0cdd647 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/content/astro/supabasejs/content.tsx @@ -0,0 +1,53 @@ +import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock' + +import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types' + +const ContentFile = ({ projectKeys }: StepContentProps) => { + const files = [ + { + name: '.env.local', + language: 'bash', + code: ` +SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'} +SUPABASE_KEY=${projectKeys.publishableKey ?? projectKeys.anonKey ?? 'your-anon-key'} + `, + }, + { + name: 'src/db/supabase.js', + language: 'js', + code: ` +import { createClient } from "@supabase/supabase-js"; + +const supabaseUrl = import.meta.env.SUPABASE_URL; +const supabaseKey = import.meta.env.SUPABASE_KEY; + +export const supabase = createClient(supabaseUrl, supabaseKey); + `, + }, + { + name: 'src/pages/index.astro', + language: 'html', + code: ` +--- +import { supabase } from '../db/supabase'; + +const { data, error } = await supabase.from("todos").select('*'); +--- + +{ + ( +
    + {data.map((entry) => ( +
  • {entry.name}
  • + ))} +
+ ) +} +`, + }, + ] + + return +} + +export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/exporeactnative/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/exporeactnative/supabasejs/content.tsx new file mode 100644 index 0000000000000..950e47594533b --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/content/exporeactnative/supabasejs/content.tsx @@ -0,0 +1,86 @@ +import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock' + +import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types' + +const ContentFile = ({ projectKeys }: StepContentProps) => { + const files = [ + { + name: '.env.local', + language: 'bash', + code: ` +EXPO_PUBLIC_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'} +EXPO_PUBLIC_SUPABASE_KEY=${projectKeys.publishableKey ?? ''} + `, + }, + { + name: 'utils/supabase.ts', + language: 'ts', + code: ` +import AsyncStorage from '@react-native-async-storage/async-storage' +import { createClient } from '@supabase/supabase-js' + +export const supabase = createClient( + process.env.EXPO_PUBLIC_SUPABASE_URL!, + process.env.EXPO_PUBLIC_SUPABASE_KEY!, + { + auth: { + storage: AsyncStorage, + autoRefreshToken: true, + persistSession: true, + detectSessionInUrl: false, + }, + }) + `, + }, + { + name: 'App.tsx', + language: 'tsx', + code: ` +import React, { useState, useEffect } from 'react'; +import { View, Text, FlatList } from 'react-native'; +import { supabase } from '../utils/supabase'; + +export default function App() { + const [todos, setTodos] = useState([]); + + useEffect(() => { + const getTodos = async () => { + try { + const { data: todos, error } = await supabase.from('todos').select(); + + if (error) { + console.error('Error fetching todos:', error.message); + return; + } + + if (todos && todos.length > 0) { + setTodos(todos); + } + } catch (error) { + console.error('Error fetching todos:', error.message); + } + }; + + getTodos(); + }, []); + + return ( + + Todo List + item.id.toString()} + renderItem={({ item }) => {item.name}} + /> + + ); +}; + +`, + }, + ] + + return +} + +export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/flask/supabasepy/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/flask/supabasepy/content.tsx new file mode 100644 index 0000000000000..644ceb769e499 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/content/flask/supabasepy/content.tsx @@ -0,0 +1,54 @@ +import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock' + +import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types' + +const ContentFile = ({ projectKeys }: StepContentProps) => { + const files = [ + { + name: '.env', + language: 'bash', + code: ` +SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'} +SUPABASE_KEY=${projectKeys.publishableKey ?? projectKeys.anonKey ?? 'your-anon-key'} + `, + }, + { + name: 'app.py', + language: 'python', + code: ` +import os +from flask import Flask +from supabase import create_client, Client +from dotenv import load_dotenv + +load_dotenv() + +app = Flask(__name__) + +supabase: Client = create_client( + os.environ.get("SUPABASE_URL"), + os.environ.get("SUPABASE_KEY") +) + +@app.route('/') +def index(): + response = supabase.table('todos').select("*").execute() + todos = response.data + + html = '

Todos

    ' + for todo in todos: + html += f'
  • {todo["name"]}
  • ' + html += '
' + + return html + +if __name__ == '__main__': + app.run(debug=True) +`, + }, + ] + + return +} + +export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/flutter/supabaseflutter/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/flutter/supabaseflutter/content.tsx new file mode 100644 index 0000000000000..90a7b36f2c475 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/content/flutter/supabaseflutter/content.tsx @@ -0,0 +1,82 @@ +import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock' + +import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types' + +const ContentFile = ({ projectKeys }: StepContentProps) => { + const files = [ + { + name: 'lib/main.dart', + language: 'dart', + code: ` +import 'package:flutter/material.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +Future main() async { + await Supabase.initialize( + url: '${projectKeys.apiUrl ?? 'your-project-url'}', + anonKey: '${projectKeys.publishableKey ?? ''}', + ); + runApp(MyApp()); +} + `, + }, + { + name: 'lib/main.dart (app)', + language: 'dart', + code: ` +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + title: 'Todos', + home: HomePage(), + ); + } +} + +class HomePage extends StatefulWidget { + const HomePage({super.key}); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + final _future = Supabase.instance.client + .from('todos') + .select(); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: FutureBuilder( + future: _future, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center(child: CircularProgressIndicator()); + } + final todos = snapshot.data!; + return ListView.builder( + itemCount: todos.length, + itemBuilder: ((context, index) { + final todo = todos[index]; + return ListTile( + title: Text(todo['name']), + ); + }), + ); + }, + ), + ); + } +} +`, + }, + ] + + return +} + +export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/ionicangular/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/ionicangular/supabasejs/content.tsx new file mode 100644 index 0000000000000..d1cd404f75235 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/content/ionicangular/supabasejs/content.tsx @@ -0,0 +1,127 @@ +import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock' + +import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types' + +const ContentFile = ({ projectKeys }: StepContentProps) => { + const files = [ + { + name: 'environments/environment.ts', + language: 'ts', + code: ` +export const environment = { + supabaseUrl: '${projectKeys.apiUrl ?? 'your-project-url'}', + supabaseKey: '${projectKeys.publishableKey ?? ''}', +}; +`, + }, + { + name: 'src/app/supabase.service.ts', + language: 'ts', + code: ` +import { Injectable } from '@angular/core'; +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { environment } from '../environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class SupabaseService { + private supabase: SupabaseClient; + constructor() { + this.supabase = createClient( + environment.supabaseUrl, + environment.supabaseKey + ); + } + + getTodos() { + return this.supabase.from('todos').select('*'); + } +} +`, + }, + { + name: 'src/app/app.component.ts', + language: 'ts', + code: ` +import { Component, OnInit } from '@angular/core'; +import { SupabaseService } from './supabase.service'; + +@Component({ + selector: 'app-root', + templateUrl: 'app.component.html', + styleUrls: ['app.component.scss'], +}) +export class AppComponent implements OnInit { + todos: any[] = []; + + constructor(private supabaseService: SupabaseService) {} + + async ngOnInit() { + await this.loadTodos(); + } + + async loadTodos() { + const { data, error } = await this.supabaseService.getTodos(); + if (error) { + console.error('Error fetching todos:', error); + } else { + this.todos = data; + } + } +} +`, + }, + { + name: 'src/app/app.component.html', + language: 'html', + code: ` + + + Todo List + + + + + + + {{ todo.name }} + + + +`, + }, + { + name: 'src/app/app.module.ts', + language: 'ts', + code: ` +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { RouterModule } from '@angular/router'; + +import { IonicModule } from '@ionic/angular'; + +import { AppComponent } from './app.component'; +import { SupabaseService } from './supabase.service'; + +@NgModule({ + imports: [ + BrowserModule, + FormsModule, + RouterModule.forRoot([]), + IonicModule.forRoot({ mode: 'ios' }), + ], + declarations: [AppComponent], + providers: [SupabaseService], + bootstrap: [AppComponent], +}) +export class AppModule {} +`, + }, + ] + + return +} + +export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/ionicreact/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/ionicreact/supabasejs/content.tsx new file mode 100644 index 0000000000000..be4f077cd5ff2 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/content/ionicreact/supabasejs/content.tsx @@ -0,0 +1,101 @@ +import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock' + +import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types' + +const ContentFile = ({ projectKeys }: StepContentProps) => { + const files = [ + { + name: '.env', + language: 'bash', + code: ` +REACT_APP_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'} +REACT_APP_SUPABASE_KEY=${projectKeys.publishableKey ?? ''} + `, + }, + { + name: 'src/supabaseClient.tsx', + language: 'ts', + code: ` +import { createClient } from '@supabase/supabase-js' + +const supabaseUrl = process.env.REACT_APP_SUPABASE_URL +const supabaseAnonKey = process.env.REACT_APP_SUPABASE_KEY + +export const supabase = createClient(supabaseUrl, supabaseAnonKey) +`, + }, + { + name: 'src/App.tsx', + language: 'ts', + code: ` +import React, { useEffect, useState } from 'react'; +import { setupIonicReact, IonApp } from '@ionic/react'; +import { + IonContent, + IonHeader, + IonTitle, + IonToolbar, + IonList, + IonItem, +} from '@ionic/react'; + +/* Core CSS required for Ionic components to work properly */ +import '@ionic/react/css/core.css'; + +/* Theme variables */ +import './theme/variables.css'; + +import { supabase } from './supabaseClient'; + +setupIonicReact(); + +export default function App() { + const [todos, setTodos] = useState([]); + useEffect(() => { + getTodos(); + }, []); + + const getTodos = async () => { + try { + const { data, error } = await supabase.from('todos').select(); + + if (error) { + console.error('Error fetching todos:', error.message); + return; + } + + if (data) { + setTodos(data); + } + } catch (error) { + console.error('Error fetching todos:', error.message); + } + }; + + return ( + + <> + + + Todos + + + + + {todos.map((todo) => ( + {todo.name} + ))} + + + + + ); +} +`, + }, + ] + + return +} + +export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/nextjs/app/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/nextjs/app/supabasejs/content.tsx new file mode 100644 index 0000000000000..81821be693079 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/content/nextjs/app/supabasejs/content.tsx @@ -0,0 +1,141 @@ +import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock' + +import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types' + +const ContentFile = ({ projectKeys }: StepContentProps) => { + const files = [ + { + name: '.env.local', + language: 'bash', + code: [ + `NEXT_PUBLIC_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`, + projectKeys?.publishableKey + ? `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${projectKeys.publishableKey}` + : `NEXT_PUBLIC_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`, + '', + ].join('\n'), + }, + { + name: 'page.tsx', + language: 'tsx', + code: ` +import { createClient } from '@/utils/supabase/server' +import { cookies } from 'next/headers' + +export default async function Page() { + const cookieStore = await cookies() + const supabase = createClient(cookieStore) + + const { data: todos } = await supabase.from('todos').select() + + return ( +
    + {todos?.map((todo) => ( +
  • {todo.name}
  • + ))} +
+ ) +} +`, + }, + { + name: 'utils/supabase/server.ts', + language: 'ts', + code: ` +import { createServerClient } from "@supabase/ssr"; +import { cookies } from "next/headers"; + +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; +const supabaseKey = process.env.${projectKeys?.publishableKey ? 'NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'NEXT_PUBLIC_SUPABASE_ANON_KEY'}; + +export const createClient = (cookieStore: Awaited>) => { + return createServerClient( + supabaseUrl!, + supabaseKey!, + { + cookies: { + getAll() { + return cookieStore.getAll() + }, + setAll(cookiesToSet) { + try { + cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options)) + } catch { + // The \`setAll\` method was called from a Server Component. + // This can be ignored if you have middleware refreshing + // user sessions. + } + }, + }, + }, + ); +}; +`, + }, + { + name: 'utils/supabase/client.ts', + language: 'ts', + code: ` +import { createBrowserClient } from "@supabase/ssr"; + +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; +const supabaseKey = process.env.${projectKeys?.publishableKey ? 'NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'NEXT_PUBLIC_SUPABASE_ANON_KEY'}; + +export const createClient = () => + createBrowserClient( + supabaseUrl!, + supabaseKey!, + ); +`, + }, + { + name: 'utils/supabase/middleware.ts', + language: 'ts', + code: ` +import { createServerClient } from "@supabase/ssr"; +import { type NextRequest, NextResponse } from "next/server"; + +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; +const supabaseKey = process.env.${projectKeys?.publishableKey ? 'NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'NEXT_PUBLIC_SUPABASE_ANON_KEY'}; + +export const createClient = (request: NextRequest) => { + // Create an unmodified response + let supabaseResponse = NextResponse.next({ + request: { + headers: request.headers, + }, + }); + + const supabase = createServerClient( + supabaseUrl!, + supabaseKey!, + { + cookies: { + getAll() { + return request.cookies.getAll() + }, + setAll(cookiesToSet) { + cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value)) + supabaseResponse = NextResponse.next({ + request, + }) + cookiesToSet.forEach(({ name, value, options }) => + supabaseResponse.cookies.set(name, value, options) + ) + }, + }, + }, + ); + + return supabaseResponse +}; +`, + }, + ] + + return +} + +// [Joshen] Used as a dynamic import +// eslint-disable-next-line no-restricted-exports +export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/nextjs/pages/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/nextjs/pages/supabasejs/content.tsx new file mode 100644 index 0000000000000..ce514b0ce4c17 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/content/nextjs/pages/supabasejs/content.tsx @@ -0,0 +1,67 @@ +import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock' + +import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types' + +const ContentFile = ({ projectKeys }: StepContentProps) => { + const files = [ + { + name: '.env.local', + language: 'bash', + code: [ + `NEXT_PUBLIC_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`, + projectKeys?.publishableKey + ? `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${projectKeys.publishableKey}` + : `NEXT_PUBLIC_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`, + '', + ].join('\n'), + }, + { + name: 'utils/supabase.ts', + language: 'ts', + code: ` +import { createClient } from "@supabase/supabase-js"; + +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; +const supabaseKey = process.env.${projectKeys?.publishableKey ? 'NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'NEXT_PUBLIC_SUPABASE_ANON_KEY'}!; + +export const supabase = createClient(supabaseUrl, supabaseKey); +`, + }, + { + name: 'pages/index.tsx', + language: 'tsx', + code: ` +import { useState, useEffect } from 'react' +import { supabase } from '../utils/supabase' + +export default function Page() { + const [todos, setTodos] = useState([]) + + useEffect(() => { + async function getTodos() { + const { data: todos } = await supabase.from('todos').select() + + if (todos) { + setTodos(todos) + } + } + + getTodos() + }, []) + + return ( +
    + {todos.map((todo) => ( +
  • {todo.name}
  • + ))} +
+ ) +} +`, + }, + ] + + return +} + +export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/nuxt/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/nuxt/supabasejs/content.tsx new file mode 100644 index 0000000000000..08357299a13c6 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/content/nuxt/supabasejs/content.tsx @@ -0,0 +1,65 @@ +import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock' + +import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types' + +const ContentFile = ({ projectKeys }: StepContentProps) => { + const files = [ + { + name: '.env.local', + language: 'bash', + code: [ + `SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`, + `SUPABASE_KEY=${projectKeys.publishableKey ?? projectKeys.anonKey ?? 'your-anon-key'}`, + '', + ].join('\n'), + }, + { + name: 'nuxt.config.ts', + language: 'ts', + code: ` +export default defineNuxtConfig({ + runtimeConfig: { + public: { + supabaseUrl: process.env.SUPABASE_URL, + supabaseKey: process.env.SUPABASE_KEY, + }, + }, +}) +`, + }, + { + name: 'app.vue', + language: 'html', + code: ` + + + +`, + }, + ] + + return +} + +export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/react/create-react-app/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/react/create-react-app/supabasejs/content.tsx new file mode 100644 index 0000000000000..fb41c6b01be96 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/content/react/create-react-app/supabasejs/content.tsx @@ -0,0 +1,67 @@ +import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock' + +import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types' + +const ContentFile = ({ projectKeys }: StepContentProps) => { + const files = [ + { + name: '.env.local', + language: 'bash', + code: [ + `REACT_APP_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`, + projectKeys?.publishableKey + ? `REACT_APP_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${projectKeys.publishableKey}` + : `REACT_APP_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`, + '', + ].join('\n'), + }, + { + name: 'utils/supabase.ts', + language: 'ts', + code: ` +import { createClient } from "@supabase/supabase-js"; + +const supabaseUrl = process.env.REACT_APP_SUPABASE_URL; +const supabaseKey = process.env.${projectKeys.publishableKey ? 'REACT_APP_SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'REACT_APP_SUPABASE_ANON_KEY'}; + +export const supabase = createClient(supabaseUrl, supabaseKey); + `, + }, + { + name: 'App.tsx', + language: 'tsx', + code: ` +import { useState, useEffect } from 'react' +import { supabase } from './utils/supabase' + +export default function App() { + const [todos, setTodos] = useState([]) + + useEffect(() => { + async function getTodos() { + const { data: todos } = await supabase.from('todos').select() + + if (todos) { + setTodos(todos) + } + } + + getTodos() + }, []) + + return ( +
    + {todos.map((todo) => ( +
  • {todo.name}
  • + ))} +
+ ) +} +`, + }, + ] + + return +} + +export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/react/vite/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/react/vite/supabasejs/content.tsx new file mode 100644 index 0000000000000..87ca660b40fdc --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/content/react/vite/supabasejs/content.tsx @@ -0,0 +1,67 @@ +import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock' + +import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types' + +const ContentFile = ({ projectKeys }: StepContentProps) => { + const files = [ + { + name: '.env', + language: 'bash', + code: [ + `VITE_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`, + projectKeys?.publishableKey + ? `VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${projectKeys.publishableKey}` + : `VITE_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`, + '', + ].join('\n'), + }, + { + name: 'utils/supabase.ts', + language: 'ts', + code: ` +import { createClient } from '@supabase/supabase-js'; + +const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; +const supabaseKey = import.meta.env.${projectKeys.publishableKey ? 'VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'VITE_SUPABASE_ANON_KEY'}; + +export const supabase = createClient(supabaseUrl, supabaseKey); +`, + }, + { + name: 'App.tsx', + language: 'tsx', + code: ` +import { useState, useEffect } from 'react' +import { supabase } from './utils/supabase' + +export default function App() { + const [todos, setTodos] = useState([]) + + useEffect(() => { + async function getTodos() { + const { data: todos } = await supabase.from('todos').select() + + if (todos) { + setTodos(todos) + } + } + + getTodos() + }, []) + + return ( +
    + {todos.map((todo) => ( +
  • {todo.name}
  • + ))} +
+ ) +} +`, + }, + ] + + return +} + +export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/refine/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/refine/supabasejs/content.tsx new file mode 100644 index 0000000000000..97b65d0932765 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/content/refine/supabasejs/content.tsx @@ -0,0 +1,102 @@ +import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock' + +import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types' + +const ContentFile = ({ projectKeys }: StepContentProps) => { + const files = [ + { + name: '.env.local', + language: 'bash', + code: [ + `SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`, + `SUPABASE_KEY=${projectKeys?.publishableKey ?? projectKeys?.anonKey ?? 'your-anon-key'}`, + '', + ].join('\n'), + }, + { + name: 'src/utility/supabaseClient.ts', + language: 'ts', + code: ` +import { createClient } from "@refinedev/supabase"; + +const SUPABASE_URL = process.env.SUPABASE_URL; +const SUPABASE_KEY = process.env.SUPABASE_KEY; + +export const supabaseClient = createClient(SUPABASE_URL, SUPABASE_KEY, { + db: { + schema: "public", + }, + auth: { + persistSession: true, + }, +}); + `, + }, + { + name: 'src/App.tsx', + language: 'tsx', + code: ` +import { Refine } from "@refinedev/core"; +import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar"; +import routerProvider, { + DocumentTitleHandler, + NavigateToResource, + UnsavedChangesNotifier, +} from "@refinedev/react-router"; +import { dataProvider, liveProvider } from "@refinedev/supabase"; +import { BrowserRouter, Route, Routes } from "react-router-dom"; + +import "./App.css"; +import authProvider from "./authProvider"; +import { supabaseClient } from "./utility"; +import { CountriesCreate, CountriesEdit, CountriesList, CountriesShow } from "./pages/countries"; + +function App() { + return ( + + + + + } + /> + + } /> + } /> + } /> + } /> + + + + + + + + + ); +} + +export default App; +`, + }, + ] + + return +} + +export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/remix/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/remix/supabasejs/content.tsx new file mode 100644 index 0000000000000..d1bf8354bc775 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/content/remix/supabasejs/content.tsx @@ -0,0 +1,91 @@ +import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock' + +import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types' + +const ContentFile = ({ projectKeys }: StepContentProps) => { + const files = [ + { + name: '.env', + language: 'bash', + code: [ + `VITE_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`, + projectKeys?.publishableKey + ? `VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${projectKeys.publishableKey}` + : `VITE_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`, + '', + ].join('\n'), + }, + { + name: 'app/utils/supabase.server.ts', + language: 'ts', + code: ` +import { + createServerClient, + parseCookieHeader, + serializeCookieHeader, +} from "@supabase/ssr"; + +export function createClient(request: Request) { + const headers = new Headers(); + + const supabase = createServerClient( + process.env.VITE_SUPABASE_URL!, + process.env.VITE_${projectKeys.publishableKey ? 'SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'SUPABASE_ANON_KEY'}, + { + cookies: { + getAll() { + return parseCookieHeader(request.headers.get("Cookie") ?? "") as { + name: string; + value: string; + }[]; + }, + setAll(cookiesToSet) { + cookiesToSet.forEach(({ name, value, options }) => + headers.append( + "Set-Cookie", + serializeCookieHeader(name, value, options) + ) + ); + }, + }, + } + ); + + return { supabase, headers }; +} +`, + }, + { + name: 'app/routes/_index.tsx', + language: 'tsx', + code: ` +import type { Route } from "./+types/home"; +import { createClient } from "~/utils/supabase.server"; + +export async function loader({ request }: Route.LoaderArgs) { + const { supabase } = createClient(request); + const { data: todos } = await supabase.from("todos").select(); + + return { todos }; +} + +export default function Home({ loaderData }: Route.ComponentProps) { + return ( + <> +
    + {loaderData.todos?.map((todo) => ( +
  • {todo.name}
  • + ))} +
+ + ); +} + +`, + }, + ] + + return +} + +export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/solidjs/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/solidjs/supabasejs/content.tsx new file mode 100644 index 0000000000000..c8a4608e34834 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/content/solidjs/supabasejs/content.tsx @@ -0,0 +1,60 @@ +import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock' + +import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types' + +const ContentFile = ({ projectKeys }: StepContentProps) => { + const files = [ + { + name: '.env.local', + language: 'bash', + code: [ + `VITE_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`, + projectKeys?.publishableKey + ? `VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${projectKeys.publishableKey}` + : `VITE_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`, + '', + ].join('\n'), + }, + { + name: 'utils/supabase.ts', + language: 'ts', + code: ` +import { createClient } from "@supabase/supabase-js"; + +const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; +const supabaseKey = import.meta.env.${projectKeys.publishableKey ? 'VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'VITE_SUPABASE_ANON_KEY'}; + +export const supabase = createClient(supabaseUrl, supabaseKey); +`, + }, + { + name: 'src/App.tsx', + language: 'tsx', + code: ` +import { supabase } from '../utils/supabase' +import { createResource, For } from "solid-js"; + +async function getTodos() { + const { data: todos } = await supabase.from("todos").select(); + return todos; +} + +function App() { + const [todos] = createResource(getTodos); + + return ( +
    + {(todo) =>
  • {todo.name}
  • }
    +
+ ); +} + +export default App; +`, + }, + ] + + return +} + +export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/steps/install/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/steps/install/content.tsx new file mode 100644 index 0000000000000..15d84d9a93a59 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/content/steps/install/content.tsx @@ -0,0 +1,54 @@ +import { Copy } from 'lucide-react' +import { useMemo, useState } from 'react' +import { Button, copyToClipboard } from 'ui' + +import { INSTALL_COMMANDS } from '../../../Connect.constants' +import type { StepContentProps } from '../../../Connect.types' + +/** + * Gets the install command for the current framework selection. + */ +function getInstallCommand(state: StepContentProps['state']): string | null { + const libraryKey = typeof state.library === 'string' ? state.library : null + + if (libraryKey && INSTALL_COMMANDS[libraryKey]) return INSTALL_COMMANDS[libraryKey] + + return null +} + +function InstallContent({ state }: StepContentProps) { + const installCommand = useMemo(() => getInstallCommand(state), [state]) + const [copyLabel, setCopyLabel] = useState('Copy') + + if (!installCommand) { + return null + } + + const handleCopy = () => { + copyToClipboard(installCommand, () => { + setCopyLabel('Copied') + setTimeout(() => setCopyLabel('Copy'), 2000) + }) + } + + return ( +
+
+ {installCommand} +
+
+ +
+
+ ) +} + +export default InstallContent diff --git a/apps/studio/components/interfaces/ConnectSheet/content/steps/shadcn/command/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/steps/shadcn/command/content.tsx new file mode 100644 index 0000000000000..c8c15e50f90dc --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/content/steps/shadcn/command/content.tsx @@ -0,0 +1,52 @@ +import { Copy } from 'lucide-react' +import { useMemo, useState } from 'react' +import { Button, copyToClipboard } from 'ui' + +import type { StepContentProps } from '../../../../Connect.types' + +function getShadcnCommand(state: StepContentProps['state']): string | null { + if (state.framework === 'nextjs') { + return 'npx shadcn@latest add @supabase/supabase-client-nextjs' + } + + if (state.framework === 'react') { + return 'npx shadcn@latest add @supabase/supabase-client-react-router' + } + + return null +} + +function ShadcnCommandContent({ state }: StepContentProps) { + const command = useMemo(() => getShadcnCommand(state), [state]) + const [copyLabel, setCopyLabel] = useState('Copy') + + if (!command) return null + + const handleCopy = () => { + copyToClipboard(command, () => { + setCopyLabel('Copied') + setTimeout(() => setCopyLabel('Copy'), 2000) + }) + } + + return ( +
+
+ {command} +
+
+ +
+
+ ) +} + +export default ShadcnCommandContent diff --git a/apps/studio/components/interfaces/ConnectSheet/content/steps/shadcn/explore/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/steps/shadcn/explore/content.tsx new file mode 100644 index 0000000000000..ee3d721072801 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/content/steps/shadcn/explore/content.tsx @@ -0,0 +1,16 @@ +import { ExternalLink } from 'lucide-react' +import { Button } from 'ui' + +import type { StepContentProps } from '../../../../Connect.types' + +function ShadcnExploreContent(_props: StepContentProps) { + return ( +
+ ) +} + +export default ShadcnExploreContent diff --git a/apps/studio/components/interfaces/ConnectSheet/content/steps/skills-install/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/steps/skills-install/content.tsx new file mode 100644 index 0000000000000..2a0bdfc8f8365 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/content/steps/skills-install/content.tsx @@ -0,0 +1,39 @@ +import { Copy } from 'lucide-react' +import { useState } from 'react' +import { Button, copyToClipboard } from 'ui' + +import type { StepContentProps } from '../../../Connect.types' + +const SKILLS_COMMAND = 'npx skills add supabase/agent-skills' + +function SkillsInstallContent(_props: StepContentProps) { + const [copyLabel, setCopyLabel] = useState('Copy') + + const handleCopy = () => { + copyToClipboard(SKILLS_COMMAND, () => { + setCopyLabel('Copied') + setTimeout(() => setCopyLabel('Copy'), 2000) + }) + } + + return ( +
+
+ {SKILLS_COMMAND} +
+
+ +
+
+ ) +} + +export default SkillsInstallContent diff --git a/apps/studio/components/interfaces/ConnectSheet/content/sveltekit/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/sveltekit/supabasejs/content.tsx new file mode 100644 index 0000000000000..608c3e7f7b452 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/content/sveltekit/supabasejs/content.tsx @@ -0,0 +1,65 @@ +import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock' + +import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types' + +const ContentFile = ({ projectKeys }: StepContentProps) => { + const files = [ + { + name: '.env.local', + language: 'bash', + code: [ + `PUBLIC_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`, + projectKeys?.publishableKey + ? `PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${projectKeys.publishableKey}` + : `PUBLIC_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`, + '', + ].join('\n'), + }, + { + name: 'src/lib/supabaseClient.js', + language: 'js', + code: ` +import { createClient } from "@supabase/supabase-js"; +import { PUBLIC_SUPABASE_URL, ${projectKeys.publishableKey ? 'PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'PUBLIC_SUPABASE_ANON_KEY'} } from "$env/static/public" + +const supabaseUrl = PUBLIC_SUPABASE_URL; +const supabaseKey = ${projectKeys.publishableKey ? 'PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'PUBLIC_SUPABASE_ANON_KEY'}; + +export const supabase = createClient(supabaseUrl, supabaseKey); + `, + }, + { + name: 'src/routes/+page.server.js', + language: 'js', + code: ` +import { supabase } from "$lib/supabaseClient"; + +export async function load() { + const { data } = await supabase.from("countries").select(); + return { + countries: data ?? [], + }; +} +`, + }, + { + name: 'src/routes/+page.svelte', + language: 'html', + code: ` + + +
    + {#each data.countries as country} +
  • {country.name}
  • + {/each} +
+`, + }, + ] + + return +} + +export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/swift/supabaseswift/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/swift/supabaseswift/content.tsx new file mode 100644 index 0000000000000..d6054f6013eac --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/content/swift/supabaseswift/content.tsx @@ -0,0 +1,70 @@ +import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock' + +import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types' + +const ContentFile = ({ projectKeys }: StepContentProps) => { + const files = [ + { + name: 'Supabase.swift', + language: 'swift', + code: ` +import Foundation +import Supabase + +let supabase = SupabaseClient( + supabaseURL: URL(string: "${projectKeys.apiUrl ?? 'your-project-url'}")!, + supabaseKey: "${projectKeys.publishableKey ?? ''}" +) + `, + }, + { + name: 'Todo.swift', + language: 'swift', + code: ` +import Foundation + +struct Todo: Identifiable, Decodable { + var id: Int + var name: String +} +`, + }, + { + name: 'ContentView.swift', + language: 'swift', + code: ` +import Supabase +import SwiftUI + +struct ContentView: View { + @State var todos: [Todo] = [] + + var body: some View { + NavigationStack { + List(todos) { todo in + Text(todo.name) + } + .navigationTitle("Todos") + .task { + do { + todos = try await supabase.from("todos").select().execute().value + } catch { + debugPrint(error) + } + } + } + } +} + +#Preview { + ContentView() +} + +`, + }, + ] + + return +} + +export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/tanstack/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/tanstack/supabasejs/content.tsx new file mode 100644 index 0000000000000..cfe42dffbc507 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/content/tanstack/supabasejs/content.tsx @@ -0,0 +1,60 @@ +import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock' + +import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types' + +const ContentFile = ({ projectKeys }: StepContentProps) => { + const files = [ + { + name: '.env', + language: 'bash', + code: ` +VITE_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'} +VITE_SUPABASE_KEY=${projectKeys.publishableKey ?? projectKeys.anonKey ?? 'your-anon-key'} + `, + }, + { + name: 'src/utils/supabase.ts', + language: 'ts', + code: ` +import { createClient } from "@supabase/supabase-js"; + +export const supabase = createClient( + import.meta.env.VITE_SUPABASE_URL, + import.meta.env.VITE_SUPABASE_KEY +); + `, + }, + { + name: 'src/routes/index.tsx', + language: 'tsx', + code: ` +import { createFileRoute } from '@tanstack/react-router' +import { supabase } from '../utils/supabase' + +export const Route = createFileRoute('/')({ + loader: async () => { + const { data: todos } = await supabase.from('todos').select() + return { todos } + }, + component: Home, +}) + +function Home() { + const { todos } = Route.useLoaderData() + + return ( +
    + {todos?.map((todo) => ( +
  • {todo.name}
  • + ))} +
+ ) +} +`, + }, + ] + + return +} + +export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/content/vuejs/supabasejs/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/vuejs/supabasejs/content.tsx new file mode 100644 index 0000000000000..b34cf8bacdc08 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/content/vuejs/supabasejs/content.tsx @@ -0,0 +1,63 @@ +import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock' + +import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types' + +const ContentFile = ({ projectKeys }: StepContentProps) => { + const files = [ + { + name: '.env.local', + language: 'bash', + code: [ + `VITE_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`, + projectKeys?.publishableKey + ? `VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${projectKeys.publishableKey}` + : `VITE_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`, + '', + ].join('\n'), + }, + { + name: 'utils/supabase.ts', + language: 'ts', + code: ` +import { createClient } from "@supabase/supabase-js"; + +const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; +const supabaseKey = import.meta.env.${projectKeys.publishableKey ? 'VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'VITE_SUPABASE_ANON_KEY'}; + +export const supabase = createClient(supabaseUrl, supabaseKey); + `, + }, + { + name: 'App.vue', + language: 'html', + code: ` + + + +`, + }, + ] + + return +} + +export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/useConnectState.test.ts b/apps/studio/components/interfaces/ConnectSheet/useConnectState.test.ts new file mode 100644 index 0000000000000..652ea244b76f2 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/useConnectState.test.ts @@ -0,0 +1,228 @@ +import { describe, test, expect } from 'vitest' +import { renderHook, act } from '@testing-library/react' +import { useConnectState } from './useConnectState' + +describe('useConnectState', () => { + // ============================================================================ + // Initial State Tests + // ============================================================================ + + describe('initial state', () => { + test('should initialize with framework mode by default', () => { + const { result } = renderHook(() => useConnectState()) + expect(result.current.state.mode).toBe('framework') + }) + + test('should initialize with nextjs as default framework', () => { + const { result } = renderHook(() => useConnectState()) + expect(result.current.state.framework).toBe('nextjs') + }) + + test('should initialize with app variant for nextjs', () => { + const { result } = renderHook(() => useConnectState()) + expect(result.current.state.frameworkVariant).toBe('app') + }) + + test('should initialize with supabasejs library', () => { + const { result } = renderHook(() => useConnectState()) + expect(result.current.state.library).toBe('supabasejs') + }) + + test('should accept initial state override', () => { + const { result } = renderHook(() => + useConnectState({ framework: 'react', frameworkVariant: 'vite' }) + ) + expect(result.current.state.mode).toBe('framework') + expect(result.current.state.framework).toBe('react') + expect(result.current.state.frameworkVariant).toBe('vite') + }) + + test('should merge initial state with defaults', () => { + const { result } = renderHook(() => useConnectState({ framework: 'react' })) + expect(result.current.state.mode).toBe('framework') + expect(result.current.state.framework).toBe('react') + }) + }) + + // ============================================================================ + // Field Update Tests + // ============================================================================ + + describe('updateField', () => { + test('should update framework selection', () => { + const { result } = renderHook(() => useConnectState()) + + act(() => { + result.current.updateField('framework', 'react') + }) + + expect(result.current.state.framework).toBe('react') + }) + + test('should cascade variant reset when changing framework', () => { + const { result } = renderHook(() => useConnectState()) + + // Start with nextjs which has variants + expect(result.current.state.frameworkVariant).toBe('app') + + // Switch to a framework with multiple variants + act(() => { + result.current.updateField('framework', 'react') + }) + + // Should have the first variant of react + expect(result.current.state.frameworkVariant).toBeDefined() + }) + + test('should remove variant when switching to framework without variants', () => { + const { result } = renderHook(() => useConnectState()) + + // Start with nextjs which has variants + expect(result.current.state.frameworkVariant).toBe('app') + + // Switch to remix which has no variants + act(() => { + result.current.updateField('framework', 'remix') + }) + + expect(result.current.state.frameworkVariant).toBeUndefined() + }) + + test('should update library when variant changes', () => { + const { result } = renderHook(() => useConnectState()) + + act(() => { + result.current.updateField('frameworkVariant', 'pages') + }) + + expect(result.current.state.library).toBe('supabasejs') + }) + + test('should update boolean fields', () => { + const { result } = renderHook(() => useConnectState()) + + act(() => { + result.current.updateField('frameworkUi', true) + }) + + expect(result.current.state.frameworkUi).toBe(true) + }) + }) + + // ============================================================================ + // Active Fields Tests + // ============================================================================ + + describe('activeFields', () => { + test('should return framework mode fields', () => { + const { result } = renderHook(() => useConnectState()) + + const fieldIds = result.current.activeFields.map((f) => f.id) + expect(fieldIds).toContain('framework') + }) + + test('should include variant field for nextjs', () => { + const { result } = renderHook(() => useConnectState({ framework: 'nextjs' })) + + const fieldIds = result.current.activeFields.map((f) => f.id) + expect(fieldIds).toContain('frameworkVariant') + }) + + test('should include frameworkUi field for nextjs', () => { + const { result } = renderHook(() => useConnectState({ framework: 'nextjs' })) + + const fieldIds = result.current.activeFields.map((f) => f.id) + expect(fieldIds).toContain('frameworkUi') + }) + + test('should not include frameworkUi for non-nextjs/react frameworks', () => { + const { result } = renderHook(() => useConnectState({ framework: 'remix' })) + + const fieldIds = result.current.activeFields.map((f) => f.id) + expect(fieldIds).not.toContain('frameworkUi') + }) + }) + + // ============================================================================ + // Resolved Steps Tests + // ============================================================================ + + describe('resolvedSteps', () => { + test('should resolve steps for framework mode', () => { + const { result } = renderHook(() => useConnectState()) + + expect(result.current.resolvedSteps.length).toBeGreaterThan(0) + }) + + test('should have install step for framework mode', () => { + const { result } = renderHook(() => useConnectState()) + + const stepIds = result.current.resolvedSteps.map((s) => s.id) + expect(stepIds).toContain('install') + }) + + test('should include skills install step', () => { + const { result } = renderHook(() => useConnectState()) + + const stepIds = result.current.resolvedSteps.map((s) => s.id) + expect(stepIds).toContain('install-skills') + }) + + test('should resolve shadcn steps when frameworkUi is true', () => { + const { result } = renderHook(() => + useConnectState({ framework: 'nextjs', frameworkUi: true }) + ) + + const stepIds = result.current.resolvedSteps.map((s) => s.id) + expect(stepIds).toContain('shadcn-add') + }) + }) + + // ============================================================================ + // Field Options Tests + // ============================================================================ + + describe('getFieldOptions', () => { + test('should return framework options from schema', () => { + const { result } = renderHook(() => useConnectState()) + + const options = result.current.getFieldOptions('framework') + expect(options.some((o) => o.value === 'nextjs')).toBe(true) + expect(options.some((o) => o.value === 'react')).toBe(true) + }) + + test('should return empty array for inactive field', () => { + const { result } = renderHook(() => useConnectState({ framework: 'remix' })) + + const options = result.current.getFieldOptions('frameworkVariant') + expect(options).toEqual([]) + }) + + test('should return empty array for unknown field', () => { + const { result } = renderHook(() => useConnectState()) + + const options = result.current.getFieldOptions('unknownField') + expect(options).toEqual([]) + }) + }) + + // ============================================================================ + // Schema Access Tests + // ============================================================================ + + describe('schema', () => { + test('should expose the connect schema', () => { + const { result } = renderHook(() => useConnectState()) + + expect(result.current.schema).toBeDefined() + expect(result.current.schema.fields).toBeDefined() + expect(result.current.schema.steps).toBeDefined() + }) + + test('should include mode field in schema', () => { + const { result } = renderHook(() => useConnectState()) + + expect(result.current.schema.fields.mode).toBeDefined() + }) + }) +}) diff --git a/apps/studio/components/interfaces/ConnectSheet/useConnectState.ts b/apps/studio/components/interfaces/ConnectSheet/useConnectState.ts new file mode 100644 index 0000000000000..3135891b15798 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/useConnectState.ts @@ -0,0 +1,55 @@ +import { useCallback, useMemo, useState } from 'react' + +import { getActiveFields, resolveState, resolveSteps } from './connect.resolver' +import { connectSchema } from './connect.schema' +import type { + ConnectSchema, + ConnectState, + FieldOption, + ResolvedField, + ResolvedStep, +} from './Connect.types' + +export interface UseConnectStateReturn { + state: ConnectState + updateField: (fieldId: string, value: string | boolean | string[]) => void + activeFields: ResolvedField[] + resolvedSteps: ResolvedStep[] + getFieldOptions: (fieldId: string) => FieldOption[] + schema: ConnectSchema +} + +export function useConnectState(initialState?: Partial): UseConnectStateReturn { + const [state, setState] = useState(() => { + return resolveState(connectSchema, initialState ?? {}) + }) + + const updateField = useCallback((fieldId: string, value: string | boolean | string[]) => { + setState((prev) => { + const next = { ...prev, [fieldId]: value } + return resolveState(connectSchema, next) + }) + }, []) + + const activeFields = useMemo(() => getActiveFields(connectSchema, state), [state]) + + const resolvedSteps = useMemo(() => resolveSteps(connectSchema, state), [state]) + + const getFieldOptions = useCallback( + (fieldId: string): FieldOption[] => { + const field = activeFields.find((f) => f.id === fieldId) + if (!field) return [] + return field.resolvedOptions + }, + [activeFields] + ) + + return { + state, + updateField, + activeFields, + resolvedSteps, + getFieldOptions, + schema: connectSchema, + } +} diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/AddNewSecretForm.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/AddNewSecretForm.tsx index cf4791ec95d65..0f38709fa227a 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/AddNewSecretForm.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/AddNewSecretForm.tsx @@ -217,6 +217,10 @@ const AddNewSecretForm = () => { handlePaste(e.nativeEvent)} /> diff --git a/apps/studio/components/interfaces/Organization/GeneralSettings/DeleteOrganizationButton.tsx b/apps/studio/components/interfaces/Organization/GeneralSettings/DeleteOrganizationButton.tsx index 8f183a617197f..759774d326906 100644 --- a/apps/studio/components/interfaces/Organization/GeneralSettings/DeleteOrganizationButton.tsx +++ b/apps/studio/components/interfaces/Organization/GeneralSettings/DeleteOrganizationButton.tsx @@ -9,7 +9,7 @@ import { useOrganizationDeleteMutation } from 'data/organizations/organization-d import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' -import { Button, Form, Input, Modal } from 'ui' +import { TextConfirmModal } from 'components/ui/TextConfirmModalWrapper' export const DeleteOrganizationButton = () => { const router = useRouter() @@ -17,7 +17,6 @@ export const DeleteOrganizationButton = () => { const { slug: orgSlug, name: orgName } = selectedOrganization ?? {} const [isOpen, setIsOpen] = useState(false) - const [value, setValue] = useState('') const [_, setLastVisitedOrganization] = useLocalStorageQuery( LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION, @@ -37,23 +36,15 @@ export const DeleteOrganizationButton = () => { }, }) - const onValidate = (values: any) => { - const errors: any = {} - if (!values.orgName) { - errors.orgName = 'Enter the name of the organization.' - } - if (values.orgName.trim() !== orgSlug?.trim()) { - errors.orgName = 'Value entered does not match the value above.' - } - return errors - } - - const onConfirmDelete = async (values: any) => { + const onConfirmDelete = () => { if (!canDeleteOrganization) { - return toast.error('You do not have the required permissions to delete this organization') + toast.error('You do not have permission to delete this organization') + return + } + if (!orgSlug) { + console.error('Org slug is required') + return } - if (!orgSlug) return console.error('Org slug is required') - deleteOrganization({ slug: orgSlug }) } @@ -62,7 +53,7 @@ export const DeleteOrganizationButton = () => {
setIsOpen(true)} tooltip={{ @@ -77,65 +68,24 @@ export const DeleteOrganizationButton = () => { Delete organization
- setIsOpen(false)} - header={ -
- Delete organization - Are you sure? -
- } > -
- {() => ( - <> - -

- This action cannot be undone. This will - permanently delete the {orgName}{' '} - organization and remove all of its projects. -

-
- - - - Please type {orgSlug} to confirm - - } - onChange={(e) => setValue(e.target.value)} - value={value} - placeholder="Enter the string above" - className="w-full" - /> - - - - - - - )} - -
+

+ This action cannot be undone. This will + permanently delete the {orgName} organization and + remove all of its projects. +

+ ) } diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx index 6c0fec385c775..582d74e3c04db 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx @@ -315,8 +315,9 @@ export const FileExplorerRow = ({ className={cn( 'storage-row group flex h-full items-center px-2.5', 'hover:bg-panel-footer-light [[data-theme*=dark]_&]:hover:bg-panel-footer-dark', - `${isOpened ? 'bg-surface-200' : ''}`, - `${isPreviewed ? 'bg-green-500 hover:bg-green-500' : ''}`, + `${isOpened ? 'bg-selection' : ''}`, + `${isSelected ? 'bg-selection' : ''}`, + `${isPreviewed ? 'bg-selection hover:bg-selection' : ''}`, `${item.status !== STORAGE_ROW_STATUS.LOADING ? 'cursor-pointer' : ''}` )} onClick={(event) => { diff --git a/apps/studio/components/interfaces/Support/__tests__/SupportFormPage.test.tsx b/apps/studio/components/interfaces/Support/__tests__/SupportFormPage.test.tsx index 9313f6d4d9c90..1fb46c32d7956 100644 --- a/apps/studio/components/interfaces/Support/__tests__/SupportFormPage.test.tsx +++ b/apps/studio/components/interfaces/Support/__tests__/SupportFormPage.test.tsx @@ -4,7 +4,7 @@ import dayjs from 'dayjs' // End of third-party imports import { API_URL, BASE_PATH } from 'lib/constants' -import { HttpResponse, http } from 'msw' +import { http, HttpResponse } from 'msw' import { createMockOrganization, createMockProject } from 'tests/helpers' import { customRender } from 'tests/lib/custom-render' import { addAPIMock, mswServer } from 'tests/lib/msw' @@ -785,6 +785,10 @@ describe('SupportFormPage', () => { expect(getSeveritySelector(screen)).toHaveTextContent('High') }) + // Wait for library selector to be available before interacting + await waitFor(() => { + expect(getLibrarySelector(screen)).toBeInTheDocument() + }) await selectLibraryOption(screen, 'JavaScript') await waitFor(() => { expect(getLibrarySelector(screen)).toHaveTextContent('JavaScript') @@ -801,9 +805,14 @@ describe('SupportFormPage', () => { const supportAccessToggle = screen.getByRole('switch', { name: /allow support access to your project/i, }) - expect(supportAccessToggle).toBeChecked() + // Wait for toggle to be in expected state before interacting + await waitFor(() => { + expect(supportAccessToggle).toBeChecked() + }) await userEvent.click(supportAccessToggle) - expect(supportAccessToggle).not.toBeChecked() + await waitFor(() => { + expect(supportAccessToggle).not.toBeChecked() + }) await userEvent.click(getSubmitButton(screen)) @@ -833,7 +842,7 @@ describe('SupportFormPage', () => { await waitFor(() => { expect(screen.getByRole('heading', { name: /support request sent/i })).toBeInTheDocument() }) - }, 10_000) + }, 15_000) test('submits urgent login issues ticket for a different organization', async () => { const submitSpy = vi.fn() diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx index 3359ba6c7ebed..08f9f317bf2d3 100644 --- a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx @@ -10,6 +10,7 @@ import { OrganizationDropdown } from 'components/layouts/AppLayout/OrganizationD import { ProjectDropdown } from 'components/layouts/AppLayout/ProjectDropdown' import { getResourcesExceededLimitsOrg } from 'components/ui/OveragesBanner/OveragesBanner.utils' import { useOrgUsageQuery } from 'data/usage/org-usage-query' +import { DevToolbarTrigger } from 'dev-tools' import { AnimatePresence, motion } from 'framer-motion' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' @@ -22,7 +23,7 @@ import { ReactNode, useMemo } from 'react' import { useAppStateSnapshot } from 'state/app-state' import { Badge, cn } from 'ui' import { CommandMenuTriggerInput } from 'ui-patterns' -import { DevToolbarTrigger } from 'dev-tools' + import { BreadcrumbsView } from './BreadcrumbsView' import { FeedbackDropdown } from './FeedbackDropdown/FeedbackDropdown' import { HelpDropdown } from './HelpDropdown/HelpDropdown' @@ -274,7 +275,7 @@ export const LayoutHeader = ({
- {isFlagResolved && isConnectSheetEnabled ? : } + {isFlagResolved ? isConnectSheetEnabled ? : : null} ) } diff --git a/apps/studio/hooks/ui/useFlag.ts b/apps/studio/hooks/ui/useFlag.ts index 55f9704f0eb54..3a72a29d848d6 100644 --- a/apps/studio/hooks/ui/useFlag.ts +++ b/apps/studio/hooks/ui/useFlag.ts @@ -1,6 +1,5 @@ import * as Sentry from '@sentry/nextjs' - -import { useFeatureFlags } from 'common' +import { IS_PLATFORM, useFeatureFlags } from 'common' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { trackFeatureFlag } from 'lib/posthog' @@ -42,6 +41,8 @@ export function usePHFlag(name: string) { const store = flagStore.posthog const flagValue = store[name] + if (!IS_PLATFORM) return false + // Flag store has not been initialized if (isObjectEmpty(store)) return undefined diff --git a/apps/studio/lib/ai/message-utils.test.ts b/apps/studio/lib/ai/message-utils.test.ts new file mode 100644 index 0000000000000..34502f43d2aad --- /dev/null +++ b/apps/studio/lib/ai/message-utils.test.ts @@ -0,0 +1,153 @@ +import type { UIMessage } from 'ai' +import { describe, expect, it } from 'vitest' + +import { prepareMessagesForAPI } from './message-utils' + +describe('prepareMessagesForAPI', () => { + it('should limit messages to last 7 entries', () => { + const messages: UIMessage[] = Array.from({ length: 10 }, (_, i) => ({ + id: `msg-${i}`, + role: 'user', + parts: [{ type: 'text', text: `Message ${i}` }], + })) + + const result = prepareMessagesForAPI(messages) + + expect(result).toHaveLength(7) + expect(result[0].parts[0]).toEqual({ type: 'text', text: 'Message 3' }) + expect(result[6].parts[0]).toEqual({ type: 'text', text: 'Message 9' }) + }) + + it('should remove results property from assistant messages', () => { + const messages = [ + { + id: 'msg-1', + role: 'assistant', + parts: [{ type: 'text', text: 'Response' }], + results: { data: 'some data' }, + }, + ] as Array + + const result = prepareMessagesForAPI(messages) + + expect(result).toHaveLength(1) + expect(result[0]).not.toHaveProperty('results') + expect(result[0].parts[0]).toEqual({ type: 'text', text: 'Response' }) + }) + + it('should preserve messages without results', () => { + const messages: UIMessage[] = [ + { + id: 'msg-1', + role: 'user', + parts: [{ type: 'text', text: 'Question' }], + }, + { + id: 'msg-2', + role: 'assistant', + parts: [{ type: 'text', text: 'Answer' }], + }, + ] + + const result = prepareMessagesForAPI(messages) + + expect(result).toHaveLength(2) + expect(result[0]).toEqual(messages[0]) + expect(result[1]).toEqual(messages[1]) + }) + + it('should handle empty array', () => { + const messages: UIMessage[] = [] + + const result = prepareMessagesForAPI(messages) + + expect(result).toHaveLength(0) + expect(result).toEqual([]) + }) + + it('should handle array with fewer than 7 messages', () => { + const messages: UIMessage[] = [ + { id: 'msg-1', role: 'user', parts: [{ type: 'text', text: 'Message 1' }] }, + { id: 'msg-2', role: 'assistant', parts: [{ type: 'text', text: 'Message 2' }] }, + { id: 'msg-3', role: 'user', parts: [{ type: 'text', text: 'Message 3' }] }, + ] + + const result = prepareMessagesForAPI(messages) + + expect(result).toHaveLength(3) + expect(result).toEqual(messages) + }) + + it('should handle array with exactly 7 messages', () => { + const messages: UIMessage[] = Array.from({ length: 7 }, (_, i) => ({ + id: `msg-${i}`, + role: i % 2 === 0 ? 'user' : 'assistant', + parts: [{ type: 'text', text: `Message ${i}` }], + })) + + const result = prepareMessagesForAPI(messages) + + expect(result).toHaveLength(7) + expect(result).toEqual(messages) + }) + + it('should only remove results from assistant messages, not user messages', () => { + const messages = [ + { + id: 'msg-1', + role: 'user', + parts: [{ type: 'text', text: 'Question' }], + results: { data: 'user data' }, + }, + { + id: 'msg-2', + role: 'assistant', + parts: [{ type: 'text', text: 'Answer' }], + results: { data: 'assistant data' }, + }, + ] as Array + + const result = prepareMessagesForAPI(messages) + + expect(result).toHaveLength(2) + // User message keeps results (not removed by the function) + expect((result[0] as any).results).toEqual({ data: 'user data' }) + // Assistant message has results removed + expect(result[1]).not.toHaveProperty('results') + }) + + it('should handle mixed messages with and without results', () => { + const messages = [ + { + id: 'msg-1', + role: 'assistant', + parts: [{ type: 'text', text: 'First' }], + results: { data: 'data1' }, + }, + { + id: 'msg-2', + role: 'user', + parts: [{ type: 'text', text: 'Second' }], + }, + { + id: 'msg-3', + role: 'assistant', + parts: [{ type: 'text', text: 'Third' }], + }, + { + id: 'msg-4', + role: 'assistant', + parts: [{ type: 'text', text: 'Fourth' }], + results: { data: 'data2' }, + }, + ] as Array + + const result = prepareMessagesForAPI(messages) + + expect(result).toHaveLength(4) + expect(result[0]).not.toHaveProperty('results') + expect(result[1]).toEqual(messages[1]) + expect(result[2]).toEqual(messages[2]) + expect(result[3]).not.toHaveProperty('results') + }) +}) diff --git a/apps/studio/lib/ai/model.utils.test.ts b/apps/studio/lib/ai/model.utils.test.ts new file mode 100644 index 0000000000000..dfddf461656fc --- /dev/null +++ b/apps/studio/lib/ai/model.utils.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, it } from 'vitest' + +import { getDefaultModelForProvider, PROVIDERS } from './model.utils' +import type { ProviderName } from './model.utils' + +describe('model.utils', () => { + describe('getDefaultModelForProvider', () => { + it('should return correct default for bedrock provider', () => { + const result = getDefaultModelForProvider('bedrock') + expect(result).toBe('openai.gpt-oss-120b-1:0') + }) + + it('should return correct default for openai provider', () => { + const result = getDefaultModelForProvider('openai') + expect(result).toBe('gpt-5-mini') + }) + + it('should return correct default for anthropic provider', () => { + const result = getDefaultModelForProvider('anthropic') + expect(result).toBe('claude-3-5-haiku-20241022') + }) + + it('should return undefined for unknown provider', () => { + const result = getDefaultModelForProvider('unknown' as ProviderName) + expect(result).toBeUndefined() + }) + }) + + describe('PROVIDERS registry', () => { + it('should have bedrock provider with models', () => { + expect(PROVIDERS.bedrock).toBeDefined() + expect(PROVIDERS.bedrock.models).toBeDefined() + expect(Object.keys(PROVIDERS.bedrock.models)).toContain( + 'anthropic.claude-3-7-sonnet-20250219-v1:0' + ) + expect(Object.keys(PROVIDERS.bedrock.models)).toContain('openai.gpt-oss-120b-1:0') + }) + + it('should have openai provider with models', () => { + expect(PROVIDERS.openai).toBeDefined() + expect(PROVIDERS.openai.models).toBeDefined() + expect(Object.keys(PROVIDERS.openai.models)).toContain('gpt-5') + expect(Object.keys(PROVIDERS.openai.models)).toContain('gpt-5-mini') + }) + + it('should have anthropic provider with models', () => { + expect(PROVIDERS.anthropic).toBeDefined() + expect(PROVIDERS.anthropic.models).toBeDefined() + expect(Object.keys(PROVIDERS.anthropic.models)).toContain('claude-sonnet-4-20250514') + expect(Object.keys(PROVIDERS.anthropic.models)).toContain('claude-3-5-haiku-20241022') + }) + + it('should have exactly one default model per provider', () => { + const providers: ProviderName[] = ['bedrock', 'openai', 'anthropic'] + + providers.forEach((provider) => { + const models = PROVIDERS[provider].models + const defaultModels = Object.entries(models).filter(([_, config]) => config.default) + expect(defaultModels.length).toBe(1) + }) + }) + + it('should have valid model configurations', () => { + const providers: ProviderName[] = ['bedrock', 'openai', 'anthropic'] + + providers.forEach((provider) => { + const models = PROVIDERS[provider].models + Object.entries(models).forEach(([modelId, config]) => { + expect(config).toHaveProperty('default') + expect(typeof config.default).toBe('boolean') + }) + }) + }) + + it('should have bedrock model with promptProviderOptions', () => { + const sonnetModel = PROVIDERS.bedrock.models['anthropic.claude-3-7-sonnet-20250219-v1:0'] + expect(sonnetModel.promptProviderOptions).toBeDefined() + expect(sonnetModel.promptProviderOptions?.bedrock).toBeDefined() + expect(sonnetModel.promptProviderOptions?.bedrock?.cachePoint).toEqual({ type: 'default' }) + }) + + it('should have openai provider with providerOptions', () => { + expect(PROVIDERS.openai.providerOptions).toBeDefined() + expect(PROVIDERS.openai.providerOptions?.openai).toBeDefined() + expect(PROVIDERS.openai.providerOptions?.openai?.reasoningEffort).toBe('minimal') + }) + }) +}) diff --git a/apps/studio/lib/ai/org-ai-details.test.ts b/apps/studio/lib/ai/org-ai-details.test.ts new file mode 100644 index 0000000000000..2a913fe95c053 --- /dev/null +++ b/apps/studio/lib/ai/org-ai-details.test.ts @@ -0,0 +1,243 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { getOrgAIDetails } from './org-ai-details' + +vi.mock('data/organizations/organizations-query', () => ({ + getOrganizations: vi.fn(), +})) + +vi.mock('data/projects/project-detail-query', () => ({ + getProjectDetail: vi.fn(), +})) + +vi.mock('hooks/misc/useOrgOptedIntoAi', () => ({ + getAiOptInLevel: vi.fn(), +})) + +describe('ai/org-ai-details', () => { + let mockGetOrganizations: ReturnType + let mockGetProjectDetail: ReturnType + let mockGetAiOptInLevel: ReturnType + + beforeEach(async () => { + vi.clearAllMocks() + + const orgsQuery = await import('data/organizations/organizations-query') + const projectQuery = await import('data/projects/project-detail-query') + const aiHook = await import('hooks/misc/useOrgOptedIntoAi') + + mockGetOrganizations = vi.mocked(orgsQuery.getOrganizations) + mockGetProjectDetail = vi.mocked(projectQuery.getProjectDetail) + mockGetAiOptInLevel = vi.mocked(aiHook.getAiOptInLevel) + }) + + describe('getOrgAIDetails', () => { + it('should fetch organizations and project details', async () => { + const mockOrg = { + id: 1, + slug: 'test-org', + plan: { id: 'pro' }, + opt_in_tags: [], + } + const mockProject = { + id: 1, + organization_id: 1, + ref: 'test-project', + } + + mockGetOrganizations.mockResolvedValue([mockOrg]) + mockGetProjectDetail.mockResolvedValue(mockProject) + mockGetAiOptInLevel.mockReturnValue('full') + + await getOrgAIDetails({ + orgSlug: 'test-org', + authorization: 'Bearer token', + projectRef: 'test-project', + }) + + expect(mockGetOrganizations).toHaveBeenCalledWith({ + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer token', + }, + }) + expect(mockGetProjectDetail).toHaveBeenCalledWith({ ref: 'test-project' }, undefined, { + 'Content-Type': 'application/json', + Authorization: 'Bearer token', + }) + }) + + it('should return AI opt-in level and limited status', async () => { + const mockOrg = { + id: 1, + slug: 'test-org', + plan: { id: 'free' }, + opt_in_tags: ['AI_SQL_GENERATOR_OPT_IN'], + } + const mockProject = { + organization_id: 1, + ref: 'test-project', + } + + mockGetOrganizations.mockResolvedValue([mockOrg]) + mockGetProjectDetail.mockResolvedValue(mockProject) + mockGetAiOptInLevel.mockReturnValue('schema_only') + + const result = await getOrgAIDetails({ + orgSlug: 'test-org', + authorization: 'Bearer token', + projectRef: 'test-project', + }) + + expect(result).toEqual({ + aiOptInLevel: 'schema_only', + isLimited: true, + }) + }) + + it('should mark pro plan as not limited', async () => { + const mockOrg = { + id: 1, + slug: 'test-org', + plan: { id: 'pro' }, + opt_in_tags: [], + } + const mockProject = { + organization_id: 1, + } + + mockGetOrganizations.mockResolvedValue([mockOrg]) + mockGetProjectDetail.mockResolvedValue(mockProject) + mockGetAiOptInLevel.mockReturnValue('full') + + const result = await getOrgAIDetails({ + orgSlug: 'test-org', + authorization: 'Bearer token', + projectRef: 'test-project', + }) + + expect(result.isLimited).toBe(false) + }) + + it('should throw error when project and org do not match', async () => { + const mockOrg = { + id: 1, + slug: 'test-org', + plan: { id: 'pro' }, + } + const mockProject = { + organization_id: 2, // Different org ID + } + + mockGetOrganizations.mockResolvedValue([mockOrg]) + mockGetProjectDetail.mockResolvedValue(mockProject) + + await expect( + getOrgAIDetails({ + orgSlug: 'test-org', + authorization: 'Bearer token', + projectRef: 'test-project', + }) + ).rejects.toThrow('Project and organization do not match') + }) + + it('should handle org not found', async () => { + const mockProject = { + organization_id: 1, + } + + mockGetOrganizations.mockResolvedValue([]) // No orgs + mockGetProjectDetail.mockResolvedValue(mockProject) + + await expect( + getOrgAIDetails({ + orgSlug: 'non-existent-org', + authorization: 'Bearer token', + projectRef: 'test-project', + }) + ).rejects.toThrow('Project and organization do not match') + }) + + it('should call getAiOptInLevel with org opt_in_tags', async () => { + const mockOptInTags = ['AI_SQL_GENERATOR_OPT_IN', 'AI_DATA_GENERATOR_OPT_IN'] + const mockOrg = { + id: 1, + slug: 'test-org', + plan: { id: 'pro' }, + opt_in_tags: mockOptInTags, + } + const mockProject = { + organization_id: 1, + } + + mockGetOrganizations.mockResolvedValue([mockOrg]) + mockGetProjectDetail.mockResolvedValue(mockProject) + mockGetAiOptInLevel.mockReturnValue('full') + + await getOrgAIDetails({ + orgSlug: 'test-org', + authorization: 'Bearer token', + projectRef: 'test-project', + }) + + expect(mockGetAiOptInLevel).toHaveBeenCalledWith(mockOptInTags) + }) + + it('should include authorization header when provided', async () => { + const mockOrg = { + id: 1, + slug: 'test-org', + plan: { id: 'pro' }, + } + const mockProject = { + organization_id: 1, + } + + mockGetOrganizations.mockResolvedValue([mockOrg]) + mockGetProjectDetail.mockResolvedValue(mockProject) + mockGetAiOptInLevel.mockReturnValue('full') + + await getOrgAIDetails({ + orgSlug: 'test-org', + authorization: 'Bearer custom-token', + projectRef: 'test-project', + }) + + expect(mockGetOrganizations).toHaveBeenCalledWith({ + headers: expect.objectContaining({ + Authorization: 'Bearer custom-token', + }), + }) + expect(mockGetProjectDetail).toHaveBeenCalledWith( + expect.anything(), + undefined, + expect.objectContaining({ + Authorization: 'Bearer custom-token', + }) + ) + }) + + it('should fetch multiple organizations and find correct one', async () => { + const mockOrgs = [ + { id: 1, slug: 'org-1', plan: { id: 'free' } }, + { id: 2, slug: 'test-org', plan: { id: 'pro' } }, + { id: 3, slug: 'org-3', plan: { id: 'team' } }, + ] + const mockProject = { + organization_id: 2, + } + + mockGetOrganizations.mockResolvedValue(mockOrgs) + mockGetProjectDetail.mockResolvedValue(mockProject) + mockGetAiOptInLevel.mockReturnValue('full') + + const result = await getOrgAIDetails({ + orgSlug: 'test-org', + authorization: 'Bearer token', + projectRef: 'test-project', + }) + + expect(result.isLimited).toBe(false) // Pro plan + }) + }) +}) diff --git a/apps/studio/lib/ai/tools/incident-tools.test.ts b/apps/studio/lib/ai/tools/incident-tools.test.ts new file mode 100644 index 0000000000000..ab39d0a94ecba --- /dev/null +++ b/apps/studio/lib/ai/tools/incident-tools.test.ts @@ -0,0 +1,220 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { getIncidentTools } from './incident-tools' + +// Mock IS_PLATFORM +vi.mock('common', () => ({ + IS_PLATFORM: true, +})) + +describe('ai/tools/incident-tools', () => { + let mockFetch: ReturnType + let mockAbortSignal: AbortSignal + + beforeEach(() => { + vi.clearAllMocks() + mockFetch = vi.fn() + global.fetch = mockFetch + + // Mock AbortSignal.timeout + mockAbortSignal = new AbortController().signal + if (!AbortSignal.timeout) { + AbortSignal.timeout = vi.fn(() => mockAbortSignal) as any + } + }) + + describe('getIncidentTools', () => { + it('should return an object with get_active_incidents tool', () => { + const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' }) + + expect(tools).toBeDefined() + expect(tools.get_active_incidents).toBeDefined() + }) + + it('should have correct description for get_active_incidents', () => { + const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' }) + + expect(tools.get_active_incidents.description).toContain('Check for active incidents') + expect(tools.get_active_incidents.description).toContain('Supabase service') + }) + + it('should have empty input schema', () => { + const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' }) + const schema = tools.get_active_incidents.inputSchema + + // The schema is a Zod object that accepts empty object + expect(schema).toBeDefined() + expect((schema as any)._def.typeName).toBe('ZodObject') + }) + + describe('execute function', () => { + it('should return empty incidents when not on platform', async () => { + const common = await import('common') + vi.spyOn(common, 'IS_PLATFORM', 'get').mockReturnValue(false) + + const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' }) + const result = await (tools.get_active_incidents.execute as any)({}) + + expect(result).toEqual({ + incidents: [], + message: 'Incident checking is only available on Supabase platform.', + }) + expect(mockFetch).not.toHaveBeenCalled() + }) + + it('should fetch incidents from correct URL', async () => { + const common = await import('common') + vi.spyOn(common, 'IS_PLATFORM', 'get').mockReturnValue(true) + + mockFetch.mockResolvedValue({ + ok: true, + json: async () => [], + }) + + const tools = getIncidentTools({ baseUrl: 'https://example.com/dashboard' }) + if (!tools.get_active_incidents.execute) throw new Error('execute is undefined') + await tools.get_active_incidents.execute({}, { toolCallId: 'test', messages: [] }) + + expect(mockFetch).toHaveBeenCalledWith( + 'https://example.com/dashboard/api/incident-status', + { + signal: expect.any(AbortSignal), + } + ) + }) + + it('should return message when no incidents', async () => { + const common = await import('common') + vi.spyOn(common, 'IS_PLATFORM', 'get').mockReturnValue(true) + + mockFetch.mockResolvedValue({ + ok: true, + json: async () => [], + }) + + const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' }) + const result = await (tools.get_active_incidents.execute as any)({}) + + expect(result).toEqual({ + incidents: [], + message: expect.stringContaining('No active incidents'), + }) + }) + + it('should return incident summaries when incidents exist', async () => { + const common = await import('common') + vi.spyOn(common, 'IS_PLATFORM', 'get').mockReturnValue(true) + + const mockIncidents = [ + { + name: 'Database slowness', + status: 'investigating', + impact: 'minor', + active_since: '2024-01-01T10:00:00Z', + extra_field: 'should be filtered', + }, + ] + + mockFetch.mockResolvedValue({ + ok: true, + json: async () => mockIncidents, + }) + + const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' }) + const result = await (tools.get_active_incidents.execute as any)({}) + + expect((result as any).incidents).toEqual([ + { + name: 'Database slowness', + status: 'investigating', + impact: 'minor', + active_since: '2024-01-01T10:00:00Z', + }, + ]) + expect((result as any).message).toContain('1 active incident') + expect((result as any).message).toContain('status.supabase.com') + }) + + it('should handle multiple incidents', async () => { + const common = await import('common') + vi.spyOn(common, 'IS_PLATFORM', 'get').mockReturnValue(true) + + const mockIncidents = [ + { + name: 'Database issue', + status: 'investigating', + impact: 'major', + active_since: '2024-01-01T10:00:00Z', + }, + { + name: 'Storage issue', + status: 'identified', + impact: 'minor', + active_since: '2024-01-01T11:00:00Z', + }, + ] + + mockFetch.mockResolvedValue({ + ok: true, + json: async () => mockIncidents, + }) + + const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' }) + const result = await (tools.get_active_incidents.execute as any)({}) + + expect((result as any).incidents).toHaveLength(2) + expect((result as any).message).toContain('2 active incidents') + }) + + it('should handle fetch errors', async () => { + const common = await import('common') + vi.spyOn(common, 'IS_PLATFORM', 'get').mockReturnValue(true) + + mockFetch.mockRejectedValue(new Error('Network error')) + + const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' }) + const result = await (tools.get_active_incidents.execute as any)({}) + + expect(result).toEqual({ + incidents: [], + error: 'Unable to check incident status at this time.', + }) + }) + + it('should handle non-ok responses', async () => { + const common = await import('common') + vi.spyOn(common, 'IS_PLATFORM', 'get').mockReturnValue(true) + + mockFetch.mockResolvedValue({ + ok: false, + status: 500, + }) + + const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' }) + const result = await (tools.get_active_incidents.execute as any)({}) + + expect(result).toEqual({ + incidents: [], + error: 'Unable to check incident status at this time.', + }) + }) + + it('should use timeout signal', async () => { + const common = await import('common') + vi.spyOn(common, 'IS_PLATFORM', 'get').mockReturnValue(true) + + mockFetch.mockResolvedValue({ + ok: true, + json: async () => [], + }) + + const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' }) + if (!tools.get_active_incidents.execute) throw new Error('execute is undefined') + await tools.get_active_incidents.execute({}, { toolCallId: 'test', messages: [] }) + + const callArgs = mockFetch.mock.calls[0] + expect(callArgs[1].signal).toBeInstanceOf(AbortSignal) + }) + }) + }) +}) diff --git a/apps/studio/lib/ai/tools/rendering-tools.test.ts b/apps/studio/lib/ai/tools/rendering-tools.test.ts new file mode 100644 index 0000000000000..5bfcab87b4c26 --- /dev/null +++ b/apps/studio/lib/ai/tools/rendering-tools.test.ts @@ -0,0 +1,141 @@ +import { describe, expect, it } from 'vitest' + +import { getRenderingTools } from './rendering-tools' + +describe('ai/tools/rendering-tools', () => { + describe('getRenderingTools', () => { + it('should return an object with tool definitions', () => { + const tools = getRenderingTools() + + expect(tools).toBeDefined() + expect(typeof tools).toBe('object') + }) + + it('should include execute_sql tool', () => { + const tools = getRenderingTools() + + expect(tools.execute_sql).toBeDefined() + expect(tools.execute_sql.description).toContain('execute a SQL statement') + }) + + it('should include deploy_edge_function tool', () => { + const tools = getRenderingTools() + + expect(tools.deploy_edge_function).toBeDefined() + expect(tools.deploy_edge_function.description).toContain('deploy a Supabase Edge Function') + }) + + it('should include rename_chat tool', () => { + const tools = getRenderingTools() + + expect(tools.rename_chat).toBeDefined() + expect(tools.rename_chat.description).toContain('Rename the current chat session') + }) + + it('should have exactly 3 tools', () => { + const tools = getRenderingTools() + const toolNames = Object.keys(tools) + + expect(toolNames).toHaveLength(3) + expect(toolNames).toContain('execute_sql') + expect(toolNames).toContain('deploy_edge_function') + expect(toolNames).toContain('rename_chat') + }) + + it('should have execute_sql with correct input schema fields', () => { + const tools = getRenderingTools() + const executeSqlTool = tools.execute_sql + + // Check that the tool has an input schema + expect(executeSqlTool.inputSchema).toBeDefined() + + // Verify the schema exists and is a Zod object + const schema = executeSqlTool.inputSchema + expect(schema).toBeDefined() + expect((schema as any)._def.typeName).toBe('ZodObject') + }) + + it('should have deploy_edge_function with input schema', () => { + const tools = getRenderingTools() + const deployTool = tools.deploy_edge_function + + expect(deployTool.inputSchema).toBeDefined() + + // Verify the schema exists and is a Zod object + expect(deployTool.inputSchema).toBeDefined() + expect((deployTool.inputSchema as any)._def.typeName).toBe('ZodObject') + }) + + it('should have rename_chat with execute function', async () => { + const tools = getRenderingTools() + const renameTool = tools.rename_chat + + expect(renameTool.execute).toBeDefined() + expect(typeof renameTool.execute).toBe('function') + + // Test the execute function + if (!renameTool.execute) throw new Error('execute is undefined') + const result = await renameTool.execute( + { newName: 'Test Chat' }, + { toolCallId: 'test', messages: [] } + ) + expect(result).toEqual({ status: 'Chat request sent to client' }) + }) + + it('should validate execute_sql input schema correctly', () => { + const tools = getRenderingTools() + const schema = tools.execute_sql.inputSchema + + // Check if schema is a Zod schema with safeParse + if ('safeParse' in schema) { + // Valid input + const validInput = { + sql: 'SELECT * FROM users', + label: 'Get users', + chartConfig: { view: 'table' as const }, + isWriteQuery: false, + } + expect(schema.safeParse(validInput).success).toBe(true) + + // Valid chart config + const validChartInput = { + sql: 'SELECT count(*) FROM users', + label: 'User count', + chartConfig: { view: 'chart' as const, xAxis: 'date', yAxis: 'count' }, + isWriteQuery: false, + } + expect(schema.safeParse(validChartInput).success).toBe(true) + + // Missing required field + const invalidInput = { + sql: 'SELECT * FROM users', + // missing label, chartConfig, isWriteQuery + } + expect(schema.safeParse(invalidInput).success).toBe(false) + } else { + // Skip test if schema doesn't have safeParse + expect(schema).toBeDefined() + } + }) + + it('should validate rename_chat input schema correctly', () => { + const tools = getRenderingTools() + const schema = tools.rename_chat.inputSchema + + // Check if schema is a Zod schema with safeParse + if ('safeParse' in schema) { + // Valid input + expect(schema.safeParse({ newName: 'My Chat' }).success).toBe(true) + + // Invalid input - missing newName + expect(schema.safeParse({}).success).toBe(false) + + // Invalid input - wrong type + expect(schema.safeParse({ newName: 123 }).success).toBe(false) + } else { + // Skip test if schema doesn't have safeParse + expect(schema).toBeDefined() + } + }) + }) +}) diff --git a/apps/studio/lib/api/self-hosted/constants.test.ts b/apps/studio/lib/api/self-hosted/constants.test.ts new file mode 100644 index 0000000000000..2a34793220507 --- /dev/null +++ b/apps/studio/lib/api/self-hosted/constants.test.ts @@ -0,0 +1,105 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +describe('api/self-hosted/constants', () => { + beforeEach(() => { + vi.resetModules() + }) + + describe('ENCRYPTION_KEY', () => { + it('should use PG_META_CRYPTO_KEY when set', async () => { + vi.stubEnv('PG_META_CRYPTO_KEY', 'my-secret-key-123') + const { ENCRYPTION_KEY } = await import('./constants') + expect(ENCRYPTION_KEY).toBe('my-secret-key-123') + }) + + it('should use SAMPLE_KEY as default', async () => { + vi.stubEnv('PG_META_CRYPTO_KEY', '') + const { ENCRYPTION_KEY } = await import('./constants') + expect(ENCRYPTION_KEY).toBe('SAMPLE_KEY') + }) + }) + + describe('POSTGRES_PORT', () => { + it('should use POSTGRES_PORT when set', async () => { + vi.stubEnv('POSTGRES_PORT', '5433') + const { POSTGRES_PORT } = await import('./constants') + expect(POSTGRES_PORT).toBe(5433) + }) + + it('should default to 5432', async () => { + vi.stubEnv('POSTGRES_PORT', '') + const { POSTGRES_PORT } = await import('./constants') + expect(POSTGRES_PORT).toBe(5432) + }) + }) + + describe('POSTGRES_HOST', () => { + it('should use POSTGRES_HOST when set', async () => { + vi.stubEnv('POSTGRES_HOST', 'my-db-host.example.com') + const { POSTGRES_HOST } = await import('./constants') + expect(POSTGRES_HOST).toBe('my-db-host.example.com') + }) + + it('should default to db', async () => { + vi.stubEnv('POSTGRES_HOST', '') + const { POSTGRES_HOST } = await import('./constants') + expect(POSTGRES_HOST).toBe('db') + }) + }) + + describe('POSTGRES_DATABASE', () => { + it('should use POSTGRES_DB when set', async () => { + vi.stubEnv('POSTGRES_DB', 'my_database') + const { POSTGRES_DATABASE } = await import('./constants') + expect(POSTGRES_DATABASE).toBe('my_database') + }) + + it('should default to postgres', async () => { + vi.stubEnv('POSTGRES_DB', '') + const { POSTGRES_DATABASE } = await import('./constants') + expect(POSTGRES_DATABASE).toBe('postgres') + }) + }) + + describe('POSTGRES_PASSWORD', () => { + it('should use POSTGRES_PASSWORD when set', async () => { + vi.stubEnv('POSTGRES_PASSWORD', 'super-secret-password') + const { POSTGRES_PASSWORD } = await import('./constants') + expect(POSTGRES_PASSWORD).toBe('super-secret-password') + }) + + it('should default to postgres', async () => { + vi.stubEnv('POSTGRES_PASSWORD', '') + const { POSTGRES_PASSWORD } = await import('./constants') + expect(POSTGRES_PASSWORD).toBe('postgres') + }) + }) + + describe('POSTGRES_USER_READ_WRITE', () => { + it('should use POSTGRES_USER_READ_WRITE when set', async () => { + vi.stubEnv('POSTGRES_USER_READ_WRITE', 'custom_admin') + const { POSTGRES_USER_READ_WRITE } = await import('./constants') + expect(POSTGRES_USER_READ_WRITE).toBe('custom_admin') + }) + + it('should default to supabase_admin', async () => { + vi.stubEnv('POSTGRES_USER_READ_WRITE', '') + const { POSTGRES_USER_READ_WRITE } = await import('./constants') + expect(POSTGRES_USER_READ_WRITE).toBe('supabase_admin') + }) + }) + + describe('POSTGRES_USER_READ_ONLY', () => { + it('should use POSTGRES_USER_READ_ONLY when set', async () => { + vi.stubEnv('POSTGRES_USER_READ_ONLY', 'custom_readonly') + const { POSTGRES_USER_READ_ONLY } = await import('./constants') + expect(POSTGRES_USER_READ_ONLY).toBe('custom_readonly') + }) + + it('should default to supabase_read_only_user', async () => { + vi.stubEnv('POSTGRES_USER_READ_ONLY', '') + const { POSTGRES_USER_READ_ONLY } = await import('./constants') + expect(POSTGRES_USER_READ_ONLY).toBe('supabase_read_only_user') + }) + }) +}) diff --git a/apps/studio/lib/api/self-hosted/constants.ts b/apps/studio/lib/api/self-hosted/constants.ts index f5d86acd30b22..0727ec3b0a33d 100644 --- a/apps/studio/lib/api/self-hosted/constants.ts +++ b/apps/studio/lib/api/self-hosted/constants.ts @@ -1,7 +1,7 @@ // Constants specific to self-hosted environments export const ENCRYPTION_KEY = process.env.PG_META_CRYPTO_KEY || 'SAMPLE_KEY' -export const POSTGRES_PORT = process.env.POSTGRES_PORT || 5432 +export const POSTGRES_PORT = parseInt(process.env.POSTGRES_PORT || '5432', 10) export const POSTGRES_HOST = process.env.POSTGRES_HOST || 'db' export const POSTGRES_DATABASE = process.env.POSTGRES_DB || 'postgres' export const POSTGRES_PASSWORD = process.env.POSTGRES_PASSWORD || 'postgres' diff --git a/apps/studio/lib/api/self-hosted/functions/index.test.ts b/apps/studio/lib/api/self-hosted/functions/index.test.ts new file mode 100644 index 0000000000000..cf131f35230fd --- /dev/null +++ b/apps/studio/lib/api/self-hosted/functions/index.test.ts @@ -0,0 +1,75 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import * as util from '../util' +import * as fileSystemStore from './fileSystemStore' +import { getFunctionsArtifactStore } from './index' + +vi.mock('../util', () => ({ + assertSelfHosted: vi.fn(), +})) + +vi.mock('./fileSystemStore', () => ({ + FileSystemFunctionsArtifactStore: vi.fn(), +})) + +describe('api/self-hosted/functions/index', () => { + let originalEdgeFunctionsFolder: string | undefined + + beforeEach(() => { + originalEdgeFunctionsFolder = process.env.EDGE_FUNCTIONS_MANAGEMENT_FOLDER + vi.resetAllMocks() + }) + + afterEach(() => { + if (originalEdgeFunctionsFolder !== undefined) { + process.env.EDGE_FUNCTIONS_MANAGEMENT_FOLDER = originalEdgeFunctionsFolder + } else { + delete process.env.EDGE_FUNCTIONS_MANAGEMENT_FOLDER + } + }) + + describe('getFunctionsArtifactStore', () => { + it('should call assertSelfHosted', () => { + process.env.EDGE_FUNCTIONS_MANAGEMENT_FOLDER = '/tmp/functions' + + getFunctionsArtifactStore() + + expect(util.assertSelfHosted).toHaveBeenCalled() + }) + + it('should throw error if EDGE_FUNCTIONS_MANAGEMENT_FOLDER is not set', () => { + delete process.env.EDGE_FUNCTIONS_MANAGEMENT_FOLDER + + expect(() => getFunctionsArtifactStore()).toThrow( + 'EDGE_FUNCTIONS_MANAGEMENT_FOLDER is required' + ) + }) + + it('should create FileSystemFunctionsArtifactStore with correct path', () => { + process.env.EDGE_FUNCTIONS_MANAGEMENT_FOLDER = '/var/lib/functions' + + getFunctionsArtifactStore() + + expect(fileSystemStore.FileSystemFunctionsArtifactStore).toHaveBeenCalledWith( + '/var/lib/functions' + ) + }) + + it('should return FileSystemFunctionsArtifactStore instance', () => { + const mockInstance = { + folderPath: '/tmp/test', + getFunctions: vi.fn(), + getFunctionBySlug: vi.fn(), + getFileEntriesBySlug: vi.fn(), + } + vi.mocked(fileSystemStore.FileSystemFunctionsArtifactStore).mockReturnValue( + mockInstance as any + ) + process.env.EDGE_FUNCTIONS_MANAGEMENT_FOLDER = '/tmp/test' + + const result = getFunctionsArtifactStore() + + expect(result).toBe(mockInstance) + }) + }) +}) diff --git a/apps/studio/lib/api/self-hosted/generate-types.test.ts b/apps/studio/lib/api/self-hosted/generate-types.test.ts new file mode 100644 index 0000000000000..e67f5674514f2 --- /dev/null +++ b/apps/studio/lib/api/self-hosted/generate-types.test.ts @@ -0,0 +1,142 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { generateTypescriptTypes } from './generate-types' + +vi.mock('data/fetchers', () => ({ + fetchGet: vi.fn(), +})) + +vi.mock('lib/constants', () => ({ + PG_META_URL: 'http://localhost:8080', +})) + +vi.mock('./util', () => ({ + assertSelfHosted: vi.fn(), +})) + +describe('api/self-hosted/generate-types', () => { + let mockFetchGet: ReturnType + let mockAssertSelfHosted: ReturnType + + beforeEach(async () => { + vi.clearAllMocks() + + const fetchers = await import('data/fetchers') + const util = await import('./util') + + mockFetchGet = vi.mocked(fetchers.fetchGet) + mockAssertSelfHosted = vi.mocked(util.assertSelfHosted) + }) + + describe('generateTypescriptTypes', () => { + it('should call assertSelfHosted', async () => { + mockFetchGet.mockResolvedValue({ types: 'export type User = {}' }) + + await generateTypescriptTypes({ headers: {} }) + + expect(mockAssertSelfHosted).toHaveBeenCalled() + }) + + it('should fetch from correct URL with schema params', async () => { + mockFetchGet.mockResolvedValue({ types: 'export type User = {}' }) + + await generateTypescriptTypes({ headers: {} }) + + expect(mockFetchGet).toHaveBeenCalledWith( + expect.stringContaining('http://localhost:8080/generators/typescript'), + expect.any(Object) + ) + + const callUrl = mockFetchGet.mock.calls[0][0] + expect(callUrl).toContain('included_schema=public,graphql_public,storage') + expect(callUrl).toContain('excluded_schemas=') + }) + + it('should include correct schemas in URL', async () => { + mockFetchGet.mockResolvedValue({ types: '' }) + + await generateTypescriptTypes({ headers: {} }) + + const callUrl = mockFetchGet.mock.calls[0][0] + expect(callUrl).toContain('public') + expect(callUrl).toContain('graphql_public') + expect(callUrl).toContain('storage') + }) + + it('should exclude system schemas', async () => { + mockFetchGet.mockResolvedValue({ types: '' }) + + await generateTypescriptTypes({ headers: {} }) + + const callUrl = mockFetchGet.mock.calls[0][0] + const excludedSchemas = [ + 'auth', + 'cron', + 'extensions', + 'graphql', + 'net', + 'pgsodium', + 'pgsodium_masks', + 'realtime', + 'supabase_functions', + 'supabase_migrations', + 'vault', + '_analytics', + '_realtime', + ] + + excludedSchemas.forEach((schema) => { + expect(callUrl).toContain(schema) + }) + }) + + it('should pass headers to fetchGet', async () => { + mockFetchGet.mockResolvedValue({ types: 'export type User = {}' }) + + const customHeaders = { + Authorization: 'Bearer token', + 'Custom-Header': 'value', + } + + await generateTypescriptTypes({ headers: customHeaders }) + + expect(mockFetchGet).toHaveBeenCalledWith(expect.any(String), { + headers: customHeaders, + }) + }) + + it('should return types from fetchGet response', async () => { + const mockTypes = 'export type User = { id: number; name: string }' + mockFetchGet.mockResolvedValue({ types: mockTypes }) + + const result = await generateTypescriptTypes({ headers: {} }) + + expect(result).toEqual({ types: mockTypes }) + }) + + it('should handle fetchGet errors', async () => { + const mockError = new Error('Network error') + mockFetchGet.mockRejectedValue(mockError) + + await expect(generateTypescriptTypes({ headers: {} })).rejects.toThrow('Network error') + }) + + it('should work without headers parameter', async () => { + mockFetchGet.mockResolvedValue({ types: '' }) + + await generateTypescriptTypes({}) + + expect(mockFetchGet).toHaveBeenCalledWith(expect.any(String), { headers: undefined }) + }) + + it('should construct URL with both included and excluded schemas', async () => { + mockFetchGet.mockResolvedValue({ types: '' }) + + await generateTypescriptTypes({ headers: {} }) + + const callUrl = mockFetchGet.mock.calls[0][0] + expect(callUrl).toContain('included_schema=') + expect(callUrl).toContain('excluded_schemas=') + }) + }) +}) diff --git a/apps/studio/lib/api/self-hosted/settings.test.ts b/apps/studio/lib/api/self-hosted/settings.test.ts new file mode 100644 index 0000000000000..1c388f820e789 --- /dev/null +++ b/apps/studio/lib/api/self-hosted/settings.test.ts @@ -0,0 +1,129 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { getProjectSettings } from './settings' + +vi.mock('./util', () => ({ + assertSelfHosted: vi.fn(), +})) + +vi.mock('lib/constants/api', () => ({ + PROJECT_ENDPOINT: 'localhost:8000', + PROJECT_ENDPOINT_PROTOCOL: 'http', +})) + +describe('api/self-hosted/settings', () => { + let mockAssertSelfHosted: ReturnType + + beforeEach(async () => { + vi.clearAllMocks() + vi.resetModules() + + const util = await import('./util') + mockAssertSelfHosted = vi.mocked(util.assertSelfHosted) + }) + + describe('getProjectSettings', () => { + it('should call assertSelfHosted', () => { + getProjectSettings() + + expect(mockAssertSelfHosted).toHaveBeenCalled() + }) + + it('should return project settings with correct structure', () => { + const settings = getProjectSettings() + + expect(settings).toHaveProperty('app_config') + expect(settings).toHaveProperty('cloud_provider') + expect(settings).toHaveProperty('db_dns_name') + expect(settings).toHaveProperty('db_host') + expect(settings).toHaveProperty('db_name') + expect(settings).toHaveProperty('jwt_secret') + expect(settings).toHaveProperty('service_api_keys') + }) + + it('should return correct default values', () => { + const settings = getProjectSettings() + + expect(settings.cloud_provider).toBe('AWS') + expect(settings.db_host).toBe('localhost') + expect(settings.db_name).toBe('postgres') + expect(settings.db_port).toBe(5432) + expect(settings.db_user).toBe('postgres') + expect(settings.ref).toBe('default') + expect(settings.region).toBe('ap-southeast-1') + expect(settings.status).toBe('ACTIVE_HEALTHY') + expect(settings.ssl_enforced).toBe(false) + }) + + it('should include app_config with endpoint and protocol', () => { + const settings = getProjectSettings() + + expect(settings.app_config).toEqual({ + db_schema: 'public', + endpoint: 'localhost:8000', + storage_endpoint: 'localhost:8000', + protocol: 'http', + }) + }) + + it('should include service_api_keys array', () => { + const settings = getProjectSettings() + + expect(settings.service_api_keys).toHaveLength(2) + expect(settings.service_api_keys[0].name).toBe('service_role key') + expect(settings.service_api_keys[0].tags).toBe('service_role') + expect(settings.service_api_keys[1].name).toBe('anon key') + expect(settings.service_api_keys[1].tags).toBe('anon') + }) + + it('should use environment variables when set', async () => { + vi.stubEnv('AUTH_JWT_SECRET', 'custom-jwt-secret-with-at-least-32-chars') + vi.stubEnv('DEFAULT_PROJECT_NAME', 'My Custom Project') + vi.stubEnv('SUPABASE_SERVICE_KEY', 'custom-service-key') + vi.stubEnv('SUPABASE_ANON_KEY', 'custom-anon-key') + + // Need to re-import to pick up new env vars + vi.resetModules() + + const { getProjectSettings: getSettings } = await import('./settings') + const settings = getSettings() + + expect(settings.jwt_secret).toBe('custom-jwt-secret-with-at-least-32-chars') + expect(settings.name).toBe('My Custom Project') + expect(settings.service_api_keys[0].api_key).toBe('custom-service-key') + expect(settings.service_api_keys[1].api_key).toBe('custom-anon-key') + }) + + it('should use default JWT secret when not set', async () => { + vi.unstubAllEnvs() + + vi.resetModules() + const { getProjectSettings: getSettings } = await import('./settings') + const settings = getSettings() + + expect(settings.jwt_secret).toBe('super-secret-jwt-token-with-at-least-32-characters-long') + }) + + it('should use default project name when not set', async () => { + vi.unstubAllEnvs() + + vi.resetModules() + const { getProjectSettings: getSettings } = await import('./settings') + const settings = getSettings() + + expect(settings.name).toBe('Default Project') + }) + + it('should have correct db_ip_addr_config', () => { + const settings = getProjectSettings() + + expect(settings.db_ip_addr_config).toBe('legacy') + }) + + it('should have correct inserted_at timestamp', () => { + const settings = getProjectSettings() + + expect(settings.inserted_at).toBe('2021-08-02T06:40:40.646Z') + }) + }) +}) diff --git a/apps/studio/lib/api/self-hosted/types.test.ts b/apps/studio/lib/api/self-hosted/types.test.ts new file mode 100644 index 0000000000000..ce543c012bf32 --- /dev/null +++ b/apps/studio/lib/api/self-hosted/types.test.ts @@ -0,0 +1,110 @@ +import { describe, expect, it } from 'vitest' + +import { databaseErrorSchema, PgMetaDatabaseError } from './types' + +describe('api/self-hosted/types', () => { + describe('databaseErrorSchema', () => { + it('should validate valid database error', () => { + const validError = { + message: 'Database connection failed', + code: '08006', + formattedError: 'Error: Connection refused', + } + + const result = databaseErrorSchema.safeParse(validError) + expect(result.success).toBe(true) + }) + + it('should reject error missing message', () => { + const invalidError = { + code: '08006', + formattedError: 'Error: Connection refused', + } + + const result = databaseErrorSchema.safeParse(invalidError) + expect(result.success).toBe(false) + }) + + it('should reject error missing code', () => { + const invalidError = { + message: 'Database connection failed', + formattedError: 'Error: Connection refused', + } + + const result = databaseErrorSchema.safeParse(invalidError) + expect(result.success).toBe(false) + }) + + it('should reject error missing formattedError', () => { + const invalidError = { + message: 'Database connection failed', + code: '08006', + } + + const result = databaseErrorSchema.safeParse(invalidError) + expect(result.success).toBe(false) + }) + + it('should reject non-string values', () => { + const invalidError = { + message: 123, + code: '08006', + formattedError: 'Error', + } + + const result = databaseErrorSchema.safeParse(invalidError) + expect(result.success).toBe(false) + }) + }) + + describe('PgMetaDatabaseError', () => { + it('should create error with all properties', () => { + const error = new PgMetaDatabaseError( + 'Syntax error', + '42601', + 400, + 'ERROR: syntax error at or near "SELCT"' + ) + + expect(error.message).toBe('Syntax error') + expect(error.code).toBe('42601') + expect(error.statusCode).toBe(400) + expect(error.formattedError).toBe('ERROR: syntax error at or near "SELCT"') + expect(error.name).toBe('PgMetaDatabaseError') + }) + + it('should be instanceof Error', () => { + const error = new PgMetaDatabaseError('Test error', '12345', 500, 'Formatted') + + expect(error).toBeInstanceOf(Error) + expect(error).toBeInstanceOf(PgMetaDatabaseError) + }) + + it('should have correct name property', () => { + const error = new PgMetaDatabaseError('Test', 'CODE', 400, 'Formatted') + + expect(error.name).toBe('PgMetaDatabaseError') + }) + + it('should preserve all custom properties', () => { + const error = new PgMetaDatabaseError( + 'Connection timeout', + '08000', + 503, + 'ERROR: connection timeout' + ) + + expect(error.code).toBe('08000') + expect(error.statusCode).toBe(503) + expect(error.formattedError).toBe('ERROR: connection timeout') + }) + + it('should work with different status codes', () => { + const error400 = new PgMetaDatabaseError('Bad request', 'ERR', 400, 'Error') + const error500 = new PgMetaDatabaseError('Server error', 'ERR', 500, 'Error') + + expect(error400.statusCode).toBe(400) + expect(error500.statusCode).toBe(500) + }) + }) +}) diff --git a/apps/studio/lib/api/self-hosted/util.test.ts b/apps/studio/lib/api/self-hosted/util.test.ts new file mode 100644 index 0000000000000..5477080cf5575 --- /dev/null +++ b/apps/studio/lib/api/self-hosted/util.test.ts @@ -0,0 +1,124 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { assertSelfHosted, encryptString, getConnectionString } from './util' + +vi.mock('lib/constants', () => ({ + IS_PLATFORM: false, +})) + +vi.mock('crypto-js', () => { + const mockEncrypt = vi.fn() + return { + default: { + AES: { + encrypt: mockEncrypt, + }, + }, + AES: { + encrypt: mockEncrypt, + }, + } +}) + +describe('api/self-hosted/util', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('assertSelfHosted', () => { + it('should not throw when IS_PLATFORM is false', async () => { + const constants = await import('lib/constants') + vi.spyOn(constants, 'IS_PLATFORM', 'get').mockReturnValue(false) + + expect(() => assertSelfHosted()).not.toThrow() + }) + + it('should throw error when IS_PLATFORM is true', async () => { + const constants = await import('lib/constants') + vi.spyOn(constants, 'IS_PLATFORM', 'get').mockReturnValue(true) + + expect(() => assertSelfHosted()).toThrow( + 'This function can only be called in self-hosted environments' + ) + }) + }) + + describe('encryptString', () => { + it('should encrypt string using AES', async () => { + const crypto = await import('crypto-js') + const mockEncrypted = 'encrypted-string-123' + vi.mocked(crypto.default.AES.encrypt).mockReturnValue({ + toString: () => mockEncrypted, + } as any) + + const result = encryptString('my-secret-data') + + expect(crypto.default.AES.encrypt).toHaveBeenCalledWith('my-secret-data', expect.any(String)) + expect(result).toBe(mockEncrypted) + }) + + it('should return encrypted string as string', async () => { + const crypto = await import('crypto-js') + vi.mocked(crypto.default.AES.encrypt).mockReturnValue({ + toString: () => 'U2FsdGVkX1+abc123', + } as any) + + const result = encryptString('test') + + expect(typeof result).toBe('string') + expect(result).toBe('U2FsdGVkX1+abc123') + }) + }) + + describe('getConnectionString', () => { + beforeEach(() => { + vi.resetModules() + }) + + it('should build connection string with read-write user', async () => { + vi.stubEnv('POSTGRES_HOST', 'localhost') + vi.stubEnv('POSTGRES_PORT', '5432') + vi.stubEnv('POSTGRES_DB', 'testdb') + vi.stubEnv('POSTGRES_PASSWORD', 'testpass') + vi.stubEnv('POSTGRES_USER_READ_WRITE', 'admin_user') + + // Re-import to get updated env values + const { getConnectionString } = await import('./util') + + const result = getConnectionString({ readOnly: false }) + + expect(result).toBe('postgresql://admin_user:testpass@localhost:5432/testdb') + }) + + it('should build connection string with read-only user', async () => { + vi.stubEnv('POSTGRES_HOST', 'db.example.com') + vi.stubEnv('POSTGRES_PORT', '5433') + vi.stubEnv('POSTGRES_DB', 'mydb') + vi.stubEnv('POSTGRES_PASSWORD', 'secret') + vi.stubEnv('POSTGRES_USER_READ_ONLY', 'readonly_user') + + const { getConnectionString } = await import('./util') + + const result = getConnectionString({ readOnly: true }) + + expect(result).toBe('postgresql://readonly_user:secret@db.example.com:5433/mydb') + }) + + it('should use default values when env vars not set', async () => { + vi.stubEnv('POSTGRES_HOST', '') + vi.stubEnv('POSTGRES_PORT', '') + vi.stubEnv('POSTGRES_DB', '') + vi.stubEnv('POSTGRES_PASSWORD', '') + vi.stubEnv('POSTGRES_USER_READ_WRITE', '') + vi.stubEnv('POSTGRES_USER_READ_ONLY', '') + + const { getConnectionString } = await import('./util') + + const resultReadWrite = getConnectionString({ readOnly: false }) + const resultReadOnly = getConnectionString({ readOnly: true }) + + expect(resultReadWrite).toBe('postgresql://supabase_admin:postgres@db:5432/postgres') + expect(resultReadOnly).toBe('postgresql://supabase_read_only_user:postgres@db:5432/postgres') + }) + }) +}) diff --git a/apps/studio/lib/breadcrumbs.test.ts b/apps/studio/lib/breadcrumbs.test.ts new file mode 100644 index 0000000000000..a23b92afae651 --- /dev/null +++ b/apps/studio/lib/breadcrumbs.test.ts @@ -0,0 +1,105 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { + getMirroredBreadcrumbs, + getOwnershipOfBreadcrumbSnapshot, + MIRRORED_BREADCRUMBS, + takeBreadcrumbSnapshot, +} from './breadcrumbs' + +describe('breadcrumbs', () => { + beforeEach(() => { + // Clear the ring buffer by popping all items + while (MIRRORED_BREADCRUMBS.length > 0) { + MIRRORED_BREADCRUMBS.popFront() + } + }) + + describe('getMirroredBreadcrumbs', () => { + it('should return an array of breadcrumbs from the ring buffer', () => { + const result = getMirroredBreadcrumbs() + expect(Array.isArray(result)).toBe(true) + }) + + it('should return empty array when no breadcrumbs exist', () => { + const result = getMirroredBreadcrumbs() + expect(result).toHaveLength(0) + }) + + it('should return breadcrumbs after they are added to ring buffer', () => { + const mockBreadcrumb = { message: 'test', timestamp: Date.now() } as any + MIRRORED_BREADCRUMBS.pushBack(mockBreadcrumb) + + const result = getMirroredBreadcrumbs() + expect(result).toHaveLength(1) + expect(result[0]).toEqual(mockBreadcrumb) + }) + }) + + describe('takeBreadcrumbSnapshot', () => { + it('should capture current breadcrumbs into a snapshot', () => { + const mockBreadcrumb = { message: 'test', timestamp: Date.now() } as any + MIRRORED_BREADCRUMBS.pushBack(mockBreadcrumb) + + takeBreadcrumbSnapshot() + const snapshot = getOwnershipOfBreadcrumbSnapshot() + + expect(snapshot).toHaveLength(1) + expect(snapshot?.[0]).toEqual(mockBreadcrumb) + }) + + it('should update snapshot when called multiple times', () => { + const mockBreadcrumb1 = { message: 'first', timestamp: Date.now() } as any + const mockBreadcrumb2 = { message: 'second', timestamp: Date.now() } as any + + MIRRORED_BREADCRUMBS.pushBack(mockBreadcrumb1) + takeBreadcrumbSnapshot() + + MIRRORED_BREADCRUMBS.pushBack(mockBreadcrumb2) + takeBreadcrumbSnapshot() + + const snapshot = getOwnershipOfBreadcrumbSnapshot() + expect(snapshot).toHaveLength(2) + }) + }) + + describe('getOwnershipOfBreadcrumbSnapshot', () => { + it('should return null initially when no snapshot has been taken', () => { + const snapshot = getOwnershipOfBreadcrumbSnapshot() + expect(snapshot).toBeNull() + }) + + it('should return the snapshot after takeBreadcrumbSnapshot is called', () => { + const mockBreadcrumb = { message: 'test', timestamp: Date.now() } as any + MIRRORED_BREADCRUMBS.pushBack(mockBreadcrumb) + + takeBreadcrumbSnapshot() + const snapshot = getOwnershipOfBreadcrumbSnapshot() + + expect(snapshot).not.toBeNull() + expect(snapshot).toHaveLength(1) + }) + + it('should clear the snapshot after returning it', () => { + const mockBreadcrumb = { message: 'test', timestamp: Date.now() } as any + MIRRORED_BREADCRUMBS.pushBack(mockBreadcrumb) + + takeBreadcrumbSnapshot() + const firstSnapshot = getOwnershipOfBreadcrumbSnapshot() + const secondSnapshot = getOwnershipOfBreadcrumbSnapshot() + + expect(firstSnapshot).not.toBeNull() + expect(secondSnapshot).toBeNull() + }) + + it('should take ownership and prevent subsequent calls from getting the same snapshot', () => { + takeBreadcrumbSnapshot() + + const snapshot1 = getOwnershipOfBreadcrumbSnapshot() + const snapshot2 = getOwnershipOfBreadcrumbSnapshot() + + expect(snapshot1).not.toBe(snapshot2) + expect(snapshot2).toBeNull() + }) + }) +}) diff --git a/apps/studio/lib/constants/api.test.ts b/apps/studio/lib/constants/api.test.ts new file mode 100644 index 0000000000000..c33e3976c0309 --- /dev/null +++ b/apps/studio/lib/constants/api.test.ts @@ -0,0 +1,98 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +describe('constants/api', () => { + beforeEach(() => { + vi.resetModules() + }) + + describe('PROJECT_ANALYTICS_URL', () => { + it('should be undefined when LOGFLARE_URL is not set', async () => { + vi.stubEnv('LOGFLARE_URL', '') + const { PROJECT_ANALYTICS_URL } = await import('./api') + expect(PROJECT_ANALYTICS_URL).toBeUndefined() + }) + + it('should use LOGFLARE_URL when set', async () => { + vi.stubEnv('LOGFLARE_URL', 'https://logflare.example.com') + const { PROJECT_ANALYTICS_URL } = await import('./api') + expect(PROJECT_ANALYTICS_URL).toBe('https://logflare.example.com/api/') + }) + }) + + describe('PROJECT_REST_URL', () => { + it('should construct URL from SUPABASE_PUBLIC_URL', async () => { + vi.stubEnv('SUPABASE_PUBLIC_URL', 'https://test.supabase.co') + const { PROJECT_REST_URL } = await import('./api') + expect(PROJECT_REST_URL).toBe('https://test.supabase.co/rest/v1/') + }) + + it('should use default localhost when SUPABASE_PUBLIC_URL is not set', async () => { + vi.stubEnv('SUPABASE_PUBLIC_URL', '') + const { PROJECT_REST_URL } = await import('./api') + expect(PROJECT_REST_URL).toBe('http://localhost:8000/rest/v1/') + }) + }) + + describe('PROJECT_ENDPOINT', () => { + it('should extract host from SUPABASE_PUBLIC_URL', async () => { + vi.stubEnv('SUPABASE_PUBLIC_URL', 'https://test.supabase.co:3000') + const { PROJECT_ENDPOINT } = await import('./api') + expect(PROJECT_ENDPOINT).toBe('test.supabase.co:3000') + }) + + it('should use default localhost host', async () => { + vi.stubEnv('SUPABASE_PUBLIC_URL', '') + const { PROJECT_ENDPOINT } = await import('./api') + expect(PROJECT_ENDPOINT).toBe('localhost:8000') + }) + }) + + describe('PROJECT_ENDPOINT_PROTOCOL', () => { + it('should extract protocol without colon from SUPABASE_PUBLIC_URL', async () => { + vi.stubEnv('SUPABASE_PUBLIC_URL', 'https://test.supabase.co') + const { PROJECT_ENDPOINT_PROTOCOL } = await import('./api') + expect(PROJECT_ENDPOINT_PROTOCOL).toBe('https') + }) + + it('should use http for default localhost', async () => { + vi.stubEnv('SUPABASE_PUBLIC_URL', '') + const { PROJECT_ENDPOINT_PROTOCOL } = await import('./api') + expect(PROJECT_ENDPOINT_PROTOCOL).toBe('http') + }) + }) + + describe('DEFAULT_PROJECT', () => { + it('should have correct default values', async () => { + vi.stubEnv('DEFAULT_PROJECT_NAME', '') + const { DEFAULT_PROJECT } = await import('./api') + + expect(DEFAULT_PROJECT).toEqual({ + id: 1, + ref: 'default', + name: 'Default Project', + organization_id: 1, + cloud_provider: 'localhost', + status: 'ACTIVE_HEALTHY', + region: 'local', + inserted_at: '2021-08-02T06:40:40.646Z', + }) + }) + + it('should use DEFAULT_PROJECT_NAME env var when set', async () => { + vi.stubEnv('DEFAULT_PROJECT_NAME', 'My Custom Project') + const { DEFAULT_PROJECT } = await import('./api') + expect(DEFAULT_PROJECT.name).toBe('My Custom Project') + }) + + it('should have static id and ref', async () => { + const { DEFAULT_PROJECT } = await import('./api') + expect(DEFAULT_PROJECT.id).toBe(1) + expect(DEFAULT_PROJECT.ref).toBe('default') + }) + + it('should have localhost cloud_provider', async () => { + const { DEFAULT_PROJECT } = await import('./api') + expect(DEFAULT_PROJECT.cloud_provider).toBe('localhost') + }) + }) +}) diff --git a/apps/studio/lib/error-reporting.test.ts b/apps/studio/lib/error-reporting.test.ts new file mode 100644 index 0000000000000..746b524b2ea13 --- /dev/null +++ b/apps/studio/lib/error-reporting.test.ts @@ -0,0 +1,173 @@ +import * as Sentry from '@sentry/nextjs' +import { ResponseError } from 'types' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { captureCriticalError } from './error-reporting' + +vi.mock('@sentry/nextjs', () => ({ + captureMessage: vi.fn(), +})) + +describe('error-reporting', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('captureCriticalError', () => { + it('should not capture error if message is empty', () => { + const error = { message: '' } + captureCriticalError(error, 'test context') + + expect(Sentry.captureMessage).not.toHaveBeenCalled() + }) + + it('should capture regular Error objects', () => { + const error = new Error('Something went wrong') + captureCriticalError(error, 'test action') + + expect(Sentry.captureMessage).toHaveBeenCalledWith( + '[CRITICAL][test action] Failed: Something went wrong' + ) + }) + + it('should not capture whitelisted error messages', () => { + const error = new Error('email must be an email') + captureCriticalError(error, 'validation') + + expect(Sentry.captureMessage).not.toHaveBeenCalled() + }) + + it('should not capture errors with partial whitelisted message', () => { + const error = new Error('User error: A user with this email already exists in the system') + captureCriticalError(error, 'sign up') + + expect(Sentry.captureMessage).not.toHaveBeenCalled() + }) + + it('should capture errors that are not whitelisted', () => { + const error = new Error('Database connection failed') + captureCriticalError(error, 'database') + + expect(Sentry.captureMessage).toHaveBeenCalledWith( + '[CRITICAL][database] Failed: Database connection failed' + ) + }) + + it('should capture 5XX ResponseError', () => { + const error = new ResponseError( + 'Internal server error', + 500, + undefined, + undefined, + '/api/test' + ) + captureCriticalError(error, 'api call') + + expect(Sentry.captureMessage).toHaveBeenCalledWith( + '[CRITICAL][api call] Failed: requestPathname /api/test w/ message: Internal server error' + ) + }) + + it('should not capture 4XX ResponseError', () => { + const error = new ResponseError('Not found', 404, undefined, undefined, '/api/test') + captureCriticalError(error, 'api call') + + expect(Sentry.captureMessage).not.toHaveBeenCalled() + }) + + it('should capture ResponseError with 5XX status code', () => { + const error = new ResponseError('Gateway timeout', 504, undefined, undefined, '/api/gateway') + captureCriticalError(error, 'gateway request') + + expect(Sentry.captureMessage).toHaveBeenCalledWith( + '[CRITICAL][gateway request] Failed: requestPathname /api/gateway w/ message: Gateway timeout' + ) + }) + + it('should capture ResponseError without code or requestPathname', () => { + const error = new ResponseError('Unknown error') + captureCriticalError(error, 'unknown') + + expect(Sentry.captureMessage).toHaveBeenCalledWith( + '[CRITICAL][unknown] Failed: Response Error (no code or requestPathname) w/ message: Unknown error' + ) + }) + + it('should capture unknown error objects with message property', () => { + const error = { message: 'Custom error object' } + captureCriticalError(error, 'custom') + + expect(Sentry.captureMessage).toHaveBeenCalledWith( + '[CRITICAL][custom] Failed: Custom error object' + ) + }) + + it('should not capture unknown error without message', () => { + const error = { foo: 'bar' } + captureCriticalError(error as any, 'no message') + + expect(Sentry.captureMessage).not.toHaveBeenCalled() + }) + + it('should not capture whitelisted password validation error', () => { + const error = new Error( + 'Password is known to be weak and easy to guess, please choose a different one' + ) + captureCriticalError(error, 'password update') + + expect(Sentry.captureMessage).not.toHaveBeenCalled() + }) + + it('should not capture whitelisted TOTP error', () => { + const error = new Error('Invalid TOTP code entered') + captureCriticalError(error, 'mfa verification') + + expect(Sentry.captureMessage).not.toHaveBeenCalled() + }) + + it('should not capture whitelisted project name error', () => { + const error = new Error('name should not contain a . string') + captureCriticalError(error, 'create project') + + expect(Sentry.captureMessage).not.toHaveBeenCalled() + }) + + it('should capture non-whitelisted errors even if similar to whitelisted ones', () => { + const error = new Error('email format is invalid') + captureCriticalError(error, 'validation') + + expect(Sentry.captureMessage).toHaveBeenCalledWith( + '[CRITICAL][validation] Failed: email format is invalid' + ) + }) + + it('should handle Error objects with empty message', () => { + const error = new Error('') + captureCriticalError(error, 'empty error') + + expect(Sentry.captureMessage).not.toHaveBeenCalled() + }) + + it('should handle ResponseError at boundary of 4XX/5XX (499)', () => { + const error = new ResponseError( + 'Client closed request', + 499, + undefined, + undefined, + '/api/test' + ) + captureCriticalError(error, 'request') + + expect(Sentry.captureMessage).not.toHaveBeenCalled() + }) + + it('should handle ResponseError at boundary of 4XX/5XX (500)', () => { + const error = new ResponseError('Internal error', 500, undefined, undefined, '/api/test') + captureCriticalError(error, 'request') + + expect(Sentry.captureMessage).toHaveBeenCalledWith( + '[CRITICAL][request] Failed: requestPathname /api/test w/ message: Internal error' + ) + }) + }) +}) diff --git a/apps/studio/lib/navigation.test.ts b/apps/studio/lib/navigation.test.ts new file mode 100644 index 0000000000000..6b593ced5195d --- /dev/null +++ b/apps/studio/lib/navigation.test.ts @@ -0,0 +1,231 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { createNavigationHandler } from './navigation' + +describe('createNavigationHandler', () => { + let mockRouter: any + let mockWindowOpen: any + + beforeEach(() => { + vi.clearAllMocks() + + // Mock router with push method + mockRouter = { + push: vi.fn(), + } + + // Mock window.open + mockWindowOpen = vi.fn() + global.window.open = mockWindowOpen + }) + + describe('keyboard navigation', () => { + it('should call router.push when Enter key is pressed without modifiers', () => { + const handler = createNavigationHandler('/test-url', mockRouter) + const event = { + key: 'Enter', + metaKey: false, + ctrlKey: false, + preventDefault: vi.fn(), + } as unknown as React.KeyboardEvent + + handler(event) + + expect(event.preventDefault).toHaveBeenCalled() + expect(mockRouter.push).toHaveBeenCalledWith('/test-url') + expect(mockWindowOpen).not.toHaveBeenCalled() + }) + + it('should call router.push when Space key is pressed without modifiers', () => { + const handler = createNavigationHandler('/test-url', mockRouter) + const event = { + key: ' ', + metaKey: false, + ctrlKey: false, + preventDefault: vi.fn(), + } as unknown as React.KeyboardEvent + + handler(event) + + expect(event.preventDefault).toHaveBeenCalled() + expect(mockRouter.push).toHaveBeenCalledWith('/test-url') + expect(mockWindowOpen).not.toHaveBeenCalled() + }) + + it('should open new tab when Enter key is pressed with metaKey', () => { + const handler = createNavigationHandler('/test-url', mockRouter) + const event = { + key: 'Enter', + metaKey: true, + ctrlKey: false, + preventDefault: vi.fn(), + } as unknown as React.KeyboardEvent + + handler(event) + + expect(event.preventDefault).toHaveBeenCalled() + expect(mockWindowOpen).toHaveBeenCalledWith('/test-url', '_blank') + expect(mockRouter.push).not.toHaveBeenCalled() + }) + + it('should open new tab when Enter key is pressed with ctrlKey', () => { + const handler = createNavigationHandler('/test-url', mockRouter) + const event = { + key: 'Enter', + metaKey: false, + ctrlKey: true, + preventDefault: vi.fn(), + } as unknown as React.KeyboardEvent + + handler(event) + + expect(event.preventDefault).toHaveBeenCalled() + expect(mockWindowOpen).toHaveBeenCalledWith('/test-url', '_blank') + expect(mockRouter.push).not.toHaveBeenCalled() + }) + + it('should open new tab when Space key is pressed with metaKey', () => { + const handler = createNavigationHandler('/test-url', mockRouter) + const event = { + key: ' ', + metaKey: true, + ctrlKey: false, + preventDefault: vi.fn(), + } as unknown as React.KeyboardEvent + + handler(event) + + expect(event.preventDefault).toHaveBeenCalled() + expect(mockWindowOpen).toHaveBeenCalledWith('/test-url', '_blank') + expect(mockRouter.push).not.toHaveBeenCalled() + }) + + it('should do nothing when other keys are pressed', () => { + const handler = createNavigationHandler('/test-url', mockRouter) + const event = { + key: 'a', + metaKey: false, + ctrlKey: false, + preventDefault: vi.fn(), + } as unknown as React.KeyboardEvent + + handler(event) + + expect(event.preventDefault).not.toHaveBeenCalled() + expect(mockRouter.push).not.toHaveBeenCalled() + expect(mockWindowOpen).not.toHaveBeenCalled() + }) + }) + + describe('mouse navigation', () => { + it('should call router.push on regular left click', () => { + const handler = createNavigationHandler('/test-url', mockRouter) + const event = { + button: 0, + metaKey: false, + ctrlKey: false, + preventDefault: vi.fn(), + } as unknown as React.MouseEvent + + handler(event) + + expect(mockRouter.push).toHaveBeenCalledWith('/test-url') + expect(mockWindowOpen).not.toHaveBeenCalled() + }) + + it('should open new tab on middle mouse button click', () => { + const handler = createNavigationHandler('/test-url', mockRouter) + const event = { + button: 1, // Middle button + metaKey: false, + ctrlKey: false, + preventDefault: vi.fn(), + } as unknown as React.MouseEvent + + handler(event) + + expect(event.preventDefault).toHaveBeenCalled() + expect(mockWindowOpen).toHaveBeenCalledWith('/test-url', '_blank') + expect(mockRouter.push).not.toHaveBeenCalled() + }) + + it('should open new tab on Cmd + left click', () => { + const handler = createNavigationHandler('/test-url', mockRouter) + const event = { + button: 0, + metaKey: true, + ctrlKey: false, + preventDefault: vi.fn(), + } as unknown as React.MouseEvent + + handler(event) + + expect(event.preventDefault).toHaveBeenCalled() + expect(mockWindowOpen).toHaveBeenCalledWith('/test-url', '_blank') + expect(mockRouter.push).not.toHaveBeenCalled() + }) + + it('should open new tab on Ctrl + left click', () => { + const handler = createNavigationHandler('/test-url', mockRouter) + const event = { + button: 0, + metaKey: false, + ctrlKey: true, + preventDefault: vi.fn(), + } as unknown as React.MouseEvent + + handler(event) + + expect(event.preventDefault).toHaveBeenCalled() + expect(mockWindowOpen).toHaveBeenCalledWith('/test-url', '_blank') + expect(mockRouter.push).not.toHaveBeenCalled() + }) + + it('should handle right click without navigation', () => { + const handler = createNavigationHandler('/test-url', mockRouter) + const event = { + button: 2, // Right button + metaKey: false, + ctrlKey: false, + preventDefault: vi.fn(), + } as unknown as React.MouseEvent + + handler(event) + + // Right click should trigger router.push (falls through to default case) + expect(mockRouter.push).toHaveBeenCalledWith('/test-url') + }) + }) + + describe('URL handling', () => { + it('should handle URLs with BASE_PATH correctly', () => { + const handler = createNavigationHandler('/project/123/settings', mockRouter) + const event = { + button: 1, // Middle button to open in new tab + metaKey: false, + ctrlKey: false, + preventDefault: vi.fn(), + } as unknown as React.MouseEvent + + handler(event) + + // Should prepend BASE_PATH when opening new tab + expect(mockWindowOpen).toHaveBeenCalledWith('/project/123/settings', '_blank') + }) + + it('should pass URL directly to router.push without BASE_PATH', () => { + const handler = createNavigationHandler('/project/123/settings', mockRouter) + const event = { + button: 0, + metaKey: false, + ctrlKey: false, + preventDefault: vi.fn(), + } as unknown as React.MouseEvent + + handler(event) + + // router.push should receive URL without BASE_PATH + expect(mockRouter.push).toHaveBeenCalledWith('/project/123/settings') + }) + }) +}) diff --git a/apps/studio/lib/project-supabase-client.test.ts b/apps/studio/lib/project-supabase-client.test.ts new file mode 100644 index 0000000000000..5de43721ee16d --- /dev/null +++ b/apps/studio/lib/project-supabase-client.test.ts @@ -0,0 +1,124 @@ +import * as supabaseJs from '@supabase/supabase-js' +import * as apiKeysUtils from 'data/api-keys/temp-api-keys-utils' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { createProjectSupabaseClient } from './project-supabase-client' + +vi.mock('@supabase/supabase-js', () => ({ + createClient: vi.fn(), +})) + +vi.mock('data/api-keys/temp-api-keys-utils', () => ({ + getOrRefreshTemporaryApiKey: vi.fn(), +})) + +describe('project-supabase-client', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('createProjectSupabaseClient', () => { + it('should create a Supabase client with temporary API key', async () => { + const mockApiKey = 'test-api-key-123' + const mockClient = { from: vi.fn() } + const projectRef = 'test-project-ref' + const clientEndpoint = 'https://test.supabase.co' + + vi.mocked(apiKeysUtils.getOrRefreshTemporaryApiKey).mockResolvedValue({ + apiKey: mockApiKey, + expiryTimeMs: Date.now() + 3600000, + }) + vi.mocked(supabaseJs.createClient).mockReturnValue(mockClient as any) + + const result = await createProjectSupabaseClient(projectRef, clientEndpoint) + + expect(apiKeysUtils.getOrRefreshTemporaryApiKey).toHaveBeenCalledWith(projectRef) + expect(supabaseJs.createClient).toHaveBeenCalledWith(clientEndpoint, mockApiKey, { + auth: { + persistSession: false, + autoRefreshToken: false, + detectSessionInUrl: false, + storage: { + getItem: expect.any(Function), + setItem: expect.any(Function), + removeItem: expect.any(Function), + }, + }, + }) + expect(result).toBe(mockClient) + }) + + it('should configure storage to not persist session', async () => { + const mockApiKey = 'test-api-key-456' + const mockClient = { from: vi.fn() } + + vi.mocked(apiKeysUtils.getOrRefreshTemporaryApiKey).mockResolvedValue({ + apiKey: mockApiKey, + expiryTimeMs: Date.now() + 3600000, + }) + vi.mocked(supabaseJs.createClient).mockReturnValue(mockClient as any) + + await createProjectSupabaseClient('ref', 'https://example.com') + + const config = vi.mocked(supabaseJs.createClient).mock.calls[0][2] + if (!config?.auth?.storage) throw new Error('storage config is missing') + const storage = config.auth.storage + + // Test storage methods return expected values + expect(storage.getItem('any-key')).toBeNull() + expect(storage.setItem('key', 'value')).toBeUndefined() + expect(storage.removeItem('key')).toBeUndefined() + }) + + it('should throw error if API key retrieval fails', async () => { + const error = new Error('Failed to get API key') + vi.mocked(apiKeysUtils.getOrRefreshTemporaryApiKey).mockRejectedValue(error) + + await expect(createProjectSupabaseClient('ref', 'https://example.com')).rejects.toThrow( + 'Failed to get API key' + ) + + expect(supabaseJs.createClient).not.toHaveBeenCalled() + }) + + it('should pass through different project refs and endpoints', async () => { + const mockApiKey = 'api-key' + const mockClient = { from: vi.fn() } + + vi.mocked(apiKeysUtils.getOrRefreshTemporaryApiKey).mockResolvedValue({ + apiKey: mockApiKey, + expiryTimeMs: Date.now() + 3600000, + }) + vi.mocked(supabaseJs.createClient).mockReturnValue(mockClient as any) + + await createProjectSupabaseClient('project-123', 'https://project123.supabase.co') + + expect(apiKeysUtils.getOrRefreshTemporaryApiKey).toHaveBeenCalledWith('project-123') + expect(supabaseJs.createClient).toHaveBeenCalledWith( + 'https://project123.supabase.co', + mockApiKey, + expect.any(Object) + ) + }) + + it('should disable session persistence options', async () => { + const mockApiKey = 'api-key' + const mockClient = { from: vi.fn() } + + vi.mocked(apiKeysUtils.getOrRefreshTemporaryApiKey).mockResolvedValue({ + apiKey: mockApiKey, + expiryTimeMs: Date.now() + 3600000, + }) + vi.mocked(supabaseJs.createClient).mockReturnValue(mockClient as any) + + await createProjectSupabaseClient('ref', 'https://example.com') + + const config = vi.mocked(supabaseJs.createClient).mock.calls[0][2] + if (!config?.auth) throw new Error('auth config is missing') + + expect(config.auth.persistSession).toBe(false) + expect(config.auth.autoRefreshToken).toBe(false) + expect(config.auth.detectSessionInUrl).toBe(false) + }) + }) +}) diff --git a/apps/studio/lib/void.test.ts b/apps/studio/lib/void.test.ts new file mode 100644 index 0000000000000..2ad837d9ed886 --- /dev/null +++ b/apps/studio/lib/void.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest' + +import { EMPTY_ARR, EMPTY_OBJ, noop } from './void' + +describe('void utilities', () => { + describe('noop', () => { + it('should return undefined', () => { + expect(noop()).toBeUndefined() + }) + }) + + describe('EMPTY_OBJ', () => { + it('should always return the same reference', () => { + expect(EMPTY_OBJ).toBe(EMPTY_OBJ) + }) + + it('should be an empty object', () => { + expect(Object.keys(EMPTY_OBJ)).toHaveLength(0) + }) + }) + + describe('EMPTY_ARR', () => { + it('should always return the same reference', () => { + expect(EMPTY_ARR).toBe(EMPTY_ARR) + }) + + it('should be an empty array', () => { + expect(EMPTY_ARR).toHaveLength(0) + }) + }) +}) diff --git a/apps/studio/pages/org/_/[[...routeSlug]].tsx b/apps/studio/pages/org/_/[[...routeSlug]].tsx index 80bbe822a606b..2039e4e9ac888 100644 --- a/apps/studio/pages/org/_/[[...routeSlug]].tsx +++ b/apps/studio/pages/org/_/[[...routeSlug]].tsx @@ -1,6 +1,3 @@ -import { NextPage } from 'next' -import { useRouter } from 'next/router' - import { Header, LoadingCardView, @@ -8,29 +5,31 @@ import { } from 'components/interfaces/Home/ProjectList/EmptyStates' import { PageLayout } from 'components/layouts/PageLayout/PageLayout' import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' -import CardButton from 'components/ui/CardButton' import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { withAuth } from 'hooks/misc/withAuth' +import { NextPage } from 'next' +import { useRouter } from 'next/router' import { cn } from 'ui' -// [Joshen] Thinking we can deprecate this page in favor of /organizations +import { OrganizationCard } from '@/components/interfaces/Organization/OrganizationCard' + const GenericOrganizationPage: NextPage = () => { const router = useRouter() - - const { data: organizations, isPending: isLoading } = useOrganizationsQuery() const { routeSlug, ...queryParams } = router.query const queryString = Object.keys(queryParams).length > 0 ? new URLSearchParams(queryParams as Record).toString() : '' + const { data: organizations, isPending: isLoading } = useOrganizationsQuery() + const urlRewriterFactory = (slug: string | string[] | undefined) => { return (orgSlug: string) => { if (!Array.isArray(slug)) { - return `/org/${orgSlug}/general?${queryString}` + return `/org/${orgSlug}/general${!!queryString ? `?${queryString}` : ''}` } else { const slugPath = slug.reduce((a: string, b: string) => `${a}/${b}`, '').slice(1) - return `/org/${orgSlug}/${slugPath}?${queryString}` + return `/org/${orgSlug}/${slugPath}${!!queryString ? `?${queryString}` : ''}` } } } @@ -57,24 +56,12 @@ const GenericOrganizationPage: NextPage = () => { 'sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3' )} > - {organizations?.map((organization) => ( -
  • - - {organization.name} -
  • - } - footer={ -
    - - {organization.slug} - -
    - } - /> - + {organizations?.map((org) => ( + ))} )} diff --git a/apps/studio/pages/project/[ref]/index.tsx b/apps/studio/pages/project/[ref]/index.tsx index a28d4c452dd9e..a81026a90f4f3 100644 --- a/apps/studio/pages/project/[ref]/index.tsx +++ b/apps/studio/pages/project/[ref]/index.tsx @@ -11,7 +11,10 @@ const HomePage: NextPageWithLayout = () => { const homeNewVariant = usePHFlag('homeNew') const isHomeNew = homeNewVariant === 'new-home' - useTrackExperimentExposure('home_new', IS_PLATFORM ? homeNewVariant : undefined) + useTrackExperimentExposure( + 'home_new', + IS_PLATFORM && typeof homeNewVariant !== 'boolean' ? homeNewVariant : undefined + ) if (isHomeNew) { return diff --git a/apps/studio/vitest.config.ts b/apps/studio/vitest.config.ts index 4091d3e7cadb3..638d563a41d1e 100644 --- a/apps/studio/vitest.config.ts +++ b/apps/studio/vitest.config.ts @@ -1,8 +1,8 @@ import { resolve } from 'node:path' import { fileURLToPath } from 'node:url' -import { configDefaults, defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react' import tsconfigPaths from 'vite-tsconfig-paths' +import { configDefaults, defineConfig } from 'vitest/config' // Some tools like Vitest VSCode extensions, have trouble with resolving relative paths, // as they use the directory of the test file as `cwd`, which makes them believe that @@ -30,10 +30,15 @@ export default defineConfig({ resolve(dirname, './tests/setup/radix.js'), ], // Don't look for tests in the nextjs output directory - exclude: [...configDefaults.exclude, `.next/*`], + exclude: [ + ...configDefaults.exclude, + `.next/*`, + 'tests/features/logs/logs-query.test.tsx', + 'tests/features/reports/storage-report.test.tsx', + ], reporters: [['default']], coverage: { - reporter: ['lcov'], + reporter: ['text', 'text-summary', 'lcov'], exclude: [ '**/*.test.ts', '**/*.test.tsx', diff --git a/apps/ui-library/.env b/apps/ui-library/.env index 93da796409a42..47f9a48d19b6b 100644 --- a/apps/ui-library/.env +++ b/apps/ui-library/.env @@ -1,5 +1,5 @@ # Default to local supabase for local development NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321 -NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0 +NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0 NEXT_PUBLIC_BASE_PATH=/ui NEXT_PUBLIC_API_URL="http://localhost:8080/platform" \ No newline at end of file diff --git a/apps/ui-library/content/docs/nextjs/client.mdx b/apps/ui-library/content/docs/nextjs/client.mdx index 57b803cf90c4f..10e7245ace50a 100644 --- a/apps/ui-library/content/docs/nextjs/client.mdx +++ b/apps/ui-library/content/docs/nextjs/client.mdx @@ -23,7 +23,7 @@ After installing the block, you'll have the following environment variables in y ```env NEXT_PUBLIC_SUPABASE_URL= -NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY= +NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY= ``` - If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=nextjs) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api). diff --git a/apps/ui-library/content/docs/nextjs/password-based-auth.mdx b/apps/ui-library/content/docs/nextjs/password-based-auth.mdx index 42ca65c26df6f..07b126ab2ea0d 100644 --- a/apps/ui-library/content/docs/nextjs/password-based-auth.mdx +++ b/apps/ui-library/content/docs/nextjs/password-based-auth.mdx @@ -28,7 +28,7 @@ After installing the block, you'll have the following environment variables in y ```env NEXT_PUBLIC_SUPABASE_URL= -NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY= +NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY= ``` - If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=nextjs) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api). diff --git a/apps/ui-library/content/docs/nextjs/social-auth.mdx b/apps/ui-library/content/docs/nextjs/social-auth.mdx index 9f7d7ef471ff1..114c60bb71789 100644 --- a/apps/ui-library/content/docs/nextjs/social-auth.mdx +++ b/apps/ui-library/content/docs/nextjs/social-auth.mdx @@ -30,7 +30,7 @@ After installing the block, you'll have the following environment variables in y ```env NEXT_PUBLIC_SUPABASE_URL= -NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY= +NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY= ``` - If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=nextjs) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api). diff --git a/apps/ui-library/content/docs/nuxtjs/client.mdx b/apps/ui-library/content/docs/nuxtjs/client.mdx index 01982c0a2dbd1..e5a39cd5606dd 100644 --- a/apps/ui-library/content/docs/nuxtjs/client.mdx +++ b/apps/ui-library/content/docs/nuxtjs/client.mdx @@ -23,7 +23,7 @@ After installing the block, you'll have the following environment variables in y ```env NUXT_PUBLIC_SUPABASE_URL= -NUXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY= +NUXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY= ``` - If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=nuxt&using=supabasejs) under App Frameworks or in your project's [API keys](https://supabase.com/dashboard/project/_/settings/api-keys). diff --git a/apps/ui-library/content/docs/nuxtjs/password-based-auth.mdx b/apps/ui-library/content/docs/nuxtjs/password-based-auth.mdx index a49337cf2b763..6b606e77ca412 100644 --- a/apps/ui-library/content/docs/nuxtjs/password-based-auth.mdx +++ b/apps/ui-library/content/docs/nuxtjs/password-based-auth.mdx @@ -28,7 +28,7 @@ After installing the block, you'll have the following environment variables in y ```env NUXT_PUBLIC_SUPABASE_URL= -NUXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY= +NUXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY= ``` - If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api). diff --git a/apps/ui-library/content/docs/nuxtjs/social-auth.mdx b/apps/ui-library/content/docs/nuxtjs/social-auth.mdx index c1f12d6d57ec5..47b0f62b2ab53 100644 --- a/apps/ui-library/content/docs/nuxtjs/social-auth.mdx +++ b/apps/ui-library/content/docs/nuxtjs/social-auth.mdx @@ -30,7 +30,7 @@ After installing the block, you'll have the following environment variables in y ```env NUXT_PUBLIC_SUPABASE_URL= -NUXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY= +NUXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY= ``` - If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=nuxtjs&using=supabasejs) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api). diff --git a/apps/ui-library/content/docs/react-router/client.mdx b/apps/ui-library/content/docs/react-router/client.mdx index e82cf87070723..c8ba91f03f0e8 100644 --- a/apps/ui-library/content/docs/react-router/client.mdx +++ b/apps/ui-library/content/docs/react-router/client.mdx @@ -23,7 +23,7 @@ After installing the block, you'll have the following environment variables in y ```env VITE_SUPABASE_URL= -VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY= +VITE_SUPABASE_PUBLISHABLE_KEY= ``` - If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=react&using=vite&with=supabasejs) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api). diff --git a/apps/ui-library/content/docs/react-router/password-based-auth.mdx b/apps/ui-library/content/docs/react-router/password-based-auth.mdx index f9c77ca586f6b..a271e69d9ba03 100644 --- a/apps/ui-library/content/docs/react-router/password-based-auth.mdx +++ b/apps/ui-library/content/docs/react-router/password-based-auth.mdx @@ -28,7 +28,7 @@ After installing the block, you'll have the following environment variables in y ```env VITE_SUPABASE_URL= -VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY= +VITE_SUPABASE_PUBLISHABLE_KEY= ``` - If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=react&using=vite&with=supabasejs) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api). diff --git a/apps/ui-library/content/docs/react-router/social-auth.mdx b/apps/ui-library/content/docs/react-router/social-auth.mdx index 27322ca24ec03..b0689a4f52df5 100644 --- a/apps/ui-library/content/docs/react-router/social-auth.mdx +++ b/apps/ui-library/content/docs/react-router/social-auth.mdx @@ -33,7 +33,7 @@ After installing the block, you'll have the following environment variables in y ```env VITE_SUPABASE_URL= -VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY= +VITE_SUPABASE_PUBLISHABLE_KEY= ``` - If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=react&using=vite&with=supabasejs) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api). diff --git a/apps/ui-library/content/docs/react/client.mdx b/apps/ui-library/content/docs/react/client.mdx index c1ad0c4e7708b..6a04875c602e9 100644 --- a/apps/ui-library/content/docs/react/client.mdx +++ b/apps/ui-library/content/docs/react/client.mdx @@ -23,7 +23,7 @@ After installing the block, you'll have the following environment variables in y ```env VITE_SUPABASE_URL= -VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY= +VITE_SUPABASE_PUBLISHABLE_KEY= ``` - If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=react&using=vite&with=supabasejs) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api). diff --git a/apps/ui-library/content/docs/react/password-based-auth.mdx b/apps/ui-library/content/docs/react/password-based-auth.mdx index 58caffd2e7ea2..5b69ac6ca96b4 100644 --- a/apps/ui-library/content/docs/react/password-based-auth.mdx +++ b/apps/ui-library/content/docs/react/password-based-auth.mdx @@ -28,7 +28,7 @@ After installing the block, you'll have the following environment variables in y ```env VITE_SUPABASE_URL= -VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY= +VITE_SUPABASE_PUBLISHABLE_KEY= ``` - If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=react&using=vite&with=supabasejs) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api). diff --git a/apps/ui-library/content/docs/react/social-auth.mdx b/apps/ui-library/content/docs/react/social-auth.mdx index 5f2561cc95073..3d617f35e06c0 100644 --- a/apps/ui-library/content/docs/react/social-auth.mdx +++ b/apps/ui-library/content/docs/react/social-auth.mdx @@ -30,7 +30,7 @@ After installing the block, you'll have the following environment variables in y ```env VITE_SUPABASE_URL= -VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY= +VITE_SUPABASE_PUBLISHABLE_KEY= ``` - If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=react&using=vite&with=supabasejs) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api). diff --git a/apps/ui-library/content/docs/tanstack/client.mdx b/apps/ui-library/content/docs/tanstack/client.mdx index 25ea4660d01b2..ae707e8f7ef1b 100644 --- a/apps/ui-library/content/docs/tanstack/client.mdx +++ b/apps/ui-library/content/docs/tanstack/client.mdx @@ -25,7 +25,7 @@ After installing the block, you'll have the following environment variables in y ```env VITE_SUPABASE_URL= -VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY= +VITE_SUPABASE_PUBLISHABLE_KEY= ``` - If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=react&using=vite&with=supabasejs) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api). diff --git a/apps/ui-library/content/docs/tanstack/password-based-auth.mdx b/apps/ui-library/content/docs/tanstack/password-based-auth.mdx index a141edc95747f..22380eae482f9 100644 --- a/apps/ui-library/content/docs/tanstack/password-based-auth.mdx +++ b/apps/ui-library/content/docs/tanstack/password-based-auth.mdx @@ -25,7 +25,7 @@ After installing the block, you'll have the following environment variables in y ```env VITE_SUPABASE_URL= -VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY= +VITE_SUPABASE_PUBLISHABLE_KEY= ``` - If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=react&using=vite&with=supabasejs) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api). diff --git a/apps/ui-library/content/docs/tanstack/social-auth.mdx b/apps/ui-library/content/docs/tanstack/social-auth.mdx index 6e00652150fd7..5081ff963d86b 100644 --- a/apps/ui-library/content/docs/tanstack/social-auth.mdx +++ b/apps/ui-library/content/docs/tanstack/social-auth.mdx @@ -30,7 +30,7 @@ After installing the block, you'll have the following environment variables in y ```env VITE_SUPABASE_URL= -VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY= +VITE_SUPABASE_PUBLISHABLE_KEY= ``` - If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=react&using=vite&with=supabasejs) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api). diff --git a/apps/ui-library/content/docs/vue/client.mdx b/apps/ui-library/content/docs/vue/client.mdx index c56d6b87aebcd..8b4804eae2226 100644 --- a/apps/ui-library/content/docs/vue/client.mdx +++ b/apps/ui-library/content/docs/vue/client.mdx @@ -23,7 +23,7 @@ After installing the block, you'll have the following environment variables in y ```env VITE_SUPABASE_URL= -VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY= +VITE_SUPABASE_PUBLISHABLE_KEY= ``` - If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true&tab=frameworks&framework=vuejs&using=supabasejs) under App Frameworks or in your project's [API keys](https://supabase.com/dashboard/project/_/settings/api-keys). diff --git a/apps/ui-library/content/docs/vue/password-based-auth.mdx b/apps/ui-library/content/docs/vue/password-based-auth.mdx index d52fe66b058d6..b16d398e24264 100644 --- a/apps/ui-library/content/docs/vue/password-based-auth.mdx +++ b/apps/ui-library/content/docs/vue/password-based-auth.mdx @@ -28,7 +28,7 @@ After installing the block, you'll have the following environment variables in y ```env VITE_SUPABASE_URL= -VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY= +VITE_SUPABASE_PUBLISHABLE_KEY= ``` - If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true&tab=frameworks&framework=vuejs&using=supabasejs) under App Frameworks or in your project's [API keys](https://supabase.com/dashboard/project/_/settings/api-keys). diff --git a/apps/ui-library/content/docs/vue/social-auth.mdx b/apps/ui-library/content/docs/vue/social-auth.mdx index 729dc532b5a82..e49cd60096979 100644 --- a/apps/ui-library/content/docs/vue/social-auth.mdx +++ b/apps/ui-library/content/docs/vue/social-auth.mdx @@ -30,7 +30,7 @@ After installing the block, you'll have the following environment variables in y ```env VITE_SUPABASE_URL= -VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY= +VITE_SUPABASE_PUBLISHABLE_KEY= ``` - If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=react&using=vite&with=supabasejs) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api). diff --git a/apps/ui-library/public/r/ai-editor-rules.json b/apps/ui-library/public/r/ai-editor-rules.json index 375824e29c8d2..4307763b36ff7 100644 --- a/apps/ui-library/public/r/ai-editor-rules.json +++ b/apps/ui-library/public/r/ai-editor-rules.json @@ -33,7 +33,7 @@ }, { "path": "registry/default/ai-editor-rules/writing-supabase-edge-functions.mdc", - "content": "---\ndescription: Coding rules for Supabase Edge Functions\nalwaysApply: false\n---\n\n# Writing Supabase Edge Functions\n\nYou're an expert in writing TypeScript and Deno JavaScript runtime. Generate **high-quality Supabase Edge Functions** that adhere to the following best practices:\n\n## Guidelines\n\n1. Try to use Web APIs and Deno’s core APIs instead of external dependencies (eg: use fetch instead of Axios, use WebSockets API instead of node-ws)\n2. If you are reusing utility methods between Edge Functions, add them to `supabase/functions/_shared` and import using a relative path. Do NOT have cross dependencies between Edge Functions.\n3. Do NOT use bare specifiers when importing dependencies. If you need to use an external dependency, make sure it's prefixed with either `npm:` or `jsr:`. For example, `@supabase/supabase-js` should be written as `npm:@supabase/supabase-js`.\n4. For external imports, always define a version. For example, `npm:@express` should be written as `npm:express@4.18.2`.\n5. For external dependencies, importing via `npm:` and `jsr:` is preferred. Minimize the use of imports from @`deno.land/x` , `esm.sh` and @`unpkg.com` . If you have a package from one of those CDNs, you can replace the CDN hostname with `npm:` specifier.\n6. You can also use Node built-in APIs. You will need to import them using `node:` specifier. For example, to import Node process: `import process from \"node:process\". Use Node APIs when you find gaps in Deno APIs.\n7. Do NOT use `import { serve } from \"https://deno.land/std@0.168.0/http/server.ts\"`. Instead use the built-in `Deno.serve`.\n8. Following environment variables (ie. secrets) are pre-populated in both local and hosted Supabase environments. Users don't need to manually set them:\n - SUPABASE_URL\n - SUPABASE_PUBLISHABLE_OR_ANON_KEY\n - SUPABASE_SERVICE_ROLE_KEY\n - SUPABASE_DB_URL\n9. To set other environment variables (ie. secrets) users can put them in a env file and run the `supabase secrets set --env-file path/to/env-file`\n10. A single Edge Function can handle multiple routes. It is recommended to use a library like Express or Hono to handle the routes as it's easier for developer to understand and maintain. Each route must be prefixed with `/function-name` so they are routed correctly.\n11. File write operations are ONLY permitted on `/tmp` directory. You can use either Deno or Node File APIs.\n12. Use `EdgeRuntime.waitUntil(promise)` static method to run long-running tasks in the background without blocking response to a request. Do NOT assume it is available in the request / execution context.\n\n## Example Templates\n\n### Simple Hello World Function\n\n```tsx\ninterface reqPayload {\n name: string\n}\n\nconsole.info('server started')\n\nDeno.serve(async (req: Request) => {\n const { name }: reqPayload = await req.json()\n const data = {\n message: `Hello ${name} from foo!`,\n }\n\n return new Response(JSON.stringify(data), {\n headers: { 'Content-Type': 'application/json', Connection: 'keep-alive' },\n })\n})\n```\n\n### Example Function using Node built-in API\n\n```tsx\nimport { randomBytes } from 'node:crypto'\nimport { createServer } from 'node:http'\nimport process from 'node:process'\n\nconst generateRandomString = (length) => {\n const buffer = randomBytes(length)\n return buffer.toString('hex')\n}\n\nconst randomString = generateRandomString(10)\nconsole.log(randomString)\n\nconst server = createServer((req, res) => {\n const message = `Hello`\n res.end(message)\n})\n\nserver.listen(9999)\n```\n\n### Using npm packages in Functions\n\n```tsx\nimport express from 'npm:express@4.18.2'\n\nconst app = express()\n\napp.get(/(.*)/, (req, res) => {\n res.send('Welcome to Supabase')\n})\n\napp.listen(8000)\n```\n\n### Generate embeddings using built-in @Supabase.ai API\n\n```tsx\nconst model = new Supabase.ai.Session('gte-small')\n\nDeno.serve(async (req: Request) => {\n const params = new URL(req.url).searchParams\n const input = params.get('text')\n const output = await model.run(input, { mean_pool: true, normalize: true })\n return new Response(JSON.stringify(output), {\n headers: {\n 'Content-Type': 'application/json',\n Connection: 'keep-alive',\n },\n })\n})\n```\n", + "content": "---\ndescription: Coding rules for Supabase Edge Functions\nalwaysApply: false\n---\n\n# Writing Supabase Edge Functions\n\nYou're an expert in writing TypeScript and Deno JavaScript runtime. Generate **high-quality Supabase Edge Functions** that adhere to the following best practices:\n\n## Guidelines\n\n1. Try to use Web APIs and Deno’s core APIs instead of external dependencies (eg: use fetch instead of Axios, use WebSockets API instead of node-ws)\n2. If you are reusing utility methods between Edge Functions, add them to `supabase/functions/_shared` and import using a relative path. Do NOT have cross dependencies between Edge Functions.\n3. Do NOT use bare specifiers when importing dependencies. If you need to use an external dependency, make sure it's prefixed with either `npm:` or `jsr:`. For example, `@supabase/supabase-js` should be written as `npm:@supabase/supabase-js`.\n4. For external imports, always define a version. For example, `npm:@express` should be written as `npm:express@4.18.2`.\n5. For external dependencies, importing via `npm:` and `jsr:` is preferred. Minimize the use of imports from @`deno.land/x` , `esm.sh` and @`unpkg.com` . If you have a package from one of those CDNs, you can replace the CDN hostname with `npm:` specifier.\n6. You can also use Node built-in APIs. You will need to import them using `node:` specifier. For example, to import Node process: `import process from \"node:process\". Use Node APIs when you find gaps in Deno APIs.\n7. Do NOT use `import { serve } from \"https://deno.land/std@0.168.0/http/server.ts\"`. Instead use the built-in `Deno.serve`.\n8. Following environment variables (ie. secrets) are pre-populated in both local and hosted Supabase environments. Users don't need to manually set them:\n - SUPABASE_URL\n - SUPABASE_PUBLISHABLE_KEY\n - SUPABASE_SERVICE_ROLE_KEY\n - SUPABASE_DB_URL\n9. To set other environment variables (ie. secrets) users can put them in a env file and run the `supabase secrets set --env-file path/to/env-file`\n10. A single Edge Function can handle multiple routes. It is recommended to use a library like Express or Hono to handle the routes as it's easier for developer to understand and maintain. Each route must be prefixed with `/function-name` so they are routed correctly.\n11. File write operations are ONLY permitted on `/tmp` directory. You can use either Deno or Node File APIs.\n12. Use `EdgeRuntime.waitUntil(promise)` static method to run long-running tasks in the background without blocking response to a request. Do NOT assume it is available in the request / execution context.\n\n## Example Templates\n\n### Simple Hello World Function\n\n```tsx\ninterface reqPayload {\n name: string\n}\n\nconsole.info('server started')\n\nDeno.serve(async (req: Request) => {\n const { name }: reqPayload = await req.json()\n const data = {\n message: `Hello ${name} from foo!`,\n }\n\n return new Response(JSON.stringify(data), {\n headers: { 'Content-Type': 'application/json', Connection: 'keep-alive' },\n })\n})\n```\n\n### Example Function using Node built-in API\n\n```tsx\nimport { randomBytes } from 'node:crypto'\nimport { createServer } from 'node:http'\nimport process from 'node:process'\n\nconst generateRandomString = (length) => {\n const buffer = randomBytes(length)\n return buffer.toString('hex')\n}\n\nconst randomString = generateRandomString(10)\nconsole.log(randomString)\n\nconst server = createServer((req, res) => {\n const message = `Hello`\n res.end(message)\n})\n\nserver.listen(9999)\n```\n\n### Using npm packages in Functions\n\n```tsx\nimport express from 'npm:express@4.18.2'\n\nconst app = express()\n\napp.get(/(.*)/, (req, res) => {\n res.send('Welcome to Supabase')\n})\n\napp.listen(8000)\n```\n\n### Generate embeddings using built-in @Supabase.ai API\n\n```tsx\nconst model = new Supabase.ai.Session('gte-small')\n\nDeno.serve(async (req: Request) => {\n const params = new URL(req.url).searchParams\n const input = params.get('text')\n const output = await model.run(input, { mean_pool: true, normalize: true })\n return new Response(JSON.stringify(output), {\n headers: {\n 'Content-Type': 'application/json',\n Connection: 'keep-alive',\n },\n })\n})\n```\n", "type": "registry:file", "target": "~/.cursor/rules/writing-supabase-edge-functions.mdc" }, diff --git a/apps/ui-library/public/r/current-user-avatar-nextjs.json b/apps/ui-library/public/r/current-user-avatar-nextjs.json index f6f6749e243d0..44046fecd08c4 100644 --- a/apps/ui-library/public/r/current-user-avatar-nextjs.json +++ b/apps/ui-library/public/r/current-user-avatar-nextjs.json @@ -29,23 +29,23 @@ }, { "path": "registry/default/clients/nextjs/lib/supabase/client.ts", - "content": "import { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "import { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/nextjs/lib/supabase/middleware.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/nextjs/lib/supabase/server.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { cookies } from 'next/headers'\n\n/**\n * If using Fluid compute: Don't put this client in a global variable. Always create a new client within each\n * function when using it.\n */\nexport async function createClient() {\n const cookieStore = await cookies()\n\n return createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return cookieStore.getAll()\n },\n setAll(cookiesToSet) {\n try {\n cookiesToSet.forEach(({ name, value, options }) =>\n cookieStore.set(name, value, options)\n )\n } catch {\n // The `setAll` method was called from a Server Component.\n // This can be ignored if you have middleware refreshing\n // user sessions.\n }\n },\n },\n }\n )\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { cookies } from 'next/headers'\n\n/**\n * If using Fluid compute: Don't put this client in a global variable. Always create a new client within each\n * function when using it.\n */\nexport async function createClient() {\n const cookieStore = await cookies()\n\n return createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return cookieStore.getAll()\n },\n setAll(cookiesToSet) {\n try {\n cookiesToSet.forEach(({ name, value, options }) =>\n cookieStore.set(name, value, options)\n )\n } catch {\n // The `setAll` method was called from a Server Component.\n // This can be ignored if you have middleware refreshing\n // user sessions.\n }\n },\n },\n }\n )\n}\n", "type": "registry:lib" } ], "envVars": { "NEXT_PUBLIC_SUPABASE_URL": "", - "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/current-user-avatar-react-router.json b/apps/ui-library/public/r/current-user-avatar-react-router.json index cd0b18845839f..f8ebac349bb30 100644 --- a/apps/ui-library/public/r/current-user-avatar-react-router.json +++ b/apps/ui-library/public/r/current-user-avatar-react-router.json @@ -29,18 +29,18 @@ }, { "path": "registry/default/clients/react-router/lib/supabase/client.ts", - "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/react-router/lib/supabase/server.ts", - "content": "import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'\n\nexport function createClient(request: Request) {\n const headers = new Headers()\n\n const supabase = createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return parseCookieHeader(request.headers.get('Cookie') ?? '') as {\n name: string\n value: string\n }[]\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value, options }) =>\n headers.append('Set-Cookie', serializeCookieHeader(name, value, options))\n )\n },\n },\n }\n )\n\n return { supabase, headers }\n}\n", + "content": "import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'\n\nexport function createClient(request: Request) {\n const headers = new Headers()\n\n const supabase = createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return parseCookieHeader(request.headers.get('Cookie') ?? '') as {\n name: string\n value: string\n }[]\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value, options }) =>\n headers.append('Set-Cookie', serializeCookieHeader(name, value, options))\n )\n },\n },\n }\n )\n\n return { supabase, headers }\n}\n", "type": "registry:lib" } ], "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/current-user-avatar-react.json b/apps/ui-library/public/r/current-user-avatar-react.json index d9211798232ae..2c4d3ed974ed9 100644 --- a/apps/ui-library/public/r/current-user-avatar-react.json +++ b/apps/ui-library/public/r/current-user-avatar-react.json @@ -28,13 +28,13 @@ }, { "path": "registry/default/clients/react/lib/supabase/client.ts", - "content": "import { createClient as createSupabaseClient } from '@supabase/supabase-js'\n\nexport function createClient() {\n return createSupabaseClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "import { createClient as createSupabaseClient } from '@supabase/supabase-js'\n\nexport function createClient() {\n return createSupabaseClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" } ], "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/current-user-avatar-tanstack.json b/apps/ui-library/public/r/current-user-avatar-tanstack.json index ad9e45d6cfd48..505f3dd6a5744 100644 --- a/apps/ui-library/public/r/current-user-avatar-tanstack.json +++ b/apps/ui-library/public/r/current-user-avatar-tanstack.json @@ -29,18 +29,18 @@ }, { "path": "registry/default/clients/tanstack/lib/supabase/client.ts", - "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/tanstack/lib/supabase/server.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { getCookies, setCookie } from '@tanstack/react-start/server'\n\nexport function createClient() {\n return createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return Object.entries(getCookies()).map(\n ([name, value]) =>\n ({\n name,\n value,\n }) as { name: string; value: string }\n )\n },\n setAll(cookies) {\n cookies.forEach((cookie) => {\n setCookie(cookie.name, cookie.value)\n })\n },\n },\n }\n )\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { getCookies, setCookie } from '@tanstack/react-start/server'\n\nexport function createClient() {\n return createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return Object.entries(getCookies()).map(\n ([name, value]) =>\n ({\n name,\n value,\n }) as { name: string; value: string }\n )\n },\n setAll(cookies) {\n cookies.forEach((cookie) => {\n setCookie(cookie.name, cookie.value)\n })\n },\n },\n }\n )\n}\n", "type": "registry:lib" } ], "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/dropzone-nextjs.json b/apps/ui-library/public/r/dropzone-nextjs.json index fd8580d9d5962..9bbfcb1427310 100644 --- a/apps/ui-library/public/r/dropzone-nextjs.json +++ b/apps/ui-library/public/r/dropzone-nextjs.json @@ -26,23 +26,23 @@ }, { "path": "registry/default/clients/nextjs/lib/supabase/client.ts", - "content": "import { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "import { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/nextjs/lib/supabase/middleware.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/nextjs/lib/supabase/server.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { cookies } from 'next/headers'\n\n/**\n * If using Fluid compute: Don't put this client in a global variable. Always create a new client within each\n * function when using it.\n */\nexport async function createClient() {\n const cookieStore = await cookies()\n\n return createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return cookieStore.getAll()\n },\n setAll(cookiesToSet) {\n try {\n cookiesToSet.forEach(({ name, value, options }) =>\n cookieStore.set(name, value, options)\n )\n } catch {\n // The `setAll` method was called from a Server Component.\n // This can be ignored if you have middleware refreshing\n // user sessions.\n }\n },\n },\n }\n )\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { cookies } from 'next/headers'\n\n/**\n * If using Fluid compute: Don't put this client in a global variable. Always create a new client within each\n * function when using it.\n */\nexport async function createClient() {\n const cookieStore = await cookies()\n\n return createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return cookieStore.getAll()\n },\n setAll(cookiesToSet) {\n try {\n cookiesToSet.forEach(({ name, value, options }) =>\n cookieStore.set(name, value, options)\n )\n } catch {\n // The `setAll` method was called from a Server Component.\n // This can be ignored if you have middleware refreshing\n // user sessions.\n }\n },\n },\n }\n )\n}\n", "type": "registry:lib" } ], "envVars": { "NEXT_PUBLIC_SUPABASE_URL": "", - "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/dropzone-nuxtjs.json b/apps/ui-library/public/r/dropzone-nuxtjs.json index 91f4668894cb7..f3df59c09869c 100644 --- a/apps/ui-library/public/r/dropzone-nuxtjs.json +++ b/apps/ui-library/public/r/dropzone-nuxtjs.json @@ -33,7 +33,7 @@ }, { "path": "registry/default/dropzone/nuxtjs/app/composables/useSupabaseUpload.ts", - "content": "import { ref, computed, watch, onUnmounted } from 'vue'\nimport { useDropZone } from '@vueuse/core'\n// @ts-ignore\nimport { createClient } from \"@/lib/supabase/client\"\n\nconst supabase = createClient()\n\nexport interface FileWithPreview extends File {\n preview?: string\n errors: { code: string; message: string }[]\n}\n\nexport type UseSupabaseUploadOptions = {\n bucketName: string\n path?: string\n allowedMimeTypes?: string[]\n maxFileSize?: number\n maxFiles?: number\n cacheControl?: number\n upsert?: boolean\n}\n\nfunction validateFileType(file: File, allowedTypes: string[]) {\n if (!allowedTypes.length) return []\n const isValid = allowedTypes.some(t =>\n t.endsWith('/*')\n ? file.type.startsWith(t.replace('/*', ''))\n : file.type === t\n )\n return isValid\n ? []\n : [{ code: 'invalid-type', message: 'Invalid file type' }]\n}\n\nfunction validateFileSize(file: File, maxSize: number) {\n return file.size > maxSize\n ? [{ code: 'file-too-large', message: `File is larger than allowed size` }]\n : []\n}\n\nexport function useSupabaseUpload(options: UseSupabaseUploadOptions) {\n const {\n bucketName,\n path,\n allowedMimeTypes = [],\n maxFileSize = Number.POSITIVE_INFINITY,\n maxFiles = 1,\n cacheControl = 3600,\n upsert = false,\n } = options\n\n const files = ref([])\n const loading = ref(false)\n const errors = ref<{ name: string; message: string }[]>([])\n const successes = ref([])\n\n const isSuccess = computed(() => {\n if (!errors.value.length && !successes.value.length) return false\n return !errors.value.length && successes.value.length === files.value.length\n })\n\n const dropZoneRef = ref(null)\n\n const { isOverDropZone } = useDropZone(dropZoneRef, {\n onDrop(droppedFiles: File[] | null) {\n if (!droppedFiles) return\n\n const newFiles: FileWithPreview[] = droppedFiles.map(file => ({\n ...(file as FileWithPreview),\n preview: URL.createObjectURL(file),\n errors: [\n ...validateFileType(file, allowedMimeTypes),\n ...validateFileSize(file, maxFileSize),\n ],\n }))\n\n files.value = [...files.value, ...newFiles]\n },\n })\n\n const onUpload = async () => {\n loading.value = true\n\n try {\n const filesWithErrors = errors.value.map(e => e.name)\n\n const filesToUpload =\n filesWithErrors.length > 0\n ? files.value.filter(\n f =>\n filesWithErrors.includes(f.name) ||\n !successes.value.includes(f.name)\n )\n : files.value\n\n const responses = await Promise.all(\n filesToUpload.map(async file => {\n const { error } = await supabase.storage\n .from(bucketName)\n .upload(path ? `${path}/${file.name}` : file.name, file, {\n cacheControl: cacheControl.toString(),\n upsert,\n })\n\n return error\n ? { name: file.name, message: error.message }\n : { name: file.name, message: undefined }\n })\n )\n\n errors.value = responses.filter((r): r is { name: string; message: string } => r.message !== undefined)\n\n const successful = responses\n .filter(r => !r.message)\n .map(r => r.name)\n\n successes.value = Array.from(\n new Set([...successes.value, ...successful])\n )\n } catch (err) {\n console.error('Upload failed unexpectedly:', err)\n\n errors.value.push({\n name: 'upload',\n message: 'An unexpected error occurred during upload.',\n })\n } finally {\n loading.value = false\n }\n }\n\n\n watch(\n () => files.value.length,\n () => {\n if (!files.value.length) {\n errors.value = []\n successes.value = []\n }\n\n if (files.value.length > maxFiles) {\n errors.value.push({\n name: 'files',\n message: `You may upload up to ${maxFiles} files`,\n })\n }\n }\n )\n\n watch(\n files,\n (newFiles, oldFiles) => {\n const newPreviews = new Set(newFiles.map(f => f.preview))\n oldFiles.forEach(file => {\n if (file.preview && !newPreviews.has(file.preview)) {\n URL.revokeObjectURL(file.preview)\n }\n })\n },\n { deep: true }\n )\n\n onUnmounted(() => {\n files.value.forEach(file => {\n if (file.preview) {\n URL.revokeObjectURL(file.preview)\n }\n })\n })\n\n return {\n dropZoneRef,\n isOverDropZone,\n\n files,\n setFiles: (v: FileWithPreview[]) => (files.value = v),\n\n errors,\n setErrors: (v: { name: string; message: string }[]) => (errors.value = v),\n\n successes,\n isSuccess,\n loading,\n onUpload,\n\n maxFileSize,\n maxFiles,\n allowedMimeTypes,\n }\n}\n", + "content": "import { useDropZone } from '@vueuse/core'\nimport { computed, onUnmounted, ref, watch } from 'vue'\n\n// @ts-ignore\nimport { createClient } from '@/lib/supabase/client'\n\nconst supabase = createClient()\n\nexport interface FileWithPreview extends File {\n preview?: string\n errors: { code: string; message: string }[]\n}\n\nexport type UseSupabaseUploadOptions = {\n bucketName: string\n path?: string\n allowedMimeTypes?: string[]\n maxFileSize?: number\n maxFiles?: number\n cacheControl?: number\n upsert?: boolean\n}\n\nfunction validateFileType(file: File, allowedTypes: string[]) {\n if (!allowedTypes.length) return []\n const isValid = allowedTypes.some((t) =>\n t.endsWith('/*') ? file.type.startsWith(t.replace('/*', '')) : file.type === t\n )\n return isValid ? [] : [{ code: 'invalid-type', message: 'Invalid file type' }]\n}\n\nfunction validateFileSize(file: File, maxSize: number) {\n return file.size > maxSize\n ? [{ code: 'file-too-large', message: `File is larger than allowed size` }]\n : []\n}\n\nexport function useSupabaseUpload(options: UseSupabaseUploadOptions) {\n const {\n bucketName,\n path,\n allowedMimeTypes = [],\n maxFileSize = Number.POSITIVE_INFINITY,\n maxFiles = 1,\n cacheControl = 3600,\n upsert = false,\n } = options\n\n const files = ref([])\n const loading = ref(false)\n const errors = ref<{ name: string; message: string }[]>([])\n const successes = ref([])\n\n const isSuccess = computed(() => {\n if (!errors.value.length && !successes.value.length) return false\n return !errors.value.length && successes.value.length === files.value.length\n })\n\n const dropZoneRef = ref(null)\n\n const { isOverDropZone } = useDropZone(dropZoneRef, {\n onDrop(droppedFiles: File[] | null) {\n if (!droppedFiles) return\n\n const newFiles: FileWithPreview[] = droppedFiles.map((file) => ({\n ...(file as FileWithPreview),\n preview: URL.createObjectURL(file),\n errors: [\n ...validateFileType(file, allowedMimeTypes),\n ...validateFileSize(file, maxFileSize),\n ],\n }))\n\n files.value = [...files.value, ...newFiles]\n },\n })\n\n const onUpload = async () => {\n loading.value = true\n\n try {\n const filesWithErrors = errors.value.map((e) => e.name)\n\n const filesToUpload =\n filesWithErrors.length > 0\n ? files.value.filter(\n (f) => filesWithErrors.includes(f.name) || !successes.value.includes(f.name)\n )\n : files.value\n\n const responses = await Promise.all(\n filesToUpload.map(async (file) => {\n const { error } = await supabase.storage\n .from(bucketName)\n .upload(path ? `${path}/${file.name}` : file.name, file, {\n cacheControl: cacheControl.toString(),\n upsert,\n })\n\n return error\n ? { name: file.name, message: error.message }\n : { name: file.name, message: undefined }\n })\n )\n\n errors.value = responses.filter(\n (r): r is { name: string; message: string } => r.message !== undefined\n )\n\n const successful = responses.filter((r) => !r.message).map((r) => r.name)\n\n successes.value = Array.from(new Set([...successes.value, ...successful]))\n } catch (err) {\n console.error('Upload failed unexpectedly:', err)\n\n errors.value.push({\n name: 'upload',\n message: 'An unexpected error occurred during upload.',\n })\n } finally {\n loading.value = false\n }\n }\n\n watch(\n () => files.value.length,\n () => {\n if (!files.value.length) {\n errors.value = []\n successes.value = []\n }\n\n if (files.value.length > maxFiles) {\n errors.value.push({\n name: 'files',\n message: `You may upload up to ${maxFiles} files`,\n })\n }\n }\n )\n\n watch(\n files,\n (newFiles, oldFiles) => {\n const newPreviews = new Set(newFiles.map((f) => f.preview))\n oldFiles.forEach((file) => {\n if (file.preview && !newPreviews.has(file.preview)) {\n URL.revokeObjectURL(file.preview)\n }\n })\n },\n { deep: true }\n )\n\n onUnmounted(() => {\n files.value.forEach((file) => {\n if (file.preview) {\n URL.revokeObjectURL(file.preview)\n }\n })\n })\n\n return {\n dropZoneRef,\n isOverDropZone,\n\n files,\n setFiles: (v: FileWithPreview[]) => (files.value = v),\n\n errors,\n setErrors: (v: { name: string; message: string }[]) => (errors.value = v),\n\n successes,\n isSuccess,\n loading,\n onUpload,\n\n maxFileSize,\n maxFiles,\n allowedMimeTypes,\n }\n}\n", "type": "registry:component", "target": "app/composables/useSupabaseUpload.ts" } diff --git a/apps/ui-library/public/r/dropzone-react-router.json b/apps/ui-library/public/r/dropzone-react-router.json index 60d6a973af5bd..20ceefaef39f8 100644 --- a/apps/ui-library/public/r/dropzone-react-router.json +++ b/apps/ui-library/public/r/dropzone-react-router.json @@ -26,18 +26,18 @@ }, { "path": "registry/default/clients/react-router/lib/supabase/client.ts", - "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/react-router/lib/supabase/server.ts", - "content": "import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'\n\nexport function createClient(request: Request) {\n const headers = new Headers()\n\n const supabase = createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return parseCookieHeader(request.headers.get('Cookie') ?? '') as {\n name: string\n value: string\n }[]\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value, options }) =>\n headers.append('Set-Cookie', serializeCookieHeader(name, value, options))\n )\n },\n },\n }\n )\n\n return { supabase, headers }\n}\n", + "content": "import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'\n\nexport function createClient(request: Request) {\n const headers = new Headers()\n\n const supabase = createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return parseCookieHeader(request.headers.get('Cookie') ?? '') as {\n name: string\n value: string\n }[]\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value, options }) =>\n headers.append('Set-Cookie', serializeCookieHeader(name, value, options))\n )\n },\n },\n }\n )\n\n return { supabase, headers }\n}\n", "type": "registry:lib" } ], "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/dropzone-react.json b/apps/ui-library/public/r/dropzone-react.json index 946f4cb911fe8..29c18c95cd292 100644 --- a/apps/ui-library/public/r/dropzone-react.json +++ b/apps/ui-library/public/r/dropzone-react.json @@ -25,13 +25,13 @@ }, { "path": "registry/default/clients/react/lib/supabase/client.ts", - "content": "import { createClient as createSupabaseClient } from '@supabase/supabase-js'\n\nexport function createClient() {\n return createSupabaseClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "import { createClient as createSupabaseClient } from '@supabase/supabase-js'\n\nexport function createClient() {\n return createSupabaseClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" } ], "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/dropzone-tanstack.json b/apps/ui-library/public/r/dropzone-tanstack.json index 36af19fde9ee1..470f6adf54e73 100644 --- a/apps/ui-library/public/r/dropzone-tanstack.json +++ b/apps/ui-library/public/r/dropzone-tanstack.json @@ -26,18 +26,18 @@ }, { "path": "registry/default/clients/tanstack/lib/supabase/client.ts", - "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/tanstack/lib/supabase/server.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { getCookies, setCookie } from '@tanstack/react-start/server'\n\nexport function createClient() {\n return createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return Object.entries(getCookies()).map(\n ([name, value]) =>\n ({\n name,\n value,\n }) as { name: string; value: string }\n )\n },\n setAll(cookies) {\n cookies.forEach((cookie) => {\n setCookie(cookie.name, cookie.value)\n })\n },\n },\n }\n )\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { getCookies, setCookie } from '@tanstack/react-start/server'\n\nexport function createClient() {\n return createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return Object.entries(getCookies()).map(\n ([name, value]) =>\n ({\n name,\n value,\n }) as { name: string; value: string }\n )\n },\n setAll(cookies) {\n cookies.forEach((cookie) => {\n setCookie(cookie.name, cookie.value)\n })\n },\n },\n }\n )\n}\n", "type": "registry:lib" } ], "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/dropzone-vue.json b/apps/ui-library/public/r/dropzone-vue.json index 1c5e1947b62a3..79232d83dfec4 100644 --- a/apps/ui-library/public/r/dropzone-vue.json +++ b/apps/ui-library/public/r/dropzone-vue.json @@ -33,7 +33,7 @@ }, { "path": "registry/default/dropzone/vue/composables/useSupabaseUpload.ts", - "content": "import { ref, computed, watch, onUnmounted } from 'vue'\nimport { useDropZone } from '@vueuse/core'\n// @ts-ignore\nimport { createClient } from \"@/lib/supabase/client\"\n\nconst supabase = createClient()\n\nexport interface FileWithPreview extends File {\n preview?: string\n errors: { code: string; message: string }[]\n}\n\nexport type UseSupabaseUploadOptions = {\n bucketName: string\n path?: string\n allowedMimeTypes?: string[]\n maxFileSize?: number\n maxFiles?: number\n cacheControl?: number\n upsert?: boolean\n}\n\nfunction validateFileType(file: File, allowedTypes: string[]) {\n if (!allowedTypes.length) return []\n const isValid = allowedTypes.some(t =>\n t.endsWith('/*')\n ? file.type.startsWith(t.replace('/*', ''))\n : file.type === t\n )\n return isValid\n ? []\n : [{ code: 'invalid-type', message: 'Invalid file type' }]\n}\n\nfunction validateFileSize(file: File, maxSize: number) {\n return file.size > maxSize\n ? [{ code: 'file-too-large', message: `File is larger than allowed size` }]\n : []\n}\n\nexport function useSupabaseUpload(options: UseSupabaseUploadOptions) {\n const {\n bucketName,\n path,\n allowedMimeTypes = [],\n maxFileSize = Number.POSITIVE_INFINITY,\n maxFiles = 1,\n cacheControl = 3600,\n upsert = false,\n } = options\n\n const files = ref([])\n const loading = ref(false)\n const errors = ref<{ name: string; message: string }[]>([])\n const successes = ref([])\n\n const isSuccess = computed(() => {\n if (!errors.value.length && !successes.value.length) return false\n return !errors.value.length && successes.value.length === files.value.length\n })\n\n const dropZoneRef = ref(null)\n\n const { isOverDropZone } = useDropZone(dropZoneRef, {\n onDrop(droppedFiles: File[] | null) {\n if (!droppedFiles) return\n\n const newFiles: FileWithPreview[] = droppedFiles.map(file => ({\n ...(file as FileWithPreview),\n preview: URL.createObjectURL(file),\n errors: [\n ...validateFileType(file, allowedMimeTypes),\n ...validateFileSize(file, maxFileSize),\n ],\n }))\n\n files.value = [...files.value, ...newFiles]\n },\n })\n\n const onUpload = async () => {\n loading.value = true\n\n try {\n const filesWithErrors = errors.value.map(e => e.name)\n\n const filesToUpload =\n filesWithErrors.length > 0\n ? files.value.filter(\n f =>\n filesWithErrors.includes(f.name) ||\n !successes.value.includes(f.name)\n )\n : files.value\n\n const responses = await Promise.all(\n filesToUpload.map(async file => {\n const { error } = await supabase.storage\n .from(bucketName)\n .upload(path ? `${path}/${file.name}` : file.name, file, {\n cacheControl: cacheControl.toString(),\n upsert,\n })\n\n return error\n ? { name: file.name, message: error.message }\n : { name: file.name, message: undefined }\n })\n )\n\n errors.value = responses.filter((r): r is { name: string; message: string } => r.message !== undefined)\n\n const successful = responses\n .filter(r => !r.message)\n .map(r => r.name)\n\n successes.value = Array.from(\n new Set([...successes.value, ...successful])\n )\n } catch (err) {\n console.error('Upload failed unexpectedly:', err)\n\n errors.value.push({\n name: 'upload',\n message: 'An unexpected error occurred during upload.',\n })\n } finally {\n loading.value = false\n }\n }\n\n\n watch(\n () => files.value.length,\n () => {\n if (!files.value.length) {\n errors.value = []\n successes.value = []\n }\n\n if (files.value.length > maxFiles) {\n errors.value.push({\n name: 'files',\n message: `You may upload up to ${maxFiles} files`,\n })\n }\n }\n )\n\n watch(\n files,\n (newFiles, oldFiles) => {\n const newPreviews = new Set(newFiles.map(f => f.preview))\n oldFiles.forEach(file => {\n if (file.preview && !newPreviews.has(file.preview)) {\n URL.revokeObjectURL(file.preview)\n }\n })\n },\n { deep: true }\n )\n\n onUnmounted(() => {\n files.value.forEach(file => {\n if (file.preview) {\n URL.revokeObjectURL(file.preview)\n }\n })\n })\n\n return {\n dropZoneRef,\n isOverDropZone,\n\n files,\n setFiles: (v: FileWithPreview[]) => (files.value = v),\n\n errors,\n setErrors: (v: { name: string; message: string }[]) => (errors.value = v),\n\n successes,\n isSuccess,\n loading,\n onUpload,\n\n maxFileSize,\n maxFiles,\n allowedMimeTypes,\n }\n}\n", + "content": "import { useDropZone } from '@vueuse/core'\nimport { computed, onUnmounted, ref, watch } from 'vue'\n\n// @ts-ignore\nimport { createClient } from '@/lib/supabase/client'\n\nconst supabase = createClient()\n\nexport interface FileWithPreview extends File {\n preview?: string\n errors: { code: string; message: string }[]\n}\n\nexport type UseSupabaseUploadOptions = {\n bucketName: string\n path?: string\n allowedMimeTypes?: string[]\n maxFileSize?: number\n maxFiles?: number\n cacheControl?: number\n upsert?: boolean\n}\n\nfunction validateFileType(file: File, allowedTypes: string[]) {\n if (!allowedTypes.length) return []\n const isValid = allowedTypes.some((t) =>\n t.endsWith('/*') ? file.type.startsWith(t.replace('/*', '')) : file.type === t\n )\n return isValid ? [] : [{ code: 'invalid-type', message: 'Invalid file type' }]\n}\n\nfunction validateFileSize(file: File, maxSize: number) {\n return file.size > maxSize\n ? [{ code: 'file-too-large', message: `File is larger than allowed size` }]\n : []\n}\n\nexport function useSupabaseUpload(options: UseSupabaseUploadOptions) {\n const {\n bucketName,\n path,\n allowedMimeTypes = [],\n maxFileSize = Number.POSITIVE_INFINITY,\n maxFiles = 1,\n cacheControl = 3600,\n upsert = false,\n } = options\n\n const files = ref([])\n const loading = ref(false)\n const errors = ref<{ name: string; message: string }[]>([])\n const successes = ref([])\n\n const isSuccess = computed(() => {\n if (!errors.value.length && !successes.value.length) return false\n return !errors.value.length && successes.value.length === files.value.length\n })\n\n const dropZoneRef = ref(null)\n\n const { isOverDropZone } = useDropZone(dropZoneRef, {\n onDrop(droppedFiles: File[] | null) {\n if (!droppedFiles) return\n\n const newFiles: FileWithPreview[] = droppedFiles.map((file) => ({\n ...(file as FileWithPreview),\n preview: URL.createObjectURL(file),\n errors: [\n ...validateFileType(file, allowedMimeTypes),\n ...validateFileSize(file, maxFileSize),\n ],\n }))\n\n files.value = [...files.value, ...newFiles]\n },\n })\n\n const onUpload = async () => {\n loading.value = true\n\n try {\n const filesWithErrors = errors.value.map((e) => e.name)\n\n const filesToUpload =\n filesWithErrors.length > 0\n ? files.value.filter(\n (f) => filesWithErrors.includes(f.name) || !successes.value.includes(f.name)\n )\n : files.value\n\n const responses = await Promise.all(\n filesToUpload.map(async (file) => {\n const { error } = await supabase.storage\n .from(bucketName)\n .upload(path ? `${path}/${file.name}` : file.name, file, {\n cacheControl: cacheControl.toString(),\n upsert,\n })\n\n return error\n ? { name: file.name, message: error.message }\n : { name: file.name, message: undefined }\n })\n )\n\n errors.value = responses.filter(\n (r): r is { name: string; message: string } => r.message !== undefined\n )\n\n const successful = responses.filter((r) => !r.message).map((r) => r.name)\n\n successes.value = Array.from(new Set([...successes.value, ...successful]))\n } catch (err) {\n console.error('Upload failed unexpectedly:', err)\n\n errors.value.push({\n name: 'upload',\n message: 'An unexpected error occurred during upload.',\n })\n } finally {\n loading.value = false\n }\n }\n\n watch(\n () => files.value.length,\n () => {\n if (!files.value.length) {\n errors.value = []\n successes.value = []\n }\n\n if (files.value.length > maxFiles) {\n errors.value.push({\n name: 'files',\n message: `You may upload up to ${maxFiles} files`,\n })\n }\n }\n )\n\n watch(\n files,\n (newFiles, oldFiles) => {\n const newPreviews = new Set(newFiles.map((f) => f.preview))\n oldFiles.forEach((file) => {\n if (file.preview && !newPreviews.has(file.preview)) {\n URL.revokeObjectURL(file.preview)\n }\n })\n },\n { deep: true }\n )\n\n onUnmounted(() => {\n files.value.forEach((file) => {\n if (file.preview) {\n URL.revokeObjectURL(file.preview)\n }\n })\n })\n\n return {\n dropZoneRef,\n isOverDropZone,\n\n files,\n setFiles: (v: FileWithPreview[]) => (files.value = v),\n\n errors,\n setErrors: (v: { name: string; message: string }[]) => (errors.value = v),\n\n successes,\n isSuccess,\n loading,\n onUpload,\n\n maxFileSize,\n maxFiles,\n allowedMimeTypes,\n }\n}\n", "type": "registry:component", "target": "composables/useSupabaseUpload.ts" } diff --git a/apps/ui-library/public/r/password-based-auth-nextjs.json b/apps/ui-library/public/r/password-based-auth-nextjs.json index 8e983697f48a8..787fc6ed2f04a 100644 --- a/apps/ui-library/public/r/password-based-auth-nextjs.json +++ b/apps/ui-library/public/r/password-based-auth-nextjs.json @@ -96,23 +96,23 @@ }, { "path": "registry/default/clients/nextjs/lib/supabase/client.ts", - "content": "import { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "import { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/nextjs/lib/supabase/middleware.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/nextjs/lib/supabase/server.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { cookies } from 'next/headers'\n\n/**\n * If using Fluid compute: Don't put this client in a global variable. Always create a new client within each\n * function when using it.\n */\nexport async function createClient() {\n const cookieStore = await cookies()\n\n return createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return cookieStore.getAll()\n },\n setAll(cookiesToSet) {\n try {\n cookiesToSet.forEach(({ name, value, options }) =>\n cookieStore.set(name, value, options)\n )\n } catch {\n // The `setAll` method was called from a Server Component.\n // This can be ignored if you have middleware refreshing\n // user sessions.\n }\n },\n },\n }\n )\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { cookies } from 'next/headers'\n\n/**\n * If using Fluid compute: Don't put this client in a global variable. Always create a new client within each\n * function when using it.\n */\nexport async function createClient() {\n const cookieStore = await cookies()\n\n return createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return cookieStore.getAll()\n },\n setAll(cookiesToSet) {\n try {\n cookiesToSet.forEach(({ name, value, options }) =>\n cookieStore.set(name, value, options)\n )\n } catch {\n // The `setAll` method was called from a Server Component.\n // This can be ignored if you have middleware refreshing\n // user sessions.\n }\n },\n },\n }\n )\n}\n", "type": "registry:lib" } ], "envVars": { "NEXT_PUBLIC_SUPABASE_URL": "", - "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/password-based-auth-nuxtjs.json b/apps/ui-library/public/r/password-based-auth-nuxtjs.json index b0ea37e763140..099e598b34938 100644 --- a/apps/ui-library/public/r/password-based-auth-nuxtjs.json +++ b/apps/ui-library/public/r/password-based-auth-nuxtjs.json @@ -83,7 +83,7 @@ }, { "path": "registry/default/password-based-auth/nuxtjs/server/routes/auth/confirm.get.ts", - "content": "import { getQuery, defineEventHandler, sendRedirect } from 'h3'\nimport type { EmailOtpType } from '@supabase/supabase-js'\nimport { createSupabaseServerClient } from '@/registry/default/clients/nuxtjs/server/supabase/client'\n\nexport default defineEventHandler(async (event) => {\n const query = getQuery(event)\n const token_hash = query.token_hash as string | null\n const type = query.type as EmailOtpType | null\n const _next = query.next as string | undefined\n const next = _next?.startsWith('/') ? _next : '/'\n\n if (token_hash && type) {\n const supabase = createSupabaseServerClient(event)\n\n const { error } = await supabase.auth.verifyOtp({\n type,\n token_hash,\n })\n\n if (!error) {\n return sendRedirect(event, next)\n } else {\n return sendRedirect(event, `/auth/error?error=${encodeURIComponent(error.message)}`)\n }\n }\n\n return sendRedirect(event, `/auth/error?error=${encodeURIComponent('No token hash or type')}`)\n})\n", + "content": "import type { EmailOtpType } from '@supabase/supabase-js'\nimport { defineEventHandler, getQuery, sendRedirect } from 'h3'\n\nimport { createSupabaseServerClient } from '@/registry/default/clients/nuxtjs/server/supabase/client'\n\nexport default defineEventHandler(async (event) => {\n const query = getQuery(event)\n const token_hash = query.token_hash as string | null\n const type = query.type as EmailOtpType | null\n const _next = query.next as string | undefined\n const next = _next?.startsWith('/') ? _next : '/'\n\n if (token_hash && type) {\n const supabase = createSupabaseServerClient(event)\n\n const { error } = await supabase.auth.verifyOtp({\n type,\n token_hash,\n })\n\n if (!error) {\n return sendRedirect(event, next)\n } else {\n return sendRedirect(event, `/auth/error?error=${encodeURIComponent(error.message)}`)\n }\n }\n\n return sendRedirect(event, `/auth/error?error=${encodeURIComponent('No token hash or type')}`)\n})\n", "type": "registry:file", "target": "server/routes/auth/confirm.get.ts" } diff --git a/apps/ui-library/public/r/password-based-auth-react-router.json b/apps/ui-library/public/r/password-based-auth-react-router.json index 5210579243bcb..0b3e87063b1cf 100644 --- a/apps/ui-library/public/r/password-based-auth-react-router.json +++ b/apps/ui-library/public/r/password-based-auth-react-router.json @@ -73,18 +73,18 @@ }, { "path": "registry/default/clients/react-router/lib/supabase/client.ts", - "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/react-router/lib/supabase/server.ts", - "content": "import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'\n\nexport function createClient(request: Request) {\n const headers = new Headers()\n\n const supabase = createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return parseCookieHeader(request.headers.get('Cookie') ?? '') as {\n name: string\n value: string\n }[]\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value, options }) =>\n headers.append('Set-Cookie', serializeCookieHeader(name, value, options))\n )\n },\n },\n }\n )\n\n return { supabase, headers }\n}\n", + "content": "import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'\n\nexport function createClient(request: Request) {\n const headers = new Headers()\n\n const supabase = createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return parseCookieHeader(request.headers.get('Cookie') ?? '') as {\n name: string\n value: string\n }[]\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value, options }) =>\n headers.append('Set-Cookie', serializeCookieHeader(name, value, options))\n )\n },\n },\n }\n )\n\n return { supabase, headers }\n}\n", "type": "registry:lib" } ], "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/password-based-auth-react.json b/apps/ui-library/public/r/password-based-auth-react.json index 423ec5b625d13..e9cfb105d24a4 100644 --- a/apps/ui-library/public/r/password-based-auth-react.json +++ b/apps/ui-library/public/r/password-based-auth-react.json @@ -36,13 +36,13 @@ }, { "path": "registry/default/clients/react/lib/supabase/client.ts", - "content": "import { createClient as createSupabaseClient } from '@supabase/supabase-js'\n\nexport function createClient() {\n return createSupabaseClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "import { createClient as createSupabaseClient } from '@supabase/supabase-js'\n\nexport function createClient() {\n return createSupabaseClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" } ], "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/password-based-auth-tanstack.json b/apps/ui-library/public/r/password-based-auth-tanstack.json index b9678a4dd5aa2..3866d11932877 100644 --- a/apps/ui-library/public/r/password-based-auth-tanstack.json +++ b/apps/ui-library/public/r/password-based-auth-tanstack.json @@ -96,18 +96,18 @@ }, { "path": "registry/default/clients/tanstack/lib/supabase/client.ts", - "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/tanstack/lib/supabase/server.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { getCookies, setCookie } from '@tanstack/react-start/server'\n\nexport function createClient() {\n return createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return Object.entries(getCookies()).map(\n ([name, value]) =>\n ({\n name,\n value,\n }) as { name: string; value: string }\n )\n },\n setAll(cookies) {\n cookies.forEach((cookie) => {\n setCookie(cookie.name, cookie.value)\n })\n },\n },\n }\n )\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { getCookies, setCookie } from '@tanstack/react-start/server'\n\nexport function createClient() {\n return createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return Object.entries(getCookies()).map(\n ([name, value]) =>\n ({\n name,\n value,\n }) as { name: string; value: string }\n )\n },\n setAll(cookies) {\n cookies.forEach((cookie) => {\n setCookie(cookie.name, cookie.value)\n })\n },\n },\n }\n )\n}\n", "type": "registry:lib" } ], "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/realtime-avatar-stack-nextjs.json b/apps/ui-library/public/r/realtime-avatar-stack-nextjs.json index 7d18cec045e80..3d325494cbe8e 100644 --- a/apps/ui-library/public/r/realtime-avatar-stack-nextjs.json +++ b/apps/ui-library/public/r/realtime-avatar-stack-nextjs.json @@ -40,23 +40,23 @@ }, { "path": "registry/default/clients/nextjs/lib/supabase/client.ts", - "content": "import { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "import { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/nextjs/lib/supabase/middleware.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/nextjs/lib/supabase/server.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { cookies } from 'next/headers'\n\n/**\n * If using Fluid compute: Don't put this client in a global variable. Always create a new client within each\n * function when using it.\n */\nexport async function createClient() {\n const cookieStore = await cookies()\n\n return createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return cookieStore.getAll()\n },\n setAll(cookiesToSet) {\n try {\n cookiesToSet.forEach(({ name, value, options }) =>\n cookieStore.set(name, value, options)\n )\n } catch {\n // The `setAll` method was called from a Server Component.\n // This can be ignored if you have middleware refreshing\n // user sessions.\n }\n },\n },\n }\n )\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { cookies } from 'next/headers'\n\n/**\n * If using Fluid compute: Don't put this client in a global variable. Always create a new client within each\n * function when using it.\n */\nexport async function createClient() {\n const cookieStore = await cookies()\n\n return createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return cookieStore.getAll()\n },\n setAll(cookiesToSet) {\n try {\n cookiesToSet.forEach(({ name, value, options }) =>\n cookieStore.set(name, value, options)\n )\n } catch {\n // The `setAll` method was called from a Server Component.\n // This can be ignored if you have middleware refreshing\n // user sessions.\n }\n },\n },\n }\n )\n}\n", "type": "registry:lib" } ], "envVars": { "NEXT_PUBLIC_SUPABASE_URL": "", - "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/realtime-avatar-stack-react-router.json b/apps/ui-library/public/r/realtime-avatar-stack-react-router.json index d015da3493c29..d1fbd6e3a97be 100644 --- a/apps/ui-library/public/r/realtime-avatar-stack-react-router.json +++ b/apps/ui-library/public/r/realtime-avatar-stack-react-router.json @@ -40,18 +40,18 @@ }, { "path": "registry/default/clients/react-router/lib/supabase/client.ts", - "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/react-router/lib/supabase/server.ts", - "content": "import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'\n\nexport function createClient(request: Request) {\n const headers = new Headers()\n\n const supabase = createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return parseCookieHeader(request.headers.get('Cookie') ?? '') as {\n name: string\n value: string\n }[]\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value, options }) =>\n headers.append('Set-Cookie', serializeCookieHeader(name, value, options))\n )\n },\n },\n }\n )\n\n return { supabase, headers }\n}\n", + "content": "import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'\n\nexport function createClient(request: Request) {\n const headers = new Headers()\n\n const supabase = createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return parseCookieHeader(request.headers.get('Cookie') ?? '') as {\n name: string\n value: string\n }[]\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value, options }) =>\n headers.append('Set-Cookie', serializeCookieHeader(name, value, options))\n )\n },\n },\n }\n )\n\n return { supabase, headers }\n}\n", "type": "registry:lib" } ], "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/realtime-avatar-stack-react.json b/apps/ui-library/public/r/realtime-avatar-stack-react.json index 3cf5a8621f926..f11d14a29f1d7 100644 --- a/apps/ui-library/public/r/realtime-avatar-stack-react.json +++ b/apps/ui-library/public/r/realtime-avatar-stack-react.json @@ -39,13 +39,13 @@ }, { "path": "registry/default/clients/react/lib/supabase/client.ts", - "content": "import { createClient as createSupabaseClient } from '@supabase/supabase-js'\n\nexport function createClient() {\n return createSupabaseClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "import { createClient as createSupabaseClient } from '@supabase/supabase-js'\n\nexport function createClient() {\n return createSupabaseClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" } ], "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/realtime-avatar-stack-tanstack.json b/apps/ui-library/public/r/realtime-avatar-stack-tanstack.json index 4a675bb6c18b3..8062b07679808 100644 --- a/apps/ui-library/public/r/realtime-avatar-stack-tanstack.json +++ b/apps/ui-library/public/r/realtime-avatar-stack-tanstack.json @@ -40,18 +40,18 @@ }, { "path": "registry/default/clients/tanstack/lib/supabase/client.ts", - "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/tanstack/lib/supabase/server.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { getCookies, setCookie } from '@tanstack/react-start/server'\n\nexport function createClient() {\n return createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return Object.entries(getCookies()).map(\n ([name, value]) =>\n ({\n name,\n value,\n }) as { name: string; value: string }\n )\n },\n setAll(cookies) {\n cookies.forEach((cookie) => {\n setCookie(cookie.name, cookie.value)\n })\n },\n },\n }\n )\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { getCookies, setCookie } from '@tanstack/react-start/server'\n\nexport function createClient() {\n return createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return Object.entries(getCookies()).map(\n ([name, value]) =>\n ({\n name,\n value,\n }) as { name: string; value: string }\n )\n },\n setAll(cookies) {\n cookies.forEach((cookie) => {\n setCookie(cookie.name, cookie.value)\n })\n },\n },\n }\n )\n}\n", "type": "registry:lib" } ], "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/realtime-chat-nextjs.json b/apps/ui-library/public/r/realtime-chat-nextjs.json index d3d7fd12c77b8..72385d8c17fb8 100644 --- a/apps/ui-library/public/r/realtime-chat-nextjs.json +++ b/apps/ui-library/public/r/realtime-chat-nextjs.json @@ -36,23 +36,23 @@ }, { "path": "registry/default/clients/nextjs/lib/supabase/client.ts", - "content": "import { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "import { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/nextjs/lib/supabase/middleware.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/nextjs/lib/supabase/server.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { cookies } from 'next/headers'\n\n/**\n * If using Fluid compute: Don't put this client in a global variable. Always create a new client within each\n * function when using it.\n */\nexport async function createClient() {\n const cookieStore = await cookies()\n\n return createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return cookieStore.getAll()\n },\n setAll(cookiesToSet) {\n try {\n cookiesToSet.forEach(({ name, value, options }) =>\n cookieStore.set(name, value, options)\n )\n } catch {\n // The `setAll` method was called from a Server Component.\n // This can be ignored if you have middleware refreshing\n // user sessions.\n }\n },\n },\n }\n )\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { cookies } from 'next/headers'\n\n/**\n * If using Fluid compute: Don't put this client in a global variable. Always create a new client within each\n * function when using it.\n */\nexport async function createClient() {\n const cookieStore = await cookies()\n\n return createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return cookieStore.getAll()\n },\n setAll(cookiesToSet) {\n try {\n cookiesToSet.forEach(({ name, value, options }) =>\n cookieStore.set(name, value, options)\n )\n } catch {\n // The `setAll` method was called from a Server Component.\n // This can be ignored if you have middleware refreshing\n // user sessions.\n }\n },\n },\n }\n )\n}\n", "type": "registry:lib" } ], "envVars": { "NEXT_PUBLIC_SUPABASE_URL": "", - "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/realtime-chat-react-router.json b/apps/ui-library/public/r/realtime-chat-react-router.json index 52bfef06c03d2..03d6f440570c0 100644 --- a/apps/ui-library/public/r/realtime-chat-react-router.json +++ b/apps/ui-library/public/r/realtime-chat-react-router.json @@ -36,18 +36,18 @@ }, { "path": "registry/default/clients/react-router/lib/supabase/client.ts", - "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/react-router/lib/supabase/server.ts", - "content": "import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'\n\nexport function createClient(request: Request) {\n const headers = new Headers()\n\n const supabase = createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return parseCookieHeader(request.headers.get('Cookie') ?? '') as {\n name: string\n value: string\n }[]\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value, options }) =>\n headers.append('Set-Cookie', serializeCookieHeader(name, value, options))\n )\n },\n },\n }\n )\n\n return { supabase, headers }\n}\n", + "content": "import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'\n\nexport function createClient(request: Request) {\n const headers = new Headers()\n\n const supabase = createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return parseCookieHeader(request.headers.get('Cookie') ?? '') as {\n name: string\n value: string\n }[]\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value, options }) =>\n headers.append('Set-Cookie', serializeCookieHeader(name, value, options))\n )\n },\n },\n }\n )\n\n return { supabase, headers }\n}\n", "type": "registry:lib" } ], "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/realtime-chat-react.json b/apps/ui-library/public/r/realtime-chat-react.json index 5d111fe2ca97b..48d4257d0ed84 100644 --- a/apps/ui-library/public/r/realtime-chat-react.json +++ b/apps/ui-library/public/r/realtime-chat-react.json @@ -35,13 +35,13 @@ }, { "path": "registry/default/clients/react/lib/supabase/client.ts", - "content": "import { createClient as createSupabaseClient } from '@supabase/supabase-js'\n\nexport function createClient() {\n return createSupabaseClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "import { createClient as createSupabaseClient } from '@supabase/supabase-js'\n\nexport function createClient() {\n return createSupabaseClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" } ], "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/realtime-chat-tanstack.json b/apps/ui-library/public/r/realtime-chat-tanstack.json index f6b689b4a22a0..081050218e3a7 100644 --- a/apps/ui-library/public/r/realtime-chat-tanstack.json +++ b/apps/ui-library/public/r/realtime-chat-tanstack.json @@ -36,18 +36,18 @@ }, { "path": "registry/default/clients/tanstack/lib/supabase/client.ts", - "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/tanstack/lib/supabase/server.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { getCookies, setCookie } from '@tanstack/react-start/server'\n\nexport function createClient() {\n return createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return Object.entries(getCookies()).map(\n ([name, value]) =>\n ({\n name,\n value,\n }) as { name: string; value: string }\n )\n },\n setAll(cookies) {\n cookies.forEach((cookie) => {\n setCookie(cookie.name, cookie.value)\n })\n },\n },\n }\n )\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { getCookies, setCookie } from '@tanstack/react-start/server'\n\nexport function createClient() {\n return createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return Object.entries(getCookies()).map(\n ([name, value]) =>\n ({\n name,\n value,\n }) as { name: string; value: string }\n )\n },\n setAll(cookies) {\n cookies.forEach((cookie) => {\n setCookie(cookie.name, cookie.value)\n })\n },\n },\n }\n )\n}\n", "type": "registry:lib" } ], "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/realtime-cursor-nextjs.json b/apps/ui-library/public/r/realtime-cursor-nextjs.json index 6e048da2094cb..e391316657505 100644 --- a/apps/ui-library/public/r/realtime-cursor-nextjs.json +++ b/apps/ui-library/public/r/realtime-cursor-nextjs.json @@ -28,23 +28,23 @@ }, { "path": "registry/default/clients/nextjs/lib/supabase/client.ts", - "content": "import { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "import { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/nextjs/lib/supabase/middleware.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/nextjs/lib/supabase/server.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { cookies } from 'next/headers'\n\n/**\n * If using Fluid compute: Don't put this client in a global variable. Always create a new client within each\n * function when using it.\n */\nexport async function createClient() {\n const cookieStore = await cookies()\n\n return createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return cookieStore.getAll()\n },\n setAll(cookiesToSet) {\n try {\n cookiesToSet.forEach(({ name, value, options }) =>\n cookieStore.set(name, value, options)\n )\n } catch {\n // The `setAll` method was called from a Server Component.\n // This can be ignored if you have middleware refreshing\n // user sessions.\n }\n },\n },\n }\n )\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { cookies } from 'next/headers'\n\n/**\n * If using Fluid compute: Don't put this client in a global variable. Always create a new client within each\n * function when using it.\n */\nexport async function createClient() {\n const cookieStore = await cookies()\n\n return createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return cookieStore.getAll()\n },\n setAll(cookiesToSet) {\n try {\n cookiesToSet.forEach(({ name, value, options }) =>\n cookieStore.set(name, value, options)\n )\n } catch {\n // The `setAll` method was called from a Server Component.\n // This can be ignored if you have middleware refreshing\n // user sessions.\n }\n },\n },\n }\n )\n}\n", "type": "registry:lib" } ], "envVars": { "NEXT_PUBLIC_SUPABASE_URL": "", - "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/realtime-cursor-nuxtjs.json b/apps/ui-library/public/r/realtime-cursor-nuxtjs.json index f392f2c2c6434..77c6a12e85de6 100644 --- a/apps/ui-library/public/r/realtime-cursor-nuxtjs.json +++ b/apps/ui-library/public/r/realtime-cursor-nuxtjs.json @@ -24,7 +24,7 @@ }, { "path": "registry/default/realtime-cursor/nuxtjs/app/composables/useRealtimeCursors.ts", - "content": "import { ref, reactive, onMounted, onUnmounted } from 'vue'\nimport { REALTIME_SUBSCRIBE_STATES, type RealtimeChannel } from '@supabase/supabase-js'\n// @ts-ignore\nimport { createClient } from '@/lib/supabase/client'\n\n/**\n * Throttle a callback to a certain delay.\n * It will only call the callback if the delay has passed,\n * using the arguments from the last call.\n */\nfunction useThrottleCallback(\n callback: (...args: Params) => void,\n delay: number\n) {\n let lastCall = 0\n let timeout: ReturnType | null = null\n\n const run = (...args: Params) => {\n const now = Date.now()\n const remainingTime = delay - (now - lastCall)\n\n if (remainingTime <= 0) {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n lastCall = now\n callback(...args)\n } else if (!timeout) {\n timeout = setTimeout(() => {\n lastCall = Date.now()\n timeout = null\n callback(...args)\n }, remainingTime)\n }\n }\n\n const cancel = () => {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return { run, cancel }\n}\n\nconst supabase = createClient()\n\nconst generateRandomColor = () =>\n `hsl(${Math.floor(Math.random() * 360)}, 100%, 70%)`\n\nconst generateRandomNumber = () =>\n Math.floor(Math.random() * 100)\n\nconst EVENT_NAME = 'realtime-cursor-move'\n\nexport type CursorEventPayload = {\n position: { x: number; y: number }\n user: { id: number; name: string }\n color: string\n timestamp: number\n}\n\nexport function useRealtimeCursors({\n roomName,\n username,\n throttleMs,\n}: {\n roomName: string\n username: string\n throttleMs: number\n}) {\n const color = generateRandomColor()\n const userId = generateRandomNumber()\n\n const cursors = reactive>({})\n const cursorPayload = ref(null)\n const channelRef = ref(null)\n\n const sendCursor = (event: MouseEvent) => {\n const payload: CursorEventPayload = {\n position: {\n x: event.clientX,\n y: event.clientY,\n },\n user: {\n id: userId,\n name: username,\n },\n color,\n timestamp: Date.now(),\n }\n\n cursorPayload.value = payload\n\n channelRef.value?.send({\n type: 'broadcast',\n event: EVENT_NAME,\n payload,\n })\n }\n\n const { run: handleMouseMove, cancel: cancelThrottle } =\n useThrottleCallback(sendCursor, throttleMs)\n\n onMounted(() => {\n const channel = supabase.channel(roomName)\n\n channel\n .on('system', {}, (payload: CursorEventPayload) => {\n console.error('Realtime system error:', payload)\n\n // Defensive cleanup\n Object.keys(cursors).forEach((k) => delete cursors[k])\n channelRef.value = null\n })\n .on('presence', { event: 'leave' }, ({ leftPresences }: { leftPresences: Array<{ key: string }> }) => {\n leftPresences.forEach(({ key }) => {\n delete cursors[key]\n })\n })\n .on('presence', { event: 'join' }, () => {\n if (!cursorPayload.value) return\n\n channelRef.value?.send({\n type: 'broadcast',\n event: EVENT_NAME,\n payload: cursorPayload.value,\n })\n })\n .on('broadcast', { event: EVENT_NAME }, ({ payload }: { payload: CursorEventPayload }) => {\n if (payload.user.id === userId) return\n\n cursors[payload.user.id] = payload\n })\n .subscribe(async (status: REALTIME_SUBSCRIBE_STATES) => {\n if (status === REALTIME_SUBSCRIBE_STATES.SUBSCRIBED) {\n try {\n await channel.track({ key: userId })\n channelRef.value = channel\n } catch (err) {\n console.error('Failed to track presence for current user:', err)\n channelRef.value = null\n }\n } else {\n Object.keys(cursors).forEach((k) => delete cursors[k])\n channelRef.value = null\n }\n })\n\n window.addEventListener('mousemove', handleMouseMove)\n })\n\n onUnmounted(() => {\n window.removeEventListener('mousemove', handleMouseMove)\n\n cancelThrottle()\n\n if (channelRef.value) {\n channelRef.value.unsubscribe()\n channelRef.value = null\n }\n\n Object.keys(cursors).forEach((k) => delete cursors[k])\n })\n\n return { cursors }\n}\n", + "content": "import { REALTIME_SUBSCRIBE_STATES, type RealtimeChannel } from '@supabase/supabase-js'\nimport { onMounted, onUnmounted, reactive, ref } from 'vue'\n\n// @ts-ignore\nimport { createClient } from '@/lib/supabase/client'\n\n/**\n * Throttle a callback to a certain delay.\n * It will only call the callback if the delay has passed,\n * using the arguments from the last call.\n */\nfunction useThrottleCallback(\n callback: (...args: Params) => void,\n delay: number\n) {\n let lastCall = 0\n let timeout: ReturnType | null = null\n\n const run = (...args: Params) => {\n const now = Date.now()\n const remainingTime = delay - (now - lastCall)\n\n if (remainingTime <= 0) {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n lastCall = now\n callback(...args)\n } else if (!timeout) {\n timeout = setTimeout(() => {\n lastCall = Date.now()\n timeout = null\n callback(...args)\n }, remainingTime)\n }\n }\n\n const cancel = () => {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return { run, cancel }\n}\n\nconst supabase = createClient()\n\nconst generateRandomColor = () => `hsl(${Math.floor(Math.random() * 360)}, 100%, 70%)`\n\nconst generateRandomNumber = () => Math.floor(Math.random() * 100)\n\nconst EVENT_NAME = 'realtime-cursor-move'\n\nexport type CursorEventPayload = {\n position: { x: number; y: number }\n user: { id: number; name: string }\n color: string\n timestamp: number\n}\n\nexport function useRealtimeCursors({\n roomName,\n username,\n throttleMs,\n}: {\n roomName: string\n username: string\n throttleMs: number\n}) {\n const color = generateRandomColor()\n const userId = generateRandomNumber()\n\n const cursors = reactive>({})\n const cursorPayload = ref(null)\n const channelRef = ref(null)\n\n const sendCursor = (event: MouseEvent) => {\n const payload: CursorEventPayload = {\n position: {\n x: event.clientX,\n y: event.clientY,\n },\n user: {\n id: userId,\n name: username,\n },\n color,\n timestamp: Date.now(),\n }\n\n cursorPayload.value = payload\n\n channelRef.value?.send({\n type: 'broadcast',\n event: EVENT_NAME,\n payload,\n })\n }\n\n const { run: handleMouseMove, cancel: cancelThrottle } = useThrottleCallback(\n sendCursor,\n throttleMs\n )\n\n onMounted(() => {\n const channel = supabase.channel(roomName)\n\n channel\n .on('system', {}, (payload: CursorEventPayload) => {\n console.error('Realtime system error:', payload)\n\n // Defensive cleanup\n Object.keys(cursors).forEach((k) => delete cursors[k])\n channelRef.value = null\n })\n .on(\n 'presence',\n { event: 'leave' },\n ({ leftPresences }: { leftPresences: Array<{ key: string }> }) => {\n leftPresences.forEach(({ key }) => {\n delete cursors[key]\n })\n }\n )\n .on('presence', { event: 'join' }, () => {\n if (!cursorPayload.value) return\n\n channelRef.value?.send({\n type: 'broadcast',\n event: EVENT_NAME,\n payload: cursorPayload.value,\n })\n })\n .on('broadcast', { event: EVENT_NAME }, ({ payload }: { payload: CursorEventPayload }) => {\n if (payload.user.id === userId) return\n\n cursors[payload.user.id] = payload\n })\n .subscribe(async (status: REALTIME_SUBSCRIBE_STATES) => {\n if (status === REALTIME_SUBSCRIBE_STATES.SUBSCRIBED) {\n try {\n await channel.track({ key: userId })\n channelRef.value = channel\n } catch (err) {\n console.error('Failed to track presence for current user:', err)\n channelRef.value = null\n }\n } else {\n Object.keys(cursors).forEach((k) => delete cursors[k])\n channelRef.value = null\n }\n })\n\n window.addEventListener('mousemove', handleMouseMove)\n })\n\n onUnmounted(() => {\n window.removeEventListener('mousemove', handleMouseMove)\n\n cancelThrottle()\n\n if (channelRef.value) {\n channelRef.value.unsubscribe()\n channelRef.value = null\n }\n\n Object.keys(cursors).forEach((k) => delete cursors[k])\n })\n\n return { cursors }\n}\n", "type": "registry:component", "target": "app/composables/useRealtimeCursors.ts" } diff --git a/apps/ui-library/public/r/realtime-cursor-react-router.json b/apps/ui-library/public/r/realtime-cursor-react-router.json index 8a97b0e1119c5..9585ad66f3f4e 100644 --- a/apps/ui-library/public/r/realtime-cursor-react-router.json +++ b/apps/ui-library/public/r/realtime-cursor-react-router.json @@ -28,18 +28,18 @@ }, { "path": "registry/default/clients/react-router/lib/supabase/client.ts", - "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/react-router/lib/supabase/server.ts", - "content": "import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'\n\nexport function createClient(request: Request) {\n const headers = new Headers()\n\n const supabase = createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return parseCookieHeader(request.headers.get('Cookie') ?? '') as {\n name: string\n value: string\n }[]\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value, options }) =>\n headers.append('Set-Cookie', serializeCookieHeader(name, value, options))\n )\n },\n },\n }\n )\n\n return { supabase, headers }\n}\n", + "content": "import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'\n\nexport function createClient(request: Request) {\n const headers = new Headers()\n\n const supabase = createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return parseCookieHeader(request.headers.get('Cookie') ?? '') as {\n name: string\n value: string\n }[]\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value, options }) =>\n headers.append('Set-Cookie', serializeCookieHeader(name, value, options))\n )\n },\n },\n }\n )\n\n return { supabase, headers }\n}\n", "type": "registry:lib" } ], "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/realtime-cursor-react.json b/apps/ui-library/public/r/realtime-cursor-react.json index 51a176d65498f..52b98fe284ad4 100644 --- a/apps/ui-library/public/r/realtime-cursor-react.json +++ b/apps/ui-library/public/r/realtime-cursor-react.json @@ -27,13 +27,13 @@ }, { "path": "registry/default/clients/react/lib/supabase/client.ts", - "content": "import { createClient as createSupabaseClient } from '@supabase/supabase-js'\n\nexport function createClient() {\n return createSupabaseClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "import { createClient as createSupabaseClient } from '@supabase/supabase-js'\n\nexport function createClient() {\n return createSupabaseClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" } ], "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/realtime-cursor-tanstack.json b/apps/ui-library/public/r/realtime-cursor-tanstack.json index 0c310bcf90b7b..8d092101aa52f 100644 --- a/apps/ui-library/public/r/realtime-cursor-tanstack.json +++ b/apps/ui-library/public/r/realtime-cursor-tanstack.json @@ -28,18 +28,18 @@ }, { "path": "registry/default/clients/tanstack/lib/supabase/client.ts", - "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/tanstack/lib/supabase/server.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { getCookies, setCookie } from '@tanstack/react-start/server'\n\nexport function createClient() {\n return createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return Object.entries(getCookies()).map(\n ([name, value]) =>\n ({\n name,\n value,\n }) as { name: string; value: string }\n )\n },\n setAll(cookies) {\n cookies.forEach((cookie) => {\n setCookie(cookie.name, cookie.value)\n })\n },\n },\n }\n )\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { getCookies, setCookie } from '@tanstack/react-start/server'\n\nexport function createClient() {\n return createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return Object.entries(getCookies()).map(\n ([name, value]) =>\n ({\n name,\n value,\n }) as { name: string; value: string }\n )\n },\n setAll(cookies) {\n cookies.forEach((cookie) => {\n setCookie(cookie.name, cookie.value)\n })\n },\n },\n }\n )\n}\n", "type": "registry:lib" } ], "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/realtime-cursor-vue.json b/apps/ui-library/public/r/realtime-cursor-vue.json index dca9a2af3c1a4..62a7ee1b61f24 100644 --- a/apps/ui-library/public/r/realtime-cursor-vue.json +++ b/apps/ui-library/public/r/realtime-cursor-vue.json @@ -24,7 +24,7 @@ }, { "path": "registry/default/realtime-cursor/vue/composables/useRealtimeCursors.ts", - "content": "import { ref, reactive, onMounted, onUnmounted } from 'vue'\nimport { REALTIME_SUBSCRIBE_STATES, type RealtimeChannel } from '@supabase/supabase-js'\n// @ts-ignore\nimport { createClient } from '@/lib/supabase/client'\n\n/**\n * Throttle a callback to a certain delay.\n * It will only call the callback if the delay has passed,\n * using the arguments from the last call.\n */\nfunction useThrottleCallback(\n callback: (...args: Params) => void,\n delay: number\n) {\n let lastCall = 0\n let timeout: ReturnType | null = null\n\n const run = (...args: Params) => {\n const now = Date.now()\n const remainingTime = delay - (now - lastCall)\n\n if (remainingTime <= 0) {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n lastCall = now\n callback(...args)\n } else if (!timeout) {\n timeout = setTimeout(() => {\n lastCall = Date.now()\n timeout = null\n callback(...args)\n }, remainingTime)\n }\n }\n\n const cancel = () => {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return { run, cancel }\n}\n\nconst supabase = createClient()\n\nconst generateRandomColor = () =>\n `hsl(${Math.floor(Math.random() * 360)}, 100%, 70%)`\n\nconst generateRandomNumber = () =>\n Math.floor(Math.random() * 100)\n\nconst EVENT_NAME = 'realtime-cursor-move'\n\nexport type CursorEventPayload = {\n position: { x: number; y: number }\n user: { id: number; name: string }\n color: string\n timestamp: number\n}\n\nexport function useRealtimeCursors({\n roomName,\n username,\n throttleMs,\n}: {\n roomName: string\n username: string\n throttleMs: number\n}) {\n const color = generateRandomColor()\n const userId = generateRandomNumber()\n\n const cursors = reactive>({})\n const cursorPayload = ref(null)\n const channelRef = ref(null)\n\n const sendCursor = (event: MouseEvent) => {\n const payload: CursorEventPayload = {\n position: {\n x: event.clientX,\n y: event.clientY,\n },\n user: {\n id: userId,\n name: username,\n },\n color,\n timestamp: Date.now(),\n }\n\n cursorPayload.value = payload\n\n channelRef.value?.send({\n type: 'broadcast',\n event: EVENT_NAME,\n payload,\n })\n }\n\n const { run: handleMouseMove, cancel: cancelThrottle } =\n useThrottleCallback(sendCursor, throttleMs)\n\n onMounted(() => {\n const channel = supabase.channel(roomName)\n\n channel\n .on('system', {}, (payload: CursorEventPayload) => {\n console.error('Realtime system error:', payload)\n\n // Defensive cleanup\n Object.keys(cursors).forEach((k) => delete cursors[k])\n channelRef.value = null\n })\n .on('presence', { event: 'leave' }, ({ leftPresences }: { leftPresences: Array<{ key: string }> }) => {\n leftPresences.forEach(({ key }) => {\n delete cursors[key]\n })\n })\n .on('presence', { event: 'join' }, () => {\n if (!cursorPayload.value) return\n\n channelRef.value?.send({\n type: 'broadcast',\n event: EVENT_NAME,\n payload: cursorPayload.value,\n })\n })\n .on('broadcast', { event: EVENT_NAME }, ({ payload }: { payload: CursorEventPayload }) => {\n if (payload.user.id === userId) return\n\n cursors[payload.user.id] = payload\n })\n .subscribe(async (status: REALTIME_SUBSCRIBE_STATES) => {\n if (status === REALTIME_SUBSCRIBE_STATES.SUBSCRIBED) {\n try {\n await channel.track({ key: userId })\n channelRef.value = channel\n } catch (err) {\n console.error('Failed to track presence for current user:', err)\n channelRef.value = null\n }\n } else {\n Object.keys(cursors).forEach((k) => delete cursors[k])\n channelRef.value = null\n }\n })\n\n window.addEventListener('mousemove', handleMouseMove)\n })\n\n onUnmounted(() => {\n window.removeEventListener('mousemove', handleMouseMove)\n\n cancelThrottle()\n\n if (channelRef.value) {\n channelRef.value.unsubscribe()\n channelRef.value = null\n }\n\n Object.keys(cursors).forEach((k) => delete cursors[k])\n })\n\n return { cursors }\n}\n", + "content": "import { REALTIME_SUBSCRIBE_STATES, type RealtimeChannel } from '@supabase/supabase-js'\nimport { onMounted, onUnmounted, reactive, ref } from 'vue'\n\n// @ts-ignore\nimport { createClient } from '@/lib/supabase/client'\n\n/**\n * Throttle a callback to a certain delay.\n * It will only call the callback if the delay has passed,\n * using the arguments from the last call.\n */\nfunction useThrottleCallback(\n callback: (...args: Params) => void,\n delay: number\n) {\n let lastCall = 0\n let timeout: ReturnType | null = null\n\n const run = (...args: Params) => {\n const now = Date.now()\n const remainingTime = delay - (now - lastCall)\n\n if (remainingTime <= 0) {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n lastCall = now\n callback(...args)\n } else if (!timeout) {\n timeout = setTimeout(() => {\n lastCall = Date.now()\n timeout = null\n callback(...args)\n }, remainingTime)\n }\n }\n\n const cancel = () => {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return { run, cancel }\n}\n\nconst supabase = createClient()\n\nconst generateRandomColor = () => `hsl(${Math.floor(Math.random() * 360)}, 100%, 70%)`\n\nconst generateRandomNumber = () => Math.floor(Math.random() * 100)\n\nconst EVENT_NAME = 'realtime-cursor-move'\n\nexport type CursorEventPayload = {\n position: { x: number; y: number }\n user: { id: number; name: string }\n color: string\n timestamp: number\n}\n\nexport function useRealtimeCursors({\n roomName,\n username,\n throttleMs,\n}: {\n roomName: string\n username: string\n throttleMs: number\n}) {\n const color = generateRandomColor()\n const userId = generateRandomNumber()\n\n const cursors = reactive>({})\n const cursorPayload = ref(null)\n const channelRef = ref(null)\n\n const sendCursor = (event: MouseEvent) => {\n const payload: CursorEventPayload = {\n position: {\n x: event.clientX,\n y: event.clientY,\n },\n user: {\n id: userId,\n name: username,\n },\n color,\n timestamp: Date.now(),\n }\n\n cursorPayload.value = payload\n\n channelRef.value?.send({\n type: 'broadcast',\n event: EVENT_NAME,\n payload,\n })\n }\n\n const { run: handleMouseMove, cancel: cancelThrottle } = useThrottleCallback(\n sendCursor,\n throttleMs\n )\n\n onMounted(() => {\n const channel = supabase.channel(roomName)\n\n channel\n .on('system', {}, (payload: CursorEventPayload) => {\n console.error('Realtime system error:', payload)\n\n // Defensive cleanup\n Object.keys(cursors).forEach((k) => delete cursors[k])\n channelRef.value = null\n })\n .on(\n 'presence',\n { event: 'leave' },\n ({ leftPresences }: { leftPresences: Array<{ key: string }> }) => {\n leftPresences.forEach(({ key }) => {\n delete cursors[key]\n })\n }\n )\n .on('presence', { event: 'join' }, () => {\n if (!cursorPayload.value) return\n\n channelRef.value?.send({\n type: 'broadcast',\n event: EVENT_NAME,\n payload: cursorPayload.value,\n })\n })\n .on('broadcast', { event: EVENT_NAME }, ({ payload }: { payload: CursorEventPayload }) => {\n if (payload.user.id === userId) return\n\n cursors[payload.user.id] = payload\n })\n .subscribe(async (status: REALTIME_SUBSCRIBE_STATES) => {\n if (status === REALTIME_SUBSCRIBE_STATES.SUBSCRIBED) {\n try {\n await channel.track({ key: userId })\n channelRef.value = channel\n } catch (err) {\n console.error('Failed to track presence for current user:', err)\n channelRef.value = null\n }\n } else {\n Object.keys(cursors).forEach((k) => delete cursors[k])\n channelRef.value = null\n }\n })\n\n window.addEventListener('mousemove', handleMouseMove)\n })\n\n onUnmounted(() => {\n window.removeEventListener('mousemove', handleMouseMove)\n\n cancelThrottle()\n\n if (channelRef.value) {\n channelRef.value.unsubscribe()\n channelRef.value = null\n }\n\n Object.keys(cursors).forEach((k) => delete cursors[k])\n })\n\n return { cursors }\n}\n", "type": "registry:component", "target": "composables/useRealtimeCursors.ts" } diff --git a/apps/ui-library/public/r/registry.json b/apps/ui-library/public/r/registry.json index 41edf2c6e6e7a..a757ca0ae4982 100644 --- a/apps/ui-library/public/r/registry.json +++ b/apps/ui-library/public/r/registry.json @@ -97,10 +97,10 @@ "type": "registry:lib" } ], - "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "NEXT_PUBLIC_SUPABASE_URL": "", - "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY": "" } }, { @@ -139,10 +139,10 @@ "dependencies": [ "@supabase/supabase-js@latest" ], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" } }, { @@ -217,10 +217,10 @@ "type": "registry:lib" } ], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" } }, { @@ -313,10 +313,10 @@ "type": "registry:lib" } ], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" } }, { @@ -379,10 +379,10 @@ "type": "registry:lib" } ], - "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "NEXT_PUBLIC_SUPABASE_URL": "", - "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY": "" } }, { @@ -407,10 +407,10 @@ "dependencies": [ "@supabase/supabase-js@latest" ], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" } }, { @@ -468,10 +468,10 @@ "type": "registry:lib" } ], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" } }, { @@ -530,10 +530,10 @@ "type": "registry:lib" } ], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" } }, { @@ -573,10 +573,10 @@ "type": "registry:lib" } ], - "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "NEXT_PUBLIC_SUPABASE_URL": "", - "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY": "" } }, { @@ -607,10 +607,10 @@ "type": "registry:lib" } ], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" } }, { @@ -646,10 +646,10 @@ "type": "registry:lib" } ], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" } }, { @@ -685,10 +685,10 @@ "type": "registry:lib" } ], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" } }, { @@ -729,10 +729,10 @@ "type": "registry:lib" } ], - "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "NEXT_PUBLIC_SUPABASE_URL": "", - "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY": "" } }, { @@ -764,10 +764,10 @@ "type": "registry:lib" } ], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" } }, { @@ -804,10 +804,10 @@ "type": "registry:lib" } ], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" } }, { @@ -844,10 +844,10 @@ "type": "registry:lib" } ], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" } }, { @@ -889,10 +889,10 @@ "type": "registry:lib" } ], - "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "NEXT_PUBLIC_SUPABASE_URL": "", - "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY": "" } }, { @@ -925,10 +925,10 @@ "type": "registry:lib" } ], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" } }, { @@ -966,10 +966,10 @@ "type": "registry:lib" } ], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" } }, { @@ -1007,10 +1007,10 @@ "type": "registry:lib" } ], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" } }, { @@ -1061,10 +1061,10 @@ "type": "registry:lib" } ], - "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "NEXT_PUBLIC_SUPABASE_URL": "", - "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY": "" } }, { @@ -1106,10 +1106,10 @@ "type": "registry:lib" } ], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" } }, { @@ -1156,10 +1156,10 @@ "type": "registry:lib" } ], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" } }, { @@ -1206,10 +1206,10 @@ "type": "registry:lib" } ], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" } }, { @@ -1257,10 +1257,10 @@ "type": "registry:lib" } ], - "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "NEXT_PUBLIC_SUPABASE_URL": "", - "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY": "" } }, { @@ -1299,10 +1299,10 @@ "type": "registry:lib" } ], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" } }, { @@ -1346,10 +1346,10 @@ "type": "registry:lib" } ], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" } }, { @@ -1393,10 +1393,10 @@ "type": "registry:lib" } ], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" } }, { @@ -1428,10 +1428,10 @@ "@supabase/ssr@latest", "@supabase/supabase-js@latest" ], - "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "NEXT_PUBLIC_SUPABASE_URL": "", - "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY": "" }, "files": [ { @@ -1458,10 +1458,10 @@ "dependencies": [ "@supabase/supabase-js@latest" ], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, "files": [ { @@ -1481,10 +1481,10 @@ "@supabase/ssr@latest", "@supabase/supabase-js@latest" ], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, "files": [ { @@ -1508,10 +1508,10 @@ "@supabase/ssr@latest", "@supabase/supabase-js@latest" ], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, "files": [ { @@ -1712,10 +1712,10 @@ "@supabase/ssr@latest", "@supabase/supabase-js@latest" ], - "docs": "You'll need to set the following environment variables in your project: `NUXT_PUBLIC_SUPABASE_URL` and `NUXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `NUXT_PUBLIC_SUPABASE_URL` and `NUXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "NUXT_PUBLIC_SUPABASE_URL": "", - "NUXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "NUXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY": "" }, "files": [ { @@ -1749,10 +1749,10 @@ "dependencies": [ "@supabase/supabase-js@latest" ], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, "files": [ { diff --git a/apps/ui-library/public/r/social-auth-nextjs.json b/apps/ui-library/public/r/social-auth-nextjs.json index 5a40cb2ab603c..6463dd1b85eab 100644 --- a/apps/ui-library/public/r/social-auth-nextjs.json +++ b/apps/ui-library/public/r/social-auth-nextjs.json @@ -55,23 +55,23 @@ }, { "path": "registry/default/clients/nextjs/lib/supabase/client.ts", - "content": "import { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "import { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/nextjs/lib/supabase/middleware.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/nextjs/lib/supabase/server.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { cookies } from 'next/headers'\n\n/**\n * If using Fluid compute: Don't put this client in a global variable. Always create a new client within each\n * function when using it.\n */\nexport async function createClient() {\n const cookieStore = await cookies()\n\n return createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return cookieStore.getAll()\n },\n setAll(cookiesToSet) {\n try {\n cookiesToSet.forEach(({ name, value, options }) =>\n cookieStore.set(name, value, options)\n )\n } catch {\n // The `setAll` method was called from a Server Component.\n // This can be ignored if you have middleware refreshing\n // user sessions.\n }\n },\n },\n }\n )\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { cookies } from 'next/headers'\n\n/**\n * If using Fluid compute: Don't put this client in a global variable. Always create a new client within each\n * function when using it.\n */\nexport async function createClient() {\n const cookieStore = await cookies()\n\n return createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return cookieStore.getAll()\n },\n setAll(cookiesToSet) {\n try {\n cookiesToSet.forEach(({ name, value, options }) =>\n cookieStore.set(name, value, options)\n )\n } catch {\n // The `setAll` method was called from a Server Component.\n // This can be ignored if you have middleware refreshing\n // user sessions.\n }\n },\n },\n }\n )\n}\n", "type": "registry:lib" } ], "envVars": { "NEXT_PUBLIC_SUPABASE_URL": "", - "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/social-auth-nuxtjs.json b/apps/ui-library/public/r/social-auth-nuxtjs.json index 64639fe3b99e7..1d42467b7237e 100644 --- a/apps/ui-library/public/r/social-auth-nuxtjs.json +++ b/apps/ui-library/public/r/social-auth-nuxtjs.json @@ -47,13 +47,13 @@ }, { "path": "registry/default/social-auth/nuxtjs/server/middleware/auth.ts", - "content": "import { defineEventHandler, sendRedirect } from 'h3'\nimport { createSupabaseServerClient } from '@/registry/default/clients/nuxtjs/server/supabase/client'\n\nexport default defineEventHandler(async (event) => {\n const supabase = createSupabaseServerClient(event)\n\n // Get user claims\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n const pathname = event.node.req.url || '/'\n\n // Redirect if no user and not already on login/auth route\n if (\n !user &&\n !pathname.startsWith('/login') &&\n !pathname.startsWith('/auth')\n ) {\n return sendRedirect(event, '/auth/login')\n }\n\n // Return event as-is (you could return any object if needed)\n return { user }\n})\n", + "content": "import { defineEventHandler, sendRedirect } from 'h3'\n\nimport { createSupabaseServerClient } from '@/registry/default/clients/nuxtjs/server/supabase/client'\n\nexport default defineEventHandler(async (event) => {\n const supabase = createSupabaseServerClient(event)\n\n // Get user claims\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n const pathname = event.node.req.url || '/'\n\n // Redirect if no user and not already on login/auth route\n if (!user && !pathname.startsWith('/login') && !pathname.startsWith('/auth')) {\n return sendRedirect(event, '/auth/login')\n }\n\n // Return event as-is (you could return any object if needed)\n return { user }\n})\n", "type": "registry:file", "target": "server/middleware/auth.ts" }, { "path": "registry/default/social-auth/nuxtjs/server/routes/auth/oauth.ts", - "content": "import { createSupabaseServerClient } from '@/registry/default/clients/nuxtjs/server/supabase/client'\nimport { defineEventHandler, getQuery, sendRedirect, getRequestURL } from \"h3\"\n\nexport default defineEventHandler(async (event) => {\n const url = getRequestURL(event) // URL object of the current request\n const query = getQuery(event)\n\n const code = query.code as string | undefined\n let next = (query.next as string | undefined) ?? \"/\"\n\n if (!next.startsWith(\"/\")) {\n next = \"/\"\n }\n\n if (code) {\n const supabase = createSupabaseServerClient(event)\n const { error } = await supabase.auth.exchangeCodeForSession(code)\n\n if (!error) {\n // Determine origin\n const forwardedHost = event.node.req.headers[\"x-forwarded-host\"] as string | undefined\n const isLocalEnv = process.env.NODE_ENV === \"development\"\n const origin = `${url.protocol}//${url.host}`\n\n if (isLocalEnv) {\n return sendRedirect(event, `${origin}${next}`)\n } else if (forwardedHost) {\n return sendRedirect(event, `https://${forwardedHost}${next}`)\n } else {\n return sendRedirect(event, `${origin}${next}`)\n }\n }\n }\n\n // fallback to error page\n const origin = `${url.protocol}//${url.host}`\n return sendRedirect(event, `${origin}/auth/error`)\n})\n", + "content": "import { defineEventHandler, getQuery, getRequestURL, sendRedirect } from 'h3'\n\nimport { createSupabaseServerClient } from '@/registry/default/clients/nuxtjs/server/supabase/client'\n\nexport default defineEventHandler(async (event) => {\n const url = getRequestURL(event) // URL object of the current request\n const query = getQuery(event)\n\n const code = query.code as string | undefined\n let next = (query.next as string | undefined) ?? '/'\n\n if (!next.startsWith('/')) {\n next = '/'\n }\n\n if (code) {\n const supabase = createSupabaseServerClient(event)\n const { error } = await supabase.auth.exchangeCodeForSession(code)\n\n if (!error) {\n // Determine origin\n const forwardedHost = event.node.req.headers['x-forwarded-host'] as string | undefined\n const isLocalEnv = process.env.NODE_ENV === 'development'\n const origin = `${url.protocol}//${url.host}`\n\n if (isLocalEnv) {\n return sendRedirect(event, `${origin}${next}`)\n } else if (forwardedHost) {\n return sendRedirect(event, `https://${forwardedHost}${next}`)\n } else {\n return sendRedirect(event, `${origin}${next}`)\n }\n }\n }\n\n // fallback to error page\n const origin = `${url.protocol}//${url.host}`\n return sendRedirect(event, `${origin}/auth/error`)\n})\n", "type": "registry:file", "target": "server/routes/auth/oauth.ts" } diff --git a/apps/ui-library/public/r/social-auth-react-router.json b/apps/ui-library/public/r/social-auth-react-router.json index f51750595e37b..b6d80cfa45945 100644 --- a/apps/ui-library/public/r/social-auth-react-router.json +++ b/apps/ui-library/public/r/social-auth-react-router.json @@ -53,18 +53,18 @@ }, { "path": "registry/default/clients/react-router/lib/supabase/client.ts", - "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/react-router/lib/supabase/server.ts", - "content": "import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'\n\nexport function createClient(request: Request) {\n const headers = new Headers()\n\n const supabase = createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return parseCookieHeader(request.headers.get('Cookie') ?? '') as {\n name: string\n value: string\n }[]\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value, options }) =>\n headers.append('Set-Cookie', serializeCookieHeader(name, value, options))\n )\n },\n },\n }\n )\n\n return { supabase, headers }\n}\n", + "content": "import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'\n\nexport function createClient(request: Request) {\n const headers = new Headers()\n\n const supabase = createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return parseCookieHeader(request.headers.get('Cookie') ?? '') as {\n name: string\n value: string\n }[]\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value, options }) =>\n headers.append('Set-Cookie', serializeCookieHeader(name, value, options))\n )\n },\n },\n }\n )\n\n return { supabase, headers }\n}\n", "type": "registry:lib" } ], "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/social-auth-react.json b/apps/ui-library/public/r/social-auth-react.json index 21b7e55a99923..c2d0283d20b38 100644 --- a/apps/ui-library/public/r/social-auth-react.json +++ b/apps/ui-library/public/r/social-auth-react.json @@ -19,13 +19,13 @@ }, { "path": "registry/default/clients/react/lib/supabase/client.ts", - "content": "import { createClient as createSupabaseClient } from '@supabase/supabase-js'\n\nexport function createClient() {\n return createSupabaseClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "import { createClient as createSupabaseClient } from '@supabase/supabase-js'\n\nexport function createClient() {\n return createSupabaseClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" } ], "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/social-auth-tanstack.json b/apps/ui-library/public/r/social-auth-tanstack.json index 8dbcf95c31095..4dddd3c74694a 100644 --- a/apps/ui-library/public/r/social-auth-tanstack.json +++ b/apps/ui-library/public/r/social-auth-tanstack.json @@ -55,18 +55,18 @@ }, { "path": "registry/default/clients/tanstack/lib/supabase/client.ts", - "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/tanstack/lib/supabase/server.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { getCookies, setCookie } from '@tanstack/react-start/server'\n\nexport function createClient() {\n return createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return Object.entries(getCookies()).map(\n ([name, value]) =>\n ({\n name,\n value,\n }) as { name: string; value: string }\n )\n },\n setAll(cookies) {\n cookies.forEach((cookie) => {\n setCookie(cookie.name, cookie.value)\n })\n },\n },\n }\n )\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { getCookies, setCookie } from '@tanstack/react-start/server'\n\nexport function createClient() {\n return createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return Object.entries(getCookies()).map(\n ([name, value]) =>\n ({\n name,\n value,\n }) as { name: string; value: string }\n )\n },\n setAll(cookies) {\n cookies.forEach((cookie) => {\n setCookie(cookie.name, cookie.value)\n })\n },\n },\n }\n )\n}\n", "type": "registry:lib" } ], "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/supabase-client-nextjs.json b/apps/ui-library/public/r/supabase-client-nextjs.json index 79aecf0b75bfc..c7925d9f8d7ce 100644 --- a/apps/ui-library/public/r/supabase-client-nextjs.json +++ b/apps/ui-library/public/r/supabase-client-nextjs.json @@ -12,23 +12,23 @@ "files": [ { "path": "registry/default/clients/nextjs/lib/supabase/client.ts", - "content": "import { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "import { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/nextjs/lib/supabase/middleware.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/nextjs/lib/supabase/server.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { cookies } from 'next/headers'\n\n/**\n * If using Fluid compute: Don't put this client in a global variable. Always create a new client within each\n * function when using it.\n */\nexport async function createClient() {\n const cookieStore = await cookies()\n\n return createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return cookieStore.getAll()\n },\n setAll(cookiesToSet) {\n try {\n cookiesToSet.forEach(({ name, value, options }) =>\n cookieStore.set(name, value, options)\n )\n } catch {\n // The `setAll` method was called from a Server Component.\n // This can be ignored if you have middleware refreshing\n // user sessions.\n }\n },\n },\n }\n )\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { cookies } from 'next/headers'\n\n/**\n * If using Fluid compute: Don't put this client in a global variable. Always create a new client within each\n * function when using it.\n */\nexport async function createClient() {\n const cookieStore = await cookies()\n\n return createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return cookieStore.getAll()\n },\n setAll(cookiesToSet) {\n try {\n cookiesToSet.forEach(({ name, value, options }) =>\n cookieStore.set(name, value, options)\n )\n } catch {\n // The `setAll` method was called from a Server Component.\n // This can be ignored if you have middleware refreshing\n // user sessions.\n }\n },\n },\n }\n )\n}\n", "type": "registry:lib" } ], "envVars": { "NEXT_PUBLIC_SUPABASE_URL": "", - "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/supabase-client-nuxtjs.json b/apps/ui-library/public/r/supabase-client-nuxtjs.json index 6aa1f242d47da..ba80599e34c68 100644 --- a/apps/ui-library/public/r/supabase-client-nuxtjs.json +++ b/apps/ui-library/public/r/supabase-client-nuxtjs.json @@ -12,31 +12,31 @@ "files": [ { "path": "registry/default/clients/nuxtjs/lib/supabase/client.ts", - "content": "import { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n process.env.NUXT_PUBLIC_SUPABASE_URL!,\n process.env.NUXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "import { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n process.env.NUXT_PUBLIC_SUPABASE_URL!,\n process.env.NUXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/nuxtjs/server/middleware/is-authenticated.ts", - "content": "import { defineNuxtRouteMiddleware, navigateTo, useRequestEvent } from 'nuxt/app'\nimport { createSupabaseServerClient } from '../supabase/client'\n\nexport default defineNuxtRouteMiddleware(async (to) => {\n const event = useRequestEvent()\n\n // create Supabase SSR client directly here\n const supabase = createSupabaseServerClient(event);\n\n // check current user\n const { data: { user } } = await supabase.auth.getUser()\n\n if (!user && to.path !== '/login') {\n return navigateTo('/login')\n }\n})\n", + "content": "import { defineNuxtRouteMiddleware, navigateTo, useRequestEvent } from 'nuxt/app'\n\nimport { createSupabaseServerClient } from '../supabase/client'\n\nexport default defineNuxtRouteMiddleware(async (to) => {\n const event = useRequestEvent()\n\n // create Supabase SSR client directly here\n const supabase = createSupabaseServerClient(event)\n\n // check current user\n const {\n data: { user },\n } = await supabase.auth.getUser()\n\n if (!user && to.path !== '/login') {\n return navigateTo('/login')\n }\n})\n", "type": "registry:file", "target": "server/middleware/is-authenticated.ts" }, { "path": "registry/default/clients/nuxtjs/server/api/profile.get.ts", - "content": "import { createError, defineEventHandler } from 'h3';\nimport { createSupabaseServerClient } from '../supabase/client';\n\nexport default defineEventHandler(async (event) => {\n // Create Supabase SSR client\n const supabase = createSupabaseServerClient(event)\n\n // Example: get user session\n const {\n data: { user },\n } = await supabase.auth.getUser();\n\n if (!user) {\n return { error: 'Not authenticated' };\n }\n\n // Fetch profile row\n const { data, error } = await supabase\n .from('profiles')\n .select('*')\n .eq('id', user.id)\n .single();\n\n if (error) {\n throw createError({ statusCode: 500, statusMessage: error.message });\n }\n\n return { profile: data };\n});\n", + "content": "import { createError, defineEventHandler } from 'h3'\n\nimport { createSupabaseServerClient } from '../supabase/client'\n\nexport default defineEventHandler(async (event) => {\n // Create Supabase SSR client\n const supabase = createSupabaseServerClient(event)\n\n // Example: get user session\n const {\n data: { user },\n } = await supabase.auth.getUser()\n\n if (!user) {\n return { error: 'Not authenticated' }\n }\n\n // Fetch profile row\n const { data, error } = await supabase.from('profiles').select('*').eq('id', user.id).single()\n\n if (error) {\n throw createError({ statusCode: 500, statusMessage: error.message })\n }\n\n return { profile: data }\n})\n", "type": "registry:file", "target": "server/api/profile.get.ts" }, { "path": "registry/default/clients/nuxtjs/server/supabase/client.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { getCookie, setCookie, deleteCookie, H3Event, EventHandlerRequest } from 'h3'\n\nexport const createSupabaseServerClient = (event: H3Event | undefined) => {\n return createServerClient(\n process.env.NUXT_PUBLIC_SUPABASE_URL!,\n process.env.NUXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n get: (key) => getCookie(event!, key),\n set: (key, value, options) => setCookie(event!, key, value, options),\n remove: (key, options) => deleteCookie(event!, key, options),\n },\n }\n )\n}", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { deleteCookie, EventHandlerRequest, getCookie, H3Event, setCookie } from 'h3'\n\nexport const createSupabaseServerClient = (event: H3Event | undefined) => {\n return createServerClient(\n process.env.NUXT_PUBLIC_SUPABASE_URL!,\n process.env.NUXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n get: (key) => getCookie(event!, key),\n set: (key, value, options) => setCookie(event!, key, value, options),\n remove: (key, options) => deleteCookie(event!, key, options),\n },\n }\n )\n}\n", "type": "registry:file", "target": "server/supabase/client.ts" } ], "envVars": { "NUXT_PUBLIC_SUPABASE_URL": "", - "NUXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "NUXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `NUXT_PUBLIC_SUPABASE_URL` and `NUXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `NUXT_PUBLIC_SUPABASE_URL` and `NUXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/supabase-client-react-router.json b/apps/ui-library/public/r/supabase-client-react-router.json index f8f716f37f6d3..f2aeaae96ffad 100644 --- a/apps/ui-library/public/r/supabase-client-react-router.json +++ b/apps/ui-library/public/r/supabase-client-react-router.json @@ -12,18 +12,18 @@ "files": [ { "path": "registry/default/clients/react-router/lib/supabase/client.ts", - "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/react-router/lib/supabase/server.ts", - "content": "import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'\n\nexport function createClient(request: Request) {\n const headers = new Headers()\n\n const supabase = createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return parseCookieHeader(request.headers.get('Cookie') ?? '') as {\n name: string\n value: string\n }[]\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value, options }) =>\n headers.append('Set-Cookie', serializeCookieHeader(name, value, options))\n )\n },\n },\n }\n )\n\n return { supabase, headers }\n}\n", + "content": "import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'\n\nexport function createClient(request: Request) {\n const headers = new Headers()\n\n const supabase = createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return parseCookieHeader(request.headers.get('Cookie') ?? '') as {\n name: string\n value: string\n }[]\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value, options }) =>\n headers.append('Set-Cookie', serializeCookieHeader(name, value, options))\n )\n },\n },\n }\n )\n\n return { supabase, headers }\n}\n", "type": "registry:lib" } ], "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/supabase-client-react.json b/apps/ui-library/public/r/supabase-client-react.json index 992f3105f7314..6787d3e699ff9 100644 --- a/apps/ui-library/public/r/supabase-client-react.json +++ b/apps/ui-library/public/r/supabase-client-react.json @@ -11,13 +11,13 @@ "files": [ { "path": "registry/default/clients/react/lib/supabase/client.ts", - "content": "import { createClient as createSupabaseClient } from '@supabase/supabase-js'\n\nexport function createClient() {\n return createSupabaseClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "import { createClient as createSupabaseClient } from '@supabase/supabase-js'\n\nexport function createClient() {\n return createSupabaseClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" } ], "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/supabase-client-tanstack.json b/apps/ui-library/public/r/supabase-client-tanstack.json index dcac7a273b227..ceb6837199d29 100644 --- a/apps/ui-library/public/r/supabase-client-tanstack.json +++ b/apps/ui-library/public/r/supabase-client-tanstack.json @@ -12,18 +12,18 @@ "files": [ { "path": "registry/default/clients/tanstack/lib/supabase/client.ts", - "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" }, { "path": "registry/default/clients/tanstack/lib/supabase/server.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { getCookies, setCookie } from '@tanstack/react-start/server'\n\nexport function createClient() {\n return createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return Object.entries(getCookies()).map(\n ([name, value]) =>\n ({\n name,\n value,\n }) as { name: string; value: string }\n )\n },\n setAll(cookies) {\n cookies.forEach((cookie) => {\n setCookie(cookie.name, cookie.value)\n })\n },\n },\n }\n )\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { getCookies, setCookie } from '@tanstack/react-start/server'\n\nexport function createClient() {\n return createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return Object.entries(getCookies()).map(\n ([name, value]) =>\n ({\n name,\n value,\n }) as { name: string; value: string }\n )\n },\n setAll(cookies) {\n cookies.forEach((cookie) => {\n setCookie(cookie.name, cookie.value)\n })\n },\n },\n }\n )\n}\n", "type": "registry:lib" } ], "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/public/r/supabase-client-vue.json b/apps/ui-library/public/r/supabase-client-vue.json index 18c9f129fa2e7..72943f4a08bab 100644 --- a/apps/ui-library/public/r/supabase-client-vue.json +++ b/apps/ui-library/public/r/supabase-client-vue.json @@ -11,13 +11,13 @@ "files": [ { "path": "registry/default/clients/vue/lib/supabase/client.ts", - "content": "/// \nimport { createClient as createSupabaseClient } from '@supabase/supabase-js'\n\nexport function createClient() {\n return createSupabaseClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "content": "/// \nimport { createClient as createSupabaseClient } from '@supabase/supabase-js'\n\nexport function createClient() {\n return createSupabaseClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", "type": "registry:lib" } ], "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`." } \ No newline at end of file diff --git a/apps/ui-library/registry/default/ai-editor-rules/writing-supabase-edge-functions.mdc b/apps/ui-library/registry/default/ai-editor-rules/writing-supabase-edge-functions.mdc index aea3f09db38f6..d81d46826f43f 100644 --- a/apps/ui-library/registry/default/ai-editor-rules/writing-supabase-edge-functions.mdc +++ b/apps/ui-library/registry/default/ai-editor-rules/writing-supabase-edge-functions.mdc @@ -18,7 +18,7 @@ You're an expert in writing TypeScript and Deno JavaScript runtime. Generate **h 7. Do NOT use `import { serve } from "https://deno.land/std@0.168.0/http/server.ts"`. Instead use the built-in `Deno.serve`. 8. Following environment variables (ie. secrets) are pre-populated in both local and hosted Supabase environments. Users don't need to manually set them: - SUPABASE_URL - - SUPABASE_PUBLISHABLE_OR_ANON_KEY + - SUPABASE_PUBLISHABLE_KEY - SUPABASE_SERVICE_ROLE_KEY - SUPABASE_DB_URL 9. To set other environment variables (ie. secrets) users can put them in a env file and run the `supabase secrets set --env-file path/to/env-file` diff --git a/apps/ui-library/registry/default/clients/nextjs/lib/supabase/client.ts b/apps/ui-library/registry/default/clients/nextjs/lib/supabase/client.ts index ccc8cf2e900ef..f5e7647176201 100644 --- a/apps/ui-library/registry/default/clients/nextjs/lib/supabase/client.ts +++ b/apps/ui-library/registry/default/clients/nextjs/lib/supabase/client.ts @@ -3,6 +3,6 @@ import { createBrowserClient } from '@supabase/ssr' export function createClient() { return createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY! + process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY! ) } diff --git a/apps/ui-library/registry/default/clients/nextjs/lib/supabase/middleware.ts b/apps/ui-library/registry/default/clients/nextjs/lib/supabase/middleware.ts index 7a07f440f7cce..a022ea53f68a2 100644 --- a/apps/ui-library/registry/default/clients/nextjs/lib/supabase/middleware.ts +++ b/apps/ui-library/registry/default/clients/nextjs/lib/supabase/middleware.ts @@ -10,7 +10,7 @@ export async function updateSession(request: NextRequest) { // variable. Always create a new one on each request. const supabase = createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!, + process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!, { cookies: { getAll() { diff --git a/apps/ui-library/registry/default/clients/nextjs/lib/supabase/server.ts b/apps/ui-library/registry/default/clients/nextjs/lib/supabase/server.ts index 3d42f5f99e2b0..31d476ea056e7 100644 --- a/apps/ui-library/registry/default/clients/nextjs/lib/supabase/server.ts +++ b/apps/ui-library/registry/default/clients/nextjs/lib/supabase/server.ts @@ -10,7 +10,7 @@ export async function createClient() { return createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!, + process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!, { cookies: { getAll() { diff --git a/apps/ui-library/registry/default/clients/nextjs/registry-item.json b/apps/ui-library/registry/default/clients/nextjs/registry-item.json index 95d2fe595d460..14af3620e5a13 100644 --- a/apps/ui-library/registry/default/clients/nextjs/registry-item.json +++ b/apps/ui-library/registry/default/clients/nextjs/registry-item.json @@ -6,10 +6,10 @@ "description": "", "registryDependencies": [], "dependencies": ["@supabase/ssr@latest", "@supabase/supabase-js@latest"], - "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "NEXT_PUBLIC_SUPABASE_URL": "", - "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY": "" }, "files": [ { diff --git a/apps/ui-library/registry/default/clients/react-router/lib/supabase/client.ts b/apps/ui-library/registry/default/clients/react-router/lib/supabase/client.ts index 2377b970a8bfe..3f34921e9f69d 100644 --- a/apps/ui-library/registry/default/clients/react-router/lib/supabase/client.ts +++ b/apps/ui-library/registry/default/clients/react-router/lib/supabase/client.ts @@ -4,6 +4,6 @@ import { createBrowserClient } from '@supabase/ssr' export function createClient() { return createBrowserClient( import.meta.env.VITE_SUPABASE_URL!, - import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY! + import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY! ) } diff --git a/apps/ui-library/registry/default/clients/react-router/lib/supabase/server.ts b/apps/ui-library/registry/default/clients/react-router/lib/supabase/server.ts index 28985885f37a0..2b62bb8de8325 100644 --- a/apps/ui-library/registry/default/clients/react-router/lib/supabase/server.ts +++ b/apps/ui-library/registry/default/clients/react-router/lib/supabase/server.ts @@ -5,7 +5,7 @@ export function createClient(request: Request) { const supabase = createServerClient( process.env.VITE_SUPABASE_URL!, - process.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!, + process.env.VITE_SUPABASE_PUBLISHABLE_KEY!, { cookies: { getAll() { diff --git a/apps/ui-library/registry/default/clients/react-router/registry-item.json b/apps/ui-library/registry/default/clients/react-router/registry-item.json index fdb7f8f60b975..e6ad7f7c26db6 100644 --- a/apps/ui-library/registry/default/clients/react-router/registry-item.json +++ b/apps/ui-library/registry/default/clients/react-router/registry-item.json @@ -6,10 +6,10 @@ "description": "", "registryDependencies": [], "dependencies": ["@supabase/ssr@latest", "@supabase/supabase-js@latest"], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, "files": [ { diff --git a/apps/ui-library/registry/default/clients/react/lib/supabase/client.ts b/apps/ui-library/registry/default/clients/react/lib/supabase/client.ts index c4ea49d065bb6..d8791b2bd1a18 100644 --- a/apps/ui-library/registry/default/clients/react/lib/supabase/client.ts +++ b/apps/ui-library/registry/default/clients/react/lib/supabase/client.ts @@ -3,6 +3,6 @@ import { createClient as createSupabaseClient } from '@supabase/supabase-js' export function createClient() { return createSupabaseClient( import.meta.env.VITE_SUPABASE_URL!, - import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY! + import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY! ) } diff --git a/apps/ui-library/registry/default/clients/react/registry-item.json b/apps/ui-library/registry/default/clients/react/registry-item.json index 0e85cea12bf18..7b1a5206b4fe4 100644 --- a/apps/ui-library/registry/default/clients/react/registry-item.json +++ b/apps/ui-library/registry/default/clients/react/registry-item.json @@ -6,10 +6,10 @@ "description": "", "registryDependencies": [], "dependencies": ["@supabase/supabase-js@latest"], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, "files": [ { diff --git a/apps/ui-library/registry/default/clients/tanstack/lib/supabase/client.ts b/apps/ui-library/registry/default/clients/tanstack/lib/supabase/client.ts index 2377b970a8bfe..3f34921e9f69d 100644 --- a/apps/ui-library/registry/default/clients/tanstack/lib/supabase/client.ts +++ b/apps/ui-library/registry/default/clients/tanstack/lib/supabase/client.ts @@ -4,6 +4,6 @@ import { createBrowserClient } from '@supabase/ssr' export function createClient() { return createBrowserClient( import.meta.env.VITE_SUPABASE_URL!, - import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY! + import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY! ) } diff --git a/apps/ui-library/registry/default/clients/tanstack/lib/supabase/server.ts b/apps/ui-library/registry/default/clients/tanstack/lib/supabase/server.ts index 57d3618959a03..5ed0ab2989f4b 100644 --- a/apps/ui-library/registry/default/clients/tanstack/lib/supabase/server.ts +++ b/apps/ui-library/registry/default/clients/tanstack/lib/supabase/server.ts @@ -4,7 +4,7 @@ import { getCookies, setCookie } from '@tanstack/react-start/server' export function createClient() { return createServerClient( process.env.VITE_SUPABASE_URL!, - process.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!, + process.env.VITE_SUPABASE_PUBLISHABLE_KEY!, { cookies: { getAll() { diff --git a/apps/ui-library/registry/default/clients/tanstack/registry-item.json b/apps/ui-library/registry/default/clients/tanstack/registry-item.json index c2b4cd2572574..19e879b8adb30 100644 --- a/apps/ui-library/registry/default/clients/tanstack/registry-item.json +++ b/apps/ui-library/registry/default/clients/tanstack/registry-item.json @@ -6,10 +6,10 @@ "description": "", "registryDependencies": [], "dependencies": ["@supabase/ssr@latest", "@supabase/supabase-js@latest"], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, "files": [ { diff --git a/apps/ui-library/registry/default/fixtures/lib/supabase/client.ts b/apps/ui-library/registry/default/fixtures/lib/supabase/client.ts index 2b27a35bd12b4..6fca173f0c077 100644 --- a/apps/ui-library/registry/default/fixtures/lib/supabase/client.ts +++ b/apps/ui-library/registry/default/fixtures/lib/supabase/client.ts @@ -6,6 +6,6 @@ import { Database } from '../../database.types' export function createClient() { return createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY! + process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY! ) } diff --git a/blocks/vue/__registry__/index.tsx b/blocks/vue/__registry__/index.tsx deleted file mode 100644 index 06dc12b2921db..0000000000000 --- a/blocks/vue/__registry__/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ - -// @ts-nocheck -// This file is autogenerated by scripts/build-registry.ts -// Do not edit this file directly. -import * as React from "react" - -export const Index: Record = { - "default": { - - "supabase-client-nuxtjs": { - name: "supabase-client-nuxtjs", - type: "registry:lib", - registryDependencies: [], - source: "", - files: ["registry/default/clients/nuxtjs/lib/supabase/client.ts","registry/default/clients/nuxtjs/lib/supabase/middleware.ts","registry/default/clients/nuxtjs/lib/supabase/server.ts"], - category: "undefined", - subcategory: "undefined", - chunks: [] - } - , - "supabase-client-vue": { - name: "supabase-client-vue", - type: "registry:lib", - registryDependencies: [], - source: "", - files: ["registry/default/clients/vue/lib/supabase/client.ts"], - category: "undefined", - subcategory: "undefined", - chunks: [] - } - - }, -} diff --git a/blocks/vue/lib/process-registry.ts b/blocks/vue/lib/process-registry.ts deleted file mode 100644 index 3d8bd3d04bfef..0000000000000 --- a/blocks/vue/lib/process-registry.ts +++ /dev/null @@ -1,86 +0,0 @@ -import * as fs from 'fs' - -export interface RegistryNode { - name: string - path: string - originalPath: string - type: 'directory' | 'file' - children?: RegistryNode[] - content?: string -} - -interface RegistryFile { - path: string - target?: string - type: string - content: string -} - -const DEFAULT_PATHS = { - component: '/components', - hook: '/hooks', - util: '/lib', -} as const - -/** - * Converts a flat registry array into a hierarchical file tree structure - */ -export function generateRegistryTree(registryPath: string): RegistryNode[] { - const registry = JSON.parse(fs.readFileSync(registryPath, 'utf-8')) as { files: RegistryFile[] } - const tree: RegistryNode[] = [] - - const sortedRegistry = [...registry.files].sort((a, b) => a.path.localeCompare(b.path)) - - for (const file of sortedRegistry) { - const itemPath = file.target || getDefaultPath(file) - const pathParts = itemPath.split('/').filter(Boolean) - let currentLevel = tree - - for (let i = 0; i < pathParts.length; i++) { - const part = pathParts[i] - const isLast = i === pathParts.length - 1 - const path = '/' + pathParts.slice(0, i + 1).join('/') - - let node = currentLevel.find((n) => n.name === part) - - // Remove any paths in the file content that point to the block directory. - const content = file.content - .replaceAll(/@\/registry\/default\/blocks\/.+?\//gi, '@/') - .replaceAll(/@\/registry\/default\/fixtures\//gi, '@/') - .replaceAll(/@\/registry\/default\//gi, '@/') - .replaceAll(/@\/clients\/.+?\//gi, '@/') - - if (!node) { - node = { - name: part, - path, - originalPath: file.path, - type: isLast ? 'file' : 'directory', - ...(isLast ? { content } : { children: [] }), - } - currentLevel.push(node) - } - - if (!isLast) { - node.children = node.children || [] - currentLevel = node.children - } - } - } - - return tree -} - -/** - * Determines the default path for an item based on its type - */ -function getDefaultPath(item: RegistryFile): string { - const type = item.type.toLowerCase() || '' - const basePath = DEFAULT_PATHS[type as keyof typeof DEFAULT_PATHS] || '' - // clean all paths that start with paths specific to this repo organization - const filePath = item.path - .replace(/registry\/default\/blocks\/.+?\//, '') - .replace(/registry\/default\/clients\/.+?\//, '') - - return `${basePath}/${filePath}` -} diff --git a/blocks/vue/package.json b/blocks/vue/package.json index 1eea69f6a699f..a2fc561f19e63 100644 --- a/blocks/vue/package.json +++ b/blocks/vue/package.json @@ -23,7 +23,8 @@ "vue-router": "^4.5.1" }, "devDependencies": { - "shadcn": "^3.0.0", + "@supabase/ssr": "workspace:*", + "shadcn": "^3.3.1", "tsconfig": "workspace:*", "vite": "catalog:" } diff --git a/blocks/vue/public/r/password-based-auth-nuxtjs.json b/blocks/vue/public/r/password-based-auth-nuxtjs.json deleted file mode 100644 index ee53a2e435ba2..0000000000000 --- a/blocks/vue/public/r/password-based-auth-nuxtjs.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "password-based-auth-nuxtjs", - "type": "registry:lib", - "title": "Password Based Auth flow for Nuxt.js and Supabase", - "description": "", - "dependencies": [ - "@supabase/ssr@latest", - "@supabase/supabase-js@latest" - ], - "registryDependencies": [ - "button", - "card", - "input", - "label" - ], - "files": [ - { - "path": "registry/default/password-based-auth/nuxtjs/app/components/login-form.vue", - "content": "\n\n\n", - "type": "registry:file", - "target": "app/components/login-form.vue" - }, - { - "path": "registry/default/password-based-auth/nuxtjs/app/components/sign-up-form.vue", - "content": "\n\n\n", - "type": "registry:file", - "target": "app/components/sign-up-form.vue" - }, - { - "path": "registry/default/password-based-auth/nuxtjs/app/components/forgot-password-form.vue", - "content": "\n\n\n", - "type": "registry:file", - "target": "app/components/forgot-password-form.vue" - }, - { - "path": "registry/default/password-based-auth/nuxtjs/app/components/update-password-form.vue", - "content": "\n\n\n", - "type": "registry:file", - "target": "app/components/update-password-form.vue" - }, - { - "path": "registry/default/password-based-auth/nuxtjs/app/pages/auth/login.vue", - "content": "\n\n", - "type": "registry:file", - "target": "app/pages/auth/login.vue" - }, - { - "path": "registry/default/password-based-auth/nuxtjs/app/pages/auth/sign-up.vue", - "content": "\n\n\n", - "type": "registry:file", - "target": "app/pages/auth/sign-up.vue" - }, - { - "path": "registry/default/password-based-auth/nuxtjs/app/pages/auth/forgot-password.vue", - "content": "\n\n\n", - "type": "registry:file", - "target": "app/pages/auth/forgot-password.vue" - }, - { - "path": "registry/default/password-based-auth/nuxtjs/app/pages/auth/update-password.vue", - "content": "\n\n\n", - "type": "registry:file", - "target": "app/pages/auth/update-password.vue" - }, - { - "path": "registry/default/password-based-auth/nuxtjs/app/pages/auth/error.vue", - "content": "\n\n", - "type": "registry:file", - "target": "app/pages/auth/error.vue" - }, - { - "path": "registry/default/password-based-auth/nuxtjs/app/pages/auth/sign-up-success.vue", - "content": "\n\n\n", - "type": "registry:file", - "target": "app/pages/auth/sign-up-success.vue" - }, - { - "path": "registry/default/password-based-auth/nuxtjs/app/pages/protected/index.vue", - "content": "\n\n\n", - "type": "registry:file", - "target": "app/pages/protected/index.vue" - }, - { - "path": "registry/default/password-based-auth/nuxtjs/server/routes/auth/confirm.get.ts", - "content": "import { createSupabaseServerClient } from '../../supabase/client'\nimport { getQuery, defineEventHandler, sendRedirect } from 'h3';\nimport type { EmailOtpType } from \"@supabase/supabase-js\"\n\nexport default defineEventHandler(async (event) => {\n const query = getQuery(event)\n const token_hash = query.token_hash as string | null\n const type = query.type as EmailOtpType | null\n const _next = query.next as string | undefined\n const next = _next?.startsWith(\"/\") ? _next : \"/\"\n\n if (token_hash && type) {\n const supabase = createSupabaseServerClient(event)\n\n const { error } = await supabase.auth.verifyOtp({\n type,\n token_hash,\n })\n\n if (!error) {\n return sendRedirect(event, next)\n } else {\n return sendRedirect(\n event,\n `/auth/error?error=${encodeURIComponent(error.message)}`\n )\n }\n }\n\n return sendRedirect(\n event,\n `/auth/error?error=${encodeURIComponent(\"No token hash or type\")}`\n )\n})", - "type": "registry:file", - "target": "server/routes/auth/confirm.get.ts" - } - ], - "envVars": { - "NUXT_PUBLIC_SUPABASE_URL": "", - "NUXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" - }, - "docs": "You'll need to set the following environment variables in your project: `NUXT_PUBLIC_SUPABASE_URL` and `NUXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." -} \ No newline at end of file diff --git a/blocks/vue/public/r/password-based-auth-vue.json b/blocks/vue/public/r/password-based-auth-vue.json deleted file mode 100644 index 7a54f1ff83cd8..0000000000000 --- a/blocks/vue/public/r/password-based-auth-vue.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "password-based-auth-vue", - "type": "registry:block", - "title": "Password Based Auth flow for Vue and Supabase", - "description": "Password Based Auth flow for Vue and Supabase", - "dependencies": [ - "@supabase/supabase-js@latest" - ], - "registryDependencies": [ - "button", - "card", - "input", - "label" - ], - "files": [ - { - "path": "registry/default/password-based-auth/vue/components/login-form.vue", - "content": "\n\n\n", - "type": "registry:file", - "target": "components/login-form.vue" - }, - { - "path": "registry/default/password-based-auth/vue/components/sign-up-form.vue", - "content": "\n\n\n", - "type": "registry:file", - "target": "components/sign-up-form.vue" - }, - { - "path": "registry/default/password-based-auth/vue/components/forgot-password-form.vue", - "content": "\n\n\n", - "type": "registry:file", - "target": "components/forgot-password-form.vue" - }, - { - "path": "registry/default/password-based-auth/vue/components/update-password-form.vue", - "content": "\n\n\n", - "type": "registry:file", - "target": "components/update-password-form.vue" - } - ] -} \ No newline at end of file diff --git a/blocks/vue/registry.json b/blocks/vue/registry.json deleted file mode 100644 index 7b719fbc10e49..0000000000000 --- a/blocks/vue/registry.json +++ /dev/null @@ -1,168 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry.json", - "name": "Supabase UI Library", - "homepage": "https://supabase.com/ui", - "items": [ - { - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "supabase-client-nuxtjs", - "type": "registry:lib", - "title": "Supabase Client for Nuxt.js", - "description": "", - "registryDependencies": [], - "dependencies": ["@supabase/ssr@latest", "@supabase/supabase-js@latest"], - "docs": "You'll need to set the following environment variables in your project: `NUXT_PUBLIC_SUPABASE_URL` and `NUXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", - "envVars": { - "NUXT_PUBLIC_SUPABASE_URL": "", - "NUXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" - }, - "files": [ - { - "path": "registry/default/clients/nuxtjs/lib/supabase/client.ts", - "type": "registry:lib" - }, - { - "path": "registry/default/clients/nuxtjs/server/middleware/is-authenticated.ts", - "type": "registry:file", - "target": "server/middleware/is-authenticated.ts" - }, - { - "path": "registry/default/clients/nuxtjs/server/api/profile.get.ts", - "type": "registry:file", - "target": "server/api/profile.get.ts" - }, - { - "path": "registry/default/clients/nuxtjs/server/supabase/client.ts", - "type": "registry:file", - "target": "server/supabase/client.ts" - } - ] - }, - { - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "supabase-client-vue", - "type": "registry:lib", - "title": "Supabase Client for Vue", - "description": "", - "registryDependencies": [], - "dependencies": ["@supabase/supabase-js@latest"], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", - "envVars": { - "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" - }, - "files": [ - { - "path": "registry/default/clients/vue/lib/supabase/client.ts", - "type": "registry:lib" - } - ] - }, - { - "name": "password-based-auth-vue", - "type": "registry:block", - "title": "Password Based Auth flow for Vue and Supabase", - "description": "Password Based Auth flow for Vue and Supabase", - "registryDependencies": ["button", "card", "input", "label"], - "files": [ - { - "path": "registry/default/password-based-auth/vue/components/login-form.vue", - "type": "registry:file", - "target": "components/login-form.vue" - }, - { - "path": "registry/default/password-based-auth/vue/components/sign-up-form.vue", - "type": "registry:file", - "target": "components/sign-up-form.vue" - }, - { - "path": "registry/default/password-based-auth/vue/components/forgot-password-form.vue", - "type": "registry:file", - "target": "components/forgot-password-form.vue" - }, - { - "path": "registry/default/password-based-auth/vue/components/update-password-form.vue", - "type": "registry:file", - "target": "components/update-password-form.vue" - } - ], - "dependencies": ["@supabase/supabase-js@latest"] - }, - { - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "password-based-auth-nuxtjs", - "type": "registry:lib", - "title": "Password Based Auth flow for Nuxt.js and Supabase", - "description": "", - "registryDependencies": ["button", "card", "input", "label"], - "dependencies": ["@supabase/ssr@latest", "@supabase/supabase-js@latest"], - "docs": "You'll need to set the following environment variables in your project: `NUXT_PUBLIC_SUPABASE_URL` and `NUXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", - "envVars": { - "NUXT_PUBLIC_SUPABASE_URL": "", - "NUXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" - }, - "files": [ - { - "path": "registry/default/password-based-auth/nuxtjs/app/components/login-form.vue", - "type": "registry:file", - "target": "app/components/login-form.vue" - }, - { - "path": "registry/default/password-based-auth/nuxtjs/app/components/sign-up-form.vue", - "type": "registry:file", - "target": "app/components/sign-up-form.vue" - }, - { - "path": "registry/default/password-based-auth/nuxtjs/app/components/forgot-password-form.vue", - "type": "registry:file", - "target": "app/components/forgot-password-form.vue" - }, - { - "path": "registry/default/password-based-auth/nuxtjs/app/components/update-password-form.vue", - "type": "registry:file", - "target": "app/components/update-password-form.vue" - }, - { - "path": "registry/default/password-based-auth/nuxtjs/app/pages/auth/login.vue", - "type": "registry:file", - "target": "app/pages/auth/login.vue" - }, - { - "path": "registry/default/password-based-auth/nuxtjs/app/pages/auth/sign-up.vue", - "type": "registry:file", - "target": "app/pages/auth/sign-up.vue" - }, - { - "path": "registry/default/password-based-auth/nuxtjs/app/pages/auth/forgot-password.vue", - "type": "registry:file", - "target": "app/pages/auth/forgot-password.vue" - }, - { - "path": "registry/default/password-based-auth/nuxtjs/app/pages/auth/update-password.vue", - "type": "registry:file", - "target": "app/pages/auth/update-password.vue" - }, - { - "path": "registry/default/password-based-auth/nuxtjs/app/pages/auth/error.vue", - "type": "registry:file", - "target": "app/pages/auth/error.vue" - }, - { - "path": "registry/default/password-based-auth/nuxtjs/app/pages/auth/sign-up-success.vue", - "type": "registry:file", - "target": "app/pages/auth/sign-up-success.vue" - }, - { - "path": "registry/default/password-based-auth/nuxtjs/app/pages/protected/index.vue", - "type": "registry:file", - "target": "app/pages/protected/index.vue" - }, - { - "path": "registry/default/password-based-auth/nuxtjs/server/routes/auth/confirm.get.ts", - "type": "registry:file", - "target": "server/routes/auth/confirm.get.ts" - } - ] - } - ] -} diff --git a/blocks/vue/registry/default/clients/nuxtjs/lib/supabase/client.ts b/blocks/vue/registry/default/clients/nuxtjs/lib/supabase/client.ts index 727df15730228..5c313eadc01e9 100644 --- a/blocks/vue/registry/default/clients/nuxtjs/lib/supabase/client.ts +++ b/blocks/vue/registry/default/clients/nuxtjs/lib/supabase/client.ts @@ -3,6 +3,6 @@ import { createBrowserClient } from '@supabase/ssr' export function createClient() { return createBrowserClient( process.env.NUXT_PUBLIC_SUPABASE_URL!, - process.env.NUXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY! + process.env.NUXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY! ) } diff --git a/blocks/vue/registry/default/clients/nuxtjs/registry-item.json b/blocks/vue/registry/default/clients/nuxtjs/registry-item.json index 7a12eb7cf38e4..17d1e4b15a537 100644 --- a/blocks/vue/registry/default/clients/nuxtjs/registry-item.json +++ b/blocks/vue/registry/default/clients/nuxtjs/registry-item.json @@ -6,10 +6,10 @@ "description": "", "registryDependencies": [], "dependencies": ["@supabase/ssr@latest", "@supabase/supabase-js@latest"], - "docs": "You'll need to set the following environment variables in your project: `NUXT_PUBLIC_SUPABASE_URL` and `NUXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `NUXT_PUBLIC_SUPABASE_URL` and `NUXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "NUXT_PUBLIC_SUPABASE_URL": "", - "NUXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "NUXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY": "" }, "files": [ { diff --git a/blocks/vue/registry/default/clients/nuxtjs/server/api/profile.get.ts b/blocks/vue/registry/default/clients/nuxtjs/server/api/profile.get.ts index bddd10a7d3e32..f97a2ff0fec51 100644 --- a/blocks/vue/registry/default/clients/nuxtjs/server/api/profile.get.ts +++ b/blocks/vue/registry/default/clients/nuxtjs/server/api/profile.get.ts @@ -1,5 +1,6 @@ -import { createError, defineEventHandler } from 'h3'; -import { createSupabaseServerClient } from '../supabase/client'; +import { createError, defineEventHandler } from 'h3' + +import { createSupabaseServerClient } from '../supabase/client' export default defineEventHandler(async (event) => { // Create Supabase SSR client @@ -8,22 +9,18 @@ export default defineEventHandler(async (event) => { // Example: get user session const { data: { user }, - } = await supabase.auth.getUser(); + } = await supabase.auth.getUser() if (!user) { - return { error: 'Not authenticated' }; + return { error: 'Not authenticated' } } // Fetch profile row - const { data, error } = await supabase - .from('profiles') - .select('*') - .eq('id', user.id) - .single(); + const { data, error } = await supabase.from('profiles').select('*').eq('id', user.id).single() if (error) { - throw createError({ statusCode: 500, statusMessage: error.message }); + throw createError({ statusCode: 500, statusMessage: error.message }) } - return { profile: data }; -}); + return { profile: data } +}) diff --git a/blocks/vue/registry/default/clients/nuxtjs/server/middleware/is-authenticated.ts b/blocks/vue/registry/default/clients/nuxtjs/server/middleware/is-authenticated.ts index f95070dd6cef4..96a360c7dc719 100644 --- a/blocks/vue/registry/default/clients/nuxtjs/server/middleware/is-authenticated.ts +++ b/blocks/vue/registry/default/clients/nuxtjs/server/middleware/is-authenticated.ts @@ -1,14 +1,17 @@ import { defineNuxtRouteMiddleware, navigateTo, useRequestEvent } from 'nuxt/app' + import { createSupabaseServerClient } from '../supabase/client' export default defineNuxtRouteMiddleware(async (to) => { const event = useRequestEvent() // create Supabase SSR client directly here - const supabase = createSupabaseServerClient(event); + const supabase = createSupabaseServerClient(event) // check current user - const { data: { user } } = await supabase.auth.getUser() + const { + data: { user }, + } = await supabase.auth.getUser() if (!user && to.path !== '/login') { return navigateTo('/login') diff --git a/blocks/vue/registry/default/clients/nuxtjs/server/supabase/client.ts b/blocks/vue/registry/default/clients/nuxtjs/server/supabase/client.ts index abbffcc2d1e8d..50254e89b8401 100644 --- a/blocks/vue/registry/default/clients/nuxtjs/server/supabase/client.ts +++ b/blocks/vue/registry/default/clients/nuxtjs/server/supabase/client.ts @@ -1,10 +1,10 @@ import { createServerClient } from '@supabase/ssr' -import { getCookie, setCookie, deleteCookie, H3Event, EventHandlerRequest } from 'h3' +import { deleteCookie, EventHandlerRequest, getCookie, H3Event, setCookie } from 'h3' export const createSupabaseServerClient = (event: H3Event | undefined) => { return createServerClient( process.env.NUXT_PUBLIC_SUPABASE_URL!, - process.env.NUXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!, + process.env.NUXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!, { cookies: { get: (key) => getCookie(event!, key), @@ -13,4 +13,4 @@ export const createSupabaseServerClient = (event: H3Event | }, } ) -} \ No newline at end of file +} diff --git a/blocks/vue/registry/default/clients/vue/lib/supabase/client.ts b/blocks/vue/registry/default/clients/vue/lib/supabase/client.ts index cd4773bfcaddb..4601d63f73f6a 100644 --- a/blocks/vue/registry/default/clients/vue/lib/supabase/client.ts +++ b/blocks/vue/registry/default/clients/vue/lib/supabase/client.ts @@ -4,6 +4,6 @@ import { createClient as createSupabaseClient } from '@supabase/supabase-js' export function createClient() { return createSupabaseClient( import.meta.env.VITE_SUPABASE_URL!, - import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY! + import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY! ) } diff --git a/blocks/vue/registry/default/clients/vue/registry-item.json b/blocks/vue/registry/default/clients/vue/registry-item.json index 438879c0a04ad..60d252587bacc 100644 --- a/blocks/vue/registry/default/clients/vue/registry-item.json +++ b/blocks/vue/registry/default/clients/vue/registry-item.json @@ -6,10 +6,10 @@ "description": "", "registryDependencies": [], "dependencies": ["@supabase/supabase-js@latest"], - "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", "envVars": { "VITE_SUPABASE_URL": "", - "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + "VITE_SUPABASE_PUBLISHABLE_KEY": "" }, "files": [ { diff --git a/blocks/vue/registry/default/dropzone/nuxtjs/app/composables/useSupabaseUpload.ts b/blocks/vue/registry/default/dropzone/nuxtjs/app/composables/useSupabaseUpload.ts index b057c6ef7cf3a..e1c93d67a1dbc 100644 --- a/blocks/vue/registry/default/dropzone/nuxtjs/app/composables/useSupabaseUpload.ts +++ b/blocks/vue/registry/default/dropzone/nuxtjs/app/composables/useSupabaseUpload.ts @@ -1,7 +1,8 @@ -import { ref, computed, watch, onUnmounted } from 'vue' import { useDropZone } from '@vueuse/core' +import { computed, onUnmounted, ref, watch } from 'vue' + // @ts-ignore -import { createClient } from "@/lib/supabase/client" +import { createClient } from '@/lib/supabase/client' const supabase = createClient() @@ -22,14 +23,10 @@ export type UseSupabaseUploadOptions = { function validateFileType(file: File, allowedTypes: string[]) { if (!allowedTypes.length) return [] - const isValid = allowedTypes.some(t => - t.endsWith('/*') - ? file.type.startsWith(t.replace('/*', '')) - : file.type === t + const isValid = allowedTypes.some((t) => + t.endsWith('/*') ? file.type.startsWith(t.replace('/*', '')) : file.type === t ) - return isValid - ? [] - : [{ code: 'invalid-type', message: 'Invalid file type' }] + return isValid ? [] : [{ code: 'invalid-type', message: 'Invalid file type' }] } function validateFileSize(file: File, maxSize: number) { @@ -65,7 +62,7 @@ export function useSupabaseUpload(options: UseSupabaseUploadOptions) { onDrop(droppedFiles: File[] | null) { if (!droppedFiles) return - const newFiles: FileWithPreview[] = droppedFiles.map(file => ({ + const newFiles: FileWithPreview[] = droppedFiles.map((file) => ({ ...(file as FileWithPreview), preview: URL.createObjectURL(file), errors: [ @@ -82,19 +79,17 @@ export function useSupabaseUpload(options: UseSupabaseUploadOptions) { loading.value = true try { - const filesWithErrors = errors.value.map(e => e.name) + const filesWithErrors = errors.value.map((e) => e.name) const filesToUpload = filesWithErrors.length > 0 ? files.value.filter( - f => - filesWithErrors.includes(f.name) || - !successes.value.includes(f.name) + (f) => filesWithErrors.includes(f.name) || !successes.value.includes(f.name) ) : files.value const responses = await Promise.all( - filesToUpload.map(async file => { + filesToUpload.map(async (file) => { const { error } = await supabase.storage .from(bucketName) .upload(path ? `${path}/${file.name}` : file.name, file, { @@ -108,15 +103,13 @@ export function useSupabaseUpload(options: UseSupabaseUploadOptions) { }) ) - errors.value = responses.filter((r): r is { name: string; message: string } => r.message !== undefined) + errors.value = responses.filter( + (r): r is { name: string; message: string } => r.message !== undefined + ) - const successful = responses - .filter(r => !r.message) - .map(r => r.name) + const successful = responses.filter((r) => !r.message).map((r) => r.name) - successes.value = Array.from( - new Set([...successes.value, ...successful]) - ) + successes.value = Array.from(new Set([...successes.value, ...successful])) } catch (err) { console.error('Upload failed unexpectedly:', err) @@ -129,7 +122,6 @@ export function useSupabaseUpload(options: UseSupabaseUploadOptions) { } } - watch( () => files.value.length, () => { @@ -150,8 +142,8 @@ export function useSupabaseUpload(options: UseSupabaseUploadOptions) { watch( files, (newFiles, oldFiles) => { - const newPreviews = new Set(newFiles.map(f => f.preview)) - oldFiles.forEach(file => { + const newPreviews = new Set(newFiles.map((f) => f.preview)) + oldFiles.forEach((file) => { if (file.preview && !newPreviews.has(file.preview)) { URL.revokeObjectURL(file.preview) } @@ -161,7 +153,7 @@ export function useSupabaseUpload(options: UseSupabaseUploadOptions) { ) onUnmounted(() => { - files.value.forEach(file => { + files.value.forEach((file) => { if (file.preview) { URL.revokeObjectURL(file.preview) } diff --git a/blocks/vue/registry/default/dropzone/nuxtjs/registry-item.json b/blocks/vue/registry/default/dropzone/nuxtjs/registry-item.json index e4be40a8c6cf4..2a098cb785aa4 100644 --- a/blocks/vue/registry/default/dropzone/nuxtjs/registry-item.json +++ b/blocks/vue/registry/default/dropzone/nuxtjs/registry-item.json @@ -27,4 +27,4 @@ } ], "dependencies": ["@supabase/supabase-js@latest", "@vueuse/core", "lucide-vue-next"] -} \ No newline at end of file +} diff --git a/blocks/vue/registry/default/dropzone/vue/composables/useSupabaseUpload.ts b/blocks/vue/registry/default/dropzone/vue/composables/useSupabaseUpload.ts index b057c6ef7cf3a..e1c93d67a1dbc 100644 --- a/blocks/vue/registry/default/dropzone/vue/composables/useSupabaseUpload.ts +++ b/blocks/vue/registry/default/dropzone/vue/composables/useSupabaseUpload.ts @@ -1,7 +1,8 @@ -import { ref, computed, watch, onUnmounted } from 'vue' import { useDropZone } from '@vueuse/core' +import { computed, onUnmounted, ref, watch } from 'vue' + // @ts-ignore -import { createClient } from "@/lib/supabase/client" +import { createClient } from '@/lib/supabase/client' const supabase = createClient() @@ -22,14 +23,10 @@ export type UseSupabaseUploadOptions = { function validateFileType(file: File, allowedTypes: string[]) { if (!allowedTypes.length) return [] - const isValid = allowedTypes.some(t => - t.endsWith('/*') - ? file.type.startsWith(t.replace('/*', '')) - : file.type === t + const isValid = allowedTypes.some((t) => + t.endsWith('/*') ? file.type.startsWith(t.replace('/*', '')) : file.type === t ) - return isValid - ? [] - : [{ code: 'invalid-type', message: 'Invalid file type' }] + return isValid ? [] : [{ code: 'invalid-type', message: 'Invalid file type' }] } function validateFileSize(file: File, maxSize: number) { @@ -65,7 +62,7 @@ export function useSupabaseUpload(options: UseSupabaseUploadOptions) { onDrop(droppedFiles: File[] | null) { if (!droppedFiles) return - const newFiles: FileWithPreview[] = droppedFiles.map(file => ({ + const newFiles: FileWithPreview[] = droppedFiles.map((file) => ({ ...(file as FileWithPreview), preview: URL.createObjectURL(file), errors: [ @@ -82,19 +79,17 @@ export function useSupabaseUpload(options: UseSupabaseUploadOptions) { loading.value = true try { - const filesWithErrors = errors.value.map(e => e.name) + const filesWithErrors = errors.value.map((e) => e.name) const filesToUpload = filesWithErrors.length > 0 ? files.value.filter( - f => - filesWithErrors.includes(f.name) || - !successes.value.includes(f.name) + (f) => filesWithErrors.includes(f.name) || !successes.value.includes(f.name) ) : files.value const responses = await Promise.all( - filesToUpload.map(async file => { + filesToUpload.map(async (file) => { const { error } = await supabase.storage .from(bucketName) .upload(path ? `${path}/${file.name}` : file.name, file, { @@ -108,15 +103,13 @@ export function useSupabaseUpload(options: UseSupabaseUploadOptions) { }) ) - errors.value = responses.filter((r): r is { name: string; message: string } => r.message !== undefined) + errors.value = responses.filter( + (r): r is { name: string; message: string } => r.message !== undefined + ) - const successful = responses - .filter(r => !r.message) - .map(r => r.name) + const successful = responses.filter((r) => !r.message).map((r) => r.name) - successes.value = Array.from( - new Set([...successes.value, ...successful]) - ) + successes.value = Array.from(new Set([...successes.value, ...successful])) } catch (err) { console.error('Upload failed unexpectedly:', err) @@ -129,7 +122,6 @@ export function useSupabaseUpload(options: UseSupabaseUploadOptions) { } } - watch( () => files.value.length, () => { @@ -150,8 +142,8 @@ export function useSupabaseUpload(options: UseSupabaseUploadOptions) { watch( files, (newFiles, oldFiles) => { - const newPreviews = new Set(newFiles.map(f => f.preview)) - oldFiles.forEach(file => { + const newPreviews = new Set(newFiles.map((f) => f.preview)) + oldFiles.forEach((file) => { if (file.preview && !newPreviews.has(file.preview)) { URL.revokeObjectURL(file.preview) } @@ -161,7 +153,7 @@ export function useSupabaseUpload(options: UseSupabaseUploadOptions) { ) onUnmounted(() => { - files.value.forEach(file => { + files.value.forEach((file) => { if (file.preview) { URL.revokeObjectURL(file.preview) } diff --git a/blocks/vue/registry/default/dropzone/vue/registry-item.json b/blocks/vue/registry/default/dropzone/vue/registry-item.json index 03578ca5cce05..483c302eea7b4 100644 --- a/blocks/vue/registry/default/dropzone/vue/registry-item.json +++ b/blocks/vue/registry/default/dropzone/vue/registry-item.json @@ -27,4 +27,4 @@ } ], "dependencies": ["@supabase/supabase-js@latest", "@vueuse/core", "lucide-vue-next"] -} \ No newline at end of file +} diff --git a/blocks/vue/registry/default/lib/utils.ts b/blocks/vue/registry/default/lib/utils.ts index 951433cb3b363..abba253f04c1f 100644 --- a/blocks/vue/registry/default/lib/utils.ts +++ b/blocks/vue/registry/default/lib/utils.ts @@ -1,7 +1,7 @@ -import type { ClassValue } from "clsx" -import { clsx } from "clsx" -import { twMerge } from "tailwind-merge" +import type { ClassValue } from 'clsx' +import { clsx } from 'clsx' +import { twMerge } from 'tailwind-merge' export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) -} \ No newline at end of file +} diff --git a/blocks/vue/registry/default/password-based-auth/nuxtjs/server/routes/auth/confirm.get.ts b/blocks/vue/registry/default/password-based-auth/nuxtjs/server/routes/auth/confirm.get.ts index c6f18dfecadfe..018b4fe2ce144 100644 --- a/blocks/vue/registry/default/password-based-auth/nuxtjs/server/routes/auth/confirm.get.ts +++ b/blocks/vue/registry/default/password-based-auth/nuxtjs/server/routes/auth/confirm.get.ts @@ -1,5 +1,6 @@ -import { getQuery, defineEventHandler, sendRedirect } from 'h3' import type { EmailOtpType } from '@supabase/supabase-js' +import { defineEventHandler, getQuery, sendRedirect } from 'h3' + import { createSupabaseServerClient } from '@/registry/default/clients/nuxtjs/server/supabase/client' export default defineEventHandler(async (event) => { diff --git a/blocks/vue/registry/default/realtime-cursor/nuxtjs/app/composables/useRealtimeCursors.ts b/blocks/vue/registry/default/realtime-cursor/nuxtjs/app/composables/useRealtimeCursors.ts index 7fccf11e5c65c..108e33313eb28 100644 --- a/blocks/vue/registry/default/realtime-cursor/nuxtjs/app/composables/useRealtimeCursors.ts +++ b/blocks/vue/registry/default/realtime-cursor/nuxtjs/app/composables/useRealtimeCursors.ts @@ -1,5 +1,6 @@ -import { ref, reactive, onMounted, onUnmounted } from 'vue' import { REALTIME_SUBSCRIBE_STATES, type RealtimeChannel } from '@supabase/supabase-js' +import { onMounted, onUnmounted, reactive, ref } from 'vue' + // @ts-ignore import { createClient } from '@/lib/supabase/client' @@ -47,11 +48,9 @@ function useThrottleCallback( const supabase = createClient() -const generateRandomColor = () => - `hsl(${Math.floor(Math.random() * 360)}, 100%, 70%)` +const generateRandomColor = () => `hsl(${Math.floor(Math.random() * 360)}, 100%, 70%)` -const generateRandomNumber = () => - Math.floor(Math.random() * 100) +const generateRandomNumber = () => Math.floor(Math.random() * 100) const EVENT_NAME = 'realtime-cursor-move' @@ -101,8 +100,10 @@ export function useRealtimeCursors({ }) } - const { run: handleMouseMove, cancel: cancelThrottle } = - useThrottleCallback(sendCursor, throttleMs) + const { run: handleMouseMove, cancel: cancelThrottle } = useThrottleCallback( + sendCursor, + throttleMs + ) onMounted(() => { const channel = supabase.channel(roomName) @@ -115,11 +116,15 @@ export function useRealtimeCursors({ Object.keys(cursors).forEach((k) => delete cursors[k]) channelRef.value = null }) - .on('presence', { event: 'leave' }, ({ leftPresences }: { leftPresences: Array<{ key: string }> }) => { - leftPresences.forEach(({ key }) => { - delete cursors[key] - }) - }) + .on( + 'presence', + { event: 'leave' }, + ({ leftPresences }: { leftPresences: Array<{ key: string }> }) => { + leftPresences.forEach(({ key }) => { + delete cursors[key] + }) + } + ) .on('presence', { event: 'join' }, () => { if (!cursorPayload.value) return diff --git a/blocks/vue/registry/default/realtime-cursor/nuxtjs/registry-item.json b/blocks/vue/registry/default/realtime-cursor/nuxtjs/registry-item.json index 3a2adc712634d..f39cf9a5fc41c 100644 --- a/blocks/vue/registry/default/realtime-cursor/nuxtjs/registry-item.json +++ b/blocks/vue/registry/default/realtime-cursor/nuxtjs/registry-item.json @@ -21,4 +21,4 @@ } ], "dependencies": ["@supabase/supabase-js@latest", "@vueuse/core", "lucide-vue-next"] -} \ No newline at end of file +} diff --git a/blocks/vue/registry/default/realtime-cursor/vue/composables/useRealtimeCursors.ts b/blocks/vue/registry/default/realtime-cursor/vue/composables/useRealtimeCursors.ts index 7fccf11e5c65c..108e33313eb28 100644 --- a/blocks/vue/registry/default/realtime-cursor/vue/composables/useRealtimeCursors.ts +++ b/blocks/vue/registry/default/realtime-cursor/vue/composables/useRealtimeCursors.ts @@ -1,5 +1,6 @@ -import { ref, reactive, onMounted, onUnmounted } from 'vue' import { REALTIME_SUBSCRIBE_STATES, type RealtimeChannel } from '@supabase/supabase-js' +import { onMounted, onUnmounted, reactive, ref } from 'vue' + // @ts-ignore import { createClient } from '@/lib/supabase/client' @@ -47,11 +48,9 @@ function useThrottleCallback( const supabase = createClient() -const generateRandomColor = () => - `hsl(${Math.floor(Math.random() * 360)}, 100%, 70%)` +const generateRandomColor = () => `hsl(${Math.floor(Math.random() * 360)}, 100%, 70%)` -const generateRandomNumber = () => - Math.floor(Math.random() * 100) +const generateRandomNumber = () => Math.floor(Math.random() * 100) const EVENT_NAME = 'realtime-cursor-move' @@ -101,8 +100,10 @@ export function useRealtimeCursors({ }) } - const { run: handleMouseMove, cancel: cancelThrottle } = - useThrottleCallback(sendCursor, throttleMs) + const { run: handleMouseMove, cancel: cancelThrottle } = useThrottleCallback( + sendCursor, + throttleMs + ) onMounted(() => { const channel = supabase.channel(roomName) @@ -115,11 +116,15 @@ export function useRealtimeCursors({ Object.keys(cursors).forEach((k) => delete cursors[k]) channelRef.value = null }) - .on('presence', { event: 'leave' }, ({ leftPresences }: { leftPresences: Array<{ key: string }> }) => { - leftPresences.forEach(({ key }) => { - delete cursors[key] - }) - }) + .on( + 'presence', + { event: 'leave' }, + ({ leftPresences }: { leftPresences: Array<{ key: string }> }) => { + leftPresences.forEach(({ key }) => { + delete cursors[key] + }) + } + ) .on('presence', { event: 'join' }, () => { if (!cursorPayload.value) return diff --git a/blocks/vue/registry/default/realtime-cursor/vue/registry-item.json b/blocks/vue/registry/default/realtime-cursor/vue/registry-item.json index 728f0007f767e..6c91266a459d3 100644 --- a/blocks/vue/registry/default/realtime-cursor/vue/registry-item.json +++ b/blocks/vue/registry/default/realtime-cursor/vue/registry-item.json @@ -3,7 +3,7 @@ "type": "registry:component", "title": "Realtime Cursor for Vue and Supabase", "description": "Component which renders realtime cursors from other users in a room.", - "files": [ + "files": [ { "path": "registry/default/realtime-cursor/vue/components/cursor.vue", "type": "registry:component", diff --git a/blocks/vue/registry/default/social-auth/nuxtjs/server/middleware/auth.ts b/blocks/vue/registry/default/social-auth/nuxtjs/server/middleware/auth.ts index e87ca43ea0ea7..c5cec1be2ffd9 100644 --- a/blocks/vue/registry/default/social-auth/nuxtjs/server/middleware/auth.ts +++ b/blocks/vue/registry/default/social-auth/nuxtjs/server/middleware/auth.ts @@ -1,4 +1,5 @@ import { defineEventHandler, sendRedirect } from 'h3' + import { createSupabaseServerClient } from '@/registry/default/clients/nuxtjs/server/supabase/client' export default defineEventHandler(async (event) => { @@ -11,11 +12,7 @@ export default defineEventHandler(async (event) => { const pathname = event.node.req.url || '/' // Redirect if no user and not already on login/auth route - if ( - !user && - !pathname.startsWith('/login') && - !pathname.startsWith('/auth') - ) { + if (!user && !pathname.startsWith('/login') && !pathname.startsWith('/auth')) { return sendRedirect(event, '/auth/login') } diff --git a/blocks/vue/registry/default/social-auth/nuxtjs/server/routes/auth/oauth.ts b/blocks/vue/registry/default/social-auth/nuxtjs/server/routes/auth/oauth.ts index 16994eb83d572..303b8817ed76d 100644 --- a/blocks/vue/registry/default/social-auth/nuxtjs/server/routes/auth/oauth.ts +++ b/blocks/vue/registry/default/social-auth/nuxtjs/server/routes/auth/oauth.ts @@ -1,15 +1,16 @@ +import { defineEventHandler, getQuery, getRequestURL, sendRedirect } from 'h3' + import { createSupabaseServerClient } from '@/registry/default/clients/nuxtjs/server/supabase/client' -import { defineEventHandler, getQuery, sendRedirect, getRequestURL } from "h3" export default defineEventHandler(async (event) => { const url = getRequestURL(event) // URL object of the current request const query = getQuery(event) const code = query.code as string | undefined - let next = (query.next as string | undefined) ?? "/" + let next = (query.next as string | undefined) ?? '/' - if (!next.startsWith("/")) { - next = "/" + if (!next.startsWith('/')) { + next = '/' } if (code) { @@ -18,8 +19,8 @@ export default defineEventHandler(async (event) => { if (!error) { // Determine origin - const forwardedHost = event.node.req.headers["x-forwarded-host"] as string | undefined - const isLocalEnv = process.env.NODE_ENV === "development" + const forwardedHost = event.node.req.headers['x-forwarded-host'] as string | undefined + const isLocalEnv = process.env.NODE_ENV === 'development' const origin = `${url.protocol}//${url.host}` if (isLocalEnv) { diff --git a/blocks/vue/registry/password-based-auth.ts b/blocks/vue/registry/password-based-auth.ts index 065b2843fd098..c84c0fd56270b 100644 --- a/blocks/vue/registry/password-based-auth.ts +++ b/blocks/vue/registry/password-based-auth.ts @@ -1,4 +1,5 @@ import { type Registry } from 'shadcn/schema' + import nuxtjs from './default/password-based-auth/nuxtjs/registry-item.json' with { type: 'json' } import vue from './default/password-based-auth/vue/registry-item.json' with { type: 'json' } diff --git a/blocks/vue/registry/social-auth.ts b/blocks/vue/registry/social-auth.ts index 8d7a0cabff6d1..a18671fb86832 100644 --- a/blocks/vue/registry/social-auth.ts +++ b/blocks/vue/registry/social-auth.ts @@ -1,5 +1,6 @@ import { type Registry } from 'shadcn/schema' -import vue from './default/social-auth/vue/registry-item.json' with { type: 'json' } + import nuxt from './default/social-auth/nuxtjs/registry-item.json' with { type: 'json' } +import vue from './default/social-auth/vue/registry-item.json' with { type: 'json' } export const socialAuth = [vue, nuxt] as Registry['items'] diff --git a/blocks/vue/tsconfig.base.json b/blocks/vue/tsconfig.base.json deleted file mode 100644 index d72a9f3a27835..0000000000000 --- a/blocks/vue/tsconfig.base.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "display": "Default", - "compilerOptions": { - "composite": false, - "declaration": true, - "declarationMap": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "inlineSources": false, - "isolatedModules": true, - "moduleResolution": "node", - "noUnusedLocals": false, - "noUnusedParameters": false, - "preserveWatchOutput": true, - "skipLibCheck": true, - "strict": true - }, - "exclude": ["node_modules"] -} diff --git a/blocks/vue/tsconfig.json b/blocks/vue/tsconfig.json index 7d0123109ecf5..6ac505703e98f 100644 --- a/blocks/vue/tsconfig.json +++ b/blocks/vue/tsconfig.json @@ -1,30 +1,31 @@ { "$schema": "https://json.schemastore.org/tsconfig", - "extends": "tsconfig/base.json", "compilerOptions": { + "composite": false, + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "inlineSources": false, + "isolatedModules": true, + "moduleResolution": "bundler", + "noUnusedLocals": false, + "noUnusedParameters": false, + "preserveWatchOutput": true, + "skipLibCheck": true, + "strict": true, "target": "es5", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, - "skipLibCheck": true, - "strict": true, - "forceConsistentCasingInFileNames": true, "noEmit": true, "incremental": true, - "esModuleInterop": true, "module": "esnext", - "moduleResolution": "bundler", "resolveJsonModule": true, - "isolatedModules": true, "jsx": "preserve", "paths": { "@/*": ["./*"] - }, - "plugins": [ - { - "name": "next" - } - ] + } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules", "./scripts/build-registry.mts"] + "include": ["**/*.ts", "**/*.tsx", "**/*.vue"], + "exclude": ["node_modules"] } diff --git a/blocks/vue/tsconfig.scripts.json b/blocks/vue/tsconfig.scripts.json deleted file mode 100644 index 7d81d8fcb5f4c..0000000000000 --- a/blocks/vue/tsconfig.scripts.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "extends": "./tsconfig.json", - "compilerOptions": { - "target": "es6", - "module": "ESNext", - "moduleResolution": "node", - "esModuleInterop": true, - "isolatedModules": false - }, - "include": [".contentlayer/generated", "scripts/**/*.ts"], - "exclude": ["node_modules"] -} diff --git a/package.json b/package.json index 927c83a605942..82556d44d35eb 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,8 @@ "dev:design-system": "turbo run dev --filter=design-system --parallel", "lint": "turbo run lint", "typecheck": "turbo --continue typecheck", - "test:prettier": "SORT_IMPORTS=false prettier --cache --check '{apps,packages}/**/*.{js,jsx,ts,tsx,css,md,json}'", - "format": "SORT_IMPORTS=false prettier --cache --write '{apps,packages}/**/*.{js,jsx,ts,tsx,css,md,mdx,json}'", + "test:prettier": "SORT_IMPORTS=false prettier --cache --check '{apps,packages,blocks}/**/*.{js,jsx,ts,tsx,css,md,json}'", + "format": "SORT_IMPORTS=false prettier --cache --write '{apps,packages,blocks}/**/*.{js,jsx,ts,tsx,css,md,mdx,json}'", "test:docs": "turbo run test --filter=docs", "test:ui": "turbo run test --filter=ui", "test:ui-patterns": "turbo run test --filter=ui-patterns", diff --git a/packages/ui-patterns/package.json b/packages/ui-patterns/package.json index 33465589fc36b..8368eb6fb6131 100644 --- a/packages/ui-patterns/package.json +++ b/packages/ui-patterns/package.json @@ -582,6 +582,10 @@ "import": "./src/admonition.tsx", "types": "./src/admonition.tsx" }, + "./MultipleCodeBlock": { + "import": "./src/MultipleCodeBlock/index.tsx", + "types": "./src/MultipleCodeBlock/index.tsx" + }, "./consent": { "import": "./src/consent.tsx", "types": "./src/consent.tsx" diff --git a/packages/ui-patterns/src/MultipleCodeBlock/index.tsx b/packages/ui-patterns/src/MultipleCodeBlock/index.tsx new file mode 100644 index 0000000000000..f9bb257ca1e6a --- /dev/null +++ b/packages/ui-patterns/src/MultipleCodeBlock/index.tsx @@ -0,0 +1,148 @@ +import { + CodeBlock, + CodeBlockLang, + TabsContent_Shadcn_, + TabsList_Shadcn_, + TabsTrigger_Shadcn_, + Tabs_Shadcn_, +} from 'ui' + +interface MultipleCodeBlockFile { + name: string + code: string + language?: string +} + +interface MultipleCodeBlockProps { + files: MultipleCodeBlockFile[] + value?: string + onValueChange?: (value: string) => void +} + +const languageAliases: Record = { + bash: 'bash', + csharp: 'csharp', + cs: 'csharp', + curl: 'curl', + dart: 'dart', + go: 'go', + http: 'http', + javascript: 'js', + js: 'js', + json: 'json', + jsx: 'jsx', + kotlin: 'kotlin', + pgsql: 'pgsql', + php: 'php', + py: 'python', + python: 'python', + sh: 'bash', + shell: 'bash', + sql: 'sql', + swift: 'swift', + ts: 'ts', + typescript: 'ts', + yaml: 'yaml', + yml: 'yaml', +} + +const extensionLanguageMap: Record = { + astro: 'html', + bash: 'bash', + cjs: 'js', + dart: 'dart', + go: 'go', + js: 'js', + json: 'json', + jsx: 'jsx', + kt: 'kotlin', + mjs: 'js', + php: 'php', + pgsql: 'pgsql', + py: 'python', + sh: 'bash', + sql: 'sql', + swift: 'swift', + svelte: 'html', + ts: 'ts', + vue: 'html', + yaml: 'yaml', + yml: 'yaml', +} + +const inferLanguageFromName = (name: string): CodeBlockLang | undefined => { + const lowerName = name.toLowerCase() + if (lowerName.startsWith('.env')) { + return 'bash' + } + + const extension = lowerName.split('.').pop() + if (!extension || extension === lowerName) { + return undefined + } + + return extensionLanguageMap[extension] +} + +const resolveLanguage = (language: string | undefined, name: string): CodeBlockLang => { + if (language) { + const normalized = language.toLowerCase() + const resolved = languageAliases[normalized] + if (resolved) { + return resolved + } + } + + return inferLanguageFromName(name) ?? 'js' +} + +export const MultipleCodeBlock = ({ files, value, onValueChange }: MultipleCodeBlockProps) => { + if (!files?.length) { + return null + } + + const defaultValue = files[0]?.name ?? '' + + const trimmedFiles = files.map((file) => ({ + ...file, + code: typeof file.code === 'string' ? file.code.trim() : file.code, + })) + + return ( + + + {files.map((file) => ( + + {file.name} + + ))} + + + {trimmedFiles.map((file) => ( + + + + ))} + + ) +} diff --git a/packages/ui/src/components/CodeBlock/CodeBlock.tsx b/packages/ui/src/components/CodeBlock/CodeBlock.tsx index 6d411ac568689..537083302060b 100644 --- a/packages/ui/src/components/CodeBlock/CodeBlock.tsx +++ b/packages/ui/src/components/CodeBlock/CodeBlock.tsx @@ -11,6 +11,7 @@ import csharp from 'react-syntax-highlighter/dist/cjs/languages/hljs/csharp' import dart from 'react-syntax-highlighter/dist/cjs/languages/hljs/dart' import go from 'react-syntax-highlighter/dist/cjs/languages/hljs/go' import http from 'react-syntax-highlighter/dist/cjs/languages/hljs/http' +import ini from 'react-syntax-highlighter/dist/cjs/languages/hljs/ini' import js from 'react-syntax-highlighter/dist/cjs/languages/hljs/javascript' import json from 'react-syntax-highlighter/dist/cjs/languages/hljs/json' import kotlin from 'react-syntax-highlighter/dist/cjs/languages/hljs/kotlin' @@ -21,7 +22,9 @@ import { default as python, } from 'react-syntax-highlighter/dist/cjs/languages/hljs/python' import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql' +import swift from 'react-syntax-highlighter/dist/cjs/languages/hljs/swift' import ts from 'react-syntax-highlighter/dist/cjs/languages/hljs/typescript' +import xml from 'react-syntax-highlighter/dist/cjs/languages/hljs/xml' import yaml from 'react-syntax-highlighter/dist/cjs/languages/hljs/yaml' import { copyToClipboard } from '../../lib/utils' @@ -46,7 +49,10 @@ export type CodeBlockLang = | 'python' | 'go' | 'pgsql' + | 'swift' | 'yaml' + | 'toml' + | 'html' export interface CodeBlockProps { title?: ReactNode @@ -159,6 +165,9 @@ export const CodeBlock = ({ SyntaxHighlighter.registerLanguage('python', python) SyntaxHighlighter.registerLanguage('go', go) SyntaxHighlighter.registerLanguage('pgsql', pgsql) + SyntaxHighlighter.registerLanguage('swift', swift) + SyntaxHighlighter.registerLanguage('html', xml) + SyntaxHighlighter.registerLanguage('toml', ini) SyntaxHighlighter.registerLanguage('yaml', yaml) const large = false diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1c1aeb52d947e..47f1440658198 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1993,7 +1993,7 @@ importers: version: 4.5.1(vue@3.5.21(typescript@5.9.2)) devDependencies: shadcn: - specifier: ^3.0.0 + specifier: ^3.3.1 version: 3.3.1(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(supports-color@8.1.1)(typescript@5.9.2) tsconfig: specifier: workspace:* @@ -12978,12 +12978,12 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + deprecated: Glob versions prior to v9 are no longer supported glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + deprecated: Glob versions prior to v9 are no longer supported glob@9.3.5: resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==}