diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 89dac734b..9478e85ba 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -2,11 +2,13 @@ The repository is a monorepo for the VitNode framework, which includes a backend API, frontend documentation site, and shared packages. The codebase uses modern web technologies and follows specific conventions for development based on Next.js 15 and Hono.js 4. +- Do not nest ternary operators, + ## Architecture & Key Patterns - **Monorepo Structure:** - `apps/` contains main apps (`api` for backend, `docs` for docs site) - - `packages/` holds shared code, core framework, ESLint/Prettier configs, and CLI tools + - `packages/` holds shared code, core framework, Biome configs, and CLI tools - `plugins/` for extendable features - **Frontend:** - Next.js 15, App Router, Server Components @@ -33,9 +35,9 @@ The repository is a monorepo for the VitNode framework, which includes a backend - `pnpm dev` (dev server), `pnpm build`, `pnpm lint`, `pnpm db:push`, `pnpm db:migrate`, `pnpm docker:dev` - **CLI:** - Create apps/plugins via `pnpm create vitnode-app@canary` (see `packages/create-vitnode-app`) - - CLI prompts for package manager, app mode, ESLint, Docker, install (see `questions.ts`) + - CLI prompts for package manager, app mode, Biome, Docker, install (see `questions.ts`) - **Linting/Formatting:** - - Use configs from `packages/eslint/` + - Use configs from `packages/config/` - File names: snake_case, ESModule only - TypeScript 5 strict mode - **Testing:** diff --git a/.github/docs/prd.md b/.github/docs/prd.md index 727839c0c..44b46333d 100644 --- a/.github/docs/prd.md +++ b/.github/docs/prd.md @@ -46,7 +46,7 @@ VitNode is designed for individual developers and small teams who need a structu ### CI/CD - Automated workflows using GitHub Actions: - - Code quality checks (ESLint, Prettier, TypeScript) + - Code quality checks (Biome, TypeScript) - Test suite execution with Vitest and Playwright - Dependency security scanning with npm audit - Automated builds and deployments to Vercel @@ -165,7 +165,7 @@ VitNode is designed for individual developers and small teams who need a structu - Turborepo for monorepo management - Vitest for unit testing - Playwright for end-to-end testing -- ESLint and Prettier for code quality +- Biome for code quality - Docker for containerization ## Features Planned for Future Releases diff --git a/.github/workflows/bump_publish.yml b/.github/workflows/bump_publish.yml index e5a558412..074e6b4c9 100644 --- a/.github/workflows/bump_publish.yml +++ b/.github/workflows/bump_publish.yml @@ -12,7 +12,7 @@ on: - stable - release-candidate type: - description: 'Type of package to publish' + description: "Type of package to publish" required: true type: choice options: @@ -20,7 +20,7 @@ on: - minor - major mode: - description: 'Mode of package to publish' + description: "Mode of package to publish" required: true type: choice options: @@ -28,13 +28,13 @@ on: - bump - publish skip_release_github_patch_notes: - description: 'Skip generating release notes for GitHub patch notes?' + description: "Skip generating release notes for GitHub patch notes?" required: false type: boolean jobs: bump-version: - name: 'Bump Version' + name: "Bump Version" runs-on: ubuntu-latest permissions: contents: write @@ -57,7 +57,7 @@ jobs: - name: Install Node.js uses: actions/setup-node@v4 with: - registry-url: 'https://registry.npmjs.org/' + registry-url: "https://registry.npmjs.org/" node-version: 22 - name: Install dependencies @@ -82,21 +82,21 @@ jobs: - name: Publish canary if: ${{ (github.event.inputs.mode == 'bump_and_publish' || github.event.inputs.mode == 'publish') && github.event.inputs.release == 'canary' }} - run: pnpm publish --filter create-vitnode-app --filter @vitnode/core --filter @vitnode/eslint-config --filter @vitnode/blog --tag canary --no-git-checks --access public + run: pnpm publish --filter create-vitnode-app --filter @vitnode/core --filter @vitnode/config --filter @vitnode/blog --tag canary --no-git-checks --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_CONFIG_PROVENANCE: true - name: Publish release candidate if: ${{ (github.event.inputs.mode == 'bump_and_publish' || github.event.inputs.mode == 'publish') && github.event.inputs.release == 'release-candidate' }} - run: pnpm publish --filter create-vitnode-app --filter @vitnode/core --filter @vitnode/eslint-config --filter @vitnode/blog --tag rc --no-git-checks --access public + run: pnpm publish --filter create-vitnode-app --filter @vitnode/core --filter @vitnode/config --filter @vitnode/blog --tag rc --no-git-checks --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_CONFIG_PROVENANCE: true - name: Publish stable if: ${{ (github.event.inputs.mode == 'bump_and_publish' || github.event.inputs.mode == 'publish') && github.event.inputs.release == 'stable' }} - run: pnpm publish --filter create-vitnode-app --filter @vitnode/core --filter @vitnode/eslint-config --filter @vitnode/blog --tag latest --no-git-checks --access public + run: pnpm publish --filter create-vitnode-app --filter @vitnode/core --filter @vitnode/config --filter @vitnode/blog --tag latest --no-git-checks --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_CONFIG_PROVENANCE: true diff --git a/.prettierrc.mjs b/.prettierrc.mjs deleted file mode 100644 index 92fe83d4d..000000000 --- a/.prettierrc.mjs +++ /dev/null @@ -1,11 +0,0 @@ -import vitnodePrettier from '@vitnode/eslint-config/prettierrc'; - -/** - * @see https://prettier.io/docs/en/configuration.html - * @type {import("prettier").Config} - */ -const config = { - ...vitnodePrettier, -}; - -export default config; diff --git a/.vscode/settings.json b/.vscode/settings.json index 3fac19c54..3af39be10 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,29 @@ ], "search.exclude": { "**/(plugins)/*": true + }, + "[javascriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[css]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[jsonc]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[html]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[json]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" } } diff --git a/apps/api/drizzle.config.ts b/apps/api/drizzle.config.ts index 0d630172b..789f77082 100644 --- a/apps/api/drizzle.config.ts +++ b/apps/api/drizzle.config.ts @@ -1,11 +1,11 @@ -import { defineVitNodeDrizzleConfig } from '@vitnode/core/drizzle.config'; +import { defineVitNodeDrizzleConfig } from "@vitnode/core/drizzle.config"; -import { POSTGRES_URL, vitNodeApiConfig } from './src/vitnode.api.config'; +import { POSTGRES_URL, vitNodeApiConfig } from "./src/vitnode.api.config"; export default defineVitNodeDrizzleConfig({ vitNodeApiConfig, - out: './migrations/', - dialect: 'postgresql', + out: "./migrations/", + dialect: "postgresql", dbCredentials: { url: POSTGRES_URL, }, diff --git a/apps/api/eslint.config.mjs b/apps/api/eslint.config.mjs deleted file mode 100644 index c1de0a4ee..000000000 --- a/apps/api/eslint.config.mjs +++ /dev/null @@ -1,20 +0,0 @@ -import eslintVitNode from '@vitnode/eslint-config/eslint'; -import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -export default [ - ...eslintVitNode, - { - ignores: ['drizzle.config.ts'], - }, - { - languageOptions: { - parserOptions: { - project: './tsconfig.json', - tsconfigRootDir: __dirname, - }, - }, - }, -]; diff --git a/apps/api/package.json b/apps/api/package.json index 911621dce..038b02b30 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -11,8 +11,6 @@ "dev:email": "email dev --dir src/emails", "build": "tsc && tsc-alias -p tsconfig.json", "start": "node dist/index.js", - "lint": "eslint .", - "lint:fix": "eslint . --fix", "drizzle-kit": "drizzle-kit" }, "dependencies": { @@ -34,9 +32,8 @@ "@types/node": "^24.3.0", "@types/react": "^19.1.10", "@types/react-dom": "^19.1.7", - "@vitnode/eslint-config": "workspace:*", + "@vitnode/config": "workspace:*", "dotenv": "^17.2.1", - "eslint": "^9.33.0", "react-email": "^4.2.8", "tsc-alias": "^1.8.16", "tsx": "^4.20.4", diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 28ac18442..27b8d96fd 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -1,10 +1,10 @@ -import { serve } from '@hono/node-server'; -import { OpenAPIHono } from '@hono/zod-openapi'; -import { VitNodeAPI } from '@vitnode/core/api/config'; +import { serve } from "@hono/node-server"; +import { OpenAPIHono } from "@hono/zod-openapi"; +import { VitNodeAPI } from "@vitnode/core/api/config"; -import { vitNodeApiConfig } from './vitnode.api.config.js'; +import { vitNodeApiConfig } from "./vitnode.api.config.js"; -const app = new OpenAPIHono().basePath('/api'); +const app = new OpenAPIHono().basePath("/api"); VitNodeAPI({ app, @@ -17,9 +17,9 @@ serve( port: 8080, }, info => { - const initMessage = '\x1b[34m[VitNode]\x1b[0m'; + const initMessage = "\x1b[34m[VitNode]\x1b[0m"; - // eslint-disable-next-line no-console + // biome-ignore lint/suspicious/noConsole: console.log( `${initMessage} API server is running on http://localhost:${info.port}`, ); diff --git a/apps/api/src/vitnode.api.config.ts b/apps/api/src/vitnode.api.config.ts index b0e127a3c..4e0d071bd 100644 --- a/apps/api/src/vitnode.api.config.ts +++ b/apps/api/src/vitnode.api.config.ts @@ -1,22 +1,21 @@ -import { NodemailerEmailAdapter } from '@vitnode/core/api/adapters/email/nodemailer'; -import { buildApiConfig } from '@vitnode/core/vitnode.config'; -import * as dotenv from 'dotenv'; -import { drizzle } from 'drizzle-orm/postgres-js'; +import { NodemailerEmailAdapter } from "@vitnode/core/api/adapters/email/nodemailer"; +import { buildApiConfig } from "@vitnode/core/vitnode.config"; +import { config } from "dotenv"; +import { drizzle } from "drizzle-orm/postgres-js"; -dotenv.config({ +config({ quiet: true, }); export const POSTGRES_URL = - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - process.env.POSTGRES_URL || 'postgresql://root:root@localhost:5432/vitnode'; + process.env.POSTGRES_URL || "postgresql://root:root@localhost:5432/vitnode"; export const vitNodeApiConfig = buildApiConfig({ plugins: [], pathToMessages: async path => await import(`./locales/${path}`), dbProvider: drizzle({ connection: POSTGRES_URL, - casing: 'camelCase', + casing: "camelCase", }), email: { adapter: NodemailerEmailAdapter({ @@ -26,12 +25,12 @@ export const vitNodeApiConfig = buildApiConfig({ user: process.env.NOD_EMAILER_USER, }), logo: { - text: 'VitNode Email Test', - src: 'http://localhost:3000/logo_vitnode_dark.png', + text: "VitNode Email Test", + src: "http://localhost:3000/logo_vitnode_dark.png", }, }, metadata: { - title: 'VitNode API', - shortTitle: 'VitNode', + title: "VitNode API", + shortTitle: "VitNode", }, }); diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json index 9498a3d91..4bc15ed4a 100644 --- a/apps/api/tsconfig.json +++ b/apps/api/tsconfig.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/tsconfig", - "extends": "@vitnode/eslint-config/tsconfig", + "extends": "@vitnode/config/tsconfig", "compilerOptions": { "target": "ESNext", "module": "NodeNext", diff --git a/apps/docs/content/docs/dev/contribution.mdx b/apps/docs/content/docs/dev/contribution.mdx index 724795825..e64ff982d 100644 --- a/apps/docs/content/docs/dev/contribution.mdx +++ b/apps/docs/content/docs/dev/contribution.mdx @@ -84,21 +84,21 @@ vitnode/ │ └── docs/ # Documentation site ├── packages/ │ ├── vitnode/ # Core framework code -│ ├── eslint/ # ESLint configurations for workspaces +│ ├── config/ # Shared Biome,TypeScript configs │ └── create-vitnode-app/ # CLI tool for creating new projects └── plugins/ # Official open-source plugins ``` - VitNode uses Turborepo to keep everything organized. Apps, packages, and - plugins all live together in harmony! + VitNode uses Turborepo to keep everything organized. Apps, packages, and plugins all live together + in harmony! ## Coding Standards - We use TypeScript for type safety ([TypeScript Docs](https://www.typescriptlang.org/)) - Follow ESM (ECMAScript Modules) conventions ([ESM Guide](https://nodejs.org/api/esm.html)) -- Respect the ESLint configuration in each workspace ([ESLint](https://eslint.org/)) +- Respect the Biome configuration in each workspace ([Biome](https://biomejs.dev/)) - Format your code with Prettier ([Prettier](https://prettier.io/)) - Use React Server Components where appropriate - Write meaningful commit messages ([Conventional Commits](https://www.conventionalcommits.org/)) @@ -139,7 +139,7 @@ pnpm db:migrate # Run migrations - TypeScript for type safety - ESM module conventions - React Server Components where appropriate - - Respect ESLint rules in each workspace + - Respect Biome rules in each workspace Don't forget to test your changes locally! 🧪 @@ -192,6 +192,4 @@ pnpm db:migrate # Run migrations Thank you for contributing to VitNode! 🚀 - - Every contribution makes VitNode better. We appreciate you! 💜 - +Every contribution makes VitNode better. We appreciate you! 💜 diff --git a/apps/docs/content/docs/ui/combobox.mdx b/apps/docs/content/docs/ui/combobox.mdx index 3190414cc..47c20801a 100644 --- a/apps/docs/content/docs/ui/combobox.mdx +++ b/apps/docs/content/docs/ui/combobox.mdx @@ -9,20 +9,20 @@ description: Select from a list of options with a search input. ## Usage -import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; +import { Tab, Tabs } from "fumadocs-ui/components/tabs"; ```ts -import { z } from 'zod'; -import { AutoForm } from '@vitnode/core/components/form/auto-form'; -import { AutoFormCombobox } from '@vitnode/core/components/form/fields/combobox'; +import { z } from "zod"; +import { AutoForm } from "@vitnode/core/components/form/auto-form"; +import { AutoFormCombobox } from "@vitnode/core/components/form/fields/combobox"; ``` ```ts const formSchema = z.object({ - type: z.enum(['option-one', 'option-two']), + type: z.enum(["option-one", "option-two"]) }); ``` @@ -31,25 +31,25 @@ const formSchema = z.object({ formSchema={formSchema} fields={[ { - id: 'type', - component: props => ( + id: "type", + component: (props) => ( - ), - }, + ) + } ]} /> ``` @@ -59,53 +59,49 @@ const formSchema = z.object({ ```tsx -'use client'; +"use client"; -import * as React from 'react'; -import { CheckIcon, ChevronsUpDownIcon } from 'lucide-react'; +import React from "react"; +import { CheckIcon, ChevronsUpDownIcon } from "lucide-react"; -import { cn } from '@vitnode/core/lib/utils'; -import { Button } from '@vitnode/core/components/ui/button'; +import { cn } from "@vitnode/core/lib/utils"; +import { Button } from "@vitnode/core/components/ui/button"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, - CommandList, -} from '@vitnode/core/components/ui/command'; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@vitnode/core/components/ui/popover'; + CommandList +} from "@vitnode/core/components/ui/command"; +import { Popover, PopoverContent, PopoverTrigger } from "@vitnode/core/components/ui/popover"; const frameworks = [ { - value: 'next.js', - label: 'Next.js', + value: "next.js", + label: "Next.js" }, { - value: 'sveltekit', - label: 'SvelteKit', + value: "sveltekit", + label: "SvelteKit" }, { - value: 'nuxt.js', - label: 'Nuxt.js', + value: "nuxt.js", + label: "Nuxt.js" }, { - value: 'remix', - label: 'Remix', + value: "remix", + label: "Remix" }, { - value: 'astro', - label: 'Astro', - }, + value: "astro", + label: "Astro" + } ]; export function ExampleCombobox() { const [open, setOpen] = React.useState(false); - const [value, setValue] = React.useState(''); + const [value, setValue] = React.useState(""); return ( @@ -117,8 +113,8 @@ export function ExampleCombobox() { className="w-[200px] justify-between" > {value - ? frameworks.find(framework => framework.value === value)?.label - : 'Select framework...'} + ? frameworks.find((framework) => framework.value === value)?.label + : "Select framework..."} @@ -128,19 +124,19 @@ export function ExampleCombobox() { No framework found. - {frameworks.map(framework => ( + {frameworks.map((framework) => ( { - setValue(currentValue === value ? '' : currentValue); + onSelect={(currentValue) => { + setValue(currentValue === value ? "" : currentValue); setOpen(false); }} > {framework.label} @@ -161,36 +157,34 @@ export function ExampleCombobox() { ## Props -import { TypeTable } from 'fumadocs-ui/components/type-table'; +import { TypeTable } from "fumadocs-ui/components/type-table"; ', - default: '[]', + description: "An array of options for the combobox, each with a value and label.", + type: "Array<{ value: string; label: string }>", + default: "[]" }, placeholder: { - description: 'Placeholder text for the combobox input.', - type: 'string', - default: 'Select an option', + description: "Placeholder text for the combobox input.", + type: "string", + default: "Select an option" }, searchPlaceholder: { - description: 'Placeholder text for the search input within the combobox.', - type: 'string', - default: 'Search...', - }, + description: "Placeholder text for the search input within the combobox.", + type: "string", + default: "Search..." + } }} /> diff --git a/apps/docs/drizzle.config.ts b/apps/docs/drizzle.config.ts index 0d630172b..789f77082 100644 --- a/apps/docs/drizzle.config.ts +++ b/apps/docs/drizzle.config.ts @@ -1,11 +1,11 @@ -import { defineVitNodeDrizzleConfig } from '@vitnode/core/drizzle.config'; +import { defineVitNodeDrizzleConfig } from "@vitnode/core/drizzle.config"; -import { POSTGRES_URL, vitNodeApiConfig } from './src/vitnode.api.config'; +import { POSTGRES_URL, vitNodeApiConfig } from "./src/vitnode.api.config"; export default defineVitNodeDrizzleConfig({ vitNodeApiConfig, - out: './migrations/', - dialect: 'postgresql', + out: "./migrations/", + dialect: "postgresql", dbCredentials: { url: POSTGRES_URL, }, diff --git a/apps/docs/e2e/auth.spec.ts b/apps/docs/e2e/auth.spec.ts index 929f0e584..247facef2 100644 --- a/apps/docs/e2e/auth.spec.ts +++ b/apps/docs/e2e/auth.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@playwright/test'; +import { expect, test } from "@playwright/test"; // Generate random user data for test isolation function generateTestUser() { @@ -11,16 +11,16 @@ function generateTestUser() { }; } -test.describe('Authentication', () => { +test.describe("Authentication", () => { const testUser = generateTestUser(); - test('should allow user registration', async ({ page }) => { + test("should allow user registration", async ({ page }) => { // Navigate to registration page - await page.goto('/register'); + await page.goto("/register"); // Wait for the form to be visible await expect( - page.getByRole('heading', { name: /register/i }), + page.getByRole("heading", { name: /register/i }), ).toBeVisible(); // Fill out registration form @@ -36,12 +36,12 @@ test.describe('Authentication', () => { await expect(page).toHaveURL(/\/verify-email|\/dashboard/); }); - test('should allow user login', async ({ page }) => { + test("should allow user login", async ({ page }) => { // Navigate to login page - await page.goto('/login'); + await page.goto("/login"); // Wait for the form to be visible - await expect(page.getByRole('heading', { name: /login/i })).toBeVisible(); + await expect(page.getByRole("heading", { name: /login/i })).toBeVisible(); // Fill out login form with previously registered credentials await page.getByLabel(/email/i).fill(testUser.email); @@ -50,6 +50,6 @@ test.describe('Authentication', () => { // Submit form await page.locator('button[type="submit"]').click(); - await expect(page.getByText('Start Your Journey!')).toBeVisible(); + await expect(page.getByText("Start Your Journey!")).toBeVisible(); }); }); diff --git a/apps/docs/e2e/homepage.spec.ts b/apps/docs/e2e/homepage.spec.ts index 6897e5e06..97da110b1 100644 --- a/apps/docs/e2e/homepage.spec.ts +++ b/apps/docs/e2e/homepage.spec.ts @@ -1,9 +1,11 @@ -import { expect, test } from '@playwright/test'; +import { expect, test } from "@playwright/test"; -test.describe('Homepage', () => { - test('should load successfully', async ({ page }) => { - await page.goto('/'); - await expect(page).toHaveTitle(/VitNode/); - await expect(page.getByText('Start Your Journey!')).toBeVisible(); +const vitNodeTitleRegex = /VitNode/; + +test.describe("Homepage", () => { + test("should load successfully", async ({ page }) => { + await page.goto("/"); + await expect(page).toHaveTitle(vitNodeTitleRegex); + await expect(page.getByText("Start Your Journey!")).toBeVisible(); }); }); diff --git a/apps/docs/eslint.config.mjs b/apps/docs/eslint.config.mjs deleted file mode 100644 index f049e77eb..000000000 --- a/apps/docs/eslint.config.mjs +++ /dev/null @@ -1,20 +0,0 @@ -import eslintVitNode from '@vitnode/eslint-config/eslint'; -import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -export default [ - ...eslintVitNode, - { - ignores: ['.source'], - }, - { - languageOptions: { - parserOptions: { - project: './tsconfig.json', - tsconfigRootDir: __dirname, - }, - }, - }, -]; diff --git a/apps/docs/global.d.ts b/apps/docs/global.d.ts index 604002bd0..2b865e5a5 100644 --- a/apps/docs/global.d.ts +++ b/apps/docs/global.d.ts @@ -1,9 +1,9 @@ /// -import type core from './src/locales/@vitnode/core/en.json'; -import type blog from './src/locales/@vitnode/blog/en.json'; +import blog from "./src/locales/@vitnode/blog/en.json" with { type: "json" }; +import core from "./src/locales/@vitnode/core/en.json" with { type: "json" }; -declare module 'next-intl' { +declare module "next-intl" { interface AppConfig { Messages: typeof core & typeof blog; } diff --git a/apps/docs/migrations/meta/0000_snapshot.json b/apps/docs/migrations/meta/0000_snapshot.json index b92d2f949..674182859 100644 --- a/apps/docs/migrations/meta/0000_snapshot.json +++ b/apps/docs/migrations/meta/0000_snapshot.json @@ -84,12 +84,8 @@ "name": "core_admin_permissions_roleId_core_roles_id_fk", "tableFrom": "core_admin_permissions", "tableTo": "core_roles", - "columnsFrom": [ - "roleId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["roleId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -97,12 +93,8 @@ "name": "core_admin_permissions_userId_core_users_id_fk", "tableFrom": "core_admin_permissions", "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -199,12 +191,8 @@ "name": "core_admin_sessions_userId_core_users_id_fk", "tableFrom": "core_admin_sessions", "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -212,12 +200,8 @@ "name": "core_admin_sessions_deviceId_core_sessions_known_devices_id_fk", "tableFrom": "core_admin_sessions", "tableTo": "core_sessions_known_devices", - "columnsFrom": [ - "deviceId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deviceId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -227,9 +211,7 @@ "core_admin_sessions_token_unique": { "name": "core_admin_sessions_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -345,9 +327,7 @@ "core_languages_code_unique": { "name": "core_languages_code_unique", "nullsNotDistinct": false, - "columns": [ - "code" - ] + "columns": ["code"] } }, "policies": {}, @@ -423,12 +403,8 @@ "name": "core_languages_words_languageCode_core_languages_code_fk", "tableFrom": "core_languages_words", "tableTo": "core_languages", - "columnsFrom": [ - "languageCode" - ], - "columnsTo": [ - "code" - ], + "columnsFrom": ["languageCode"], + "columnsTo": ["code"], "onDelete": "cascade", "onUpdate": "no action" } @@ -527,12 +503,8 @@ "name": "core_logs_userId_core_users_id_fk", "tableFrom": "core_logs", "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" } @@ -623,12 +595,8 @@ "name": "core_moderators_permissions_roleId_core_roles_id_fk", "tableFrom": "core_moderators_permissions", "tableTo": "core_roles", - "columnsFrom": [ - "roleId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["roleId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -636,12 +604,8 @@ "name": "core_moderators_permissions_userId_core_users_id_fk", "tableFrom": "core_moderators_permissions", "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -782,12 +746,8 @@ "name": "core_sessions_userId_core_users_id_fk", "tableFrom": "core_sessions", "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -795,12 +755,8 @@ "name": "core_sessions_deviceId_core_sessions_known_devices_id_fk", "tableFrom": "core_sessions", "tableTo": "core_sessions_known_devices", - "columnsFrom": [ - "deviceId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deviceId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -810,9 +766,7 @@ "core_sessions_token_unique": { "name": "core_sessions_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -878,9 +832,7 @@ "core_sessions_known_devices_publicId_unique": { "name": "core_sessions_known_devices_publicId_unique", "nullsNotDistinct": false, - "columns": [ - "publicId" - ] + "columns": ["publicId"] } }, "policies": {}, @@ -1026,12 +978,8 @@ "name": "core_users_roleId_core_roles_id_fk", "tableFrom": "core_users", "tableTo": "core_roles", - "columnsFrom": [ - "roleId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["roleId"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1039,12 +987,8 @@ "name": "core_users_language_core_languages_code_fk", "tableFrom": "core_users", "tableTo": "core_languages", - "columnsFrom": [ - "language" - ], - "columnsTo": [ - "code" - ], + "columnsFrom": ["language"], + "columnsTo": ["code"], "onDelete": "set default", "onUpdate": "no action" } @@ -1054,23 +998,17 @@ "core_users_nameCode_unique": { "name": "core_users_nameCode_unique", "nullsNotDistinct": false, - "columns": [ - "nameCode" - ] + "columns": ["nameCode"] }, "core_users_name_unique": { "name": "core_users_name_unique", "nullsNotDistinct": false, - "columns": [ - "name" - ] + "columns": ["name"] }, "core_users_email_unique": { "name": "core_users_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] } }, "policies": {}, @@ -1119,12 +1057,8 @@ "name": "core_users_confirm_emails_userId_core_users_id_fk", "tableFrom": "core_users_confirm_emails", "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1134,9 +1068,7 @@ "core_users_confirm_emails_token_unique": { "name": "core_users_confirm_emails_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -1191,12 +1123,8 @@ "name": "core_users_forgot_password_userId_core_users_id_fk", "tableFrom": "core_users_forgot_password", "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1206,16 +1134,12 @@ "core_users_forgot_password_userId_unique": { "name": "core_users_forgot_password_userId_unique", "nullsNotDistinct": false, - "columns": [ - "userId" - ] + "columns": ["userId"] }, "core_users_forgot_password_token_unique": { "name": "core_users_forgot_password_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -1280,12 +1204,8 @@ "name": "core_users_sso_userId_core_users_id_fk", "tableFrom": "core_users_sso", "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1340,9 +1260,7 @@ "blog_categories_titleSeo_unique": { "name": "blog_categories_titleSeo_unique", "nullsNotDistinct": false, - "columns": [ - "titleSeo" - ] + "columns": ["titleSeo"] } }, "policies": {}, @@ -1403,12 +1321,8 @@ "name": "blog_posts_categoryId_blog_categories_id_fk", "tableFrom": "blog_posts", "tableTo": "blog_categories", - "columnsFrom": [ - "categoryId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["categoryId"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1418,9 +1332,7 @@ "blog_posts_titleSeo_unique": { "name": "blog_posts_titleSeo_unique", "nullsNotDistinct": false, - "columns": [ - "titleSeo" - ] + "columns": ["titleSeo"] } }, "policies": {}, @@ -1439,4 +1351,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/apps/docs/migrations/meta/0001_snapshot.json b/apps/docs/migrations/meta/0001_snapshot.json index 556d11ac4..6d9c544d5 100644 --- a/apps/docs/migrations/meta/0001_snapshot.json +++ b/apps/docs/migrations/meta/0001_snapshot.json @@ -84,12 +84,8 @@ "name": "core_admin_permissions_roleId_core_roles_id_fk", "tableFrom": "core_admin_permissions", "tableTo": "core_roles", - "columnsFrom": [ - "roleId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["roleId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -97,12 +93,8 @@ "name": "core_admin_permissions_userId_core_users_id_fk", "tableFrom": "core_admin_permissions", "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -199,12 +191,8 @@ "name": "core_admin_sessions_userId_core_users_id_fk", "tableFrom": "core_admin_sessions", "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -212,12 +200,8 @@ "name": "core_admin_sessions_deviceId_core_sessions_known_devices_id_fk", "tableFrom": "core_admin_sessions", "tableTo": "core_sessions_known_devices", - "columnsFrom": [ - "deviceId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deviceId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -227,9 +211,7 @@ "core_admin_sessions_token_unique": { "name": "core_admin_sessions_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -345,9 +327,7 @@ "core_languages_code_unique": { "name": "core_languages_code_unique", "nullsNotDistinct": false, - "columns": [ - "code" - ] + "columns": ["code"] } }, "policies": {}, @@ -423,12 +403,8 @@ "name": "core_languages_words_languageCode_core_languages_code_fk", "tableFrom": "core_languages_words", "tableTo": "core_languages", - "columnsFrom": [ - "languageCode" - ], - "columnsTo": [ - "code" - ], + "columnsFrom": ["languageCode"], + "columnsTo": ["code"], "onDelete": "cascade", "onUpdate": "no action" } @@ -527,12 +503,8 @@ "name": "core_logs_userId_core_users_id_fk", "tableFrom": "core_logs", "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" } @@ -623,12 +595,8 @@ "name": "core_moderators_permissions_roleId_core_roles_id_fk", "tableFrom": "core_moderators_permissions", "tableTo": "core_roles", - "columnsFrom": [ - "roleId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["roleId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -636,12 +604,8 @@ "name": "core_moderators_permissions_userId_core_users_id_fk", "tableFrom": "core_moderators_permissions", "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -782,12 +746,8 @@ "name": "core_sessions_userId_core_users_id_fk", "tableFrom": "core_sessions", "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -795,12 +755,8 @@ "name": "core_sessions_deviceId_core_sessions_known_devices_id_fk", "tableFrom": "core_sessions", "tableTo": "core_sessions_known_devices", - "columnsFrom": [ - "deviceId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deviceId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -810,9 +766,7 @@ "core_sessions_token_unique": { "name": "core_sessions_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -878,9 +832,7 @@ "core_sessions_known_devices_publicId_unique": { "name": "core_sessions_known_devices_publicId_unique", "nullsNotDistinct": false, - "columns": [ - "publicId" - ] + "columns": ["publicId"] } }, "policies": {}, @@ -1026,12 +978,8 @@ "name": "core_users_roleId_core_roles_id_fk", "tableFrom": "core_users", "tableTo": "core_roles", - "columnsFrom": [ - "roleId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["roleId"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1039,12 +987,8 @@ "name": "core_users_language_core_languages_code_fk", "tableFrom": "core_users", "tableTo": "core_languages", - "columnsFrom": [ - "language" - ], - "columnsTo": [ - "code" - ], + "columnsFrom": ["language"], + "columnsTo": ["code"], "onDelete": "set default", "onUpdate": "no action" } @@ -1054,23 +998,17 @@ "core_users_nameCode_unique": { "name": "core_users_nameCode_unique", "nullsNotDistinct": false, - "columns": [ - "nameCode" - ] + "columns": ["nameCode"] }, "core_users_name_unique": { "name": "core_users_name_unique", "nullsNotDistinct": false, - "columns": [ - "name" - ] + "columns": ["name"] }, "core_users_email_unique": { "name": "core_users_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] } }, "policies": {}, @@ -1125,12 +1063,8 @@ "name": "core_users_confirm_emails_userId_core_users_id_fk", "tableFrom": "core_users_confirm_emails", "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1140,9 +1074,7 @@ "core_users_confirm_emails_token_unique": { "name": "core_users_confirm_emails_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -1197,12 +1129,8 @@ "name": "core_users_forgot_password_userId_core_users_id_fk", "tableFrom": "core_users_forgot_password", "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1212,16 +1140,12 @@ "core_users_forgot_password_userId_unique": { "name": "core_users_forgot_password_userId_unique", "nullsNotDistinct": false, - "columns": [ - "userId" - ] + "columns": ["userId"] }, "core_users_forgot_password_token_unique": { "name": "core_users_forgot_password_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -1286,12 +1210,8 @@ "name": "core_users_sso_userId_core_users_id_fk", "tableFrom": "core_users_sso", "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1346,9 +1266,7 @@ "blog_categories_titleSeo_unique": { "name": "blog_categories_titleSeo_unique", "nullsNotDistinct": false, - "columns": [ - "titleSeo" - ] + "columns": ["titleSeo"] } }, "policies": {}, @@ -1409,12 +1327,8 @@ "name": "blog_posts_categoryId_blog_categories_id_fk", "tableFrom": "blog_posts", "tableTo": "blog_categories", - "columnsFrom": [ - "categoryId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["categoryId"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1424,9 +1338,7 @@ "blog_posts_titleSeo_unique": { "name": "blog_posts_titleSeo_unique", "nullsNotDistinct": false, - "columns": [ - "titleSeo" - ] + "columns": ["titleSeo"] } }, "policies": {}, @@ -1445,4 +1357,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/apps/docs/migrations/meta/0002_snapshot.json b/apps/docs/migrations/meta/0002_snapshot.json index cce646f3e..07b31e4e7 100644 --- a/apps/docs/migrations/meta/0002_snapshot.json +++ b/apps/docs/migrations/meta/0002_snapshot.json @@ -84,12 +84,8 @@ "name": "core_admin_permissions_roleId_core_roles_id_fk", "tableFrom": "core_admin_permissions", "tableTo": "core_roles", - "columnsFrom": [ - "roleId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["roleId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -97,12 +93,8 @@ "name": "core_admin_permissions_userId_core_users_id_fk", "tableFrom": "core_admin_permissions", "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -199,12 +191,8 @@ "name": "core_admin_sessions_userId_core_users_id_fk", "tableFrom": "core_admin_sessions", "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -212,12 +200,8 @@ "name": "core_admin_sessions_deviceId_core_sessions_known_devices_id_fk", "tableFrom": "core_admin_sessions", "tableTo": "core_sessions_known_devices", - "columnsFrom": [ - "deviceId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deviceId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -227,9 +211,7 @@ "core_admin_sessions_token_unique": { "name": "core_admin_sessions_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -345,9 +327,7 @@ "core_languages_code_unique": { "name": "core_languages_code_unique", "nullsNotDistinct": false, - "columns": [ - "code" - ] + "columns": ["code"] } }, "policies": {}, @@ -423,12 +403,8 @@ "name": "core_languages_words_languageCode_core_languages_code_fk", "tableFrom": "core_languages_words", "tableTo": "core_languages", - "columnsFrom": [ - "languageCode" - ], - "columnsTo": [ - "code" - ], + "columnsFrom": ["languageCode"], + "columnsTo": ["code"], "onDelete": "cascade", "onUpdate": "no action" } @@ -527,12 +503,8 @@ "name": "core_logs_userId_core_users_id_fk", "tableFrom": "core_logs", "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" } @@ -623,12 +595,8 @@ "name": "core_moderators_permissions_roleId_core_roles_id_fk", "tableFrom": "core_moderators_permissions", "tableTo": "core_roles", - "columnsFrom": [ - "roleId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["roleId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -636,12 +604,8 @@ "name": "core_moderators_permissions_userId_core_users_id_fk", "tableFrom": "core_moderators_permissions", "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -782,12 +746,8 @@ "name": "core_sessions_userId_core_users_id_fk", "tableFrom": "core_sessions", "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -795,12 +755,8 @@ "name": "core_sessions_deviceId_core_sessions_known_devices_id_fk", "tableFrom": "core_sessions", "tableTo": "core_sessions_known_devices", - "columnsFrom": [ - "deviceId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deviceId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -810,9 +766,7 @@ "core_sessions_token_unique": { "name": "core_sessions_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -878,9 +832,7 @@ "core_sessions_known_devices_publicId_unique": { "name": "core_sessions_known_devices_publicId_unique", "nullsNotDistinct": false, - "columns": [ - "publicId" - ] + "columns": ["publicId"] } }, "policies": {}, @@ -1026,12 +978,8 @@ "name": "core_users_roleId_core_roles_id_fk", "tableFrom": "core_users", "tableTo": "core_roles", - "columnsFrom": [ - "roleId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["roleId"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1039,12 +987,8 @@ "name": "core_users_language_core_languages_code_fk", "tableFrom": "core_users", "tableTo": "core_languages", - "columnsFrom": [ - "language" - ], - "columnsTo": [ - "code" - ], + "columnsFrom": ["language"], + "columnsTo": ["code"], "onDelete": "set default", "onUpdate": "no action" } @@ -1054,23 +998,17 @@ "core_users_nameCode_unique": { "name": "core_users_nameCode_unique", "nullsNotDistinct": false, - "columns": [ - "nameCode" - ] + "columns": ["nameCode"] }, "core_users_name_unique": { "name": "core_users_name_unique", "nullsNotDistinct": false, - "columns": [ - "name" - ] + "columns": ["name"] }, "core_users_email_unique": { "name": "core_users_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] } }, "policies": {}, @@ -1125,12 +1063,8 @@ "name": "core_users_confirm_emails_userId_core_users_id_fk", "tableFrom": "core_users_confirm_emails", "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1140,9 +1074,7 @@ "core_users_confirm_emails_token_unique": { "name": "core_users_confirm_emails_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -1197,12 +1129,8 @@ "name": "core_users_forgot_password_userId_core_users_id_fk", "tableFrom": "core_users_forgot_password", "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1212,16 +1140,12 @@ "core_users_forgot_password_userId_unique": { "name": "core_users_forgot_password_userId_unique", "nullsNotDistinct": false, - "columns": [ - "userId" - ] + "columns": ["userId"] }, "core_users_forgot_password_token_unique": { "name": "core_users_forgot_password_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -1286,12 +1210,8 @@ "name": "core_users_sso_userId_core_users_id_fk", "tableFrom": "core_users_sso", "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1346,9 +1266,7 @@ "blog_categories_titleSeo_unique": { "name": "blog_categories_titleSeo_unique", "nullsNotDistinct": false, - "columns": [ - "titleSeo" - ] + "columns": ["titleSeo"] } }, "policies": {}, @@ -1409,12 +1327,8 @@ "name": "blog_posts_categoryId_blog_categories_id_fk", "tableFrom": "blog_posts", "tableTo": "blog_categories", - "columnsFrom": [ - "categoryId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["categoryId"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1424,9 +1338,7 @@ "blog_posts_titleSeo_unique": { "name": "blog_posts_titleSeo_unique", "nullsNotDistinct": false, - "columns": [ - "titleSeo" - ] + "columns": ["titleSeo"] } }, "policies": {}, @@ -1445,4 +1357,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/apps/docs/migrations/meta/_journal.json b/apps/docs/migrations/meta/_journal.json index 7d21da2ed..adee1e171 100644 --- a/apps/docs/migrations/meta/_journal.json +++ b/apps/docs/migrations/meta/_journal.json @@ -24,4 +24,4 @@ "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/apps/docs/next.config.ts b/apps/docs/next.config.ts index 9a1375d38..235d877c7 100644 --- a/apps/docs/next.config.ts +++ b/apps/docs/next.config.ts @@ -1,14 +1,19 @@ -import type { NextConfig } from 'next'; -import { createMDX } from 'fumadocs-mdx/next'; -import { vitNodeNextConfig } from '@vitnode/core/config/next.config'; +import nextAnalyzer from "@next/bundle-analyzer"; +import { vitNodeNextConfig } from "@vitnode/core/config/next.config"; +import { createMDX } from "fumadocs-mdx/next"; +import type { NextConfig } from "next"; const withMDX = createMDX(); +const withBundleAnalyzer = nextAnalyzer({ + enabled: process.env.ANALYZE === "true", +}); + const nextConfig: NextConfig = { experimental: { inlineCss: true, - reactCompiler: true, + reactCompiler: process.env.ANALYZE !== "true", }, }; -export default withMDX(vitNodeNextConfig(nextConfig)); +export default withBundleAnalyzer(withMDX(vitNodeNextConfig(nextConfig))); diff --git a/apps/docs/package.json b/apps/docs/package.json index fd7d33b73..bb4d23d46 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -11,9 +11,8 @@ "dev": "vitnode init && next dev --turbopack", "dev:email": "email dev --dir src/emails", "build": "next build --turbopack", + "build:analyze": "ANALYZE=true next build", "start": "next start", - "lint": "eslint .", - "lint:fix": "eslint . --fix", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui", "test:e2e:debug": "playwright test --debug", @@ -44,6 +43,7 @@ "sonner": "^2.0.7" }, "devDependencies": { + "@next/bundle-analyzer": "^15.5.0", "@playwright/test": "^1.55.0", "@react-email/components": "^0.5.1", "@tailwindcss/postcss": "^4.1.12", @@ -51,9 +51,8 @@ "@types/node": "^24.3.0", "@types/react": "^19.1.10", "@types/react-dom": "^19.1.7", - "@vitnode/eslint-config": "workspace:*", + "@vitnode/config": "workspace:*", "class-variance-authority": "^0.7.1", - "eslint": "^9.33.0", "postcss": "^8.5.6", "react-email": "^4.2.8", "shiki": "^3.11.0", diff --git a/apps/docs/playwright.config.ts b/apps/docs/playwright.config.ts index 81f4c6646..d4a4479a5 100644 --- a/apps/docs/playwright.config.ts +++ b/apps/docs/playwright.config.ts @@ -1,57 +1,57 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig, devices } from "@playwright/test"; /** * See https://playwright.dev/docs/test-configuration */ export default defineConfig({ - testDir: './e2e', + testDir: "./e2e", fullyParallel: true, forbidOnly: !!process.env.CI, /* Maximum time one test can run for */ timeout: 30 * 1000, /* Fail the build on CI if test failures */ - reporter: process.env.CI ? 'github' : 'html', + reporter: process.env.CI ? "github" : "html", /* Shared settings for all projects */ use: { /* Base URL to use in actions like `await page.goto('/')` */ - baseURL: process.env.TEST_BASE_URL ?? 'http://localhost:3000', + baseURL: process.env.TEST_BASE_URL ?? "http://localhost:3000", /* Collect trace when retrying the failed test */ - trace: 'on-first-retry', + trace: "on-first-retry", /* Take screenshots on failure */ - screenshot: 'only-on-failure', + screenshot: "only-on-failure", /* Record video on failure */ - video: 'on-first-retry', + video: "on-first-retry", }, /* Configure projects for different browsers */ projects: [ { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, + name: "chromium", + use: { ...devices["Desktop Chrome"] }, }, { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, + name: "firefox", + use: { ...devices["Desktop Firefox"] }, }, { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, + name: "webkit", + use: { ...devices["Desktop Safari"] }, }, /* Test against mobile viewports. */ { - name: 'mobile-chrome', - use: { ...devices['Pixel 7'] }, + name: "mobile-chrome", + use: { ...devices["Pixel 7"] }, }, { - name: 'mobile-safari', - use: { ...devices['iPhone 14'] }, + name: "mobile-safari", + use: { ...devices["iPhone 14"] }, }, ], /* Run web server for the tests */ webServer: { - command: 'pnpm dev', - url: 'http://localhost:3000', + command: "pnpm dev", + url: "http://localhost:3000", reuseExistingServer: !process.env.CI, - stdout: 'pipe', - stderr: 'pipe', + stdout: "pipe", + stderr: "pipe", }, }); diff --git a/apps/docs/postcss.config.mjs b/apps/docs/postcss.config.mjs index 5d6d8457f..79bcf135d 100644 --- a/apps/docs/postcss.config.mjs +++ b/apps/docs/postcss.config.mjs @@ -1,7 +1,7 @@ /** @type {import('postcss-load-config').Config} */ const config = { plugins: { - '@tailwindcss/postcss': {}, + "@tailwindcss/postcss": {}, }, }; diff --git a/apps/docs/source.config.ts b/apps/docs/source.config.ts index 47c48e97f..6b183f332 100644 --- a/apps/docs/source.config.ts +++ b/apps/docs/source.config.ts @@ -1,7 +1,7 @@ -import { defineConfig, defineDocs } from 'fumadocs-mdx/config'; +import { defineConfig, defineDocs } from "fumadocs-mdx/config"; export const docs = defineDocs({ - dir: 'content/docs', + dir: "content/docs", }); export default defineConfig({ diff --git a/apps/docs/src/app/[locale]/(docs)/docs/[[...slug]]/page.client.tsx b/apps/docs/src/app/[locale]/(docs)/docs/[[...slug]]/page.client.tsx index 3a1ae54d4..c33614755 100644 --- a/apps/docs/src/app/[locale]/(docs)/docs/[[...slug]]/page.client.tsx +++ b/apps/docs/src/app/[locale]/(docs)/docs/[[...slug]]/page.client.tsx @@ -1,32 +1,31 @@ -'use client'; +"use client"; // Source: https://github.com/fuma-nama/fumadocs/blob/dev/apps/docs/app/docs/%5B...slug%5D/page.client.tsx -import { cva } from 'class-variance-authority'; -import { buttonVariants } from 'fumadocs-ui/components/ui/button'; +import { cva } from "class-variance-authority"; +import { buttonVariants } from "fumadocs-ui/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger, -} from 'fumadocs-ui/components/ui/popover'; -import { ChevronDown } from 'fumadocs-ui/internal/icons'; -import { cn } from 'fumadocs-ui/utils/cn'; -import { ExternalLinkIcon, MessageCircleIcon } from 'lucide-react'; -import React from 'react'; +} from "fumadocs-ui/components/ui/popover"; +import { ChevronDown } from "fumadocs-ui/internal/icons"; +import { cn } from "fumadocs-ui/utils/cn"; +import { ExternalLinkIcon, MessageCircleIcon } from "lucide-react"; const optionVariants = cva( - 'text-sm p-2 rounded-lg inline-flex items-center gap-2 hover:text-fd-accent-foreground hover:bg-fd-accent [&_svg]:size-4', + "text-sm p-2 rounded-lg inline-flex items-center gap-2 hover:text-fd-accent-foreground hover:bg-fd-accent [&_svg]:size-4", ); export function ViewOptions(props: { githubUrl: string; markdownUrl: string }) { - const markdownUrl = new URL(props.markdownUrl, 'https://vitnode.com/'); + const markdownUrl = new URL(props.markdownUrl, "https://vitnode.com/"); const q = `Read ${markdownUrl}, I want to ask questions about it.`; const claude = `https://claude.ai/new?${new URLSearchParams({ q, })}`; const gpt = `https://chatgpt.com/?${new URLSearchParams({ - hints: 'search', + hints: "search", q, })}`; const t3 = `https://t3.chat/new?${new URLSearchParams({ @@ -38,9 +37,9 @@ export function ViewOptions(props: { githubUrl: string; markdownUrl: string }) { @@ -50,7 +49,7 @@ export function ViewOptions(props: { githubUrl: string; markdownUrl: string }) { {[ { - title: 'Open in GitHub', + title: "Open in GitHub", href: props.githubUrl, icon: ( @@ -60,7 +59,7 @@ export function ViewOptions(props: { githubUrl: string; markdownUrl: string }) { ), }, { - title: 'Open in ChatGPT', + title: "Open in ChatGPT", href: gpt, icon: ( , }, diff --git a/apps/docs/src/app/[locale]/(docs)/docs/[[...slug]]/page.tsx b/apps/docs/src/app/[locale]/(docs)/docs/[[...slug]]/page.tsx index 8184cbe18..52678a7c8 100644 --- a/apps/docs/src/app/[locale]/(docs)/docs/[[...slug]]/page.tsx +++ b/apps/docs/src/app/[locale]/(docs)/docs/[[...slug]]/page.tsx @@ -1,21 +1,21 @@ -import { redirect } from '@vitnode/core/lib/navigation'; -import { getBreadcrumbItems } from 'fumadocs-core/breadcrumb'; -import { Step, Steps } from 'fumadocs-ui/components/steps'; -import defaultMdxComponents from 'fumadocs-ui/mdx'; -import { DocsBody, DocsPage } from 'fumadocs-ui/page'; -import { notFound } from 'next/navigation'; +import { redirect } from "@vitnode/core/lib/navigation"; +import { getBreadcrumbItems } from "fumadocs-core/breadcrumb"; +import { Step, Steps } from "fumadocs-ui/components/steps"; +import defaultMdxComponents from "fumadocs-ui/mdx"; +import { DocsBody, DocsPage } from "fumadocs-ui/page"; +import { notFound } from "next/navigation"; -import { Preview } from '@/components/fumadocs/preview'; -import { source } from '@/lib/source'; +import { Preview } from "@/components/fumadocs/preview"; +import { source } from "@/lib/source"; -import { ViewOptions } from './page.client'; +import { ViewOptions } from "./page.client"; export default async function Page(props: { params: Promise<{ slug?: string[] }>; }) { const params = await props.params; if (!params.slug) { - await redirect('/docs/dev'); + await redirect("/docs/dev"); } const page = source.getPage(params.slug); if (!page) notFound(); @@ -25,7 +25,7 @@ export default async function Page(props: { item.name as string); return { - title: `${page.data.title}${lastItemsBreadcrumb.length > 0 ? ` - ${lastItemsBreadcrumb.join(' - ')}` : ''}`, + title: `${page.data.title}${lastItemsBreadcrumb.length > 0 ? ` - ${lastItemsBreadcrumb.join(" - ")}` : ""}`, description: page.data.description, }; } diff --git a/apps/docs/src/app/[locale]/(docs)/docs/layout.tsx b/apps/docs/src/app/[locale]/(docs)/docs/layout.tsx index 49a1f3fe9..f85b9512f 100644 --- a/apps/docs/src/app/[locale]/(docs)/docs/layout.tsx +++ b/apps/docs/src/app/[locale]/(docs)/docs/layout.tsx @@ -1,22 +1,21 @@ -import type { ReactNode } from 'react'; +import { DocsLayout } from "fumadocs-ui/layouts/notebook"; +import type { ReactNode } from "react"; -import { DocsLayout } from 'fumadocs-ui/layouts/notebook'; - -import { baseOptions } from '@/app/[locale]/(main)/layout.config'; -import { source } from '@/lib/source'; +import { baseOptions } from "@/app/[locale]/(main)/layout.config"; +import { source } from "@/lib/source"; export default function Layout({ children }: { children: ReactNode }) { return ( diff --git a/apps/docs/src/app/[locale]/(docs)/docs/not-found.tsx b/apps/docs/src/app/[locale]/(docs)/docs/not-found.tsx index 1c4db2aeb..5942f5eca 100644 --- a/apps/docs/src/app/[locale]/(docs)/docs/not-found.tsx +++ b/apps/docs/src/app/[locale]/(docs)/docs/not-found.tsx @@ -1,4 +1,4 @@ -import { ErrorView } from '@vitnode/core/views/error/error-view'; +import { ErrorView } from "@vitnode/core/views/error/error-view"; export default function NotFoundPage() { return ; diff --git a/apps/docs/src/app/[locale]/(main)/(home)/page.tsx b/apps/docs/src/app/[locale]/(main)/(home)/page.tsx index 7aa8558b3..5102ea8d0 100644 --- a/apps/docs/src/app/[locale]/(main)/(home)/page.tsx +++ b/apps/docs/src/app/[locale]/(main)/(home)/page.tsx @@ -1,19 +1,18 @@ -import type { Metadata } from 'next'; +import { buttonVariants } from "@vitnode/core/components/ui/button"; +import { cn } from "@vitnode/core/lib/utils"; +import Link from "fumadocs-core/link"; +import { ChevronRight } from "lucide-react"; +import type { Metadata } from "next"; -import { buttonVariants } from '@vitnode/core/components/ui/button'; -import { cn } from '@vitnode/core/lib/utils'; -import Link from 'fumadocs-core/link'; -import { ChevronRight } from 'lucide-react'; - -import { AnimatedBeamHome } from '../../../../components/animated-beam/animated-beam-home'; -import { AdminSection } from './sections/admin/admin'; -import { CallToActionSection } from './sections/call-to-action'; -import { PoweringBySection } from './sections/powering-by/powering-by'; +import { AnimatedBeamHome } from "../../../../components/animated-beam/animated-beam-home"; +import { AdminSection } from "./sections/admin/admin"; +import { CallToActionSection } from "./sections/call-to-action"; +import { PoweringBySection } from "./sections/powering-by/powering-by"; export const metadata: Metadata = { - title: 'VitNode: Extendable Framework for Building Apps', + title: "VitNode: Extendable Framework for Building Apps", description: - 'Build with Next.js and Hono.js. It provides a structured, plugin-based architecture with Admin Control Panel that makes development faster and less complex.', + "Build with Next.js and Hono.js. It provides a structured, plugin-based architecture with Admin Control Panel that makes development faster and less complex.", }; export default function HomePage() { @@ -35,7 +34,7 @@ export default function HomePage() { + GitHub { return ( @@ -14,7 +14,7 @@ export const AdminSection = () => {
-
+
payments illustration dark { return ( @@ -20,7 +20,7 @@ export const CallToActionSection = () => { lang="bash" wrapper={{ className: cn( - 'bg-background m-0 w-full sm:w-[calc(100%_-_10rem)]', + "bg-background m-0 w-full sm:w-[calc(100%_-_10rem)]", ), }} /> diff --git a/apps/docs/src/app/[locale]/(main)/(home)/sections/powering-by/logos/drizzleorm.tsx b/apps/docs/src/app/[locale]/(main)/(home)/sections/powering-by/logos/drizzleorm.tsx index 08cb9f942..ce4c9b0a2 100644 --- a/apps/docs/src/app/[locale]/(main)/(home)/sections/powering-by/logos/drizzleorm.tsx +++ b/apps/docs/src/app/[locale]/(main)/(home)/sections/powering-by/logos/drizzleorm.tsx @@ -7,38 +7,39 @@ export const DrizzleORMLogo = () => { viewBox="0 0 202 72" xmlns="http://www.w3.org/2000/svg" > + DrizzleORM logotype + /> + /> + /> + /> + /> ); }; diff --git a/apps/docs/src/app/[locale]/(main)/(home)/sections/powering-by/logos/honojs.tsx b/apps/docs/src/app/[locale]/(main)/(home)/sections/powering-by/logos/honojs.tsx index c9c95c482..d0e870f06 100644 --- a/apps/docs/src/app/[locale]/(main)/(home)/sections/powering-by/logos/honojs.tsx +++ b/apps/docs/src/app/[locale]/(main)/(home)/sections/powering-by/logos/honojs.tsx @@ -8,6 +8,7 @@ export const HonoJSLogo = () => { viewBox="0 0 76 98" xmlns="http://www.w3.org/2000/svg" > + HonoJS logotype { viewBox="0 0 499 120" xmlns="http://www.w3.org/2000/svg" > + next-intl logotype + /> + /> + /> + /> { ry="57.12" stroke="currentColor" strokeWidth="5.4" - > + /> + /> diff --git a/apps/docs/src/app/[locale]/(main)/(home)/sections/powering-by/logos/nextjs.tsx b/apps/docs/src/app/[locale]/(main)/(home)/sections/powering-by/logos/nextjs.tsx index bfe26ff94..1e11bd94e 100644 --- a/apps/docs/src/app/[locale]/(main)/(home)/sections/powering-by/logos/nextjs.tsx +++ b/apps/docs/src/app/[locale]/(main)/(home)/sections/powering-by/logos/nextjs.tsx @@ -9,37 +9,37 @@ export const NextJSLogo = () => { + /> + /> + /> + /> + /> + /> + /> + /> ); }; diff --git a/apps/docs/src/app/[locale]/(main)/(home)/sections/powering-by/logos/postgresql.tsx b/apps/docs/src/app/[locale]/(main)/(home)/sections/powering-by/logos/postgresql.tsx index b85d7f8c5..2ed300066 100644 --- a/apps/docs/src/app/[locale]/(main)/(home)/sections/powering-by/logos/postgresql.tsx +++ b/apps/docs/src/app/[locale]/(main)/(home)/sections/powering-by/logos/postgresql.tsx @@ -7,6 +7,7 @@ export const PostgreSQLLogo = () => { viewBox="0 0 25.6 25.6" xmlns="http://www.w3.org/2000/svg" > + PostgreSQL logotype { viewBox="0 0 262 33" xmlns="http://www.w3.org/2000/svg" > + TailwindCSS logotype { viewBox="0 0 473 76" width="473" > + TurboRepo logotype + /> + /> + /> + /> + /> + /> + /> + /> + /> + /> + /> { y1="11.5372" y2="42.4236" > - - + + diff --git a/apps/docs/src/app/[locale]/(main)/(home)/sections/powering-by/powering-by.tsx b/apps/docs/src/app/[locale]/(main)/(home)/sections/powering-by/powering-by.tsx index c3e345f88..31647306f 100644 --- a/apps/docs/src/app/[locale]/(main)/(home)/sections/powering-by/powering-by.tsx +++ b/apps/docs/src/app/[locale]/(main)/(home)/sections/powering-by/powering-by.tsx @@ -1,14 +1,14 @@ -import { Link } from '@vitnode/core/lib/navigation'; +import { Link } from "@vitnode/core/lib/navigation"; -import { InfiniteSlider } from '@/components/infinite-slider'; +import { InfiniteSlider } from "@/components/infinite-slider"; -import { DrizzleORMLogo } from './logos/drizzleorm'; -import { HonoJSLogo } from './logos/honojs'; -import { NextIntlLogo } from './logos/next-intl'; -import { NextJSLogo } from './logos/nextjs'; -import { PostgreSQLLogo } from './logos/postgresql'; -import { TailwindCSSLogo } from './logos/tailwindcss'; -import { TurboRepoLogo } from './logos/turborepo'; +import { DrizzleORMLogo } from "./logos/drizzleorm"; +import { HonoJSLogo } from "./logos/honojs"; +import { NextIntlLogo } from "./logos/next-intl"; +import { NextJSLogo } from "./logos/nextjs"; +import { PostgreSQLLogo } from "./logos/postgresql"; +import { TailwindCSSLogo } from "./logos/tailwindcss"; +import { TurboRepoLogo } from "./logos/turborepo"; export const PoweringBySection = () => { return ( @@ -79,8 +79,8 @@ export const PoweringBySection = () => { -
-
+
+
diff --git a/apps/docs/src/app/[locale]/(main)/(plugins)/(vitnode-core)/login/page.tsx b/apps/docs/src/app/[locale]/(main)/(plugins)/(vitnode-core)/login/page.tsx index bbdd08cbf..5c8eabfa7 100644 --- a/apps/docs/src/app/[locale]/(main)/(plugins)/(vitnode-core)/login/page.tsx +++ b/apps/docs/src/app/[locale]/(main)/(plugins)/(vitnode-core)/login/page.tsx @@ -1,8 +1,6 @@ -import type { Metadata } from 'next/dist/types'; - -import { getTranslations } from 'next-intl/server'; - -import { SignInView } from '@vitnode/core/views/auth/sign-in/sign-in-view'; +import { SignInView } from "@vitnode/core/views/auth/sign-in/sign-in-view"; +import type { Metadata } from "next/dist/types"; +import { getTranslations } from "next-intl/server"; export const generateMetadata = async ({ params, @@ -10,10 +8,10 @@ export const generateMetadata = async ({ params: Promise<{ locale: string }>; }): Promise => { const { locale } = await params; - const t = await getTranslations({ locale, namespace: 'core.global' }); + const t = await getTranslations({ locale, namespace: "core.global" }); return { - title: t('login'), + title: t("login"), }; }; diff --git a/apps/docs/src/app/[locale]/(main)/(plugins)/(vitnode-core)/login/reset-password/page.tsx b/apps/docs/src/app/[locale]/(main)/(plugins)/(vitnode-core)/login/reset-password/page.tsx index 069f6fdd1..c96f6fc61 100644 --- a/apps/docs/src/app/[locale]/(main)/(plugins)/(vitnode-core)/login/reset-password/page.tsx +++ b/apps/docs/src/app/[locale]/(main)/(plugins)/(vitnode-core)/login/reset-password/page.tsx @@ -1,8 +1,6 @@ -import type { Metadata } from 'next/dist/types'; - -import { getTranslations } from 'next-intl/server'; - -import { PasswordResetView } from '@vitnode/core/views/auth/password-reset/password-reset-view'; +import { PasswordResetView } from "@vitnode/core/views/auth/password-reset/password-reset-view"; +import type { Metadata } from "next/dist/types"; +import { getTranslations } from "next-intl/server"; export const generateMetadata = async ({ params, @@ -12,11 +10,11 @@ export const generateMetadata = async ({ const { locale } = await params; const t = await getTranslations({ locale, - namespace: 'core.auth.reset_password', + namespace: "core.auth.reset_password", }); return { - title: t('title'), + title: t("title"), }; }; diff --git a/apps/docs/src/app/[locale]/(main)/(plugins)/(vitnode-core)/login/sso/[providerId]/page.tsx b/apps/docs/src/app/[locale]/(main)/(plugins)/(vitnode-core)/login/sso/[providerId]/page.tsx index 31b3fd3f1..e52b56cbb 100644 --- a/apps/docs/src/app/[locale]/(main)/(plugins)/(vitnode-core)/login/sso/[providerId]/page.tsx +++ b/apps/docs/src/app/[locale]/(main)/(plugins)/(vitnode-core)/login/sso/[providerId]/page.tsx @@ -1,4 +1,4 @@ -import { CallbackSSOView } from '@vitnode/core/views/auth/sso/callback/callback-sso-view'; +import { CallbackSSOView } from "@vitnode/core/views/auth/sso/callback/callback-sso-view"; export default async function Page({ params, diff --git a/apps/docs/src/app/[locale]/(main)/(plugins)/(vitnode-core)/register/page.tsx b/apps/docs/src/app/[locale]/(main)/(plugins)/(vitnode-core)/register/page.tsx index 893a243f6..5a3ad9525 100644 --- a/apps/docs/src/app/[locale]/(main)/(plugins)/(vitnode-core)/register/page.tsx +++ b/apps/docs/src/app/[locale]/(main)/(plugins)/(vitnode-core)/register/page.tsx @@ -1,8 +1,6 @@ -import type { Metadata } from 'next/dist/types'; - -import { getTranslations } from 'next-intl/server'; - -import { SignUpView } from '@vitnode/core/views/auth/sign-up/sign-up-view'; +import { SignUpView } from "@vitnode/core/views/auth/sign-up/sign-up-view"; +import type { Metadata } from "next/dist/types"; +import { getTranslations } from "next-intl/server"; export const generateMetadata = async ({ params, @@ -10,10 +8,10 @@ export const generateMetadata = async ({ params: Promise<{ locale: string }>; }): Promise => { const { locale } = await params; - const t = await getTranslations({ locale, namespace: 'core.global' }); + const t = await getTranslations({ locale, namespace: "core.global" }); return { - title: t('register'), + title: t("register"), }; }; diff --git a/apps/docs/src/app/[locale]/(main)/[...rest]/page.tsx b/apps/docs/src/app/[locale]/(main)/[...rest]/page.tsx index 560ede6f4..64011c694 100644 --- a/apps/docs/src/app/[locale]/(main)/[...rest]/page.tsx +++ b/apps/docs/src/app/[locale]/(main)/[...rest]/page.tsx @@ -1,4 +1,4 @@ -import { notFound } from 'next/navigation'; +import { notFound } from "next/navigation"; export default function RestPage() { notFound(); diff --git a/apps/docs/src/app/[locale]/(main)/layout.client.tsx b/apps/docs/src/app/[locale]/(main)/layout.client.tsx index 711ebecd9..7ff71dd9f 100644 --- a/apps/docs/src/app/[locale]/(main)/layout.client.tsx +++ b/apps/docs/src/app/[locale]/(main)/layout.client.tsx @@ -1,7 +1,7 @@ -'use client'; +"use client"; -import { cn } from 'fumadocs-ui/utils/cn'; -import { useParams } from 'next/navigation'; +import { cn } from "fumadocs-ui/utils/cn"; +import { useParams } from "next/navigation"; export function useMode(): string | undefined { const { slug } = useParams(); diff --git a/apps/docs/src/app/[locale]/(main)/layout.config.tsx b/apps/docs/src/app/[locale]/(main)/layout.config.tsx index ceda38f48..703b9cafc 100644 --- a/apps/docs/src/app/[locale]/(main)/layout.config.tsx +++ b/apps/docs/src/app/[locale]/(main)/layout.config.tsx @@ -1,6 +1,6 @@ -import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; +import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared"; -import { LogoVitNode } from '@/components/logo-vitnode'; +import { LogoVitNode } from "@/components/logo-vitnode"; /** * Shared layout configurations @@ -11,7 +11,7 @@ import { LogoVitNode } from '@/components/logo-vitnode'; */ // TODO: Remove this export const baseOptions: BaseLayoutProps = { - githubUrl: 'https://github.com/VitNode/vitnode', + githubUrl: "https://github.com/VitNode/vitnode", nav: { title: ( <> @@ -21,9 +21,9 @@ export const baseOptions: BaseLayoutProps = { }, links: [ { - text: 'Documentation', - url: '/docs', - active: 'nested-url', + text: "Documentation", + url: "/docs", + active: "nested-url", }, ], }; diff --git a/apps/docs/src/app/[locale]/(main)/layout.tsx b/apps/docs/src/app/[locale]/(main)/layout.tsx index c8fdf9759..d16c65026 100644 --- a/apps/docs/src/app/[locale]/(main)/layout.tsx +++ b/apps/docs/src/app/[locale]/(main)/layout.tsx @@ -8,10 +8,10 @@ // return {children}; // } -import { LogoVitNode } from '@vitnode/core/components/logo-vitnode'; -import { ThemeLayout } from '@vitnode/core/views/layouts/theme/layout'; +import { LogoVitNode } from "@vitnode/core/components/logo-vitnode"; +import { ThemeLayout } from "@vitnode/core/views/layouts/theme/layout"; -import { vitNodeConfig } from '../../../vitnode.config'; +import { vitNodeConfig } from "../../../vitnode.config"; export default function Layout({ children }: { children: React.ReactNode }) { return ( diff --git a/apps/docs/src/app/[locale]/(main)/not-found.tsx b/apps/docs/src/app/[locale]/(main)/not-found.tsx index 1c4db2aeb..5942f5eca 100644 --- a/apps/docs/src/app/[locale]/(main)/not-found.tsx +++ b/apps/docs/src/app/[locale]/(main)/not-found.tsx @@ -1,4 +1,4 @@ -import { ErrorView } from '@vitnode/core/views/error/error-view'; +import { ErrorView } from "@vitnode/core/views/error/error-view"; export default function NotFoundPage() { return ; diff --git a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-blog)/blog/categories/page.tsx b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-blog)/blog/categories/page.tsx index cab8e4d8a..2331c3599 100644 --- a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-blog)/blog/categories/page.tsx +++ b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-blog)/blog/categories/page.tsx @@ -1,25 +1,26 @@ -import type { Metadata } from 'next'; +import { ActionsCategoriesAdmin } from "@vitnode/blog/views/admin/categories/actions/actions"; -import { I18nProvider } from '@vitnode/core/components/i18n-provider'; -import { DataTableSkeleton } from '@vitnode/core/components/table/data-table'; -import { HeaderContent } from '@vitnode/core/components/ui/header-content'; -import { getTranslations } from 'next-intl/server'; -import dynamic from 'next/dynamic'; -import React from 'react'; - -import { ActionsCategoriesAdmin } from '@vitnode/blog/views/admin/categories/actions/actions'; +import { I18nProvider } from "@vitnode/core/components/i18n-provider"; +import { DataTableSkeleton } from "@vitnode/core/components/table/data-table"; +import { HeaderContent } from "@vitnode/core/components/ui/header-content"; +import type { Metadata } from "next"; +import dynamic from "next/dynamic"; +import { getTranslations } from "next-intl/server"; +import React from "react"; const CategoriesAdminView = dynamic(async () => - import('@vitnode/blog/views/admin/categories/table/categories-admin-view').then(mod => ({ + import( + "@vitnode/blog/views/admin/categories/table/categories-admin-view" + ).then(mod => ({ default: mod.CategoriesAdminView, })), ); export const generateMetadata = async (): Promise => { - const t = await getTranslations('@vitnode/blog.admin.nav'); + const t = await getTranslations("@vitnode/blog.admin.nav"); return { - title: t('categories'), + title: t("categories"), }; }; @@ -27,14 +28,14 @@ export default async function CategoriesPage( params: React.ComponentProps, ) { const [t, tNav] = await Promise.all([ - getTranslations('@vitnode/blog.admin.categories'), - getTranslations('@vitnode/blog.admin.nav'), + getTranslations("@vitnode/blog.admin.categories"), + getTranslations("@vitnode/blog.admin.nav"), ]); return ( - +
- + diff --git a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-blog)/blog/posts/page.tsx b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-blog)/blog/posts/page.tsx index 7fa80aa1a..5dbf8a8b3 100644 --- a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-blog)/blog/posts/page.tsx +++ b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-blog)/blog/posts/page.tsx @@ -1,25 +1,26 @@ -import type { Metadata } from 'next'; +import { ActionsPostsAdmin } from "@vitnode/blog/views/admin/posts/actions/actions"; -import { I18nProvider } from '@vitnode/core/components/i18n-provider'; -import { DataTableSkeleton } from '@vitnode/core/components/table/data-table'; -import { HeaderContent } from '@vitnode/core/components/ui/header-content'; -import { getTranslations } from 'next-intl/server'; -import dynamic from 'next/dynamic'; -import React from 'react'; - -import { ActionsPostsAdmin } from '@vitnode/blog/views/admin/posts/actions/actions'; +import { I18nProvider } from "@vitnode/core/components/i18n-provider"; +import { DataTableSkeleton } from "@vitnode/core/components/table/data-table"; +import { HeaderContent } from "@vitnode/core/components/ui/header-content"; +import type { Metadata } from "next"; +import dynamic from "next/dynamic"; +import { getTranslations } from "next-intl/server"; +import React from "react"; const PostsAdminView = dynamic(async () => - import('@vitnode/blog/views/admin/posts/table/posts-admin-view').then(mod => ({ - default: mod.PostsAdminView, - })), + import("@vitnode/blog/views/admin/posts/table/posts-admin-view").then( + mod => ({ + default: mod.PostsAdminView, + }), + ), ); export const generateMetadata = async (): Promise => { - const t = await getTranslations('@vitnode/blog.admin.nav'); + const t = await getTranslations("@vitnode/blog.admin.nav"); return { - title: t('posts'), + title: t("posts"), }; }; @@ -27,14 +28,14 @@ export default async function PostsPage( params: React.ComponentProps, ) { const [t, tNav] = await Promise.all([ - getTranslations('@vitnode/blog.admin.posts'), - getTranslations('@vitnode/blog.admin.nav'), + getTranslations("@vitnode/blog.admin.posts"), + getTranslations("@vitnode/blog.admin.nav"), ]); return ( - +
- + diff --git a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/debug/page.tsx b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/debug/page.tsx index 4c3e2ebcf..3dd64b364 100644 --- a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/debug/page.tsx +++ b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/debug/page.tsx @@ -1,42 +1,41 @@ -import { getTranslations } from 'next-intl/server'; -import dynamic from 'next/dynamic'; -import React from 'react'; - -import { I18nProvider } from '@vitnode/core/components/i18n-provider'; -import { DataTableSkeleton } from '@vitnode/core/components/table/data-table'; -import { HeaderContent } from '@vitnode/core/components/ui/header-content'; -import { ClearCacheAction } from '@vitnode/core/views/admin/views/core/debug/actions/clear-cache/clear-cache'; +import { I18nProvider } from "@vitnode/core/components/i18n-provider"; +import { DataTableSkeleton } from "@vitnode/core/components/table/data-table"; +import { HeaderContent } from "@vitnode/core/components/ui/header-content"; +import { ClearCacheAction } from "@vitnode/core/views/admin/views/core/debug/actions/clear-cache/clear-cache"; +import dynamic from "next/dynamic"; +import { getTranslations } from "next-intl/server"; +import React from "react"; const SystemLogsView = dynamic(async () => - import('@vitnode/core/views/admin/views/core/debug/system-logs/system-logs-view').then( - module => ({ - default: module.SystemLogsView, - }), - ), + import( + "@vitnode/core/views/admin/views/core/debug/system-logs/system-logs-view" + ).then(module => ({ + default: module.SystemLogsView, + })), ); export const generateMetadata = async () => { - const t = await getTranslations('admin.debug'); + const t = await getTranslations("admin.debug"); return { - title: t('title'), - description: t('desc'), + title: t("title"), + description: t("desc"), }; }; export default async function Page( props: React.ComponentProps, ) { - const t = await getTranslations('admin.debug'); + const t = await getTranslations("admin.debug"); return (
- + - + }> diff --git a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/page.tsx b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/page.tsx index dd1ff7e3d..54dd37081 100644 --- a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/page.tsx +++ b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/page.tsx @@ -1,4 +1,4 @@ -import { DashboardAdminView } from '@vitnode/core/views/admin/views/core/dashboard/dashboard-admin-view'; +import { DashboardAdminView } from "@vitnode/core/views/admin/views/core/dashboard/dashboard-admin-view"; export default function Page() { return ; diff --git a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/test/page.tsx b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/test/page.tsx index f0f142aca..00abbaee8 100644 --- a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/test/page.tsx +++ b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/test/page.tsx @@ -1,4 +1,4 @@ -import { TestView } from '@vitnode/core/views/admin/views/core/test'; +import { TestView } from "@vitnode/core/views/admin/views/core/test"; export default function Page() { return ; diff --git a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/users/page.tsx b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/users/page.tsx index 654a5b4f0..f8d0a61c3 100644 --- a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/users/page.tsx +++ b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/users/page.tsx @@ -1,23 +1,23 @@ -import type { Metadata } from 'next/dist/types'; - -import { getTranslations } from 'next-intl/server'; -import dynamic from 'next/dynamic'; -import React from 'react'; - -import { DataTableSkeleton } from '@vitnode/core/components/table/data-table'; -import { HeaderContent } from '@vitnode/core/components/ui/header-content'; +import { DataTableSkeleton } from "@vitnode/core/components/table/data-table"; +import { HeaderContent } from "@vitnode/core/components/ui/header-content"; +import type { Metadata } from "next/dist/types"; +import dynamic from "next/dynamic"; +import { getTranslations } from "next-intl/server"; +import React from "react"; const UsersAdminView = dynamic(async () => - import('@vitnode/core/views/admin/views/core/users/users-admin-view').then(module => ({ - default: module.UsersAdminView, - })), + import("@vitnode/core/views/admin/views/core/users/users-admin-view").then( + module => ({ + default: module.UsersAdminView, + }), + ), ); export const generateMetadata = async (): Promise => { - const t = await getTranslations('admin.global.nav.users'); + const t = await getTranslations("admin.global.nav.users"); return { - title: t('list'), + title: t("list"), }; }; @@ -25,13 +25,13 @@ export default async function Page( props: React.ComponentProps, ) { const [t, tNav] = await Promise.all([ - getTranslations('admin.user.list'), - getTranslations('admin.global.nav.users'), + getTranslations("admin.user.list"), + getTranslations("admin.global.nav.users"), ]); return (
- + }> diff --git a/apps/docs/src/app/[locale]/admin/(auth)/layout.tsx b/apps/docs/src/app/[locale]/admin/(auth)/layout.tsx index 01f8ac1c5..83191e110 100644 --- a/apps/docs/src/app/[locale]/admin/(auth)/layout.tsx +++ b/apps/docs/src/app/[locale]/admin/(auth)/layout.tsx @@ -1,9 +1,9 @@ import { AdminLayout, type AdminLayoutProps, -} from '@vitnode/core/views/admin/layouts/admin-layout'; +} from "@vitnode/core/views/admin/layouts/admin-layout"; -import { vitNodeConfig } from '@/vitnode.config'; +import { vitNodeConfig } from "@/vitnode.config"; export default function Layout(props: AdminLayoutProps) { return ; diff --git a/apps/docs/src/app/[locale]/admin/page.tsx b/apps/docs/src/app/[locale]/admin/page.tsx index 9238ffa31..5e82810dd 100644 --- a/apps/docs/src/app/[locale]/admin/page.tsx +++ b/apps/docs/src/app/[locale]/admin/page.tsx @@ -1,4 +1,4 @@ -import { SignInAdminView } from '@vitnode/core/views/admin/sign-in/sign-in-admin-view'; +import { SignInAdminView } from "@vitnode/core/views/admin/sign-in/sign-in-admin-view"; export default function Page() { return ; diff --git a/apps/docs/src/app/[locale]/layout.tsx b/apps/docs/src/app/[locale]/layout.tsx index 3c206b9e5..f55a6a02d 100644 --- a/apps/docs/src/app/[locale]/layout.tsx +++ b/apps/docs/src/app/[locale]/layout.tsx @@ -1,26 +1,25 @@ -import type { RootLayoutProps } from '@vitnode/core/views/layouts/root-layout'; -import type { Metadata } from 'next'; - +import type { RootLayoutProps } from "@vitnode/core/views/layouts/root-layout"; import { generateMetadataRootLayout, RootLayout, -} from '@vitnode/core/views/layouts/root-layout'; -import { RootProvider } from 'fumadocs-ui/provider'; -import { Geist, Geist_Mono } from 'next/font/google'; +} from "@vitnode/core/views/layouts/root-layout"; +import { RootProvider } from "fumadocs-ui/provider"; +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; -import { vitNodeConfig } from '@/vitnode.config'; +import { vitNodeConfig } from "@/vitnode.config"; -import SearchDialogFumadocs from '../../components/fumadocs/search-dialog'; -import { Body } from './(main)/layout.client'; +import SearchDialogFumadocs from "../../components/fumadocs/search-dialog"; +import { Body } from "./(main)/layout.client"; const geistSans = Geist({ - variable: '--font-geist-sans', - subsets: ['latin'], + variable: "--font-geist-sans", + subsets: ["latin"], }); const geistMono = Geist_Mono({ - variable: '--font-geist-mono', - subsets: ['latin'], + variable: "--font-geist-mono", + subsets: ["latin"], }); export const generateMetadata = (): Metadata => diff --git a/apps/docs/src/app/api/[...route]/route.ts b/apps/docs/src/app/api/[...route]/route.ts index 76af11c6d..c71850481 100644 --- a/apps/docs/src/app/api/[...route]/route.ts +++ b/apps/docs/src/app/api/[...route]/route.ts @@ -1,10 +1,10 @@ -import { OpenAPIHono } from '@hono/zod-openapi'; -import { VitNodeAPI } from '@vitnode/core/api/config'; -import { handle } from 'hono/vercel'; +import { OpenAPIHono } from "@hono/zod-openapi"; +import { VitNodeAPI } from "@vitnode/core/api/config"; +import { handle } from "hono/vercel"; -import { vitNodeApiConfig } from '@/vitnode.api.config'; +import { vitNodeApiConfig } from "@/vitnode.api.config"; -const app = new OpenAPIHono().basePath('/api'); +const app = new OpenAPIHono().basePath("/api"); VitNodeAPI({ app, vitNodeApiConfig, diff --git a/apps/docs/src/app/api/search/route.ts b/apps/docs/src/app/api/search/route.ts index 9e0c44333..84ede8477 100644 --- a/apps/docs/src/app/api/search/route.ts +++ b/apps/docs/src/app/api/search/route.ts @@ -1,5 +1,5 @@ -import { createFromSource } from 'fumadocs-core/search/server'; +import { createFromSource } from "fumadocs-core/search/server"; -import { source } from '@/lib/source'; +import { source } from "@/lib/source"; export const { GET } = createFromSource(source); diff --git a/apps/docs/src/app/global-error.tsx b/apps/docs/src/app/global-error.tsx index 5b8a6452b..ef398858a 100644 --- a/apps/docs/src/app/global-error.tsx +++ b/apps/docs/src/app/global-error.tsx @@ -1,27 +1,24 @@ -'use client'; +"use client"; -import { GlobalErrorView } from '@vitnode/core/views/error/global-error-view'; -import { Geist, Geist_Mono } from 'next/font/google'; +import { GlobalErrorView } from "@vitnode/core/views/error/global-error-view"; +import { Geist, Geist_Mono } from "next/font/google"; -import './global.css'; - -import { vitNodeConfig } from '@/vitnode.config'; +import "./global.css"; const geistSans = Geist({ - variable: '--font-geist-sans', - subsets: ['latin'], + variable: "--font-geist-sans", + subsets: ["latin"], }); const geistMono = Geist_Mono({ - variable: '--font-geist-mono', - subsets: ['latin'], + variable: "--font-geist-mono", + subsets: ["latin"], }); export default function GlobalError() { return ( ); } diff --git a/apps/docs/src/app/global.css b/apps/docs/src/app/global.css index cc47703f0..edaaf0fc1 100644 --- a/apps/docs/src/app/global.css +++ b/apps/docs/src/app/global.css @@ -1,8 +1,9 @@ -@import 'tailwindcss'; -@import 'fumadocs-ui/css/shadcn.css'; -@import 'fumadocs-ui/css/preset.css'; +/** biome-ignore-all lint/suspicious/noUnknownAtRules: */ +@import "tailwindcss"; +@import "fumadocs-ui/css/shadcn.css"; +@import "fumadocs-ui/css/preset.css"; -@import 'tw-animate-css'; +@import "tw-animate-css"; @source '../../node_modules/fumadocs-ui/dist/**/*.js'; @source "../../node_modules/@vitnode/core/dist/src/components"; diff --git a/apps/docs/src/app/layout.tsx b/apps/docs/src/app/layout.tsx index 8ed5dbaf5..918bea878 100644 --- a/apps/docs/src/app/layout.tsx +++ b/apps/docs/src/app/layout.tsx @@ -1,6 +1,6 @@ -import './global.css'; +import "./global.css"; -export default async function RootLayout({ +export default function RootLayout({ children, }: { children: React.ReactNode; diff --git a/apps/docs/src/components/animated-beam/animated-beam-home.tsx b/apps/docs/src/components/animated-beam/animated-beam-home.tsx index 7de512a8a..7463ae8a3 100644 --- a/apps/docs/src/components/animated-beam/animated-beam-home.tsx +++ b/apps/docs/src/components/animated-beam/animated-beam-home.tsx @@ -1,13 +1,13 @@ -'use client'; +"use client"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, -} from '@vitnode/core/components/ui/tooltip'; -import { Link } from '@vitnode/core/lib/navigation'; -import { cn } from '@vitnode/core/lib/utils'; +} from "@vitnode/core/components/ui/tooltip"; +import { Link } from "@vitnode/core/lib/navigation"; +import { cn } from "@vitnode/core/lib/utils"; import { AtSign, Database, @@ -17,11 +17,12 @@ import { ShieldCheck, Sparkle, Users, -} from 'lucide-react'; -import React, { useRef } from 'react'; +} from "lucide-react"; +import type React from "react"; +import { useRef } from "react"; -import { LogoVitNode } from '../logo-vitnode'; -import { AnimatedBeam } from './animated-beam'; +import { LogoVitNode } from "../logo-vitnode"; +import { AnimatedBeam } from "./animated-beam"; const Circle = ({ className, @@ -31,7 +32,7 @@ const Circle = ({ tooltip?: string; }) => { const classNameLink = cn( - 'bg-card hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-ring/50 z-10 flex size-12 items-center justify-center rounded-md border p-3 transition-all focus-visible:ring-[3px]', + "bg-card hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-ring/50 z-10 flex size-12 items-center justify-center rounded-md border p-3 transition-all focus-visible:ring-[3px]", className, ); @@ -52,7 +53,7 @@ const Circle = ({ ); }; -Circle.displayName = 'Circle'; +Circle.displayName = "Circle"; export function AnimatedBeamHome() { const containerRef = useRef(null); @@ -91,6 +92,7 @@ export function AnimatedBeamHome() { + diff --git a/apps/docs/src/components/animated-beam/animated-beam.tsx b/apps/docs/src/components/animated-beam/animated-beam.tsx index 074f22b95..0eb84fad9 100644 --- a/apps/docs/src/components/animated-beam/animated-beam.tsx +++ b/apps/docs/src/components/animated-beam/animated-beam.tsx @@ -1,8 +1,8 @@ -'use client'; +"use client"; -import { cn } from '@vitnode/core/lib/utils'; -import { motion } from 'motion/react'; -import { type RefObject, useEffect, useId, useState } from 'react'; +import { cn } from "@vitnode/core/lib/utils"; +import { motion } from "motion/react"; +import { type RefObject, useEffect, useId, useState } from "react"; export const AnimatedBeam = ({ className, @@ -13,11 +13,11 @@ export const AnimatedBeam = ({ reverse = false, // Include the reverse prop duration = Math.random() * 3 + 4, delay = 0, - pathColor = 'gray', + pathColor = "gray", pathWidth = 2, pathOpacity = 0.2, - gradientStartColor = '#325fbd', - gradientStopColor = '#363895', + gradientStartColor = "#325fbd", + gradientStopColor = "#363895", startXOffset = 0, startYOffset = 0, endXOffset = 0, @@ -42,22 +42,22 @@ export const AnimatedBeam = ({ toRef: RefObject; }) => { const id = useId(); - const [pathD, setPathD] = useState(''); + const [pathD, setPathD] = useState(""); const [svgDimensions, setSvgDimensions] = useState({ width: 0, height: 0 }); // Calculate the gradient coordinates based on the reverse prop const gradientCoordinates = reverse ? { - x1: ['90%', '-10%'], - x2: ['100%', '0%'], - y1: ['0%', '0%'], - y2: ['0%', '0%'], + x1: ["90%", "-10%"], + x2: ["100%", "0%"], + y1: ["0%", "0%"], + y2: ["0%", "0%"], } : { - x1: ['10%', '110%'], - x2: ['0%', '100%'], - y1: ['0%', '0%'], - y2: ['0%', '0%'], + x1: ["10%", "110%"], + x2: ["0%", "100%"], + y1: ["0%", "0%"], + y2: ["0%", "0%"], }; useEffect(() => { @@ -69,7 +69,6 @@ export const AnimatedBeam = ({ const svgWidth = containerRect.width; const svgHeight = containerRect.height; - // eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect setSvgDimensions({ width: svgWidth, height: svgHeight }); const startX = @@ -85,7 +84,6 @@ export const AnimatedBeam = ({ const d = `M ${startX},${startY} Q ${ (startX + endX) / 2 },${controlY} ${endX},${endY}`; - // eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect setPathD(d); } }; @@ -93,7 +91,6 @@ export const AnimatedBeam = ({ // Initialize ResizeObserver const resizeObserver = new ResizeObserver(entries => { // For all entries, recalculate the path - // eslint-disable-next-line @typescript-eslint/no-unused-vars for (const _entry of entries) { updatePath(); } @@ -125,7 +122,7 @@ export const AnimatedBeam = ({ return ( + Animated beam - - - - + + + + diff --git a/apps/docs/src/components/fumadocs/code-block.tsx b/apps/docs/src/components/fumadocs/code-block.tsx index f76022385..ef42d532e 100644 --- a/apps/docs/src/components/fumadocs/code-block.tsx +++ b/apps/docs/src/components/fumadocs/code-block.tsx @@ -1,23 +1,27 @@ -import { highlight } from 'fumadocs-core/highlight'; -import * as Base from 'fumadocs-ui/components/codeblock'; +import { highlight } from "fumadocs-core/highlight"; +import { + CodeBlock as BaseCodeBlock, + type CodeBlockProps as BaseCodeBlockProps, + Pre, +} from "fumadocs-ui/components/codeblock"; export interface CodeBlockProps { code: string; lang: string; - wrapper?: Base.CodeBlockProps; + wrapper?: BaseCodeBlockProps; } export async function CodeBlock({ code, lang, wrapper }: CodeBlockProps) { const rendered = await highlight(code, { lang, themes: { - light: 'github-light', - dark: 'vesper', + light: "github-light", + dark: "vesper", }, components: { - pre: Base.Pre, + pre: Pre, }, }); - return {rendered}; + return {rendered}; } diff --git a/apps/docs/src/components/fumadocs/img.tsx b/apps/docs/src/components/fumadocs/img.tsx index ddc66c543..e25baa4a6 100644 --- a/apps/docs/src/components/fumadocs/img.tsx +++ b/apps/docs/src/components/fumadocs/img.tsx @@ -1,6 +1,6 @@ -import { ImageZoom } from 'fumadocs-ui/components/image-zoom'; -import { cn } from 'fumadocs-ui/utils/cn'; -import React from 'react'; +import { ImageZoom } from "fumadocs-ui/components/image-zoom"; +import { cn } from "fumadocs-ui/utils/cn"; +import type React from "react"; export const ImgDocs = ({ className, @@ -12,11 +12,11 @@ export const ImgDocs = ({ return (
- +
); }; diff --git a/apps/docs/src/components/fumadocs/preview.tsx b/apps/docs/src/components/fumadocs/preview.tsx index 6f46e121d..bc2b3a462 100644 --- a/apps/docs/src/components/fumadocs/preview.tsx +++ b/apps/docs/src/components/fumadocs/preview.tsx @@ -1,7 +1,7 @@ -import { Loader } from '@vitnode/core/components/ui/loader'; -import { cn } from '@vitnode/core/lib/utils'; -import dynamic from 'next/dynamic'; -import React from 'react'; +import { Loader } from "@vitnode/core/components/ui/loader"; +import { cn } from "@vitnode/core/lib/utils"; +import dynamic from "next/dynamic"; +import React from "react"; export const Preview = ({ name, @@ -27,7 +27,7 @@ export const Preview = ({ return (
diff --git a/apps/docs/src/components/fumadocs/search-dialog.tsx b/apps/docs/src/components/fumadocs/search-dialog.tsx index 6186fd789..6978afadc 100644 --- a/apps/docs/src/components/fumadocs/search-dialog.tsx +++ b/apps/docs/src/components/fumadocs/search-dialog.tsx @@ -1,6 +1,6 @@ -'use client'; +"use client"; -import { useDocsSearch } from 'fumadocs-core/search/client'; +import { useDocsSearch } from "fumadocs-core/search/client"; import { SearchDialog, SearchDialogClose, @@ -11,11 +11,11 @@ import { SearchDialogList, SearchDialogOverlay, type SharedProps, -} from 'fumadocs-ui/components/dialog/search'; +} from "fumadocs-ui/components/dialog/search"; export default function SearchDialogFumadocs(props: SharedProps) { const { search, setSearch, query } = useDocsSearch({ - type: 'fetch', + type: "fetch", }); return ( @@ -32,7 +32,7 @@ export default function SearchDialogFumadocs(props: SharedProps) { - + ); diff --git a/apps/docs/src/components/infinite-slider.tsx b/apps/docs/src/components/infinite-slider.tsx index 1bc66e8be..7b3ced940 100644 --- a/apps/docs/src/components/infinite-slider.tsx +++ b/apps/docs/src/components/infinite-slider.tsx @@ -1,14 +1,19 @@ -'use client'; +"use client"; -import { cn } from '@vitnode/core/lib/utils'; -import { animate, motion, useMotionValue } from 'motion/react'; -import { useEffect, useState } from 'react'; -import { useMeasure } from 'react-use'; +import { cn } from "@vitnode/core/lib/utils"; +import { + type AnimationPlaybackControlsWithThen, + animate, + motion, + useMotionValue, +} from "motion/react"; +import React from "react"; +import { useMeasure } from "react-use"; export interface InfiniteSliderProps { children: React.ReactNode; className?: string; - direction?: 'horizontal' | 'vertical'; + direction?: "horizontal" | "vertical"; gap?: number; reverse?: boolean; speed?: number; @@ -20,19 +25,20 @@ export function InfiniteSlider({ gap = 16, speed = 100, speedOnHover, - direction = 'horizontal', + direction = "horizontal", reverse = false, className, }: InfiniteSliderProps) { - const [currentSpeed, setCurrentSpeed] = useState(speed); + const [currentSpeed, setCurrentSpeed] = React.useState(speed); const [ref, { width, height }] = useMeasure(); const translation = useMotionValue(0); - const [isTransitioning, setIsTransitioning] = useState(false); - const [key, setKey] = useState(0); + const [isTransitioning, setIsTransitioning] = React.useState(false); + const [key, setKey] = React.useState(0); - useEffect(() => { - let controls; - const size = direction === 'horizontal' ? width : height; + // biome-ignore lint/correctness/useExhaustiveDependencies: + React.useEffect(() => { + let controls: AnimationPlaybackControlsWithThen | undefined; + const size = direction === "horizontal" ? width : height; const contentSize = size + gap; const from = reverse ? -contentSize / 2 : 0; const to = reverse ? 0 : -contentSize / 2; @@ -45,7 +51,7 @@ export function InfiniteSlider({ const transitionDuration = remainingDistance / currentSpeed; controls = animate(translation, [translation.get(), to], { - ease: 'linear', + ease: "linear", duration: transitionDuration, onComplete: () => { setIsTransitioning(false); @@ -54,10 +60,10 @@ export function InfiniteSlider({ }); } else { controls = animate(translation, [from, to], { - ease: 'linear', + ease: "linear", duration: duration, repeat: Infinity, - repeatType: 'loop', + repeatType: "loop", repeatDelay: 0, onRepeat: () => { translation.set(from); @@ -92,16 +98,16 @@ export function InfiniteSlider({ : {}; return ( -
+
diff --git a/apps/docs/src/components/logo-vitnode.tsx b/apps/docs/src/components/logo-vitnode.tsx index 6268f597e..39a1f0e20 100644 --- a/apps/docs/src/components/logo-vitnode.tsx +++ b/apps/docs/src/components/logo-vitnode.tsx @@ -1,22 +1,23 @@ -import { cn } from '@vitnode/core/lib/utils'; +import { cn } from "@vitnode/core/lib/utils"; export const LogoVitNode = ({ className, small, ...props -}: React.ComponentProps<'svg'> & { +}: React.ComponentProps<"svg"> & { small?: boolean; }) => { if (small) return ( + Logo VitNode + Logo VitNode { const MotionComponent = motion.create(Component); let segments: string[] = []; switch (by) { - case 'character': - segments = children.split(''); + case "character": + segments = children.split(""); break; - case 'line': - segments = children.split('\n'); + case "line": + segments = children.split("\n"); break; - case 'word': + case "word": segments = children.split(/(\s+)/); break; - case 'text': + case "text": + segments = [children]; + break; default: segments = [children]; break; } - const finalVariants = variants - ? { - container: { - hidden: { opacity: 0 }, - show: { - opacity: 1, - transition: { - opacity: { duration: 0.01, delay }, - delayChildren: delay, - staggerChildren: duration / segments.length, - }, + let finalVariants: { container: Variants; item: Variants }; + if (variants) { + finalVariants = { + container: { + hidden: { opacity: 0 }, + show: { + opacity: 1, + transition: { + opacity: { duration: 0.01, delay }, + delayChildren: delay, + staggerChildren: duration / segments.length, + }, + }, + exit: { + opacity: 0, + transition: { + staggerChildren: duration / segments.length, + staggerDirection: -1, }, - exit: { - opacity: 0, - transition: { - staggerChildren: duration / segments.length, - staggerDirection: -1, - }, + }, + }, + item: variants, + }; + } else if (animation) { + finalVariants = { + container: { + ...defaultItemAnimationVariants[animation].container, + show: { + ...defaultItemAnimationVariants[animation].container.show, + transition: { + delayChildren: delay, + staggerChildren: duration / segments.length, }, }, - item: variants, - } - : animation - ? { - container: { - ...defaultItemAnimationVariants[animation].container, - show: { - ...defaultItemAnimationVariants[animation].container.show, - transition: { - delayChildren: delay, - staggerChildren: duration / segments.length, - }, - }, - exit: { - ...defaultItemAnimationVariants[animation].container.exit, - transition: { - staggerChildren: duration / segments.length, - staggerDirection: -1, - }, - }, + exit: { + ...defaultItemAnimationVariants[animation].container.exit, + transition: { + staggerChildren: duration / segments.length, + staggerDirection: -1, }, - item: defaultItemAnimationVariants[animation].item, - } - : { container: defaultContainerVariants, item: defaultItemVariants }; + }, + }, + item: defaultItemAnimationVariants[animation].item, + }; + } else { + finalVariants = { + container: defaultContainerVariants, + item: defaultItemVariants, + }; + } return ( {segments.map((segment, i) => ( + i + }`} variants={finalVariants.item} > {segment} diff --git a/apps/docs/src/examples/accordion.tsx b/apps/docs/src/examples/accordion.tsx index 23f710973..f5b2658ca 100644 --- a/apps/docs/src/examples/accordion.tsx +++ b/apps/docs/src/examples/accordion.tsx @@ -1,11 +1,11 @@ -'use client'; +"use client"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, -} from '@vitnode/core/components/ui/accordion'; +} from "@vitnode/core/components/ui/accordion"; export default function AccordionExample() { return ( diff --git a/apps/docs/src/examples/auto-form.tsx b/apps/docs/src/examples/auto-form.tsx index 149f2ad67..6d9119d9e 100644 --- a/apps/docs/src/examples/auto-form.tsx +++ b/apps/docs/src/examples/auto-form.tsx @@ -1,32 +1,32 @@ -'use client'; +"use client"; -import { AutoForm } from '@vitnode/core/components/form/auto-form'; -import { AutoFormCheckbox } from '@vitnode/core/components/form/fields/checkbox'; -import { AutoFormInput } from '@vitnode/core/components/form/fields/input'; -import { AutoFormSelect } from '@vitnode/core/components/form/fields/select'; -import { AutoFormTextarea } from '@vitnode/core/components/form/fields/textarea'; -import { z } from 'zod'; +import { AutoForm } from "@vitnode/core/components/form/auto-form"; +import { AutoFormCheckbox } from "@vitnode/core/components/form/fields/checkbox"; +import { AutoFormInput } from "@vitnode/core/components/form/fields/input"; +import { AutoFormSelect } from "@vitnode/core/components/form/fields/select"; +import { AutoFormTextarea } from "@vitnode/core/components/form/fields/textarea"; +import { z } from "zod"; export default function AutoFormExample() { const formSchema = z.object({ - username: z.string().min(3, 'Username must be at least 3 characters'), + username: z.string().min(3, "Username must be at least 3 characters"), email: z - .email('Please enter a valid email address') + .email("Please enter a valid email address") .describe("We'll use this email to contact you. (from zod schema)"), - user_type: z.enum(['admin', 'editor', 'viewer']), + user_type: z.enum(["admin", "editor", "viewer"]), accept_terms: z.boolean().refine(val => val, { - message: 'You must accept the terms and conditions', + message: "You must accept the terms and conditions", }), description: z .string() - .min(10, 'Description must be at least 10 characters'), + .min(10, "Description must be at least 10 characters"), }); return ( ( ( ), }, { - id: 'user_type', + id: "user_type", component: props => ( ), }, { - id: 'accept_terms', + id: "accept_terms", component: props => ( ( val, { - message: 'You must accept the terms and conditions', + message: "You must accept the terms and conditions", }), }); @@ -15,7 +15,7 @@ export default function SwitchExample() { ( ( { - toast.success('Category deleted successfully!', { - description: 'The category has been removed from your list.', + toast.success("Category deleted successfully!", { + description: "The category has been removed from your list.", }); onClose(); }} diff --git a/apps/docs/src/examples/data-table.tsx b/apps/docs/src/examples/data-table.tsx index c47e476ed..35e2b98a1 100644 --- a/apps/docs/src/examples/data-table.tsx +++ b/apps/docs/src/examples/data-table.tsx @@ -1,49 +1,49 @@ -import { DataTable } from '@vitnode/core/components/table/data-table'; +import { DataTable } from "@vitnode/core/components/table/data-table"; export default function DataTableExample() { return ( ( ( ( ( - toast('Event has been created', { - description: 'Sunday, December 03, 2023 at 9:00 AM', + toast("Event has been created", { + description: "Sunday, December 03, 2023 at 9:00 AM", action: { - label: 'Undo', - onClick: () => console.log('Undo'), + label: "Undo", + // biome-ignore lint/suspicious/noConsole: + onClick: () => console.log("Undo"), }, }) } diff --git a/apps/docs/src/examples/switch.tsx b/apps/docs/src/examples/switch.tsx index 8106b8b97..2ad21e550 100644 --- a/apps/docs/src/examples/switch.tsx +++ b/apps/docs/src/examples/switch.tsx @@ -1,13 +1,13 @@ -'use client'; +"use client"; -import { AutoForm } from '@vitnode/core/components/form/auto-form'; -import { AutoFormSwitch } from '@vitnode/core/components/form/fields/switch'; -import { z } from 'zod'; +import { AutoForm } from "@vitnode/core/components/form/auto-form"; +import { AutoFormSwitch } from "@vitnode/core/components/form/fields/switch"; +import { z } from "zod"; export default function SwitchExample() { const formSchema = z.object({ acceptTerms: z.boolean().refine(val => val, { - message: 'You must accept the terms and conditions', + message: "You must accept the terms and conditions", }), }); @@ -15,7 +15,7 @@ export default function SwitchExample() { ( ( locale.code), @@ -11,14 +11,14 @@ export default createMiddleware({ export const config = { matcher: [ // Enable a redirect to a matching locale at the root - '/', + "/", // Set a cookie to remember the previous locale for // all requests that have a locale prefix - '/(en)/:path*', + "/(en)/:path*", // Enable redirects that add missing locales // (e.g. `/pathnames` -> `/en/pathnames`) - '/((?!_next|_vercel|api|.*\\..*).*)', + "/((?!_next|_vercel|api|.*\\..*).*)", ], }; diff --git a/apps/docs/src/vitnode.api.config.ts b/apps/docs/src/vitnode.api.config.ts index 2a8ed33ec..0ad864805 100644 --- a/apps/docs/src/vitnode.api.config.ts +++ b/apps/docs/src/vitnode.api.config.ts @@ -1,30 +1,29 @@ -import { blogApiPlugin } from '@vitnode/blog/config.api'; -import { NodemailerEmailAdapter } from '@vitnode/core/api/adapters/email/nodemailer'; -import { DiscordSSOApiPlugin } from '@vitnode/core/api/adapters/sso/discord'; -import { FacebookSSOApiPlugin } from '@vitnode/core/api/adapters/sso/facebook'; -import { GoogleSSOApiPlugin } from '@vitnode/core/api/adapters/sso/google'; -import { buildApiConfig } from '@vitnode/core/vitnode.config'; -import { drizzle } from 'drizzle-orm/postgres-js'; +import { blogApiPlugin } from "@vitnode/blog/config.api"; +import { NodemailerEmailAdapter } from "@vitnode/core/api/adapters/email/nodemailer"; +import { DiscordSSOApiPlugin } from "@vitnode/core/api/adapters/sso/discord"; +import { FacebookSSOApiPlugin } from "@vitnode/core/api/adapters/sso/facebook"; +import { GoogleSSOApiPlugin } from "@vitnode/core/api/adapters/sso/google"; +import { buildApiConfig } from "@vitnode/core/vitnode.config"; +import { drizzle } from "drizzle-orm/postgres-js"; export const POSTGRES_URL = - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - process.env.POSTGRES_URL || 'postgresql://root:root@localhost:5432/vitnode'; + process.env.POSTGRES_URL || "postgresql://root:root@localhost:5432/vitnode"; export const vitNodeApiConfig = buildApiConfig({ pathToMessages: async path => await import(`./locales/${path}`), captcha: { - type: 'cloudflare_turnstile', + type: "cloudflare_turnstile", siteKey: process.env.CLOUDFLARE_TURNSTILE_SITE_KEY, secretKey: process.env.CLOUDFLARE_TURNSTILE_SECRET_KEY, }, metadata: { - title: 'VitNode API', - shortTitle: 'VitNode', + title: "VitNode API", + shortTitle: "VitNode", }, plugins: [blogApiPlugin()], dbProvider: drizzle({ connection: POSTGRES_URL, - casing: 'camelCase', + casing: "camelCase", }), rateLimiter: { points: 20, // 20 requests @@ -38,8 +37,8 @@ export const vitNodeApiConfig = buildApiConfig({ user: process.env.NOD_EMAILER_USER, }), logo: { - text: 'VitNode Email Test', - src: 'http://localhost:3000/logo_vitnode_dark.png', + text: "VitNode Email Test", + src: "http://localhost:3000/logo_vitnode_dark.png", }, }, authorization: { diff --git a/apps/docs/src/vitnode.config.ts b/apps/docs/src/vitnode.config.ts index 4dcda6d6d..ca8d7836d 100644 --- a/apps/docs/src/vitnode.config.ts +++ b/apps/docs/src/vitnode.config.ts @@ -1,25 +1,25 @@ -import { blogPlugin } from '@vitnode/blog/config'; -import { buildConfig, handleRequestConfig } from '@vitnode/core/vitnode.config'; -import { getRequestConfig } from 'next-intl/server'; +import { blogPlugin } from "@vitnode/blog/config"; +import { buildConfig, handleRequestConfig } from "@vitnode/core/vitnode.config"; +import { getRequestConfig } from "next-intl/server"; export const vitNodeConfig = buildConfig({ metadata: { - title: 'VitNode', - shortTitle: 'VitNode', + title: "VitNode", + shortTitle: "VitNode", }, plugins: [blogPlugin()], debug: true, i18n: { locales: [ { - code: 'en', - name: 'English', + code: "en", + name: "English", }, ], - defaultLocale: 'en', + defaultLocale: "en", }, theme: { - defaultTheme: 'light', + defaultTheme: "light", }, }); diff --git a/apps/docs/tsconfig.json b/apps/docs/tsconfig.json index 2dd283779..49aef2306 100644 --- a/apps/docs/tsconfig.json +++ b/apps/docs/tsconfig.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/tsconfig", - "extends": "@vitnode/eslint-config/tsconfig", + "extends": "@vitnode/config/tsconfig", "compilerOptions": { "target": "ESNext", "module": "esnext", diff --git a/biome.json b/biome.json new file mode 100644 index 000000000..13decb8d3 --- /dev/null +++ b/biome.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.2.2/schema.json", + "extends": ["@vitnode/config/biome"], + "root": true, + "files": { + "includes": ["!create-vitnode-app/biome"] + } +} diff --git a/package.json b/package.json index a5f04dc6d..7ab14316c 100644 --- a/package.json +++ b/package.json @@ -12,16 +12,15 @@ "start": "turbo start", "init": "turbo init", "dev": "pnpm build:scripts && turbo build:plugins && turbo dev", - "lint": "turbo lint", - "lint:fix": "turbo lint:fix", + "lint": "biome check", + "lint:fix": "biome check . --write", "test": "turbo test", "test:e2e": "turbo test:e2e" }, "devDependencies": { + "@biomejs/biome": "^2.2.2", "@types/node": "^24.3.0", - "@vitnode/eslint-config": "workspace:*", - "prettier": "^3.6.2", - "prettier-plugin-tailwindcss": "^0.6.14", + "@vitnode/config": "workspace:*", "tsx": "^4.20.4", "turbo": "^2.5.6", "typescript": "^5.9.2", diff --git a/packages/eslint/.npmignore b/packages/config/.npmignore similarity index 100% rename from packages/eslint/.npmignore rename to packages/config/.npmignore diff --git a/packages/eslint/README.md b/packages/config/README.md similarity index 51% rename from packages/eslint/README.md rename to packages/config/README.md index 7ea0f69da..ad42d8979 100644 --- a/packages/eslint/README.md +++ b/packages/config/README.md @@ -1,6 +1,6 @@ -# (VitNode) ESLint Config +# (VitNode) Config -This package provides a default ESLint configuration, TypeScript configuration, and Prettier configuration for VitNode projects. +This package provides a default Biome configuration, TypeScript configuration for VitNode projects.


@@ -17,34 +17,20 @@ This package provides a default ESLint configuration, TypeScript configuration, ## Usage -### ESLint (eslint.config.mjs) +### Biome (biome.json) -```js -import eslintVitNode from '@vitnode/eslint-config/eslint'; - -export default [...eslintVitNode]; +```json +{ + "$schema": "https://biomejs.dev/schemas/2.2.2/schema.json", + "extends": ["@vitnode/config/biome"], + "root": true +} ``` ### TypeScript (tsconfig.json) ```json { - "extends": "@vitnode/eslint-config/tsconfig" + "extends": "@vitnode/config/tsconfig" } ``` - -### Prettier (.prettierrc.mjs) - -```js -import vitnodePrettier from '@vitnode/eslint-config/prettierrc'; - -/** - * @see https://prettier.io/docs/en/configuration.html - * @type {import("prettier").Config} - */ -const config = { - ...vitnodePrettier, -}; - -export default config; -``` diff --git a/packages/config/biome.json b/packages/config/biome.json new file mode 100644 index 000000000..ede2ca1e9 --- /dev/null +++ b/packages/config/biome.json @@ -0,0 +1,103 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.2.2/schema.json", + "root": false, + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": true, + "includes": [ + "**", + "!node_modules", + "!.next", + "!dist", + "!build", + "!.turbo", + "!.source", + "!docker" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 80 + }, + "javascript": { + "formatter": { + "quoteStyle": "double", + "jsxQuoteStyle": "double", + "arrowParentheses": "asNeeded" + } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "complexity": { + "useLiteralKeys": "off", + "noExcessiveCognitiveComplexity": "warn", + "noExcessiveNestedTestSuites": "warn", + "noUselessStringConcat": "error", + "useSimplifiedLogicExpression": "warn" + }, + "suspicious": { + "noConsole": "error", + "noAlert": "error", + "noSkippedTests": "warn", + "noVar": "error", + "useAwait": "error", + "useIterableCallbackReturn": "off" + }, + "correctness": { + "noNestedComponentDefinitions": "error", + "noReactPropAssignments": "error", + "noRenderReturnValue": "error", + "useJsonImportAttributes": "warn", + "useUniqueElementIds": "off" + }, + "performance": { + "noAwaitInLoops": "error", + "noDelete": "error", + "noNamespaceImport": "error" + }, + "style": { + "noCommonJs": "error", + "noEnum": "error", + "noExportedImports": "warn", + "noInferrableTypes": "error", + "noNegationElse": "warn", + "noNestedTernary": "error", + "noSubstr": "error", + "noUnusedTemplateLiteral": "warn", + "noUselessElse": "warn", + "noYodaExpression": "warn", + "useAtIndex": "warn", + "useCollapsedIf": "warn", + "useConsistentArrayType": "warn", + "useConsistentBuiltinInstantiation": "error", + "useExportsLast": "warn", + "useObjectSpread": "warn", + "useSelfClosingElements": "warn", + "useShorthandAssign": "warn", + "useSingleVarDeclarator": "error", + "useThrowNewError": "error", + "useTrimStartEnd": "warn" + } + }, + "domains": { + "next": "recommended", + "react": "recommended", + "test": "recommended" + } + }, + "assist": { + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/packages/config/package.json b/packages/config/package.json new file mode 100644 index 000000000..d003d4a09 --- /dev/null +++ b/packages/config/package.json @@ -0,0 +1,37 @@ +{ + "name": "@vitnode/config", + "version": "1.2.0-canary.52", + "description": "Biome, TypeScript (TSConfig) config for VitNode", + "author": "VitNode Team", + "license": "MIT", + "homepage": "https://vitnode.com", + "repository": { + "type": "git", + "url": "git+https://github.com/aXenDeveloper/vitnode.git", + "directory": "packages/config" + }, + "keywords": [ + "vitnode", + "typescript", + "tsconfig", + "biome" + ], + "type": "module", + "exports": { + "./tsconfig": { + "import": "./tsconfig.json", + "default": "./tsconfig.json" + }, + "./biome": { + "import": "./biome.json", + "default": "./biome.json" + } + }, + "peerDependencies": { + "typescript": "5.9.x" + }, + "devDependencies": { + "@biomejs/biome": "^2.2.2", + "typescript": "^5.9.2" + } +} diff --git a/packages/eslint/tsconfig.json b/packages/config/tsconfig.json similarity index 100% rename from packages/eslint/tsconfig.json rename to packages/config/tsconfig.json diff --git a/packages/create-vitnode-app/.npmignore b/packages/create-vitnode-app/.npmignore index 0950cd240..c45c667b4 100644 --- a/packages/create-vitnode-app/.npmignore +++ b/packages/create-vitnode-app/.npmignore @@ -1,5 +1,4 @@ /src /node_modules /.turbo -/eslint.config.mjs /tsconfig.json \ No newline at end of file diff --git a/packages/create-vitnode-app/README.md b/packages/create-vitnode-app/README.md index e3b22fcb0..e8aabb07d 100644 --- a/packages/create-vitnode-app/README.md +++ b/packages/create-vitnode-app/README.md @@ -40,7 +40,7 @@ bun create vitnode-app@latest | Option | Description | | ------------------- | --------------------------------------------------------------------------------- | | `--package-manager` | Specify the package manager to use. Support `npm`, `pnpm`. | -| `--eslint` | Initialize with eslint config. | +| `--biome` | Initialize with Biome config. | | `--skip-install` | Skip installing packages after initializing the project. | | `--mode` | Specify the type of app to create. Support `singleApp`, `apiMonorepo`, `onlyApi`. | | `--monorepo` | Create project with monorepo structure. | diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/.vscode/settings.json b/packages/create-vitnode-app/copy-of-vitnode-app/.vscode/settings.json index 18f93aeb9..30a71ae63 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/.vscode/settings.json +++ b/packages/create-vitnode-app/copy-of-vitnode-app/.vscode/settings.json @@ -1,6 +1,35 @@ { "cSpell.words": ["vitnode"], + "github.copilot.chat.commitMessageGeneration.instructions": [ + { + "text": "Follow the Conventional Commits format strictly for commit messages. Use the structure below:\n\n```\n[optional scope]: \n```\n\nGuidelines:\n\n1. **Type and Scope**: Choose an appropriate type (e.g., `feat`, `fix`, `refactor`, `docs`) and optional scope to describe the affected module or feature.\n\n2. **Gitmoji**: Include a relevant `gitmoji` that best represents the nature of the change.\n\n3. **Description**: Write a concise, informative description in the header; use backticks if referencing code or specific terms.\n\nCommit messages should be clear, informative, and professional, aiding readability and project tracking." + } + ], "search.exclude": { "**/(plugins)/*": true + }, + "[javascriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[css]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[jsonc]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[html]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[json]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" } } diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/api-bun/src/index.ts b/packages/create-vitnode-app/copy-of-vitnode-app/api-bun/src/index.ts index 284aa1840..19cb3e2a3 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/api-bun/src/index.ts +++ b/packages/create-vitnode-app/copy-of-vitnode-app/api-bun/src/index.ts @@ -1,9 +1,9 @@ -import { OpenAPIHono } from '@hono/zod-openapi'; -import { VitNodeAPI } from '@vitnode/core/api/config'; +import { OpenAPIHono } from "@hono/zod-openapi"; +import { VitNodeAPI } from "@vitnode/core/api/config"; -import { vitNodeApiConfig } from './vitnode.api.config.js'; +import { vitNodeApiConfig } from "./vitnode.api.config.js"; -const app = new OpenAPIHono().basePath('/api'); +const app = new OpenAPIHono().basePath("/api"); VitNodeAPI({ app, diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/api-single-app/src/vitnode.api.config.ts b/packages/create-vitnode-app/copy-of-vitnode-app/api-single-app/src/vitnode.api.config.ts index ba53fa6d2..316a9eb9c 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/api-single-app/src/vitnode.api.config.ts +++ b/packages/create-vitnode-app/copy-of-vitnode-app/api-single-app/src/vitnode.api.config.ts @@ -1,19 +1,18 @@ -import { buildApiConfig } from '@vitnode/core/vitnode.config'; -import { drizzle } from 'drizzle-orm/postgres-js'; +import { buildApiConfig } from "@vitnode/core/vitnode.config"; +import { drizzle } from "drizzle-orm/postgres-js"; export const POSTGRES_URL = - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - process.env.POSTGRES_URL || 'postgresql://root:root@localhost:5432/vitnode'; + process.env.POSTGRES_URL || "postgresql://root:root@localhost:5432/vitnode"; export const vitNodeApiConfig = buildApiConfig({ pathToMessages: async path => await import(`./locales/${path}`), metadata: { - title: 'VitNode', - shortTitle: 'VitNode', + title: "VitNode", + shortTitle: "VitNode", }, plugins: [], dbProvider: drizzle({ connection: POSTGRES_URL, - casing: 'camelCase', + casing: "camelCase", }), }); diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/api/src/index.ts b/packages/create-vitnode-app/copy-of-vitnode-app/api/src/index.ts index 28ac18442..204f0e9b3 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/api/src/index.ts +++ b/packages/create-vitnode-app/copy-of-vitnode-app/api/src/index.ts @@ -1,10 +1,11 @@ -import { serve } from '@hono/node-server'; -import { OpenAPIHono } from '@hono/zod-openapi'; -import { VitNodeAPI } from '@vitnode/core/api/config'; +/** biome-ignore-all lint/suspicious/noConsole: */ +import { serve } from "@hono/node-server"; +import { OpenAPIHono } from "@hono/zod-openapi"; +import { VitNodeAPI } from "@vitnode/core/api/config"; -import { vitNodeApiConfig } from './vitnode.api.config.js'; +import { vitNodeApiConfig } from "./vitnode.api.config.js"; -const app = new OpenAPIHono().basePath('/api'); +const app = new OpenAPIHono().basePath("/api"); VitNodeAPI({ app, @@ -17,9 +18,8 @@ serve( port: 8080, }, info => { - const initMessage = '\x1b[34m[VitNode]\x1b[0m'; + const initMessage = "\x1b[34m[VitNode]\x1b[0m"; - // eslint-disable-next-line no-console console.log( `${initMessage} API server is running on http://localhost:${info.port}`, ); diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/api/src/vitnode.api.config.ts b/packages/create-vitnode-app/copy-of-vitnode-app/api/src/vitnode.api.config.ts index 19d053edd..7cc1e1355 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/api/src/vitnode.api.config.ts +++ b/packages/create-vitnode-app/copy-of-vitnode-app/api/src/vitnode.api.config.ts @@ -1,24 +1,23 @@ -import { buildApiConfig } from '@vitnode/core/vitnode.config'; -import * as dotenv from 'dotenv'; -import { drizzle } from 'drizzle-orm/postgres-js'; +import { buildApiConfig } from "@vitnode/core/vitnode.config"; +import { config } from "dotenv"; +import { drizzle } from "drizzle-orm/postgres-js"; -dotenv.config({ +config({ quiet: true, }); export const POSTGRES_URL = - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - process.env.POSTGRES_URL || 'postgresql://root:root@localhost:5432/vitnode'; + process.env.POSTGRES_URL || "postgresql://root:root@localhost:5432/vitnode"; export const vitNodeApiConfig = buildApiConfig({ plugins: [], pathToMessages: async path => await import(`./locales/${path}`), dbProvider: drizzle({ connection: POSTGRES_URL, - casing: 'camelCase', + casing: "camelCase", }), metadata: { - title: 'VitNode API', - shortTitle: 'VitNode', + title: "VitNode API", + shortTitle: "VitNode", }, }); diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/biome/biome.json b/packages/create-vitnode-app/copy-of-vitnode-app/biome/biome.json new file mode 100644 index 000000000..f27120cc0 --- /dev/null +++ b/packages/create-vitnode-app/copy-of-vitnode-app/biome/biome.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.2.2/schema.json", + "extends": ["@vitnode/config/biome"], + "root": false +} diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/eslint/.prettierrc.mjs b/packages/create-vitnode-app/copy-of-vitnode-app/eslint/.prettierrc.mjs deleted file mode 100644 index 92fe83d4d..000000000 --- a/packages/create-vitnode-app/copy-of-vitnode-app/eslint/.prettierrc.mjs +++ /dev/null @@ -1,11 +0,0 @@ -import vitnodePrettier from '@vitnode/eslint-config/prettierrc'; - -/** - * @see https://prettier.io/docs/en/configuration.html - * @type {import("prettier").Config} - */ -const config = { - ...vitnodePrettier, -}; - -export default config; diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/eslint/eslint.config.mjs b/packages/create-vitnode-app/copy-of-vitnode-app/eslint/eslint.config.mjs deleted file mode 100644 index 0098d1c8a..000000000 --- a/packages/create-vitnode-app/copy-of-vitnode-app/eslint/eslint.config.mjs +++ /dev/null @@ -1,17 +0,0 @@ -import eslintVitNode from '@vitnode/eslint-config/eslint'; -import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -export default [ - ...eslintVitNode, - { - languageOptions: { - parserOptions: { - project: './tsconfig.json', - tsconfigRootDir: __dirname, - }, - }, - }, -]; diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/monorepo/turbo.json b/packages/create-vitnode-app/copy-of-vitnode-app/monorepo/turbo.json index a40c2981b..ea7c091bc 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/monorepo/turbo.json +++ b/packages/create-vitnode-app/copy-of-vitnode-app/monorepo/turbo.json @@ -22,12 +22,6 @@ "outputs": [".next/**", "!.next/cache/**", "dist/**"], "env": ["POSTGRES_URL", "NEXT_PUBLIC_API_URL", "NEXT_PUBLIC_WEB_URL"] }, - "lint": { - "dependsOn": ["^lint"] - }, - "lint:fix": { - "dependsOn": ["^lint:fix"] - }, "dev": { "cache": false, "persistent": true, diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/global.d.ts b/packages/create-vitnode-app/copy-of-vitnode-app/root/global.d.ts index 599b27bef..ca6f61f83 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/root/global.d.ts +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/global.d.ts @@ -1,8 +1,8 @@ /// -import type core from './src/locales/@vitnode/core/en.json'; +import core from "./src/locales/@vitnode/core/en.json" with { type: "json" }; -declare module 'next-intl' { +declare module "next-intl" { interface AppConfig { Messages: typeof core; } diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/next.config.ts b/packages/create-vitnode-app/copy-of-vitnode-app/root/next.config.ts index 6d893c582..7a7db9334 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/root/next.config.ts +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/next.config.ts @@ -1,5 +1,5 @@ -import type { NextConfig } from 'next'; -import { vitNodeNextConfig } from '@vitnode/core/config/next.config'; +import { vitNodeNextConfig } from "@vitnode/core/config/next.config"; +import type { NextConfig } from "next"; const nextConfig: NextConfig = { experimental: { diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/[locale]/(main)/layout.tsx b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/[locale]/(main)/layout.tsx index 086bc3368..85b599a1d 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/[locale]/(main)/layout.tsx +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/[locale]/(main)/layout.tsx @@ -1,7 +1,7 @@ -import { LogoVitNode } from '@vitnode/core/components/logo-vitnode'; -import { ThemeLayout } from '@vitnode/core/views/layouts/theme/layout'; +import { LogoVitNode } from "@vitnode/core/components/logo-vitnode"; +import { ThemeLayout } from "@vitnode/core/views/layouts/theme/layout"; -import { vitNodeConfig } from '../../../vitnode.config'; +import { vitNodeConfig } from "../../../vitnode.config"; export default function Layout({ children }: { children: React.ReactNode }) { return ( diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/[locale]/(main)/page.tsx b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/[locale]/(main)/page.tsx index 220dcc433..ad15f195f 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/[locale]/(main)/page.tsx +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/[locale]/(main)/page.tsx @@ -1,8 +1,8 @@ -import { LogoVitNode } from '@vitnode/core/components/logo-vitnode'; -import { buttonVariants } from '@vitnode/core/components/ui/button'; -import { Link } from '@vitnode/core/lib/navigation'; -import { cn } from '@vitnode/core/lib/utils'; -import { ArrowRight, Book, Terminal } from 'lucide-react'; +import { LogoVitNode } from "@vitnode/core/components/logo-vitnode"; +import { buttonVariants } from "@vitnode/core/components/ui/button"; +import { Link } from "@vitnode/core/lib/navigation"; +import { cn } from "@vitnode/core/lib/utils"; +import { ArrowRight, Book, Terminal } from "lucide-react"; export default function Page() { return ( @@ -25,7 +25,7 @@ export default function Page() { + GitHub diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/global.css b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/global.css index ac899273a..3dc975b84 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/global.css +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/global.css @@ -1,6 +1,7 @@ -@import 'tailwindcss'; +/** biome-ignore-all lint/suspicious/noUnknownAtRules: */ +@import "tailwindcss"; -@import 'tw-animate-css'; +@import "tw-animate-css"; @source "../../node_modules/@vitnode/core/dist/src/components"; @source "../../node_modules/@vitnode/core/dist/src/views"; diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/middleware.ts b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/middleware.ts index 99e9d7051..dbc211b22 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/middleware.ts +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/middleware.ts @@ -1,6 +1,6 @@ -import createMiddleware from 'next-intl/middleware'; +import createMiddleware from "next-intl/middleware"; -import { vitNodeConfig } from './vitnode.config'; +import { vitNodeConfig } from "./vitnode.config"; export default createMiddleware({ locales: vitNodeConfig.i18n.locales.map(locale => locale.code), @@ -11,14 +11,14 @@ export default createMiddleware({ export const config = { matcher: [ // Enable a redirect to a matching locale at the root - '/', + "/", // Set a cookie to remember the previous locale for // all requests that have a locale prefix - '/(en)/:path*', + "/(en)/:path*", // Enable redirects that add missing locales // (e.g. `/pathnames` -> `/en/pathnames`) - '/((?!_next|_vercel|api|.*\\..*).*)', + "/((?!_next|_vercel|api|.*\\..*).*)", ], }; diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/vitnode.config.ts b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/vitnode.config.ts index 4f20e8a11..ef9d4743c 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/vitnode.config.ts +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/vitnode.config.ts @@ -1,23 +1,23 @@ -import { buildConfig, handleRequestConfig } from '@vitnode/core/vitnode.config'; -import { getRequestConfig } from 'next-intl/server'; +import { buildConfig, handleRequestConfig } from "@vitnode/core/vitnode.config"; +import { getRequestConfig } from "next-intl/server"; export const vitNodeConfig = buildConfig({ metadata: { - title: 'VitNode', - shortTitle: 'VitNode', + title: "VitNode", + shortTitle: "VitNode", }, plugins: [], i18n: { locales: [ { - code: 'en', - name: 'English', + code: "en", + name: "English", }, ], - defaultLocale: 'en', + defaultLocale: "en", }, theme: { - defaultTheme: 'light', + defaultTheme: "light", }, }); diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/tsconfig.json b/packages/create-vitnode-app/copy-of-vitnode-app/root/tsconfig.json index 13dc93961..c1767f2ac 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/root/tsconfig.json +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/tsconfig.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/tsconfig", - "extends": "@vitnode/eslint-config/tsconfig", + "extends": "@vitnode/config/tsconfig", "compilerOptions": { "target": "ESNext", "module": "esnext", diff --git a/packages/create-vitnode-app/eslint.config.mjs b/packages/create-vitnode-app/eslint.config.mjs deleted file mode 100644 index ee68cfb76..000000000 --- a/packages/create-vitnode-app/eslint.config.mjs +++ /dev/null @@ -1,25 +0,0 @@ -import eslintVitNode from '@vitnode/eslint-config/eslint'; -import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -export default [ - ...eslintVitNode, - { - rules: { - 'no-console': 'off', - }, - }, - { - ignores: ['copy-of-vitnode-app'], - }, - { - languageOptions: { - parserOptions: { - project: './tsconfig.json', - tsconfigRootDir: __dirname, - }, - }, - }, -]; diff --git a/packages/create-vitnode-app/package.json b/packages/create-vitnode-app/package.json index 692d774e9..91f5dcd83 100644 --- a/packages/create-vitnode-app/package.json +++ b/packages/create-vitnode-app/package.json @@ -16,9 +16,7 @@ }, "scripts": { "build:scripts": "tsc && node dist/src/prepare/prepare.js", - "start:scripts": "node dist/src/index.js", - "lint": "eslint .", - "lint:fix": "eslint . --fix" + "start:scripts": "node dist/src/index.js" }, "keywords": [ "vitnode", @@ -38,8 +36,7 @@ "@types/node": "^24.3.0", "@types/prompts": "^2.4.9", "@types/validate-npm-package-name": "^4.0.2", - "@vitnode/eslint-config": "workspace:*", - "eslint": "^9.33.0", + "@vitnode/config": "workspace:*", "typescript": "^5.9.2" } } diff --git a/packages/create-vitnode-app/src/create/create-package-json.ts b/packages/create-vitnode-app/src/create/create-package-json.ts index 58a8def28..9073a03d0 100644 --- a/packages/create-vitnode-app/src/create/create-package-json.ts +++ b/packages/create-vitnode-app/src/create/create-package-json.ts @@ -1,272 +1,261 @@ -import { readFile, writeFile } from 'fs/promises'; -import { dirname, join } from 'path'; -import { fileURLToPath } from 'url'; - -import type { PackageJSON } from '../helpers/packages-json.js'; -import type { CreateCliReturn } from '../questions.js'; - -import { getAvailablePackageManagers } from '../helpers/get-available-package-managers.js'; +import { readFile, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { getAvailablePackageManagers } from "../helpers/get-available-package-managers.js"; +import type { PackageJSON } from "../helpers/packages-json.js"; +import type { CreateCliReturn } from "../questions.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -type Mode = CreateCliReturn['mode']; +type Mode = CreateCliReturn["mode"]; const writeJson = async (path: string, data: unknown) => writeFile(path, JSON.stringify(data, null, 2)); const paths = (root: string) => ({ root, - api: join(root, 'apps', 'api'), - web: join(root, 'apps', 'web'), + api: join(root, "apps", "api"), + web: join(root, "apps", "web"), }); const withIf = >(cond: boolean, obj: T) => (cond ? obj : {}) as Partial; const versions = { - typesNode: '^24', - typesReact: '^19.1', - typesReactDom: '^19.1', - typesMdx: '^2.0.13', - typesBun: 'latest', - - turbo: '^2.5.6', - typescript: '^5.9.2', - tsx: '^4.20.4', - tscAlias: '^1.8.16', - eslint: '^9.33.0', - prettier: '^3.6.2', - prettierTailwind: '^0.6.14', - tailwind: '^4.1.12', - tailwindPostcss: '^4.1.12', - postcss: '^8.5.6', - twAnimateCssWeb: '^1.3.7', - twAnimateCssSingle: '^1.3.6', - - react: '^19.1', - reactDom: '^19.1', - nextSingle: '^15.5.0', - nextWebInMonorepo: '^15.4.6', - nextIntl: '^4.3.4', - useIntl: '^4.3.4', - rhf: '^7.62.0', - rhfResolvers: '^5.1.1', - lucide: '^0.540.0', - sonner: '^2.0.7', - dotenv: '^17.2.1', - - drizzleKitSingle: '^0.31.4', - drizzleKitApi: '^0.31.3', - drizzleOrm: '^0.44.4', - - hono: '^4.9.2', - honoZodOpenapi: '^1.1.0', - honoZodValidator: '^0.7.2', - reactEmail: '^4.2.8', - reactEmailComponents: '^0.5.1', - zod: '^4.0.17', - - babelReactCompiler: '19.1.0-rc.2', - cva: '^0.7.1', + typesNode: "^24", + typesReact: "^19.1", + typesReactDom: "^19.1", + typesMdx: "^2.0.13", + typesBun: "latest", + + turbo: "^2.5.6", + typescript: "^5.9.2", + tsx: "^4.20.4", + tscAlias: "^1.8.16", + biome: "^2.2.2", + tailwind: "^4.1.12", + tailwindPostcss: "^4.1.12", + postcss: "^8.5.6", + twAnimateCssWeb: "^1.3.7", + twAnimateCssSingle: "^1.3.6", + + react: "^19.1", + reactDom: "^19.1", + nextSingle: "^15.5.0", + nextWebInMonorepo: "^15.4.6", + nextIntl: "^4.3.4", + useIntl: "^4.3.4", + rhf: "^7.62.0", + rhfResolvers: "^5.1.1", + lucide: "^0.540.0", + sonner: "^2.0.7", + dotenv: "^17.2.1", + + drizzleKitSingle: "^0.31.4", + drizzleKitApi: "^0.31.3", + drizzleOrm: "^0.44.4", + + hono: "^4.9.2", + honoZodOpenapi: "^1.1.0", + honoZodValidator: "^0.7.2", + reactEmail: "^4.2.8", + reactEmailComponents: "^0.5.1", + zod: "^4.0.17", + + babelReactCompiler: "19.1.0-rc.2", + cva: "^0.7.1", }; /** * Shared blocks */ -const eslintScripts = { - lint: 'eslint .', - 'lint:fix': 'eslint . --fix', +const biomeScripts = { + lint: "biome check", + "lint:fix": "biome check . --write", }; const dockerDevScript = (appName: string) => `docker compose -f ./docker-compose.yml -p ${appName}-vitnode-dev up -d`; const rootScripts = ( - enableEslint: boolean, + enableBiome: boolean, enableDocker: boolean, appName: string, ) => ({ - 'db:migrate': 'turbo db:migrate', - 'db:push': 'turbo db:push', - init: 'turbo init', - dev: 'turbo dev', - build: 'turbo build', - start: 'turbo start', - ...withIf(enableEslint, eslintScripts), - ...withIf(enableDocker, { 'docker:dev': dockerDevScript(appName) }), + "db:migrate": "turbo db:migrate", + "db:push": "turbo db:push", + init: "turbo init", + dev: "turbo dev", + build: "turbo build", + start: "turbo start", + ...withIf(enableBiome, biomeScripts), + ...withIf(enableDocker, { "docker:dev": dockerDevScript(appName) }), }); const apiScripts = ( pm: string, - eslint: boolean, + biome: boolean, docker: boolean, onlyApi: boolean, appName: string, ) => ({ - 'db:push': 'vitnode push', - 'db:migrate': 'vitnode migrate', - init: 'vitnode init --api', - ...(pm === 'bun' + "db:push": "vitnode push", + "db:migrate": "vitnode migrate", + init: "vitnode init --api", + ...(pm === "bun" ? { - dev: 'vitnode init --api && bun run --hot src/index.ts', - start: 'NODE_ENV=production bun run src/index.ts', + dev: "vitnode init --api && bun run --hot src/index.ts", + start: "NODE_ENV=production bun run src/index.ts", } : { - dev: 'vitnode init --api && tsx watch src/index.ts', - build: 'tsc && tsc-alias -p tsconfig.json', - start: 'node dist/index.js', + dev: "vitnode init --api && tsx watch src/index.ts", + build: "tsc && tsc-alias -p tsconfig.json", + start: "node dist/index.js", }), - 'dev:email': 'email dev --dir src/emails', - ...withIf(eslint, eslintScripts), - ...withIf(docker && onlyApi, { 'docker:dev': dockerDevScript(appName) }), - 'drizzle-kit': 'drizzle-kit', + "dev:email": "email dev --dir src/emails", + ...withIf(biome, biomeScripts), + ...withIf(docker && onlyApi, { "docker:dev": dockerDevScript(appName) }), + "drizzle-kit": "drizzle-kit", }); const singleAppScripts = ( - eslint: boolean, + biome: boolean, docker: boolean, appName: string, ) => ({ - 'db:push': 'vitnode push', - 'db:migrate': 'vitnode migrate', - init: 'vitnode init', - dev: 'vitnode init && next dev --turbopack', - 'dev:email': 'email dev --dir src/emails', - build: 'next build', - start: 'next start', - ...withIf(eslint, eslintScripts), - ...withIf(docker, { 'docker:dev': dockerDevScript(appName) }), - 'drizzle-kit': 'drizzle-kit', + "db:push": "vitnode push", + "db:migrate": "vitnode migrate", + init: "vitnode init", + dev: "vitnode init && next dev --turbopack", + "dev:email": "email dev --dir src/emails", + build: "next build", + start: "next start", + ...withIf(biome, biomeScripts), + ...withIf(docker, { "docker:dev": dockerDevScript(appName) }), + "drizzle-kit": "drizzle-kit", }); -const webScripts = (eslint: boolean) => ({ - init: 'vitnode init --web', - dev: 'vitnode init --web && next dev --turbopack', - build: 'next build', - start: 'next start', - ...withIf(eslint, eslintScripts), +const webScripts = (biome: boolean) => ({ + init: "vitnode init --web", + dev: "vitnode init --web && next dev --turbopack", + build: "next build", + start: "next start", + ...withIf(biome, biomeScripts), }); /** * Dependency builders */ -const baseDevDeps = (eslint: boolean, includePrettier: boolean) => ({ - '@types/node': versions.typesNode, - '@vitnode/eslint-config': '', // filled with local version dynamically - ...withIf(eslint, { - eslint: versions.eslint, - ...withIf(includePrettier, { - prettier: versions.prettier, - 'prettier-plugin-tailwindcss': versions.prettierTailwind, - }), +const baseDevDeps = (biome: boolean) => ({ + "@types/node": versions.typesNode, + "@vitnode/config": "", // filled with local version dynamically + ...withIf(biome, { + "@biomejs/biome": versions.biome, }), }); -const rootDevDeps = (eslint: boolean) => ({ - ...baseDevDeps(eslint, true), +const rootDevDeps = (biome: boolean) => ({ + ...baseDevDeps(biome), turbo: versions.turbo, typescript: versions.typescript, zod: versions.zod, }); const apiDeps = { - '@hono/zod-openapi': versions.honoZodOpenapi, - '@hono/zod-validator': versions.honoZodValidator, - '@react-email/components': versions.reactEmailComponents, - '@vitnode/core': '', // filled dynamically - 'drizzle-kit': versions.drizzleKitApi, - 'drizzle-orm': versions.drizzleOrm, + "@hono/zod-openapi": versions.honoZodOpenapi, + "@hono/zod-validator": versions.honoZodValidator, + "@react-email/components": versions.reactEmailComponents, + "@vitnode/core": "", // filled dynamically + "drizzle-kit": versions.drizzleKitApi, + "drizzle-orm": versions.drizzleOrm, hono: versions.hono, - 'next-intl': versions.nextIntl, + "next-intl": versions.nextIntl, react: versions.react, - 'react-dom': versions.reactDom, - 'use-intl': versions.useIntl, + "react-dom": versions.reactDom, + "use-intl": versions.useIntl, zod: versions.zod, }; -const apiDevDeps = (pm: string, eslint: boolean) => ({ - '@hono/node-server': '^1.19.0', - ...(pm === 'bun' ? { '@types/bun': versions.typesBun } : {}), - '@types/node': versions.typesNode, - '@types/react': versions.typesReact, - '@types/react-dom': versions.typesReactDom, - '@vitnode/eslint-config': '', +const apiDevDeps = (pm: string, biome: boolean) => ({ + "@hono/node-server": "^1.19.0", + ...(pm === "bun" ? { "@types/bun": versions.typesBun } : {}), + "@types/node": versions.typesNode, + "@types/react": versions.typesReact, + "@types/react-dom": versions.typesReactDom, + "@vitnode/config": "", dotenv: versions.dotenv, - ...withIf(eslint, { - eslint: versions.eslint, - // Prettier in API only when onlyApi + eslint in original code – we'll preserve by passing include later if needed + ...withIf(biome, { + "@biomejs/biome": versions.biome, }), - 'react-email': versions.reactEmail, - 'tsc-alias': versions.tscAlias, + "react-email": versions.reactEmail, + "tsc-alias": versions.tscAlias, tsx: versions.tsx, typescript: versions.typescript, }); const singleAppDeps = { - '@hono/zod-openapi': versions.honoZodOpenapi, - '@hono/zod-validator': versions.honoZodValidator, - '@hookform/resolvers': versions.rhfResolvers, - '@react-email/components': versions.reactEmailComponents, - '@vitnode/core': '', - 'babel-plugin-react-compiler': versions.babelReactCompiler, - 'drizzle-kit': versions.drizzleKitSingle, - 'drizzle-orm': versions.drizzleOrm, + "@hono/zod-openapi": versions.honoZodOpenapi, + "@hono/zod-validator": versions.honoZodValidator, + "@hookform/resolvers": versions.rhfResolvers, + "@react-email/components": versions.reactEmailComponents, + "@vitnode/core": "", + "babel-plugin-react-compiler": versions.babelReactCompiler, + "drizzle-kit": versions.drizzleKitSingle, + "drizzle-orm": versions.drizzleOrm, hono: versions.hono, - 'lucide-react': versions.lucide, + "lucide-react": versions.lucide, next: versions.nextSingle, - 'next-intl': versions.nextIntl, + "next-intl": versions.nextIntl, react: versions.react, - 'react-dom': versions.reactDom, - 'react-hook-form': versions.rhf, + "react-dom": versions.reactDom, + "react-hook-form": versions.rhf, sonner: versions.sonner, - 'use-intl': versions.useIntl, + "use-intl": versions.useIntl, zod: versions.zod, }; -const singleAppDevDeps = (eslint: boolean) => ({ - '@tailwindcss/postcss': versions.tailwindPostcss, - '@types/node': versions.typesNode, - '@types/react': versions.typesReact, - '@types/react-dom': versions.typesReactDom, - '@vitnode/eslint-config': '', - ...withIf(eslint, { - eslint: versions.eslint, - prettier: versions.prettier, - 'prettier-plugin-tailwindcss': versions.prettierTailwind, +const singleAppDevDeps = (biome: boolean) => ({ + "@tailwindcss/postcss": versions.tailwindPostcss, + "@types/node": versions.typesNode, + "@types/react": versions.typesReact, + "@types/react-dom": versions.typesReactDom, + "@vitnode/config": "", + ...withIf(biome, { + "@biomejs/biome": versions.biome, }), - 'react-email': versions.reactEmail, + "react-email": versions.reactEmail, turbo: versions.turbo, tailwindcss: versions.tailwind, - 'tw-animate-css': versions.twAnimateCssSingle, + "tw-animate-css": versions.twAnimateCssSingle, typescript: versions.typescript, }); const webDeps = { - '@vitnode/core': '', - 'babel-plugin-react-compiler': versions.babelReactCompiler, - 'lucide-react': versions.lucide, + "@vitnode/core": "", + "babel-plugin-react-compiler": versions.babelReactCompiler, + "lucide-react": versions.lucide, next: versions.nextWebInMonorepo, - 'next-intl': versions.nextIntl, + "next-intl": versions.nextIntl, react: versions.react, - 'react-dom': versions.reactDom, - 'react-hook-form': versions.rhf, + "react-dom": versions.reactDom, + "react-hook-form": versions.rhf, sonner: versions.sonner, }; -const webDevDeps = (eslint: boolean) => ({ - '@hookform/resolvers': versions.rhfResolvers, - '@tailwindcss/postcss': versions.tailwindPostcss, - '@types/mdx': versions.typesMdx, - '@types/node': versions.typesNode, - '@types/react': versions.typesReact, - '@types/react-dom': versions.typesReactDom, - '@vitnode/eslint-config': '', - 'class-variance-authority': versions.cva, - ...withIf(eslint, { eslint: versions.eslint }), +const webDevDeps = (biome: boolean) => ({ + "@hookform/resolvers": versions.rhfResolvers, + "@tailwindcss/postcss": versions.tailwindPostcss, + "@types/mdx": versions.typesMdx, + "@types/node": versions.typesNode, + "@types/react": versions.typesReact, + "@types/react-dom": versions.typesReactDom, + "@vitnode/config": "", + "class-variance-authority": versions.cva, + ...withIf(biome, { "@biomejs/biome": versions.biome }), postcss: versions.postcss, tailwindcss: versions.tailwind, - 'tw-animate-css': versions.twAnimateCssWeb, + "tw-animate-css": versions.twAnimateCssWeb, typescript: versions.typescript, zod: versions.zod, }); @@ -278,14 +267,14 @@ export const createPackageJSON = async ({ appName, packageManager, root, - eslint, + biome, docker, mode, monorepo, }: { appName: string; docker?: boolean; - eslint: boolean; + biome: boolean; mode: Mode; monorepo?: boolean; packageManager: string; @@ -293,7 +282,7 @@ export const createPackageJSON = async ({ }) => { // Resolve local version of @vitnode/* based on this CLI's package.json const cliPkg: PackageJSON = JSON.parse( - await readFile(join(__dirname, '..', '..', '..', 'package.json'), 'utf-8'), + await readFile(join(__dirname, "..", "..", "..", "package.json"), "utf-8"), ); const vitnodeVersionRange = `^${cliPkg.version}`; @@ -301,105 +290,104 @@ export const createPackageJSON = async ({ const pmSpec = `${packageManager}@${pmVersions[packageManager]}`; const p = paths(root); - const isApiMonorepo = mode === 'apiMonorepo' || !!monorepo; - const isOnlyApi = mode === 'onlyApi'; - const isSingleApp = mode === 'singleApp'; + const isApiMonorepo = mode === "apiMonorepo" || !!monorepo; + const isOnlyApi = mode === "onlyApi"; + const isSingleApp = mode === "singleApp"; // 1) Root package.json (for monorepo/apiMonorepo) if (isApiMonorepo) { const rootPkg: PackageJSON = { name: appName, private: true, - scripts: rootScripts(eslint, !!docker, appName), + scripts: rootScripts(biome, !!docker, appName), devDependencies: { - ...rootDevDeps(eslint), - '@vitnode/eslint-config': vitnodeVersionRange, + ...rootDevDeps(biome), + "@vitnode/config": vitnodeVersionRange, }, packageManager: pmSpec, - workspaces: ['apps/*', 'plugins/*'], + workspaces: ["apps/*", "plugins/*"], }; - await writeJson(join(p.root, 'package.json'), rootPkg); + await writeJson(join(p.root, "package.json"), rootPkg); } // 2) API package.json (shared by onlyApi and apiMonorepo) const apiPkg: PackageJSON = { - name: isApiMonorepo ? 'api' : appName, - version: '0.1.0', + name: isApiMonorepo ? "api" : appName, + version: "0.1.0", private: true, - type: 'module', + type: "module", scripts: apiScripts( packageManager, - eslint, + biome, !!docker, - mode === 'onlyApi', + mode === "onlyApi", appName, ), dependencies: { ...apiDeps, - '@vitnode/core': vitnodeVersionRange, + "@vitnode/core": vitnodeVersionRange, }, devDependencies: { - ...apiDevDeps(packageManager, eslint), - '@vitnode/eslint-config': vitnodeVersionRange, - ...(eslint && mode === 'onlyApi' + ...apiDevDeps(packageManager, biome), + "@vitnode/config": vitnodeVersionRange, + ...(biome && mode === "onlyApi" ? { - prettier: versions.prettier, - 'prettier-plugin-tailwindcss': versions.prettierTailwind, + "@biomejs/biome": versions.biome, } : {}), // TS pipeline pieces when not using Bun for dev - ...(packageManager === 'bun' ? {} : {}), + ...(packageManager === "bun" ? {} : {}), }, }; // 3) Single app (Next.js + API inside one app) if (isSingleApp) { const singlePkg: PackageJSON = { - name: monorepo ? 'web' : appName, - version: '0.1.0', + name: monorepo ? "web" : appName, + version: "0.1.0", private: true, - type: 'module', - scripts: singleAppScripts(eslint, !!docker, appName), + type: "module", + scripts: singleAppScripts(biome, !!docker, appName), dependencies: { ...singleAppDeps, - '@vitnode/core': vitnodeVersionRange, + "@vitnode/core": vitnodeVersionRange, }, devDependencies: { - ...singleAppDevDeps(eslint), - '@vitnode/eslint-config': vitnodeVersionRange, + ...singleAppDevDeps(biome), + "@vitnode/config": vitnodeVersionRange, }, packageManager: pmSpec, }; - await writeJson(join(monorepo ? p.web : p.root, 'package.json'), singlePkg); + await writeJson(join(monorepo ? p.web : p.root, "package.json"), singlePkg); } // 4) apiMonorepo: write API + WEB - if (mode === 'apiMonorepo') { - await writeJson(join(p.api, 'package.json'), apiPkg); + if (mode === "apiMonorepo") { + await writeJson(join(p.api, "package.json"), apiPkg); const webPkg: PackageJSON = { - name: 'web', - version: '0.1.0', + name: "web", + version: "0.1.0", private: true, - type: 'module', - scripts: webScripts(eslint), + type: "module", + scripts: webScripts(biome), dependencies: { ...webDeps, - '@vitnode/core': vitnodeVersionRange, + "@vitnode/core": vitnodeVersionRange, }, devDependencies: { - ...webDevDeps(eslint), - '@vitnode/eslint-config': vitnodeVersionRange, + ...webDevDeps(biome), + "@vitnode/config": vitnodeVersionRange, }, }; - await writeJson(join(p.web, 'package.json'), webPkg); + await writeJson(join(p.web, "package.json"), webPkg); } // 5) onlyApi: write API (in root or in monorepo structure if requested) if (isOnlyApi) { - await writeJson(join(monorepo ? p.api : p.root, 'package.json'), apiPkg); + await writeJson(join(monorepo ? p.api : p.root, "package.json"), apiPkg); } }; diff --git a/packages/create-vitnode-app/src/create/create-vitnode.ts b/packages/create-vitnode-app/src/create/create-vitnode.ts index f1fc07106..596fd3382 100644 --- a/packages/create-vitnode-app/src/create/create-vitnode.ts +++ b/packages/create-vitnode-app/src/create/create-vitnode.ts @@ -1,25 +1,30 @@ -import { existsSync } from 'fs'; -import { copyFile, cp, mkdir, readFile, rename, writeFile } from 'fs/promises'; -import ora from 'ora'; -import { dirname, join } from 'path'; -import color from 'picocolors'; -import { fileURLToPath } from 'url'; - -import type { CreateCliReturn } from '../questions.js'; - +import { existsSync } from "node:fs"; +import { + copyFile, + cp, + mkdir, + readFile, + rename, + writeFile, +} from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import ora from "ora"; +import color from "picocolors"; import { generateMigrationsVitnode, initFilesVitnode, -} from '../helpers/init-vitnode.js'; -import { installDependencies } from '../helpers/install-dependencies.js'; -import { isFolderEmpty } from '../helpers/is-folder-empty.js'; -import { createPackageJSON } from './create-package-json.js'; +} from "../helpers/init-vitnode.js"; +import { installDependencies } from "../helpers/install-dependencies.js"; +import { isFolderEmpty } from "../helpers/is-folder-empty.js"; +import type { CreateCliReturn } from "../questions.js"; +import { createPackageJSON } from "./create-package-json.js"; export const createVitNode = async ({ root, appName, packageManager, - eslint, + biome, install, docker, mode, @@ -27,6 +32,7 @@ export const createVitNode = async ({ }: CreateCliReturn & { appName: string; root: string; + // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: }) => { const spinner = ora( `Creating a new VitNode app in ${color.green(root)}. Using ${color.green(packageManager)}...`, @@ -34,10 +40,10 @@ export const createVitNode = async ({ const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); - const templatePath = join(__dirname, '..', '..', '..', 'copy-of-vitnode-app'); + const templatePath = join(__dirname, "..", "..", "..", "copy-of-vitnode-app"); if (!existsSync(templatePath)) { spinner.fail( - `\n${color.red('Error!')} Template path ${color.cyan(templatePath)} does not exist.`, + `\n${color.red("Error!")} Template path ${color.cyan(templatePath)} does not exist.`, ); process.exit(1); } @@ -48,17 +54,17 @@ export const createVitNode = async ({ process.exit(1); } const monorepoStructure = { - api: join(root, 'apps', 'api'), - web: join(root, 'apps', 'web'), + api: join(root, "apps", "api"), + web: join(root, "apps", "web"), }; - spinner.text = 'Preparing the project structure...'; - if (monorepo || mode === 'apiMonorepo') { + spinner.text = "Preparing the project structure..."; + if (monorepo || mode === "apiMonorepo") { const dirsToCreate: string[] = []; - if (mode === 'apiMonorepo' || (monorepo && mode === 'onlyApi')) { + if (mode === "apiMonorepo" || (monorepo && mode === "onlyApi")) { dirsToCreate.push(monorepoStructure.api); } - if (mode === 'apiMonorepo' || (monorepo && mode === 'singleApp')) { + if (mode === "apiMonorepo" || (monorepo && mode === "singleApp")) { dirsToCreate.push(monorepoStructure.web); } await Promise.all( @@ -66,50 +72,50 @@ export const createVitNode = async ({ ); } - spinner.text = 'Copying files...'; - await cp(join(templatePath, '.vscode'), join(root, '.vscode'), { + spinner.text = "Copying files..."; + await cp(join(templatePath, ".vscode"), join(root, ".vscode"), { recursive: true, }); - if (mode === 'singleApp') { + if (mode === "singleApp") { await Promise.all([ - cp(join(templatePath, 'root'), monorepo ? monorepoStructure.web : root, { + cp(join(templatePath, "root"), monorepo ? monorepoStructure.web : root, { recursive: true, }), cp( - join(templatePath, 'api-single-app'), + join(templatePath, "api-single-app"), monorepo ? monorepoStructure.web : root, { recursive: true, }, ), ]); - } else if (mode === 'apiMonorepo') { + } else if (mode === "apiMonorepo") { await Promise.all([ - cp(join(templatePath, 'root'), monorepoStructure.web, { + cp(join(templatePath, "root"), monorepoStructure.web, { recursive: true, }), - cp(join(templatePath, 'api'), monorepoStructure.api, { + cp(join(templatePath, "api"), monorepoStructure.api, { recursive: true, }), ]); - if (packageManager === 'bun') { - await cp(join(templatePath, 'api-bun'), monorepoStructure.api, { + if (packageManager === "bun") { + await cp(join(templatePath, "api-bun"), monorepoStructure.api, { recursive: true, }); } - } else if (mode === 'onlyApi') { + } else if (mode === "onlyApi") { await cp( - join(templatePath, 'api'), + join(templatePath, "api"), monorepo ? monorepoStructure.api : root, { recursive: true, }, ); - if (packageManager === 'bun') { + if (packageManager === "bun") { await cp( - join(templatePath, 'api-bun'), + join(templatePath, "api-bun"), monorepo ? monorepoStructure.api : root, { recursive: true, @@ -118,63 +124,63 @@ export const createVitNode = async ({ } } - if (mode === 'apiMonorepo' || monorepo) { - await cp(join(templatePath, 'monorepo'), root, { + if (mode === "apiMonorepo" || monorepo) { + await cp(join(templatePath, "monorepo"), root, { recursive: true, }); } - if (eslint) { - spinner.text = 'Copying eslint files...'; - await cp(join(templatePath, 'eslint'), root, { + if (biome) { + spinner.text = "Copying Biome files..."; + await cp(join(templatePath, "biome"), root, { recursive: true, }); } - spinner.text = 'Renaming special files...'; - await rename(join(root, '.gitignore_template'), join(root, '.gitignore')); - if (mode === 'apiMonorepo' || monorepo) { - if (existsSync(join(monorepoStructure.api, '.gitignore_template'))) { + spinner.text = "Renaming special files..."; + await rename(join(root, ".gitignore_template"), join(root, ".gitignore")); + if (mode === "apiMonorepo" || monorepo) { + if (existsSync(join(monorepoStructure.api, ".gitignore_template"))) { await rename( - join(monorepoStructure.api, '.gitignore_template'), - join(monorepoStructure.api, '.gitignore'), + join(monorepoStructure.api, ".gitignore_template"), + join(monorepoStructure.api, ".gitignore"), ); } - if (existsSync(join(monorepoStructure.web, '.gitignore_template'))) { + if (existsSync(join(monorepoStructure.web, ".gitignore_template"))) { await rename( - join(monorepoStructure.web, '.gitignore_template'), - join(monorepoStructure.web, '.gitignore'), + join(monorepoStructure.web, ".gitignore_template"), + join(monorepoStructure.web, ".gitignore"), ); } } - spinner.text = 'Creating package.json...'; + spinner.text = "Creating package.json..."; await createPackageJSON({ root, appName, packageManager, - eslint, + biome, docker, mode, monorepo, }); - if ((mode === 'apiMonorepo' || monorepo) && packageManager === 'pnpm') { - spinner.text = 'Creating pnpm-workspace.yaml...'; + if ((mode === "apiMonorepo" || monorepo) && packageManager === "pnpm") { + spinner.text = "Creating pnpm-workspace.yaml..."; const pnpmWorkspaceContent = `packages:\n - 'apps/*'\n - 'plugins/*'\n`; - await writeFile(join(root, 'pnpm-workspace.yaml'), pnpmWorkspaceContent); + await writeFile(join(root, "pnpm-workspace.yaml"), pnpmWorkspaceContent); } if (docker) { - spinner.text = 'Copying docker files...'; + spinner.text = "Copying docker files..."; await copyFile( - join(templatePath, 'docker', 'docker-compose.yml'), - join(root, 'docker-compose.yml'), + join(templatePath, "docker", "docker-compose.yml"), + join(root, "docker-compose.yml"), ); // Update docker-compose.yml with app name - const dockerComposePath = join(root, 'docker-compose.yml'); - const dockerComposeContent = await readFile(dockerComposePath, 'utf-8'); + const dockerComposePath = join(root, "docker-compose.yml"); + const dockerComposeContent = await readFile(dockerComposePath, "utf-8"); const updatedContent = dockerComposeContent.replace( /vitnode_postgres_dev/g, `${appName}_vitnode_postgres_dev`, @@ -182,19 +188,19 @@ export const createVitNode = async ({ await writeFile(dockerComposePath, updatedContent); } - spinner.text = 'Updating VitNode paths...'; + spinner.text = "Updating VitNode paths..."; if ( - (mode === 'apiMonorepo' || monorepo) && - packageManager !== 'pnpm' && - mode !== 'onlyApi' + (mode === "apiMonorepo" || monorepo) && + packageManager !== "pnpm" && + mode !== "onlyApi" ) { const globalCssPath = join( monorepoStructure.web, - 'src', - 'app', - 'global.css', + "src", + "app", + "global.css", ); - const globalCssContent = await readFile(globalCssPath, 'utf-8'); + const globalCssContent = await readFile(globalCssPath, "utf-8"); const updatedGlobalCssContent = globalCssContent.replaceAll( '@source "../../node_modules/@vitnode/', '@source "../../../../node_modules/@vitnode/', @@ -202,33 +208,33 @@ export const createVitNode = async ({ await writeFile(globalCssPath, updatedGlobalCssContent); } - if (mode === 'apiMonorepo') { - spinner.text = 'Setting up environment variables...'; - const envExamplePath = join(monorepoStructure.web, '.env.example'); + if (mode === "apiMonorepo") { + spinner.text = "Setting up environment variables..."; + const envExamplePath = join(monorepoStructure.web, ".env.example"); if (existsSync(envExamplePath)) { - await rename(envExamplePath, join(monorepoStructure.web, '.env')); + await rename(envExamplePath, join(monorepoStructure.web, ".env")); } } if (install) { - spinner.text = 'Installing dependencies...'; + spinner.text = "Installing dependencies..."; await installDependencies({ packageManager, cwd: root, }); - spinner.text = 'Initializing VitNode files...'; - if (mode === 'apiMonorepo') { + spinner.text = "Initializing VitNode files..."; + if (mode === "apiMonorepo") { await Promise.all([ initFilesVitnode({ packageManager, cwd: monorepoStructure.web, - flag: 'web', + flag: "web", }), initFilesVitnode({ packageManager, cwd: monorepoStructure.api, - flag: 'api', + flag: "api", }), initFilesVitnode({ packageManager, @@ -242,11 +248,11 @@ export const createVitNode = async ({ }); } - spinner.text = 'Generating migrations...'; + spinner.text = "Generating migrations..."; let migrationsCwd: string; - if (mode === 'apiMonorepo' || (monorepo && mode !== 'singleApp')) { + if (mode === "apiMonorepo" || (monorepo && mode !== "singleApp")) { migrationsCwd = monorepoStructure.api; - } else if (mode === 'singleApp' && monorepo) { + } else if (mode === "singleApp" && monorepo) { migrationsCwd = monorepoStructure.web; } else { migrationsCwd = root; @@ -258,6 +264,6 @@ export const createVitNode = async ({ } spinner.succeed( - `${color.green('Success!')} Created ${color.cyan(appName)} at ${color.cyan(root)}`, + `${color.green("Success!")} Created ${color.cyan(appName)} at ${color.cyan(root)}`, ); }; diff --git a/packages/create-vitnode-app/src/helpers/get-available-package-managers.ts b/packages/create-vitnode-app/src/helpers/get-available-package-managers.ts index 069050220..89d22a82e 100644 --- a/packages/create-vitnode-app/src/helpers/get-available-package-managers.ts +++ b/packages/create-vitnode-app/src/helpers/get-available-package-managers.ts @@ -1,18 +1,18 @@ -import { exec } from 'child_process'; +import { exec } from "node:child_process"; -export type PackageManager = 'bun' | 'npm' | 'pnpm'; +export type PackageManager = "bun" | "npm" | "pnpm"; export const execShellCommand = async ( cmd: string, ): Promise => { - return new Promise(resolve => { + return await new Promise(resolve => { exec(cmd, (error, stdout, stderr) => { if (error) { resolve(undefined); } const result = stdout ? stdout : stderr; - resolve(result.replace(/\s+/g, '')); + resolve(result.replace(/\s+/g, "")); }); }); }; @@ -21,9 +21,9 @@ export const getAvailablePackageManagers = async (): Promise< Record > => { const [npm, pnpm, bun] = await Promise.all([ - execShellCommand('npm --version'), - execShellCommand('pnpm --version'), - execShellCommand('bun --version'), + execShellCommand("npm --version"), + execShellCommand("pnpm --version"), + execShellCommand("bun --version"), ]); return { diff --git a/packages/create-vitnode-app/src/helpers/get-package-json.ts b/packages/create-vitnode-app/src/helpers/get-package-json.ts index ead3ff3ba..78ea81a40 100644 --- a/packages/create-vitnode-app/src/helpers/get-package-json.ts +++ b/packages/create-vitnode-app/src/helpers/get-package-json.ts @@ -1,7 +1,7 @@ -import { readFileSync } from 'fs'; +import { readFileSync } from "node:fs"; -import type { PackageJSON } from './packages-json.js'; +import type { PackageJSON } from "./packages-json.js"; export const packageJson: PackageJSON = JSON.parse( - readFileSync(new URL('../../../package.json', import.meta.url), 'utf-8'), + readFileSync(new URL("../../../package.json", import.meta.url), "utf-8"), ); diff --git a/packages/create-vitnode-app/src/helpers/init-vitnode.ts b/packages/create-vitnode-app/src/helpers/init-vitnode.ts index 9f4c9af0c..a0844e7a0 100644 --- a/packages/create-vitnode-app/src/helpers/init-vitnode.ts +++ b/packages/create-vitnode-app/src/helpers/init-vitnode.ts @@ -1,20 +1,20 @@ -import { spawn } from 'child_process'; +import { spawn } from "node:child_process"; -import type { CreateCliReturn } from '../questions.js'; +import type { CreateCliReturn } from "../questions.js"; export const initFilesVitnode = ({ packageManager: pm, cwd, flag, -}: Pick & { +}: Pick & { cwd?: string; - flag?: 'api' | 'web'; + flag?: "api" | "web"; }) => { - const packageManager = pm.split('@')[0]; + const packageManager = pm.split("@")[0]; const args: string[] = [ - 'vitnode', - 'prepare-plugins', - flag ? `--${flag}` : '', + "vitnode", + "prepare-plugins", + flag ? `--${flag}` : "", ]; spawn(packageManager, args, { @@ -26,9 +26,9 @@ export const initFilesVitnode = ({ export const generateMigrationsVitnode = ({ packageManager: pm, cwd, -}: Pick & { cwd?: string }) => { - const packageManager = pm.split('@')[0]; - const args: string[] = ['vitnode', 'migrate', '--generate']; +}: Pick & { cwd?: string }) => { + const packageManager = pm.split("@")[0]; + const args: string[] = ["vitnode", "migrate", "--generate"]; spawn(packageManager, args, { cwd, diff --git a/packages/create-vitnode-app/src/helpers/install-dependencies.ts b/packages/create-vitnode-app/src/helpers/install-dependencies.ts index 9e7cf64d4..1e03da45f 100644 --- a/packages/create-vitnode-app/src/helpers/install-dependencies.ts +++ b/packages/create-vitnode-app/src/helpers/install-dependencies.ts @@ -1,108 +1,116 @@ -import { spawn } from 'child_process'; -import color from 'picocolors'; +/** biome-ignore-all lint/suspicious/noConsole: */ +import { spawn } from "node:child_process"; +import color from "picocolors"; -import type { CreateCliReturn } from '../questions.js'; +import type { CreateCliReturn } from "../questions.js"; -import { getOnline } from './is-online.js'; +import { getOnline } from "./is-online.js"; + +function printInstallErrorSuggestions( + stderr: string, + color: typeof import("picocolors"), +) { + if (stderr.includes("ENOTFOUND") || stderr.includes("network")) { + console.error( + color.yellow( + "💡 Network error detected. Please check your internet connection.", + ), + ); + } else if ( + stderr.includes("EACCES") || + stderr.includes("permission denied") + ) { + console.error( + color.yellow( + "💡 Permission error detected. Try running with elevated privileges or check file permissions.", + ), + ); + } else if (stderr.includes("ENOSPC")) { + console.error( + color.yellow( + "💡 Disk space error detected. Please free up some disk space.", + ), + ); + } else if (stderr.includes("ERR_PNPM_PEER_DEP_ISSUES")) { + console.error( + color.yellow( + "💡 Peer dependency issues detected. Consider using --force flag or resolve conflicts manually.", + ), + ); + } +} export const installDependencies = async ({ packageManager: pm, cwd, -}: Pick & { cwd?: string }) => { - const packageManager = pm.split('@')[0]; +}: Pick & { cwd?: string }) => { + const packageManager = pm.split("@")[0]; const isOnline = await getOnline(); - const args: string[] = ['install']; + const args: string[] = ["install"]; if (!isOnline) { console.log( color.yellow( - 'You appear to be offline.\nFalling back to the local cache.', + "You appear to be offline.\nFalling back to the local cache.", ), ); - args.push('--offline'); + args.push("--offline"); } /** * Return a Promise that resolves once the installation is finished. */ return new Promise((resolve, reject) => { - let stdout = ''; - let stderr = ''; + let stdout = ""; + let stderr = ""; /** * Spawn the installation process. */ const child = spawn(packageManager, args, { - stdio: 'pipe', // Change to 'pipe' to capture output + stdio: "pipe", // Change to 'pipe' to capture output cwd, // Set the working directory shell: true, // Use shell to properly handle Windows batch files env: { ...process.env, - ADBLOCK: '1', + ADBLOCK: "1", // we set NODE_ENV to development as pnpm skips dev // dependencies when production - NODE_ENV: 'development', - DISABLE_OPENCOLLECTIVE: '1', + NODE_ENV: "development", + DISABLE_OPENCOLLECTIVE: "1", }, }); // Capture stdout - child.stdout?.on('data', (data: Buffer) => { + child.stdout?.on("data", (data: Buffer) => { const output = data.toString(); stdout += output; }); // Capture stderr - child.stderr?.on('data', (data: Buffer) => { + child.stderr?.on("data", (data: Buffer) => { const output = data.toString(); stderr += output; }); - child.on('close', code => { + child.on("close", code => { if (code !== 0) { console.error( color.red(`\n❌ Installation failed with exit code: ${code}`), ); if (stderr) { - console.error(color.red('Error output:')); + console.error(color.red("Error output:")); console.error(stderr); } if (stdout) { - console.log(color.yellow('Standard output:')); + console.log(color.yellow("Standard output:")); console.log(stdout); } // Provide helpful suggestions based on common errors - if (stderr.includes('ENOTFOUND') || stderr.includes('network')) { - console.error( - color.yellow( - '💡 Network error detected. Please check your internet connection.', - ), - ); - } else if ( - stderr.includes('EACCES') || - stderr.includes('permission denied') - ) { - console.error( - color.yellow( - '💡 Permission error detected. Try running with elevated privileges or check file permissions.', - ), - ); - } else if (stderr.includes('ENOSPC')) { - console.error( - color.yellow( - '💡 Disk space error detected. Please free up some disk space.', - ), - ); - } else if (stderr.includes('ERR_PNPM_PEER_DEP_ISSUES')) { - console.error( - color.yellow( - '💡 Peer dependency issues detected. Consider using --force flag or resolve conflicts manually.', - ), - ); - } + printInstallErrorSuggestions(stderr, color); reject( new Error( @@ -117,7 +125,7 @@ export const installDependencies = async ({ }); // Handle process errors - child.on('error', error => { + child.on("error", error => { console.error( color.red(`❌ Failed to start ${packageManager}:`), error.message, diff --git a/packages/create-vitnode-app/src/helpers/is-folder-empty.ts b/packages/create-vitnode-app/src/helpers/is-folder-empty.ts index ae16ca6f6..d86210167 100644 --- a/packages/create-vitnode-app/src/helpers/is-folder-empty.ts +++ b/packages/create-vitnode-app/src/helpers/is-folder-empty.ts @@ -1,36 +1,34 @@ -import { lstatSync, readdirSync } from 'node:fs'; -import { join } from 'node:path'; -import colors from 'picocolors'; +/** biome-ignore-all lint/suspicious/noConsole: */ +import { lstatSync, readdirSync } from "node:fs"; +import { join } from "node:path"; +import colors from "picocolors"; export function isFolderEmpty(root: string, name: string): boolean { const validFiles = [ - '.DS_Store', - '.git', - '.gitattributes', - '.gitignore', - '.gitlab-ci.yml', - '.hg', - '.hgcheck', - '.hgignore', - '.idea', - '.npmignore', - '.travis.yml', - 'LICENSE', - 'Thumbs.db', - 'docs', - 'mkdocs.yml', - 'npm-debug.log', - 'yarn-debug.log', - 'yarn-error.log', - 'yarnrc.yml', - '.yarn', + ".DS_Store", + ".git", + ".gitattributes", + ".gitignore", + ".gitlab-ci.yml", + ".hg", + ".hgcheck", + ".hgignore", + ".idea", + ".npmignore", + ".travis.yml", + "LICENSE", + "Thumbs.db", + "docs", + "mkdocs.yml", + "npm-debug.log", + "yarn-debug.log", + "yarn-error.log", + "yarnrc.yml", + ".yarn", ]; const conflicts = readdirSync(root).filter( - file => - !validFiles.includes(file) && - // Support IntelliJ IDEA-based editors - !file.endsWith('.iml'), + file => !(validFiles.includes(file) || file.endsWith(".iml")), ); if (conflicts.length > 0) { @@ -51,7 +49,7 @@ export function isFolderEmpty(root: string, name: string): boolean { } console.log( - '\nEither try using a new directory name, or remove the files listed above.\n', + "\nEither try using a new directory name, or remove the files listed above.\n", ); return false; diff --git a/packages/create-vitnode-app/src/helpers/is-online.ts b/packages/create-vitnode-app/src/helpers/is-online.ts index 90046fa5b..2a667c30f 100644 --- a/packages/create-vitnode-app/src/helpers/is-online.ts +++ b/packages/create-vitnode-app/src/helpers/is-online.ts @@ -1,10 +1,24 @@ -import { execSync } from 'child_process'; -import { lookup } from 'dns/promises'; -import { URL } from 'url'; +import { execSync } from "node:child_process"; +import { lookup } from "node:dns/promises"; +import { URL } from "node:url"; + +function getProxy(): string | undefined { + if (process.env.https_proxy) { + return process.env.https_proxy; + } + + try { + const httpsProxy = execSync("npm config get https-proxy").toString().trim(); + + return httpsProxy !== "null" ? httpsProxy : undefined; + } catch { + return; + } +} export async function getOnline(): Promise { try { - await lookup('registry.yarnpkg.com'); + await lookup("registry.yarnpkg.com"); // If DNS lookup succeeds, we are online return true; @@ -32,17 +46,3 @@ export async function getOnline(): Promise { } } } - -function getProxy(): string | undefined { - if (process.env.https_proxy) { - return process.env.https_proxy; - } - - try { - const httpsProxy = execSync('npm config get https-proxy').toString().trim(); - - return httpsProxy !== 'null' ? httpsProxy : undefined; - } catch { - return; - } -} diff --git a/packages/create-vitnode-app/src/helpers/is-writeable.ts b/packages/create-vitnode-app/src/helpers/is-writeable.ts index a4455b22f..ed5b517ca 100644 --- a/packages/create-vitnode-app/src/helpers/is-writeable.ts +++ b/packages/create-vitnode-app/src/helpers/is-writeable.ts @@ -1,5 +1,5 @@ -import { W_OK } from 'constants'; -import { access } from 'fs/promises'; +import { W_OK } from "node:constants"; +import { access } from "node:fs/promises"; export async function isWriteable(directory: string): Promise { try { diff --git a/packages/create-vitnode-app/src/helpers/validate-pkg.ts b/packages/create-vitnode-app/src/helpers/validate-pkg.ts index 75e958484..b9e8f34c1 100644 --- a/packages/create-vitnode-app/src/helpers/validate-pkg.ts +++ b/packages/create-vitnode-app/src/helpers/validate-pkg.ts @@ -1,4 +1,4 @@ -import validateProjectName from 'validate-npm-package-name'; +import validateProjectName from "validate-npm-package-name"; type ValidateNpmNameResult = | { diff --git a/packages/create-vitnode-app/src/index.ts b/packages/create-vitnode-app/src/index.ts index 59da36abf..9e636bc41 100644 --- a/packages/create-vitnode-app/src/index.ts +++ b/packages/create-vitnode-app/src/index.ts @@ -1,17 +1,18 @@ #!/usr/bin/env node -import { input } from '@inquirer/prompts'; -import { Command, Option } from 'commander'; -import { basename, resolve } from 'path'; -import color from 'picocolors'; +/** biome-ignore-all lint/suspicious/noConsole: */ +import { basename, resolve } from "node:path"; +import { input } from "@inquirer/prompts"; +import { Command, Option } from "commander"; +import color from "picocolors"; -import { createVitNode } from './create/create-vitnode.js'; -import { packageJson } from './helpers/get-package-json.js'; -import { validateNpmName } from './helpers/validate-pkg.js'; -import { createPlugin } from './plugin/index.js'; -import { createQuestionsCli } from './questions.js'; -import { validationProject } from './validation.js'; +import { createVitNode } from "./create/create-vitnode.js"; +import { packageJson } from "./helpers/get-package-json.js"; +import { validateNpmName } from "./helpers/validate-pkg.js"; +import { createPlugin } from "./plugin/index.js"; +import { createQuestionsCli } from "./questions.js"; +import { validationProject } from "./validation.js"; -const [major] = process.versions.node.split('.').map(Number); +const [major] = process.versions.node.split(".").map(Number); if (major < 20) { console.error( color.red( @@ -21,48 +22,48 @@ if (major < 20) { process.exit(1); } -process.on('uncaughtException', (error: Error) => { - if (error.name === 'ExitPromptError') { - console.log(color.dim('👋 VitNode setup cancelled - see you next time!')); +process.on("uncaughtException", (error: Error) => { + if (error.name === "ExitPromptError") { + console.log(color.dim("👋 VitNode setup cancelled - see you next time!")); process.exit(0); } - console.error(color.red('An unexpected error occurred:')); + console.error(color.red("An unexpected error occurred:")); console.error(color.red(error.stack ?? error.message)); process.exit(1); }); const init = async () => { - let projectPath = ''; + let projectPath = ""; const program = new Command() - .version(packageJson.version ?? '0.1.0') - .argument('[project-directory]') - .usage(`${color.green('[project-directory]')} [options]`) + .version(packageJson.version ?? "0.1.0") + .argument("[project-directory]") + .usage(`${color.green("[project-directory]")} [options]`) .action(name => { projectPath = name; }); program.addOption( new Option( - '--package-manager ', - 'Specify the package manager to use', - ).choices(['npm', 'pnpm', 'bun']), + "--package-manager ", + "Specify the package manager to use", + ).choices(["npm", "pnpm", "bun"]), ); - program.option('--eslint', 'Initialize with eslint config.'); + program.option("--biome", "Initialize with Biome config."); program.option( - '--skip-install', - 'Skip installing packages after initializing the project.', + "--skip-install", + "Skip installing packages after initializing the project.", ); - program.option('--plugin', 'Enable plugin mode.'); + program.option("--plugin", "Enable plugin mode."); program.addOption( new Option( - '--mode ', - 'What type of app do you want to create?', - ).choices(['singleApp', 'apiMonorepo', 'onlyApi']), + "--mode ", + "What type of app do you want to create?", + ).choices(["singleApp", "apiMonorepo", "onlyApi"]), ); - program.option('--monorepo', 'Create project with monorepo structure.'); - program.option('--docker', 'Initialize with Docker support.'); + program.option("--monorepo", "Create project with monorepo structure."); + program.option("--docker", "Initialize with Docker support."); program.parse(process.argv); @@ -75,8 +76,8 @@ const init = async () => { if (!projectPath) { projectPath = await input({ - message: 'What is your project named?', - default: 'my-vitnode', + message: "What is your project named?", + default: "my-vitnode", validate: (name: string) => { const validation = validateNpmName({ name: basename(resolve(name)) }); if (validation.valid) return true; diff --git a/packages/create-vitnode-app/src/plugin/index.ts b/packages/create-vitnode-app/src/plugin/index.ts index fe79cea94..4d4349e0b 100644 --- a/packages/create-vitnode-app/src/plugin/index.ts +++ b/packages/create-vitnode-app/src/plugin/index.ts @@ -1,14 +1,14 @@ -import { input } from '@inquirer/prompts'; -import { basename, resolve } from 'path'; +import { basename, resolve } from "node:path"; +import { input } from "@inquirer/prompts"; -import { validateNpmName } from '../helpers/validate-pkg.js'; +import { validateNpmName } from "../helpers/validate-pkg.js"; export const createPlugin = async (projectPath: string) => { let name = projectPath; if (!name) { name = await input({ - message: 'What is your plugin named?', - default: 'my-vitnode-plugin', + message: "What is your plugin named?", + default: "my-vitnode-plugin", validate: (name: string) => { const validation = validateNpmName({ name: basename(resolve(name)) }); if (validation.valid) return true; diff --git a/packages/create-vitnode-app/src/prepare/prepare.ts b/packages/create-vitnode-app/src/prepare/prepare.ts index 3b9c74ed8..198e55317 100644 --- a/packages/create-vitnode-app/src/prepare/prepare.ts +++ b/packages/create-vitnode-app/src/prepare/prepare.ts @@ -1,13 +1,14 @@ -import { existsSync } from 'fs'; -import { mkdir } from 'fs/promises'; -import { join } from 'path'; +/** biome-ignore-all lint/suspicious/noConsole: */ +import { existsSync } from "node:fs"; +import { mkdir } from "node:fs/promises"; +import { join } from "node:path"; const prepare = async () => { - const toRootPath = join(process.cwd(), 'copy-of-vitnode-app'); + const toRootPath = join(process.cwd(), "copy-of-vitnode-app"); if (!existsSync(toRootPath)) { await mkdir(toRootPath); } - const fromRootPath = join(process.cwd(), '..', '..', 'apps', 'docs'); + const fromRootPath = join(process.cwd(), "..", "..", "apps", "docs"); if (!existsSync(fromRootPath)) { console.error( `\x1b[31mThe path ${fromRootPath} does not exist. Please check the directory structure.\x1b[0m`, diff --git a/packages/create-vitnode-app/src/questions.ts b/packages/create-vitnode-app/src/questions.ts index a56a50d6b..f4a566fc4 100644 --- a/packages/create-vitnode-app/src/questions.ts +++ b/packages/create-vitnode-app/src/questions.ts @@ -1,16 +1,14 @@ -import type { Command } from 'commander'; +import { confirm, select } from "@inquirer/prompts"; +import type { Command } from "commander"; +import color from "picocolors"; -import { select } from '@inquirer/prompts'; -import { confirm } from '@inquirer/prompts'; -import color from 'picocolors'; - -import { getAvailablePackageManagers } from './helpers/get-available-package-managers.js'; +import { getAvailablePackageManagers } from "./helpers/get-available-package-managers.js"; export interface CreateCliReturn { docker?: boolean; - eslint: boolean; + biome: boolean; install: boolean; - mode: 'apiMonorepo' | 'onlyApi' | 'singleApp'; + mode: "apiMonorepo" | "onlyApi" | "singleApp"; monorepo?: boolean; packageManager: string; } @@ -21,7 +19,7 @@ export const createQuestionsCli = async ( const optionsFromProgram = program.opts(); const options: CreateCliReturn = { packageManager: optionsFromProgram.packageManager, - eslint: optionsFromProgram.eslint, + biome: optionsFromProgram.biome, install: !optionsFromProgram.skipInstall, docker: optionsFromProgram.docker, mode: optionsFromProgram.mode, @@ -31,21 +29,21 @@ export const createQuestionsCli = async ( if (!optionsFromProgram.packageManager) { const availablePackageManagers = await getAvailablePackageManagers(); options.packageManager = await select({ - message: `Which ${color.blue('package manager')} do you want to use?`, + message: `Which ${color.blue("package manager")} do you want to use?`, choices: [ { - name: `bun${availablePackageManagers.bun ? `@${availablePackageManagers.bun}` : ''}`, - value: 'bun', + name: `bun${availablePackageManagers.bun ? `@${availablePackageManagers.bun}` : ""}`, + value: "bun", disabled: !availablePackageManagers.bun, }, { - name: `pnpm${availablePackageManagers.pnpm ? `@${availablePackageManagers.pnpm}` : ''}`, - value: 'pnpm', + name: `pnpm${availablePackageManagers.pnpm ? `@${availablePackageManagers.pnpm}` : ""}`, + value: "pnpm", disabled: !availablePackageManagers.pnpm, }, { - name: `npm${availablePackageManagers.npm ? `@${availablePackageManagers.npm}` : ''}`, - value: 'npm', + name: `npm${availablePackageManagers.npm ? `@${availablePackageManagers.npm}` : ""}`, + value: "npm", disabled: !availablePackageManagers.npm, }, ], @@ -54,55 +52,55 @@ export const createQuestionsCli = async ( if (optionsFromProgram.mode === undefined) { options.mode = await select({ - message: `What type of ${color.blue('app')} do you want to create?`, + message: `What type of ${color.blue("app")} do you want to create?`, choices: [ { - name: `Single App - ${color.blue('Next.js')} & ${color.blue('Hono.js')}`, + name: `Single App - ${color.blue("Next.js")} & ${color.blue("Hono.js")}`, description: - 'Create a single app with Next.js and Hono.js in the same project.', - value: 'singleApp', + "Create a single app with Next.js and Hono.js in the same project.", + value: "singleApp", }, { - name: `Monorepo App - ${color.blue('Next.js')} & ${color.blue('Hono.js')}`, + name: `Monorepo App - ${color.blue("Next.js")} & ${color.blue("Hono.js")}`, description: - 'Create a monorepo with both Next.js and Hono.js apps separately.', - value: 'apiMonorepo', + "Create a monorepo with both Next.js and Hono.js apps separately.", + value: "apiMonorepo", }, { - name: `Only API - ${color.blue('Hono.js')}`, - description: 'Create only an API app using Hono.js without Next.js.', - value: 'onlyApi', + name: `Only API - ${color.blue("Hono.js")}`, + description: "Create only an API app using Hono.js without Next.js.", + value: "onlyApi", }, ], - default: 'singleApp', + default: "singleApp", }); } if ( optionsFromProgram.monorepo === undefined && - options.mode !== 'apiMonorepo' + options.mode !== "apiMonorepo" ) { options.monorepo = await confirm({ - message: `Would you like to use ${color.blue('TurboRepo')} for monorepo management? ${color.red('(Required for plugins development)')}`, + message: `Would you like to use ${color.blue("TurboRepo")} for monorepo management? ${color.red("(Required for plugins development)")}`, default: false, }); } - if (optionsFromProgram.eslint === undefined) { - options.eslint = await confirm({ - message: `Would you like to use ${color.blue('ESLint')}?`, + if (optionsFromProgram.biome === undefined) { + options.biome = await confirm({ + message: `Would you like to use ${color.blue("Biome")}?`, }); } if (optionsFromProgram.docker === undefined) { options.docker = await confirm({ - message: `Would you like to use ${color.blue('Docker Container')}?`, + message: `Would you like to use ${color.blue("Docker Container")}?`, }); } if (optionsFromProgram.skipInstall === undefined) { options.install = await confirm({ - message: `Would you like to ${color.blue('Install dependencies')}?`, + message: `Would you like to ${color.blue("Install dependencies")}?`, }); } diff --git a/packages/create-vitnode-app/src/validation.ts b/packages/create-vitnode-app/src/validation.ts index cca1a81e2..dc18256d0 100644 --- a/packages/create-vitnode-app/src/validation.ts +++ b/packages/create-vitnode-app/src/validation.ts @@ -1,20 +1,21 @@ -import { program } from 'commander'; -import { existsSync } from 'fs'; -import { basename, dirname, resolve } from 'path'; -import color from 'picocolors'; +/** biome-ignore-all lint/suspicious/noConsole: */ +import { existsSync } from "node:fs"; +import { basename, dirname, resolve } from "node:path"; +import { program } from "commander"; +import color from "picocolors"; -import { isFolderEmpty } from './helpers/is-folder-empty.js'; -import { isWriteable } from './helpers/is-writeable.js'; -import { validateNpmName } from './helpers/validate-pkg.js'; +import { isFolderEmpty } from "./helpers/is-folder-empty.js"; +import { isWriteable } from "./helpers/is-writeable.js"; +import { validateNpmName } from "./helpers/validate-pkg.js"; export const validationProject = async (projectPath: string) => { // Verify the project path is provided if (!projectPath) { console.log( - '\nPlease specify the project directory:\n' + - ` ${color.cyan(program.name())} ${color.green('')}\n` + - 'For example:\n' + - ` ${color.cyan(program.name())} ${color.green('my-vitnode-app')}\n\n` + + "\nPlease specify the project directory:\n" + + ` ${color.cyan(program.name())} ${color.green("")}\n` + + "For example:\n" + + ` ${color.cyan(program.name())} ${color.green("my-vitnode-app")}\n\n` + `Run ${color.cyan(`${program.name()} --help`)} to see all options.`, ); process.exit(1); @@ -32,7 +33,7 @@ export const validationProject = async (projectPath: string) => { ); validation.problems.forEach(p => { - console.error(`${color.red(color.bold('*'))} ${p}`); + console.error(`${color.red(color.bold("*"))} ${p}`); }); process.exit(1); } @@ -42,17 +43,17 @@ export const validationProject = async (projectPath: string) => { const appName = basename(root); const folderExists = existsSync(root); if (folderExists && !isFolderEmpty(root, appName)) { - console.error('The specified directory is not empty.'); + console.error("The specified directory is not empty."); process.exit(1); } // Verify the project dir is writeable if (!(await isWriteable(dirname(root)))) { console.error( - 'The application path is not writable, please check folder permissions and try again.', + "The application path is not writable, please check folder permissions and try again.", ); console.error( - 'It is likely you do not have write permissions for this folder.', + "It is likely you do not have write permissions for this folder.", ); process.exit(1); } diff --git a/packages/create-vitnode-app/tsconfig.json b/packages/create-vitnode-app/tsconfig.json index f365afb11..5c28c650c 100644 --- a/packages/create-vitnode-app/tsconfig.json +++ b/packages/create-vitnode-app/tsconfig.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/tsconfig", - "extends": "@vitnode/eslint-config/tsconfig", + "extends": "@vitnode/config/tsconfig", "compilerOptions": { "target": "ESNext", "module": "NodeNext", diff --git a/packages/eslint/eslint.config.mjs b/packages/eslint/eslint.config.mjs deleted file mode 100644 index 0f8c6c266..000000000 --- a/packages/eslint/eslint.config.mjs +++ /dev/null @@ -1,174 +0,0 @@ -// @ts-check - -import { dirname } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import eslint from '@eslint/js'; -import eslintReact from '@eslint-react/eslint-plugin'; -import jsxA11y from 'eslint-plugin-jsx-a11y'; -import perfectionist from 'eslint-plugin-perfectionist'; -import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; -import reactPlugin from 'eslint-plugin-react'; -import reactCompiler from 'eslint-plugin-react-compiler'; -import hooksPlugin from 'eslint-plugin-react-hooks'; -import tsEslint from 'typescript-eslint'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -export default [ - { - ignores: [ - 'next-env.d.ts', - 'dist', - '**/\\(main\\)/\\(plugins\\)/**', - '**/\\(auth\\)/\\(plugins\\)/**', - '.prettierrc.mjs', - 'node_modules', - 'eslint.config.mjs', - 'next.config.ts', - 'config/next.config.ts', - 'postcss.config.mjs', - '.turbo', - '.next', - 'global.d.ts', - 'tsup.config.ts', - '*.test.tsx', - ], - }, - eslint.configs.recommended, - eslintReact.configs.recommended, - ...tsEslint.configs.stylisticTypeChecked, - ...tsEslint.configs.strictTypeChecked, - eslintPluginPrettierRecommended, - jsxA11y.flatConfigs.recommended, - reactPlugin.configs.flat.recommended, - perfectionist.configs['recommended-natural'], - { - plugins: { - 'react-compiler': reactCompiler, - }, - rules: { - 'react-compiler/react-compiler': 'error', - }, - }, - { - files: ['**/*.{js,mjs,cjs,ts,jsx,tsx}'], - settings: { - react: { - version: 'detect', - }, - }, - languageOptions: { - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - }, - }, - }, - { - plugins: { - 'react-hooks': hooksPlugin, - }, - rules: { - 'react/react-in-jsx-scope': 'off', - ...hooksPlugin.configs.recommended.rules, - }, - }, - { files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'] }, - { - rules: { - 'react-hooks/exhaustive-deps': 'off', - '@eslint-react/no-context-provider': 'off', - '@eslint-react/no-unstable-default-props': 'off', - 'perfectionist/sort-array-includes': 'warn', - '@typescript-eslint/consistent-type-imports': 'error', - '@typescript-eslint/no-confusing-void-expression': 'off', - '@typescript-eslint/no-unnecessary-type-parameters': 'off', - '@typescript-eslint/no-misused-spread': 'off', - 'perfectionist/sort-decorators': 'warn', - 'perfectionist/sort-modules': 'off', - 'perfectionist/sort-switch-case': 'warn', - '@typescript-eslint/no-unnecessary-condition': 'off', - 'perfectionist/sort-named-exports': 'warn', - 'perfectionist/sort-enums': 'warn', - 'perfectionist/sort-exports': 'warn', - '@typescript-eslint/no-dynamic-delete': 'off', - 'perfectionist/sort-named-imports': 'warn', - 'perfectionist/sort-intersection-types': 'warn', - 'perfectionist/sort-interfaces': 'warn', - 'perfectionist/sort-union-types': 'warn', - 'perfectionist/sort-object-types': 'warn', - 'perfectionist/sort-jsx-props': 'warn', - 'perfectionist/sort-imports': 'warn', - '@typescript-eslint/no-unsafe-call': 'off', - 'perfectionist/sort-objects': 'off', - 'perfectionist/sort-classes': [ - 'warn', - { - groups: [ - 'constructor', - 'static-block', - 'index-signature', - 'static-property', - ['protected-property', 'protected-accessor-property'], - ['private-property', 'private-accessor-property'], - ['property', 'accessor-property'], - 'static-method', - 'protected-method', - 'private-method', - 'method', - ['get-method', 'set-method'], - 'unknown', - ], - }, - ], - 'no-console': 'error', - 'consistent-return': 'off', - '@typescript-eslint/no-unused-vars': [ - 'warn', - { - ignoreRestSiblings: false, - caughtErrorsIgnorePattern: '^_', - }, - ], - '@typescript-eslint/no-empty-function': 'off', - '@typescript-eslint/unbound-method': 'off', - '@typescript-eslint/no-misused-promises': 'off', - '@typescript-eslint/no-unsafe-return': 'off', - '@typescript-eslint/no-unsafe-assignment': 'off', - '@typescript-eslint/no-unsafe-member-access': 'off', - '@typescript-eslint/no-useless-constructor': 'off', - '@typescript-eslint/prefer-readonly': 'warn', - '@typescript-eslint/require-array-sort-compare': 'error', - '@typescript-eslint/promise-function-async': 'error', - '@typescript-eslint/no-extraneous-class': 'off', - '@typescript-eslint/restrict-template-expressions': 'off', - '@typescript-eslint/consistent-type-exports': 'error', - '@typescript-eslint/no-unnecessary-qualifier': 'error', - '@typescript-eslint/no-useless-empty-export': 'error', - '@typescript-eslint/method-signature-style': 'warn', - 'newline-before-return': 'warn', - 'no-restricted-imports': [ - 'error', - { - name: 'next/link', - message: 'Please import from `vitnode-frontend/navigation` instead.', - }, - { - name: 'drizzle-orm/mysql-core', - message: 'Please import from `drizzle-orm/pg-core` instead.', - }, - { - name: 'next/navigation', - importNames: [ - 'redirect', - 'permanentRedirect', - 'useRouter', - 'usePathname', - ], - message: 'Please import from `vitnode-frontend/navigation` instead.', - }, - ], - }, - }, -]; diff --git a/packages/eslint/package.json b/packages/eslint/package.json deleted file mode 100644 index 6c90eb13f..000000000 --- a/packages/eslint/package.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "@vitnode/eslint-config", - "version": "1.2.0-canary.52", - "description": "ESLint, TypeScript (TSConfig), Prettier config for VitNode", - "author": "VitNode Team", - "license": "MIT", - "homepage": "https://vitnode.com", - "repository": { - "type": "git", - "url": "git+https://github.com/aXenDeveloper/vitnode.git", - "directory": "packages/eslint" - }, - "keywords": [ - "vitnode", - "eslint", - "eslint-config", - "typescript", - "tsconfig", - "prettier" - ], - "type": "module", - "exports": { - "./eslint": { - "import": "./eslint.config.mjs", - "default": "./eslint.config.mjs" - }, - "./tsconfig": { - "import": "./tsconfig.json", - "default": "./tsconfig.json" - }, - "./prettierrc": { - "import": "./prettierrc.mjs", - "default": "./prettierrc.mjs" - } - }, - "peerDependencies": { - "eslint": "^9.0.0", - "prettier": "^3.0.0", - "typescript": "5.9.x" - }, - "devDependencies": { - "typescript": "^5.9.2" - }, - "dependencies": { - "@eslint-react/eslint-plugin": "^1.52.6", - "@eslint/js": "^9.33.0", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-jsx-a11y": "^6.10.2", - "eslint-plugin-perfectionist": "^4.15.0", - "eslint-plugin-prettier": "^5.5.4", - "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-compiler": "19.1.0-rc.2", - "eslint-plugin-react-hooks": "6.0.0-rc1", - "prettier-plugin-tailwindcss": "^0.6.14", - "typescript-eslint": "^8.40.0" - } -} diff --git a/packages/eslint/prettierrc.mjs b/packages/eslint/prettierrc.mjs deleted file mode 100644 index 013dc965b..000000000 --- a/packages/eslint/prettierrc.mjs +++ /dev/null @@ -1,14 +0,0 @@ -/** - * @see https://prettier.io/docs/en/configuration.html - * @type {import("prettier").Config} - */ -const config = { - singleQuote: true, - arrowParens: 'avoid', - trailingComma: 'all', - printWidth: 80, - plugins: ['prettier-plugin-tailwindcss'], - tailwindFunctions: ['cn'], -}; - -export default config; \ No newline at end of file diff --git a/packages/vitnode/.npmignore b/packages/vitnode/.npmignore index d47df62a2..3039a8f6c 100644 --- a/packages/vitnode/.npmignore +++ b/packages/vitnode/.npmignore @@ -8,7 +8,6 @@ /node_modules /.turbo -/eslint.config.mjs /tsconfig.json /.swcrc /components.json diff --git a/packages/vitnode/config/next.config.ts b/packages/vitnode/config/next.config.ts index 5514ca669..a2be43106 100644 --- a/packages/vitnode/config/next.config.ts +++ b/packages/vitnode/config/next.config.ts @@ -1,7 +1,7 @@ -import type { NextConfig } from 'next'; -import createNextIntlPlugin from 'next-intl/plugin'; +import type { NextConfig } from "next"; +import createNextIntlPlugin from "next-intl/plugin"; -const withNextIntl = createNextIntlPlugin('./src/vitnode.config.ts'); +const withNextIntl = createNextIntlPlugin("./src/vitnode.config.ts"); export const vitNodeNextConfig = (config: NextConfig): NextConfig => withNextIntl({ diff --git a/packages/vitnode/eslint.config.mjs b/packages/vitnode/eslint.config.mjs deleted file mode 100644 index 0098d1c8a..000000000 --- a/packages/vitnode/eslint.config.mjs +++ /dev/null @@ -1,17 +0,0 @@ -import eslintVitNode from '@vitnode/eslint-config/eslint'; -import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -export default [ - ...eslintVitNode, - { - languageOptions: { - parserOptions: { - project: './tsconfig.json', - tsconfigRootDir: __dirname, - }, - }, - }, -]; diff --git a/packages/vitnode/global.d.ts b/packages/vitnode/global.d.ts index 1f50c6e9b..d829e1bdb 100644 --- a/packages/vitnode/global.d.ts +++ b/packages/vitnode/global.d.ts @@ -1,8 +1,8 @@ /// -import type plugin from './src/locales/en.json'; +import plugin from "./src/locales/en.json" with { type: "json" }; -declare module 'next-intl' { +declare module "next-intl" { interface AppConfig { Messages: typeof plugin; } diff --git a/packages/vitnode/package.json b/packages/vitnode/package.json index 9b8967422..9a4929683 100644 --- a/packages/vitnode/package.json +++ b/packages/vitnode/package.json @@ -51,13 +51,12 @@ "@types/react-dom": "^19.1.7", "@vitejs/plugin-react": "^5.0.1", "@vitest/coverage-v8": "^3.2.4", - "@vitnode/eslint-config": "workspace:*", + "@vitnode/config": "workspace:*", "chokidar": "^4.0.3", "concurrently": "^9.2.0", "dotenv": "^17.2.1", "drizzle-kit": "^0.31.4", "drizzle-orm": "^0.44.4", - "eslint": "^9.33.0", "hono": "^4.9.2", "jsdom": "^26.1.0", "lucide-react": "^0.540.0", @@ -101,8 +100,6 @@ "build:plugins": "tsc && swc src -d dist --config-file .swcrc && tsc-alias -p tsconfig.json", "dev": "concurrently \"tsc -w --preserveWatchOutput\" \"swc src -d dist --config-file .swcrc -w\" \"tsc-alias -w\" \"node dist/scripts/scripts.js plugin --w\"", "dev:email": "email dev --dir src/emails", - "lint": "eslint .", - "lint:fix": "eslint . --fix", "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage" diff --git a/packages/vitnode/scripts/get-config.ts b/packages/vitnode/scripts/get-config.ts index 033045cc7..04fa005f5 100644 --- a/packages/vitnode/scripts/get-config.ts +++ b/packages/vitnode/scripts/get-config.ts @@ -1,28 +1,28 @@ -/* eslint-disable no-console */ -import { join } from 'path'; -import { pathToFileURL } from 'url'; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; -import type { VitNodeApiConfig, VitNodeConfig } from '../src/vitnode.config'; +import type { VitNodeApiConfig, VitNodeConfig } from "../src/vitnode.config"; -type ConfigType = T extends 'config' +type ConfigType = T extends "config" ? VitNodeConfig : VitNodeApiConfig; -export const getConfig = async ({ - type = 'config' as T, +export const getConfig = async ({ + type = "config" as T, }: { type?: T; }): Promise> => { - const configPath = join(process.cwd(), 'src', `vitnode.${type}.ts`); + const configPath = join(process.cwd(), "src", `vitnode.${type}.ts`); try { const configUrl = pathToFileURL(configPath).href; const loaded = await import(configUrl); const config = - type === 'config' ? loaded.vitNodeConfig : loaded.vitNodeApiConfig; + type === "config" ? loaded.vitNodeConfig : loaded.vitNodeApiConfig; return config as ConfigType; } catch (error) { - console.error('Failed to load config:', error); + // biome-ignore lint/suspicious/noConsole: + console.error("Failed to load config:", error); process.exit(1); } }; diff --git a/packages/vitnode/scripts/plugin.ts b/packages/vitnode/scripts/plugin.ts index 86c7109d5..bf7c88662 100644 --- a/packages/vitnode/scripts/plugin.ts +++ b/packages/vitnode/scripts/plugin.ts @@ -1,13 +1,13 @@ -/* eslint-disable no-console */ -import chokidar from 'chokidar'; +/** biome-ignore-all lint/suspicious/noConsole: */ import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, -} from 'fs'; -import { basename, join, relative } from 'path'; +} from "node:fs"; +import { basename, join, relative } from "node:path"; +import chokidar from "chokidar"; import { buildInitialRouteMap, @@ -18,53 +18,37 @@ import { isDirectoryEmpty, routeKey, type SourceConfig, -} from './shared/file-utils'; - -export const processPlugin = ({ initMessage }: { initMessage: string }) => { - const pluginDir = process.cwd(); - const repoRoot = findRepoRoot(pluginDir); - const localeRoot = findLocaleRoot(repoRoot); - const routeMap = buildInitialRouteMap(localeRoot); - - // Get the package name from package.json for imports - let pluginName = basename(pluginDir); - try { - const packageJsonPath = join(pluginDir, 'package.json'); - - if (existsSync(packageJsonPath)) { - const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); - pluginName = packageJson.name ?? ''; - } - } catch (error) { - console.error(`\x1b[31mError reading package.json:\x1b[0m`, error); - - return; - } - - // Transform plugin name for path usage - const pluginPathName = pluginName.replace(/\//g, '-').replace(/@/g, ''); - - // Detect app types by checking for config files - const detectAppType = (appPath: string) => { - const hasWebConfig = existsSync(join(appPath, 'src', 'vitnode.config.ts')); - const hasApiConfig = existsSync( - join(appPath, 'src', 'vitnode.api.config.ts'), - ); - - if (hasApiConfig && !hasWebConfig) return 'api'; - if (hasWebConfig) return 'web'; - - return null; - }; +} from "./shared/file-utils"; + +/** + * Helper: detect if an app path is web, api, or null + */ +const detectAppType = (appPath: string) => { + const hasWebConfig = existsSync(join(appPath, "src", "vitnode.config.ts")); + const hasApiConfig = existsSync( + join(appPath, "src", "vitnode.api.config.ts"), + ); - // Check if we're in a monorepo by looking for apps directories - const appsDir = join(repoRoot, 'apps'); - const isMonorepo = existsSync(appsDir); + if (hasApiConfig && !hasWebConfig) return "api"; + if (hasWebConfig) return "web"; + return null; +}; +/** + * Helper: collect source/destination mappings for a plugin + */ +const collectSources = ( + pluginDir: string, + repoRoot: string, + pluginName: string, + pluginPathName: string, + // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: +): SourceConfig[] => { const sources: SourceConfig[] = []; + const appsDir = join(repoRoot, "apps"); + const isMonorepo = existsSync(appsDir); if (isMonorepo) { - // Monorepo: scan all apps and detect their types const appDirs = existsSync(appsDir) ? readdirSync(appsDir, { withFileTypes: true }) .filter(dirent => dirent.isDirectory()) @@ -72,109 +56,95 @@ export const processPlugin = ({ initMessage }: { initMessage: string }) => { : []; for (const appName of appDirs) { - const appPath = join(repoRoot, 'apps', appName); + const appPath = join(repoRoot, "apps", appName); const appType = detectAppType(appPath); - if (appType === 'web') { - // Web app: copy app, app_admin, and locales - const mainDest = join( - appPath, - 'src', - 'app', - '[locale]', - '(main)', - join('(plugins)', `(${pluginPathName})`), - ); - const adminDest = join( - appPath, - 'src', - 'app', - '[locale]', - 'admin', - '(auth)', - join('(plugins)', `(${pluginPathName})`), - ); - const langDest = join(appPath, 'src', 'locales', pluginName); - + if (appType === "web") { sources.push( { - sourceDir: join(pluginDir, 'src', 'app_admin'), - destinationDir: adminDest, + sourceDir: join(pluginDir, "src", "app_admin"), + destinationDir: join( + appPath, + "src", + "app", + "[locale]", + "admin", + "(auth)", + join("(plugins)", `(${pluginPathName})`), + ), }, { - sourceDir: join(pluginDir, 'src', 'app'), - destinationDir: mainDest, + sourceDir: join(pluginDir, "src", "app"), + destinationDir: join( + appPath, + "src", + "app", + "[locale]", + "(main)", + join("(plugins)", `(${pluginPathName})`), + ), }, { - sourceDir: join(pluginDir, 'src', 'locales'), - destinationDir: langDest, + sourceDir: join(pluginDir, "src", "locales"), + destinationDir: join(appPath, "src", "locales", pluginName), }, ); - } else if (appType === 'api') { - // API app: copy only locales - const apiLangDest = join(appPath, 'src', 'locales', pluginName); - + } else if (appType === "api") { sources.push({ - sourceDir: join(pluginDir, 'src', 'locales'), - destinationDir: apiLangDest, + sourceDir: join(pluginDir, "src", "locales"), + destinationDir: join(appPath, "src", "locales", pluginName), }); } } } else { - // Standalone project: check if we're running from within an app directory - // or if we need to copy to the current working directory const cwd = process.cwd(); const projectType = detectAppType(cwd); - if (projectType === 'web') { - // Web project: copy all files to current working directory - const mainDest = join( - cwd, - 'src', - 'app', - '[locale]', - '(main)', - join('(plugins)', `(${pluginPathName})`), - ); - const adminDest = join( - cwd, - 'src', - 'app', - '[locale]', - 'admin', - '(auth)', - join('(plugins)', `(${pluginPathName})`), - ); - const langDest = join(cwd, 'src', 'locales', pluginName); - + if (projectType === "web") { sources.push( { - sourceDir: join(pluginDir, 'src', 'app_admin'), - destinationDir: adminDest, + sourceDir: join(pluginDir, "src", "app_admin"), + destinationDir: join( + cwd, + "src", + "app", + "[locale]", + "admin", + "(auth)", + join("(plugins)", `(${pluginPathName})`), + ), }, { - sourceDir: join(pluginDir, 'src', 'app'), - destinationDir: mainDest, + sourceDir: join(pluginDir, "src", "app"), + destinationDir: join( + cwd, + "src", + "app", + "[locale]", + "(main)", + join("(plugins)", `(${pluginPathName})`), + ), }, { - sourceDir: join(pluginDir, 'src', 'locales'), - destinationDir: langDest, + sourceDir: join(pluginDir, "src", "locales"), + destinationDir: join(cwd, "src", "locales", pluginName), }, ); - } else if (projectType === 'api') { - // API project: copy only locales to current working directory - const langDest = join(cwd, 'src', 'locales', pluginName); - + } else if (projectType === "api") { sources.push({ - sourceDir: join(pluginDir, 'src', 'locales'), - destinationDir: langDest, + sourceDir: join(pluginDir, "src", "locales"), + destinationDir: join(cwd, "src", "locales", pluginName), }); } } - // tell the copier about both trees + return sources; +}; - // Create destination directories if they don't exist and source directories are not empty +/** + * Ensure destination directories exist for non-empty source dirs + */ +const ensureDestinationDirs = (sources: SourceConfig[]) => { for (const { sourceDir, destinationDir } of sources) { if ( existsSync(sourceDir) && @@ -184,7 +154,17 @@ export const processPlugin = ({ initMessage }: { initMessage: string }) => { mkdirSync(destinationDir, { recursive: true }); } } +}; +/** + * Create wrappers for copy and remove operations that close over routeMap/localeRoot/repoRoot + */ +const createFileOps = ( + pluginName: string, + routeMap: Map, + localeRoot: string, + repoRoot: string, +) => { const copyFileWrapper = (srcPath: string, destPath: string) => { copyFile(srcPath, destPath, pluginName, routeMap, localeRoot, repoRoot); }; @@ -197,7 +177,6 @@ export const processPlugin = ({ initMessage }: { initMessage: string }) => { if (/^page\.(tsx|ts|jsx|js)$/i.test(basename(filePath))) { const key = routeKey(filePath, localeRoot); - // only delete if this exact file is the one in the map if (routeMap.get(key) === filePath) { routeMap.delete(key); } @@ -208,56 +187,45 @@ export const processPlugin = ({ initMessage }: { initMessage: string }) => { } }; - const cleanupDeletedFiles = (sourceDir: string, destinationDir: string) => { - if (!existsSync(destinationDir)) return; + return { copyFileWrapper, removeFile }; +}; - // Check if this is a locale directory - if so, skip cleanup to preserve other language files - const isLocaleDir = destinationDir.includes(join('src', 'locales')); - if (isLocaleDir) { - return; // Skip cleanup for locale directories to preserve files from other plugins/languages +/** + * Remove files in destination that no longer exist in source (skip locales) + */ +const cleanupDeletedFiles = ( + sourceDir: string, + destinationDir: string, + removeFileFn: (p: string) => void, +) => { + if (!existsSync(destinationDir)) return; + + const isLocaleDir = destinationDir.includes(join("src", "locales")); + if (isLocaleDir) return; + + const destFiles = getAllFiles(destinationDir); + for (const destFile of destFiles) { + const relativePath = relative(destinationDir, destFile); + const sourceFile = join(sourceDir, relativePath); + + if (!existsSync(sourceFile)) { + removeFileFn(destFile); } - - const destFiles = getAllFiles(destinationDir); - for (const destFile of destFiles) { - const relativePath = relative(destinationDir, destFile); - const sourceFile = join(sourceDir, relativePath); - - if (!existsSync(sourceFile)) { - removeFile(destFile); - } - } - }; - - // Clean up any files that were deleted while the script wasn't running - // Clean up deleted files for each source directory - for (const { sourceDir, destinationDir } of sources) { - cleanupDeletedFiles(sourceDir, destinationDir); } +}; - console.log( - `${initMessage} \x1b[34mWatching for changes in plugins...\x1b[0m`, - ); - - const sourceDirs = sources - .map(s => s.sourceDir) - .filter(dir => existsSync(dir)); - - const watcher = chokidar.watch(sourceDirs, { - ignoreInitial: false, - persistent: true, - }); - - const getDestinationPaths = (srcPath: string): string[] => { - // collect all matching sourceConfigs +/** + * Resolve destination paths for a given source path across all source configs + */ +const makeGetDestinationPaths = (sources: SourceConfig[]) => { + return (srcPath: string): string[] => { const candidates = sources.filter(({ sourceDir }) => { - // Ensure exact directory matching by checking if the path starts with sourceDir - // followed by a path separator (or is exactly the sourceDir) - const normalizedSrcPath = srcPath.replace(/\\/g, '/'); - const normalizedSourceDir = sourceDir.replace(/\\/g, '/'); + const normalizedSrcPath = srcPath.replace(/\\/g, "/"); + const normalizedSourceDir = sourceDir.replace(/\\/g, "/"); return ( normalizedSrcPath === normalizedSourceDir || - normalizedSrcPath.startsWith(normalizedSourceDir + '/') + normalizedSrcPath.startsWith(`${normalizedSourceDir}/`) ); }); @@ -265,34 +233,100 @@ export const processPlugin = ({ initMessage }: { initMessage: string }) => { throw new Error(`No matching source directory for: ${srcPath}`); } - // Return all matching destination paths instead of just one return candidates.map(sourceConfig => { const relativePath = relative(sourceConfig.sourceDir, srcPath); - return join(sourceConfig.destinationDir, relativePath); }); }; +}; + +/** + * Setup chokidar watcher with handlers + */ +const setupWatcher = ( + sourceDirs: string[], + getDestinationPaths: (p: string) => string[], + copyFileWrapper: (s: string, d: string) => void, + removeFile: (p: string) => void, +) => { + const watcher = chokidar.watch(sourceDirs, { + ignoreInitial: false, + persistent: true, + }); watcher - .on('add', filePath => { + .on("add", filePath => { const destPaths = getDestinationPaths(filePath); - destPaths.forEach(destPath => { - copyFileWrapper(filePath, destPath); - }); + destPaths.forEach(destPath => copyFileWrapper(filePath, destPath)); }) - .on('change', filePath => { + .on("change", filePath => { const destPaths = getDestinationPaths(filePath); - destPaths.forEach(destPath => { - copyFileWrapper(filePath, destPath); - }); + destPaths.forEach(destPath => copyFileWrapper(filePath, destPath)); }) - .on('unlink', filePath => { + .on("unlink", filePath => { const destPaths = getDestinationPaths(filePath); - destPaths.forEach(destPath => { - removeFile(destPath); - }); + destPaths.forEach(destPath => removeFile(destPath)); }) - .on('error', error => { - console.error('\x1b[31mWatcher error:\x1b[0m', error); + .on("error", error => { + console.error("\x1b[31mWatcher error:\x1b[0m", error); }); + + return watcher; +}; + +/** + * Main exported function (kept small and delegating to helpers) + */ +export const processPlugin = ({ initMessage }: { initMessage: string }) => { + const pluginDir = process.cwd(); + const repoRoot = findRepoRoot(pluginDir); + const localeRoot = findLocaleRoot(repoRoot); + const routeMap = buildInitialRouteMap(localeRoot); + + let pluginName = basename(pluginDir); + try { + const packageJsonPath = join(pluginDir, "package.json"); + if (existsSync(packageJsonPath)) { + const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")); + pluginName = packageJson.name ?? ""; + } + } catch (error) { + console.error("\x1b[31mError reading package.json:\x1b[0m", error); + return; + } + + const pluginPathName = pluginName.replace(/\//g, "-").replace(/@/g, ""); + + const sources = collectSources( + pluginDir, + repoRoot, + pluginName, + pluginPathName, + ); + + ensureDestinationDirs(sources); + + const { copyFileWrapper, removeFile } = createFileOps( + pluginName, + routeMap, + localeRoot, + repoRoot, + ); + + // Cleanup deleted files that might have been removed while this script wasn't running + for (const { sourceDir, destinationDir } of sources) { + cleanupDeletedFiles(sourceDir, destinationDir, removeFile); + } + + console.log( + `${initMessage} \x1b[34mWatching for changes in plugins...\x1b[0m`, + ); + + const sourceDirs = sources + .map(s => s.sourceDir) + .filter(dir => existsSync(dir)); + + const getDestinationPaths = makeGetDestinationPaths(sources); + + setupWatcher(sourceDirs, getDestinationPaths, copyFileWrapper, removeFile); }; diff --git a/packages/vitnode/scripts/prepare-database.ts b/packages/vitnode/scripts/prepare-database.ts index da82196a1..721c119d8 100644 --- a/packages/vitnode/scripts/prepare-database.ts +++ b/packages/vitnode/scripts/prepare-database.ts @@ -1,46 +1,45 @@ -/* eslint-disable no-console */ +/** biome-ignore-all lint/suspicious/noConsole: */ +import { count } from "drizzle-orm"; -import { count } from 'drizzle-orm'; +import { core_admin_permissions } from "@/database/admins.js"; +import { core_languages, core_languages_words } from "@/database/languages.js"; +import { core_moderators_permissions } from "@/database/moderators.js"; +import { core_roles } from "@/database/roles.js"; -import { core_admin_permissions } from '@/database/admins.js'; -import { core_languages, core_languages_words } from '@/database/languages.js'; -import { core_moderators_permissions } from '@/database/moderators.js'; -import { core_roles } from '@/database/roles.js'; - -import { getConfig } from './get-config.js'; -import { preparePluginsFiles } from './prepare-plugins-files.js'; -import { runInteractiveShellCommand } from './run-interactive-shell-command.js'; +import { getConfig } from "./get-config.js"; +import { preparePluginsFiles } from "./prepare-plugins-files.js"; +import { runInteractiveShellCommand } from "./run-interactive-shell-command.js"; export const generateDatabaseMigrations = async () => { try { - await runInteractiveShellCommand('npm', ['run', 'drizzle-kit', 'up']); - await runInteractiveShellCommand('npm', ['run', 'drizzle-kit', 'generate']); + await runInteractiveShellCommand("npm", ["run", "drizzle-kit", "up"]); + await runInteractiveShellCommand("npm", ["run", "drizzle-kit", "generate"]); } catch (err) { - console.error('\x1b[31m%s\x1b[0m', err); + console.error("\x1b[31m%s\x1b[0m", err); process.exit(1); } }; export const runMigrations = async () => { try { - await runInteractiveShellCommand('npm', ['run', 'drizzle-kit', 'migrate']); + await runInteractiveShellCommand("npm", ["run", "drizzle-kit", "migrate"]); } catch (err) { - console.error('\x1b[31m%s\x1b[0m', err); + console.error("\x1b[31m%s\x1b[0m", err); process.exit(1); } }; export const runPush = async () => { try { - await runInteractiveShellCommand('npm', ['run', 'drizzle-kit', 'push']); + await runInteractiveShellCommand("npm", ["run", "drizzle-kit", "push"]); } catch (err) { - console.error('\x1b[31m%s\x1b[0m', err); + console.error("\x1b[31m%s\x1b[0m", err); process.exit(1); } }; export const initialDataForDatabase = async () => { - const config = await getConfig({ type: 'api.config' }); + const config = await getConfig({ type: "api.config" }); const dbClient = config.dbProvider; const [roleCount] = await dbClient @@ -58,11 +57,11 @@ export const initialDataForDatabase = async () => { if (languageCount.count === 0) { await dbClient.insert(core_languages).values([ { - code: 'en', - name: 'English (USA)', + code: "en", + name: "English (USA)", default: true, protected: true, - timezone: 'America/New_York', + timezone: "America/New_York", }, ]); } @@ -84,13 +83,13 @@ export const initialDataForDatabase = async () => { { // Moderator role protected: true, - color: 'hsl(122, 80%, 45%)', + color: "hsl(122, 80%, 45%)", }, { // Administrator role protected: true, root: true, - color: 'hsl(0, 100%, 50%)', + color: "hsl(0, 100%, 50%)", }, ]) .returning({ id: core_roles.id }); @@ -98,39 +97,39 @@ export const initialDataForDatabase = async () => { await dbClient.insert(core_languages_words).values([ { // Guest role - languageCode: 'en', - pluginCode: 'core', + languageCode: "en", + pluginCode: "core", itemId: roles[0].id, - value: 'Guest', - tableName: 'core_roles', - variable: 'name', + value: "Guest", + tableName: "core_roles", + variable: "name", }, { // Member role - languageCode: 'en', - pluginCode: 'core', + languageCode: "en", + pluginCode: "core", itemId: roles[1].id, - value: 'Member', - tableName: 'core_roles', - variable: 'name', + value: "Member", + tableName: "core_roles", + variable: "name", }, { // Moderator role - languageCode: 'en', - pluginCode: 'core', + languageCode: "en", + pluginCode: "core", itemId: roles[2].id, - value: 'Moderator', - tableName: 'core_roles', - variable: 'name', + value: "Moderator", + tableName: "core_roles", + variable: "name", }, { // Administrator role - languageCode: 'en', - pluginCode: 'core', + languageCode: "en", + pluginCode: "core", itemId: roles[3].id, - value: 'Administrator', - tableName: 'core_roles', - variable: 'name', + value: "Administrator", + tableName: "core_roles", + variable: "name", }, ]); @@ -157,61 +156,63 @@ export const prepareDatabase = async ({ }) => { const steps: { action: () => Promise; label: string }[] = []; - if (flag === '--web') { + if (flag === "--web") { steps.push({ - label: 'Prepare plugins files...', + label: "Prepare plugins files...", action: async () => await preparePluginsFiles(flag), }); - } else if (flag === '--api') { + } else if (flag === "--api") { steps.push( { - label: 'Prepare plugins files...', + label: "Prepare plugins files...", action: async () => await preparePluginsFiles(flag), }, { - label: 'Generate migrations...', + label: "Generate migrations...", action: generateDatabaseMigrations, }, { - label: 'Run migrations...', + label: "Run migrations...", action: runMigrations, }, { - label: 'Insert initial data...', + label: "Insert initial data...", action: initialDataForDatabase, }, ); } else { steps.push( { - label: 'Prepare plugins files...', + label: "Prepare plugins files...", action: async () => await preparePluginsFiles(flag), }, { - label: 'Generate migrations...', + label: "Generate migrations...", action: generateDatabaseMigrations, }, { - label: 'Run migrations...', + label: "Run migrations...", action: runMigrations, }, { - label: 'Insert initial data...', + label: "Insert initial data...", action: initialDataForDatabase, }, ); } - for (let i = 0; i < steps.length; i++) { - const step = steps[i]; - const stepNum = `[${i + 1}/${steps.length}]`; - if (step.label === 'Insert initial data...') { - console.log(`\n${initMessage} ${stepNum} ${step.label}`); - } else { - console.log(`${initMessage} ${stepNum} ${step.label}`); - } - await step.action(); - } + await Promise.all( + steps.map((step, i) => { + const stepNum = `[${i + 1}/${steps.length}]`; + if (step.label === "Insert initial data...") { + console.log(`\n${initMessage} ${stepNum} ${step.label}`); + } else { + console.log(`${initMessage} ${stepNum} ${step.label}`); + } + + return step.action(); + }), + ); console.log(`${initMessage} \x1b[32mInitial setup completed.\x1b[0m`); process.exit(0); diff --git a/packages/vitnode/scripts/prepare-plugins-files.ts b/packages/vitnode/scripts/prepare-plugins-files.ts index 156b8bef6..8d53d6368 100644 --- a/packages/vitnode/scripts/prepare-plugins-files.ts +++ b/packages/vitnode/scripts/prepare-plugins-files.ts @@ -1,9 +1,9 @@ -/* eslint-disable no-console */ -import { existsSync, readdirSync } from 'fs'; -import { readFile } from 'fs/promises'; -import { join, relative } from 'path'; +/** biome-ignore-all lint/suspicious/noConsole: */ +import { existsSync, readdirSync } from "node:fs"; +import { readFile } from "node:fs/promises"; +import { join, relative } from "node:path"; -import { getConfig } from './get-config'; +import { getConfig } from "./get-config"; import { buildInitialRouteMap, copyDirectoryRecursive, @@ -11,25 +11,25 @@ import { findRepoRoot, isDirectoryEmpty, type SourceConfig, -} from './shared/file-utils'; +} from "./shared/file-utils"; export const preparePluginsFiles = async (flag?: string) => { // Detect which config file to load based on flag or auto-detection const cwd = process.cwd(); - const hasWebConfig = existsSync(join(cwd, 'src', 'vitnode.config.ts')); - const hasApiConfig = existsSync(join(cwd, 'src', 'vitnode.api.config.ts')); + const hasWebConfig = existsSync(join(cwd, "src", "vitnode.config.ts")); + const hasApiConfig = existsSync(join(cwd, "src", "vitnode.api.config.ts")); let config: { plugins: { pluginId: string }[] }; - if (flag === '--api' && hasApiConfig) { + if (flag === "--api" && hasApiConfig) { // Force API config when --api flag is used - config = await getConfig({ type: 'api.config' }); - } else if (flag === '--web' && hasWebConfig) { + config = await getConfig({ type: "api.config" }); + } else if (flag === "--web" && hasWebConfig) { // Force web config when --web flag is used config = await getConfig({}); } else if (hasApiConfig && !hasWebConfig) { // API config only (auto-detect) - config = await getConfig({ type: 'api.config' }); + config = await getConfig({ type: "api.config" }); } else if (hasWebConfig) { // Web config (may also have API config but web takes precedence in auto-detect) config = await getConfig({}); @@ -40,7 +40,7 @@ export const preparePluginsFiles = async (flag?: string) => { const plugins: string[] = [ ...config.plugins.map(plugin => plugin.pluginId), - '@vitnode/core', + "@vitnode/core", ]; const repoRoot = findRepoRoot(process.cwd()); @@ -54,14 +54,14 @@ export const preparePluginsFiles = async (flag?: string) => { const cwd = process.cwd(); // Check in current working directory first - const cwdPluginPath = join(cwd, 'node_modules', pluginName); + const cwdPluginPath = join(cwd, "node_modules", pluginName); if (existsSync(cwdPluginPath)) { return cwdPluginPath; } // Check in monorepo root if it exists and is different from cwd if (repoRoot && repoRoot !== cwd) { - const rootPluginPath = join(repoRoot, 'node_modules', pluginName); + const rootPluginPath = join(repoRoot, "node_modules", pluginName); if (existsSync(rootPluginPath)) { return rootPluginPath; } @@ -71,6 +71,7 @@ export const preparePluginsFiles = async (flag?: string) => { }; await Promise.all( + // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: plugins.map(async pluginName => { const pluginPath = findPluginPath(pluginName); @@ -83,14 +84,14 @@ export const preparePluginsFiles = async (flag?: string) => { } // Get the package name from package.json for imports - let packageName = ''; + let packageName = ""; try { - const packageJsonPath = join(pluginPath, 'package.json'); + const packageJsonPath = join(pluginPath, "package.json"); if (existsSync(packageJsonPath)) { const packageJson = JSON.parse( - await readFile(packageJsonPath, 'utf-8'), + await readFile(packageJsonPath, "utf-8"), ); - packageName = packageJson.name ?? ''; + packageName = packageJson.name ?? ""; } } catch (error) { console.error( @@ -102,25 +103,25 @@ export const preparePluginsFiles = async (flag?: string) => { } // Transform plugin name for path usage - const pluginPathName = pluginName.replace(/\//g, '-').replace(/@/g, ''); + const pluginPathName = pluginName.replace(/\//g, "-").replace(/@/g, ""); // Detect app types by checking for config files const detectAppType = (appPath: string) => { const hasWebConfig = existsSync( - join(appPath, 'src', 'vitnode.config.ts'), + join(appPath, "src", "vitnode.config.ts"), ); const hasApiConfig = existsSync( - join(appPath, 'src', 'vitnode.api.config.ts'), + join(appPath, "src", "vitnode.api.config.ts"), ); - if (hasApiConfig && !hasWebConfig) return 'api'; - if (hasWebConfig) return 'web'; + if (hasApiConfig && !hasWebConfig) return "api"; + if (hasWebConfig) return "web"; return null; }; // Check if we're in a monorepo by looking for apps directories - const appsDir = join(repoRoot, 'apps'); + const appsDir = join(repoRoot, "apps"); const isMonorepo = existsSync(appsDir); // Define source configurations for this plugin @@ -135,50 +136,50 @@ export const preparePluginsFiles = async (flag?: string) => { : []; for (const appName of appDirs) { - const appPath = join(repoRoot, 'apps', appName); + const appPath = join(repoRoot, "apps", appName); const appType = detectAppType(appPath); - if (appType === 'web') { + if (appType === "web") { // Web app: copy app, app_admin, and locales const mainDest = join( appPath, - 'src', - 'app', - '[locale]', - '(main)', - join('(plugins)', `(${pluginPathName})`), + "src", + "app", + "[locale]", + "(main)", + join("(plugins)", `(${pluginPathName})`), ); const adminDest = join( appPath, - 'src', - 'app', - '[locale]', - 'admin', - '(auth)', - join('(plugins)', `(${pluginPathName})`), + "src", + "app", + "[locale]", + "admin", + "(auth)", + join("(plugins)", `(${pluginPathName})`), ); - const langDest = join(appPath, 'src', 'locales', pluginName); + const langDest = join(appPath, "src", "locales", pluginName); sources.push( { - sourceDir: join(pluginPath, 'src', 'app_admin'), + sourceDir: join(pluginPath, "src", "app_admin"), destinationDir: adminDest, }, { - sourceDir: join(pluginPath, 'src', 'app'), + sourceDir: join(pluginPath, "src", "app"), destinationDir: mainDest, }, { - sourceDir: join(pluginPath, 'src', 'locales'), + sourceDir: join(pluginPath, "src", "locales"), destinationDir: langDest, }, ); - } else if (appType === 'api') { + } else if (appType === "api") { // API app: copy only locales - const apiLangDest = join(appPath, 'src', 'locales', pluginName); + const apiLangDest = join(appPath, "src", "locales", pluginName); sources.push({ - sourceDir: join(pluginPath, 'src', 'locales'), + sourceDir: join(pluginPath, "src", "locales"), destinationDir: apiLangDest, }); } @@ -187,34 +188,34 @@ export const preparePluginsFiles = async (flag?: string) => { // Standalone project: use current directory as base const mainDest = join( baseDir, - 'src', - 'app', - '[locale]', - '(main)', - join('(plugins)', `(${pluginPathName})`), + "src", + "app", + "[locale]", + "(main)", + join("(plugins)", `(${pluginPathName})`), ); const adminDest = join( baseDir, - 'src', - 'app', - '[locale]', - 'admin', - '(auth)', - join('(plugins)', `(${pluginPathName})`), + "src", + "app", + "[locale]", + "admin", + "(auth)", + join("(plugins)", `(${pluginPathName})`), ); - const langDest = join(baseDir, 'src', 'locales', pluginName); + const langDest = join(baseDir, "src", "locales", pluginName); sources.push( { - sourceDir: join(pluginPath, 'src', 'app_admin'), + sourceDir: join(pluginPath, "src", "app_admin"), destinationDir: adminDest, }, { - sourceDir: join(pluginPath, 'src', 'app'), + sourceDir: join(pluginPath, "src", "app"), destinationDir: mainDest, }, { - sourceDir: join(pluginPath, 'src', 'locales'), + sourceDir: join(pluginPath, "src", "locales"), destinationDir: langDest, }, ); diff --git a/packages/vitnode/scripts/run-interactive-shell-command.ts b/packages/vitnode/scripts/run-interactive-shell-command.ts index 1fcd7ae12..185bf07dc 100644 --- a/packages/vitnode/scripts/run-interactive-shell-command.ts +++ b/packages/vitnode/scripts/run-interactive-shell-command.ts @@ -1,21 +1,21 @@ -import { spawn } from 'child_process'; +import { spawn } from "node:child_process"; export const runInteractiveShellCommand = async ( cmd: string, args: string[] = [], ) => { - return new Promise((resolve, reject) => { + return await new Promise((resolve, reject) => { const child = spawn(cmd, args, { - stdio: 'inherit', + stdio: "inherit", shell: true, env: process.env, }); - child.on('error', error => { + child.on("error", error => { reject(error); }); - child.on('close', code => { + child.on("close", code => { if (code !== 0) { reject(new Error(`Command failed with exit code ${code}`)); } else { diff --git a/packages/vitnode/scripts/scripts.ts b/packages/vitnode/scripts/scripts.ts index 6c957d714..78e6537b8 100644 --- a/packages/vitnode/scripts/scripts.ts +++ b/packages/vitnode/scripts/scripts.ts @@ -1,35 +1,35 @@ #!/usr/bin/env node -/* eslint-disable no-console */ +/** biome-ignore-all lint/suspicious/noConsole: */ -import * as dotenv from 'dotenv'; +import { config } from "dotenv"; -import { processPlugin } from './plugin.js'; +import { processPlugin } from "./plugin.js"; import { generateDatabaseMigrations, initialDataForDatabase, prepareDatabase, runMigrations, runPush, -} from './prepare-database.js'; -import { preparePluginsFiles } from './prepare-plugins-files.js'; +} from "./prepare-database.js"; +import { preparePluginsFiles } from "./prepare-plugins-files.js"; -dotenv.config({ +config({ quiet: true, }); -const initMessage = '\x1b[34m[VitNode]\x1b[0m'; +const initMessage = "\x1b[34m[VitNode]\x1b[0m"; const command = process.argv[2]; const flag = process.argv[3]; switch (command) { - case 'init': + case "init": void prepareDatabase({ initMessage, flag }); break; - case 'migrate': + case "migrate": await generateDatabaseMigrations(); - if (flag === '--generate') { + if (flag === "--generate") { console.log( `${initMessage} \x1b[32mDatabase migrations generated successfully.\x1b[0m`, ); @@ -44,20 +44,20 @@ switch (command) { process.exit(0); break; - case 'plugin': - if (flag === '--w' || flag === '--watch') { + case "plugin": + if (flag === "--w" || flag === "--watch") { processPlugin({ initMessage }); } break; - case 'prepare-plugins': + case "prepare-plugins": await preparePluginsFiles(flag); console.log(`${initMessage} \x1b[32mPlugins prepared successfully.\x1b[0m`); process.exit(0); break; - case 'push': + case "push": await runPush(); await initialDataForDatabase(); @@ -67,7 +67,7 @@ switch (command) { default: console.log( - `${initMessage} \x1b[31mCommand not found: "${command ?? ''}"\x1b[0m`, + `${initMessage} \x1b[31mCommand not found: "${command ?? ""}"\x1b[0m`, ); process.exit(1); } diff --git a/packages/vitnode/scripts/shared/file-utils.ts b/packages/vitnode/scripts/shared/file-utils.ts index b818bd0b0..8b4a7b414 100644 --- a/packages/vitnode/scripts/shared/file-utils.ts +++ b/packages/vitnode/scripts/shared/file-utils.ts @@ -1,4 +1,4 @@ -/* eslint-disable no-console */ +/** biome-ignore-all lint/suspicious/noConsole: */ import { copyFileSync, existsSync, @@ -6,7 +6,7 @@ import { readdirSync, readFileSync, writeFileSync, -} from 'fs'; +} from "node:fs"; import { basename, dirname, @@ -16,7 +16,7 @@ import { relative, resolve, sep, -} from 'path'; +} from "node:path"; // Regex patterns for import statements const relativeImportRegex = @@ -35,10 +35,10 @@ export const routeKey = (filePath: string, localeRoot: string): string => { parts.pop(); // drop any group folders - const filtered = parts.filter(p => !p.startsWith('(')); + const filtered = parts.filter(p => !p.startsWith("(")); // '' represents the root route - return normalize(filtered.join('/')); + return normalize(filtered.join("/")); }; export const buildInitialRouteMap = ( @@ -79,11 +79,11 @@ export const transformFileImports = ( relativeImportRegex, (match, importPath: string) => { // Only transform relative imports (starting with ./ or ../) - if (importPath.startsWith('.')) { + if (importPath.startsWith(".")) { // Remove any file extensions from the import path - const cleanPath = importPath.replace(jsExtensionRegex, ''); + const cleanPath = importPath.replace(jsExtensionRegex, ""); // Extract the path after removing leading '../' sequences - const normalizedPath = cleanPath.replace(/^(?:\.\.\/)+/, ''); + const normalizedPath = cleanPath.replace(/^(?:\.\.\/)+/, ""); // Return the package import format return match.replace(importPath, `${pluginName}/${normalizedPath}`); @@ -99,8 +99,8 @@ export const transformFileImports = ( (match, importPath: string) => { // Remove '@/' prefix and any file extensions const cleanPath = importPath - .replace(/^@\//, '') - .replace(jsExtensionRegex, ''); + .replace(/^@\//, "") + .replace(jsExtensionRegex, ""); // Return the package import format return match.replace(importPath, `${pluginName}/${cleanPath}`); @@ -113,8 +113,8 @@ export const transformFileImports = ( (match, importPath: string) => { // Remove '@/' prefix and any file extensions const cleanPath = importPath - .replace(/^@\//, '') - .replace(jsExtensionRegex, ''); + .replace(/^@\//, "") + .replace(jsExtensionRegex, ""); // Return the package import format return match.replace(importPath, `${pluginName}/${cleanPath}`); @@ -127,8 +127,8 @@ export const transformFileImports = ( /dynamic\s*\(\s*\(\s*=>\s*import\s*\(\s*['"](@\/[^'"]*)['"]\s*\)\s*\)\s*\)/g, (match, importPath: string) => { const cleanPath = importPath - .replace(/^@\//, '') - .replace(jsExtensionRegex, ''); + .replace(/^@\//, "") + .replace(jsExtensionRegex, ""); return match.replace(importPath, `${pluginName}/${cleanPath}`); }, @@ -139,26 +139,26 @@ export const transformFileImports = ( export function findRepoRoot(startPath: string): string { let currentPath = startPath; - while (currentPath !== resolve(currentPath, '..')) { - const turboConfigPath = join(currentPath, 'turbo.json'); + while (currentPath !== resolve(currentPath, "..")) { + const turboConfigPath = join(currentPath, "turbo.json"); if (existsSync(turboConfigPath)) { return currentPath; } - currentPath = resolve(currentPath, '..'); + currentPath = resolve(currentPath, ".."); } - const packagePath = join(startPath, 'package.json'); + const packagePath = join(startPath, "package.json"); if (existsSync(packagePath)) { return startPath; } - throw new Error('❌ Could not locate project root'); + throw new Error("❌ Could not locate project root"); } export function findLocaleRoot(repoRoot: string): string { // Check for standalone structure (src/app/[locale]) - const standalonePath = join(repoRoot, 'src', 'app', '[locale]'); + const standalonePath = join(repoRoot, "src", "app", "[locale]"); if (existsSync(standalonePath)) { return standalonePath; } @@ -168,6 +168,7 @@ export function findLocaleRoot(repoRoot: string): string { const localeDirectories: string[] = []; if (!existsSync(searchDir)) return localeDirectories; + // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: const visit = (currentDir: string, depth = 0) => { // Limit search depth to avoid infinite recursion and performance issues if (depth > 4) return; @@ -181,10 +182,10 @@ export function findLocaleRoot(repoRoot: string): string { const fullPath = join(currentDir, entry.name); // Check if this is a [locale] directory with app structure - if (entry.name === '[locale]') { + if (entry.name === "[locale]") { // Verify it's inside an app directory structure const parentPath = dirname(fullPath); - if (parentPath.endsWith(join('src', 'app'))) { + if (parentPath.endsWith(join("src", "app"))) { localeDirectories.push(fullPath); continue; } @@ -214,11 +215,11 @@ export function findLocaleRoot(repoRoot: string): string { // Default to apps/docs structure if nothing is found (for new projects) const defaultAppDir = join( repoRoot, - 'apps', - 'docs', - 'src', - 'app', - '[locale]', + "apps", + "docs", + "src", + "app", + "[locale]", ); return defaultAppDir; @@ -263,6 +264,7 @@ export const copyFile = ( localeRoot: string, repoRoot: string, verbose = true, + // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: ) => { const fileName = basename(srcPath); if (pageFileRegex.test(fileName)) { @@ -292,9 +294,9 @@ export const copyFile = ( // Check if file should have imports processed (like .js, .jsx, .ts, .tsx files) const ext = extname(srcPath); - if (pluginName && ['.js', '.jsx', '.ts', '.tsx'].includes(ext)) { + if (pluginName && [".js", ".jsx", ".ts", ".tsx"].includes(ext)) { // Read file content - const content = readFileSync(srcPath, 'utf-8'); + const content = readFileSync(srcPath, "utf-8"); // Transform imports const transformedContent = transformFileImports(content, pluginName); // Write transformed content @@ -307,13 +309,15 @@ export const copyFile = ( if (verbose) { // Show even shorter, project-rooted paths for clarity // Remove everything before '/src/app' in the source path if present - const srcAppIdx = srcPath.indexOf(join('src', 'app')); - const shortSrc = - srcAppIdx !== -1 - ? srcPath.substring(srcAppIdx) - : srcPath.startsWith(repoRoot) - ? relative(repoRoot, srcPath) - : srcPath; + const srcAppIdx = srcPath.indexOf(join("src", "app")); + let shortSrc: string; + if (srcAppIdx !== -1) { + shortSrc = srcPath.substring(srcAppIdx); + } else if (srcPath.startsWith(repoRoot)) { + shortSrc = relative(repoRoot, srcPath); + } else { + shortSrc = srcPath; + } const shortDest = destPath.startsWith(repoRoot) ? relative(repoRoot, destPath) : destPath; diff --git a/packages/vitnode/src/api/adapters/email/nodemailer.ts b/packages/vitnode/src/api/adapters/email/nodemailer.ts index 57e856203..7f00ecfec 100644 --- a/packages/vitnode/src/api/adapters/email/nodemailer.ts +++ b/packages/vitnode/src/api/adapters/email/nodemailer.ts @@ -1,14 +1,14 @@ -import { createTransport } from 'nodemailer'; +import { createTransport } from "nodemailer"; -import type { EmailApiPlugin } from '@/api/models/email'; +import type { EmailApiPlugin } from "@/api/models/email"; export const NodemailerEmailAdapter = ({ - host = '', + host = "", port = 587, secure = false, - user = '', - password = '', - from = '', + user = "", + password = "", + from = "", }: { from: string | undefined; host: string | undefined; @@ -19,8 +19,8 @@ export const NodemailerEmailAdapter = ({ }): EmailApiPlugin => { return { sendEmail: async ({ metadata, to, subject, html, replyTo }) => { - if (!host || !user || !password || !from) { - throw new Error('Missing nodemailer configuration'); + if (!(host && user && password && from)) { + throw new Error("Missing nodemailer configuration"); } const transporter = createTransport( diff --git a/packages/vitnode/src/api/adapters/email/resend.ts b/packages/vitnode/src/api/adapters/email/resend.ts index 26dd0986d..8c2f3cf3a 100644 --- a/packages/vitnode/src/api/adapters/email/resend.ts +++ b/packages/vitnode/src/api/adapters/email/resend.ts @@ -1,6 +1,6 @@ -import { Resend } from 'resend'; +import { Resend } from "resend"; -import type { EmailApiPlugin } from '@/api/models/email'; +import type { EmailApiPlugin } from "@/api/models/email"; export const ResendEmailAdapter = ({ apiKey, @@ -11,8 +11,8 @@ export const ResendEmailAdapter = ({ }): EmailApiPlugin => { return { sendEmail: async ({ to, subject, replyTo, metadata, html }) => { - if (!apiKey || !from) { - throw new Error('Missing Resend configuration'); + if (!(apiKey && from)) { + throw new Error("Missing Resend configuration"); } const resend = new Resend(apiKey); diff --git a/packages/vitnode/src/api/adapters/sso/discord.ts b/packages/vitnode/src/api/adapters/sso/discord.ts index 3ffd266ae..a9ef621df 100644 --- a/packages/vitnode/src/api/adapters/sso/discord.ts +++ b/packages/vitnode/src/api/adapters/sso/discord.ts @@ -1,20 +1,19 @@ -import type { ContentfulStatusCode } from 'hono/utils/http-status'; +import { HTTPException } from "hono/http-exception"; +import type { ContentfulStatusCode } from "hono/utils/http-status"; +import { z } from "zod"; -import { HTTPException } from 'hono/http-exception'; -import { z } from 'zod'; +import type { SSOApiPlugin } from "@/api/models/sso"; -import type { SSOApiPlugin } from '@/api/models/sso'; - -import { getRedirectUri } from '@/api/models/sso'; +import { getRedirectUri } from "@/api/models/sso"; export const DiscordSSOApiPlugin = ({ - clientId = '', - clientSecret = '', + clientId = "", + clientSecret = "", }: { clientId: string | undefined; clientSecret: string | undefined; }): SSOApiPlugin => { - const id = 'discord'; + const id = "discord"; const redirectUri = getRedirectUri(id); const userSchema = z.object({ id: z.string(), @@ -28,20 +27,20 @@ export const DiscordSSOApiPlugin = ({ return { fetchToken: async code => { - if (!clientId || !clientSecret) { - throw new Error('Missing Discord client ID or secret'); + if (!(clientId && clientSecret)) { + throw new Error("Missing Discord client ID or secret"); } - const res = await fetch('https://discord.com/api/oauth2/token', { - method: 'POST', + const res = await fetch("https://discord.com/api/oauth2/token", { + method: "POST", headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - Accept: 'application/json', + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", }, body: new URLSearchParams({ code, redirect_uri: redirectUri, - grant_type: 'authorization_code', + grant_type: "authorization_code", client_id: clientId, client_secret: clientSecret, }), @@ -51,7 +50,7 @@ export const DiscordSSOApiPlugin = ({ throw new HTTPException( +res.status.toString() as ContentfulStatusCode, { - message: 'Internal error requesting token', + message: "Internal error requesting token", }, ); } @@ -59,14 +58,14 @@ export const DiscordSSOApiPlugin = ({ const { data, error } = tokenSchema.safeParse(await res.json()); if (error || !data) { throw new HTTPException(400, { - message: 'Invalid token response', + message: "Invalid token response", }); } return data; }, fetchUser: async ({ token_type, access_token }) => { - const res = await fetch('https://discord.com/api/users/@me', { + const res = await fetch("https://discord.com/api/users/@me", { headers: { Authorization: `${token_type} ${access_token}`, }, @@ -74,7 +73,7 @@ export const DiscordSSOApiPlugin = ({ const { data, error } = userSchema.safeParse(await res.json()); if (error || !data) { throw new HTTPException(400, { - message: 'Invalid user response', + message: "Invalid user response", }); } @@ -82,19 +81,19 @@ export const DiscordSSOApiPlugin = ({ }, getUrl: ({ state }) => { if (!clientId) { - throw new Error('Missing Discord client ID'); + throw new Error("Missing Discord client ID"); } - const url = new URL('https://discord.com/oauth2/authorize'); - url.searchParams.set('client_id', clientId); - url.searchParams.set('redirect_uri', redirectUri); - url.searchParams.set('response_type', 'code'); - url.searchParams.set('scope', 'identify email'); - url.searchParams.set('state', state); + const url = new URL("https://discord.com/oauth2/authorize"); + url.searchParams.set("client_id", clientId); + url.searchParams.set("redirect_uri", redirectUri); + url.searchParams.set("response_type", "code"); + url.searchParams.set("scope", "identify email"); + url.searchParams.set("state", state); return url.toString(); }, id, - name: 'Discord', + name: "Discord", }; }; diff --git a/packages/vitnode/src/api/adapters/sso/facebook.ts b/packages/vitnode/src/api/adapters/sso/facebook.ts index d6e352a6e..d9942fa37 100644 --- a/packages/vitnode/src/api/adapters/sso/facebook.ts +++ b/packages/vitnode/src/api/adapters/sso/facebook.ts @@ -1,11 +1,10 @@ -import type { ContentfulStatusCode } from 'hono/utils/http-status'; +import { HTTPException } from "hono/http-exception"; +import type { ContentfulStatusCode } from "hono/utils/http-status"; +import { z } from "zod"; -import { HTTPException } from 'hono/http-exception'; -import { z } from 'zod'; +import type { SSOApiPlugin } from "@/api/models/sso"; -import type { SSOApiPlugin } from '@/api/models/sso'; - -import { getRedirectUri } from '@/api/models/sso'; +import { getRedirectUri } from "@/api/models/sso"; export const FacebookSSOApiPlugin = ({ clientId, @@ -14,7 +13,7 @@ export const FacebookSSOApiPlugin = ({ clientId: string | undefined; clientSecret: string | undefined; }): SSOApiPlugin => { - const id = 'facebook'; + const id = "facebook"; const redirectUri = getRedirectUri(id); const tokenSchema = z.object({ access_token: z.string(), @@ -28,25 +27,25 @@ export const FacebookSSOApiPlugin = ({ return { id, - name: 'Facebook', + name: "Facebook", fetchToken: async code => { - if (!clientId || !clientSecret) { - throw new Error('Missing Facebook client ID or secret'); + if (!(clientId && clientSecret)) { + throw new Error("Missing Facebook client ID or secret"); } const url = new URL( - 'https://graph.facebook.com/v22.0/oauth/access_token', + "https://graph.facebook.com/v22.0/oauth/access_token", ); - url.searchParams.set('code', code); - url.searchParams.set('redirect_uri', redirectUri); - url.searchParams.set('client_id', clientId); - url.searchParams.set('client_secret', clientSecret); + url.searchParams.set("code", code); + url.searchParams.set("redirect_uri", redirectUri); + url.searchParams.set("client_id", clientId); + url.searchParams.set("client_secret", clientSecret); const res = await fetch(url.toString()); if (!res.ok) { throw new HTTPException( +res.status.toString() as ContentfulStatusCode, { - message: 'Internal error requesting token', + message: "Internal error requesting token", }, ); } @@ -54,7 +53,7 @@ export const FacebookSSOApiPlugin = ({ const { data, error } = tokenSchema.safeParse(await res.json()); if (error || !data) { throw new HTTPException(400, { - message: 'Invalid token response', + message: "Invalid token response", }); } @@ -62,15 +61,15 @@ export const FacebookSSOApiPlugin = ({ }, fetchUser: async ({ access_token }) => { - const url = new URL('https://graph.facebook.com/v22.0/me'); - url.searchParams.set('fields', 'id,name,email'); - url.searchParams.set('access_token', access_token); + const url = new URL("https://graph.facebook.com/v22.0/me"); + url.searchParams.set("fields", "id,name,email"); + url.searchParams.set("access_token", access_token); const res = await fetch(url.toString()); if (!res.ok) { throw new HTTPException( +res.status.toString() as ContentfulStatusCode, { - message: 'Internal error requesting user', + message: "Internal error requesting user", }, ); } @@ -79,7 +78,7 @@ export const FacebookSSOApiPlugin = ({ ); if (userError || !userData) { throw new HTTPException(400, { - message: 'Invalid user response', + message: "Invalid user response", }); } @@ -88,15 +87,15 @@ export const FacebookSSOApiPlugin = ({ getUrl: ({ state }) => { if (!clientId) { - throw new Error('Missing Facebook client ID'); + throw new Error("Missing Facebook client ID"); } - const url = new URL('https://www.facebook.com/v22.0/dialog/oauth'); - url.searchParams.set('client_id', clientId); - url.searchParams.set('redirect_uri', redirectUri); - url.searchParams.set('scope', 'public_profile,email'); - url.searchParams.set('response_type', 'code'); - url.searchParams.set('state', state); + const url = new URL("https://www.facebook.com/v22.0/dialog/oauth"); + url.searchParams.set("client_id", clientId); + url.searchParams.set("redirect_uri", redirectUri); + url.searchParams.set("scope", "public_profile,email"); + url.searchParams.set("response_type", "code"); + url.searchParams.set("state", state); return url.toString(); }, diff --git a/packages/vitnode/src/api/adapters/sso/google.ts b/packages/vitnode/src/api/adapters/sso/google.ts index 597514b3e..c4602e0fb 100644 --- a/packages/vitnode/src/api/adapters/sso/google.ts +++ b/packages/vitnode/src/api/adapters/sso/google.ts @@ -1,11 +1,10 @@ -import type { ContentfulStatusCode } from 'hono/utils/http-status'; +import { HTTPException } from "hono/http-exception"; +import type { ContentfulStatusCode } from "hono/utils/http-status"; +import { z } from "zod"; -import { HTTPException } from 'hono/http-exception'; -import { z } from 'zod'; +import type { SSOApiPlugin } from "@/api/models/sso"; -import type { SSOApiPlugin } from '@/api/models/sso'; - -import { getRedirectUri } from '@/api/models/sso'; +import { getRedirectUri } from "@/api/models/sso"; export const GoogleSSOApiPlugin = ({ clientId, @@ -14,7 +13,7 @@ export const GoogleSSOApiPlugin = ({ clientId: string | undefined; clientSecret: string | undefined; }): SSOApiPlugin => { - const id = 'google'; + const id = "google"; const redirectUri = getRedirectUri(id); const tokenSchema = z.object({ access_token: z.string(), @@ -29,22 +28,22 @@ export const GoogleSSOApiPlugin = ({ return { id, - name: 'Google', + name: "Google", fetchToken: async code => { - if (!clientId || !clientSecret) { - throw new Error('Missing Google client ID or secret'); + if (!(clientId && clientSecret)) { + throw new Error("Missing Google client ID or secret"); } - const res = await fetch('https://oauth2.googleapis.com/token', { - method: 'POST', + const res = await fetch("https://oauth2.googleapis.com/token", { + method: "POST", headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - Accept: 'application/json', + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", }, body: new URLSearchParams({ code, redirect_uri: redirectUri, - grant_type: 'authorization_code', + grant_type: "authorization_code", client_id: clientId, client_secret: clientSecret, }), @@ -54,7 +53,7 @@ export const GoogleSSOApiPlugin = ({ throw new HTTPException( +res.status.toString() as ContentfulStatusCode, { - message: 'Internal error requesting token', + message: "Internal error requesting token", }, ); } @@ -62,14 +61,14 @@ export const GoogleSSOApiPlugin = ({ const { data, error } = tokenSchema.safeParse(await res.json()); if (error || !data) { throw new HTTPException(400, { - message: 'Invalid token response', + message: "Invalid token response", }); } return data; }, fetchUser: async ({ token_type, access_token }) => { - const res = await fetch('https://www.googleapis.com/oauth2/v1/userinfo', { + const res = await fetch("https://www.googleapis.com/oauth2/v1/userinfo", { headers: { Authorization: `${token_type} ${access_token}`, }, @@ -78,13 +77,13 @@ export const GoogleSSOApiPlugin = ({ const { data, error } = userSchema.safeParse(dataFromRes); if (error || !data) { throw new HTTPException(400, { - message: 'Invalid user response', + message: "Invalid user response", }); } if (!data.verified_email) { throw new HTTPException(400, { - message: 'Email not verified', + message: "Email not verified", }); } @@ -95,15 +94,15 @@ export const GoogleSSOApiPlugin = ({ }, getUrl: ({ state }) => { if (!clientId) { - throw new Error('Missing Google client ID'); + throw new Error("Missing Google client ID"); } - const url = new URL('https://accounts.google.com/o/oauth2/auth'); - url.searchParams.set('client_id', clientId); - url.searchParams.set('redirect_uri', redirectUri); - url.searchParams.set('response_type', 'code'); - url.searchParams.set('scope', 'openid profile email'); - url.searchParams.set('state', state); + const url = new URL("https://accounts.google.com/o/oauth2/auth"); + url.searchParams.set("client_id", clientId); + url.searchParams.set("redirect_uri", redirectUri); + url.searchParams.set("response_type", "code"); + url.searchParams.set("scope", "openid profile email"); + url.searchParams.set("state", state); return url.toString(); }, diff --git a/packages/vitnode/src/api/config.ts b/packages/vitnode/src/api/config.ts index f97de73b4..feecd8e8a 100644 --- a/packages/vitnode/src/api/config.ts +++ b/packages/vitnode/src/api/config.ts @@ -1,21 +1,18 @@ -import type { OpenAPIHono } from '@hono/zod-openapi'; -import type { Context, Env, Schema } from 'hono'; - -import { swaggerUI } from '@hono/swagger-ui'; -import { cors } from 'hono/cors'; -import { csrf } from 'hono/csrf'; -import { HTTPException } from 'hono/http-exception'; - -import type { VitNodeApiConfig } from '@/vitnode.config'; - -import { newBuildPluginApiCore } from '@/api/plugin'; -import { CONFIG_PLUGIN } from '@/config'; +import { swaggerUI } from "@hono/swagger-ui"; +import type { OpenAPIHono } from "@hono/zod-openapi"; +import type { Context, Env, Schema } from "hono"; +import { cors } from "hono/cors"; +import { csrf } from "hono/csrf"; +import { HTTPException } from "hono/http-exception"; +import { newBuildPluginApiCore } from "@/api/plugin"; +import { CONFIG_PLUGIN } from "@/config"; +import type { VitNodeApiConfig } from "@/vitnode.config"; import { globalAdminMiddleware, globalMiddleware, -} from './middlewares/global.middleware'; -import { rateLimiterMiddleware } from './middlewares/rate-limiter.middleware'; +} from "./middlewares/global.middleware"; +import { rateLimiterMiddleware } from "./middlewares/rate-limiter.middleware"; interface CORSOptions { allowHeaders?: string[]; @@ -45,19 +42,19 @@ export function VitNodeAPI({ csrf?: CSRFOptions; vitNodeApiConfig: VitNodeApiConfig; }) { - app.doc('/swagger/doc', { - openapi: '3.0.0', + app.doc("/swagger/doc", { + openapi: "3.0.0", info: { version: CONFIG_PLUGIN.version, - title: 'VitNode API', + title: "VitNode API", }, }); app.use(cors(corsOptions)); app.use(csrf(csrfOptions)); - app.use('*', rateLimiterMiddleware(vitNodeApiConfig.rateLimiter)); - app.get('/swagger', swaggerUI({ url: `/api/swagger/doc` })); + app.use("*", rateLimiterMiddleware(vitNodeApiConfig.rateLimiter)); + app.get("/swagger", swaggerUI({ url: "/api/swagger/doc" })); app.use( - '*', + "*", globalMiddleware({ pathToMessages: vitNodeApiConfig.pathToMessages, email: vitNodeApiConfig.email, @@ -69,8 +66,8 @@ export function VitNodeAPI({ }), ); app.use(async (c, next) => { - if (c.req.path.includes('/admin/')) { - return globalAdminMiddleware()(c, next); + if (c.req.path.includes("/admin/")) { + return await globalAdminMiddleware()(c, next); } return next(); @@ -81,12 +78,12 @@ export function VitNodeAPI({ return error.getResponse(); } - await c.get('log').error(`Unhandled error: ${error.message}`); + await c.get("log").error(`Unhandled error: ${error.message}`); return new Response( - process.env.NODE_ENV === 'development' + process.env.NODE_ENV === "development" ? error.message - : 'Internal Server Error', + : "Internal Server Error", { status: 500, }, diff --git a/packages/vitnode/src/api/lib/check-plugin-id.ts b/packages/vitnode/src/api/lib/check-plugin-id.ts index d937c2aab..9b2835d90 100644 --- a/packages/vitnode/src/api/lib/check-plugin-id.ts +++ b/packages/vitnode/src/api/lib/check-plugin-id.ts @@ -1,7 +1,7 @@ -import { existsSync, readFileSync } from 'fs'; -import { join, resolve } from 'path'; +import { existsSync, readFileSync } from "node:fs"; +import { join, resolve } from "node:path"; -import { CONFIG } from '../../lib/config'; +import { CONFIG } from "../../lib/config"; export interface PackageJSON { dependencies?: Record; @@ -22,13 +22,13 @@ export const checkPluginId = (pluginName: string): null | PackageJSON => { const findMonorepoRoot = (startPath: string): null | string => { let currentPath = startPath; - while (currentPath !== resolve(currentPath, '..')) { - const turboConfigPath = join(currentPath, 'turbo.json'); + while (currentPath !== resolve(currentPath, "..")) { + const turboConfigPath = join(currentPath, "turbo.json"); if (existsSync(turboConfigPath)) { return currentPath; } - currentPath = resolve(currentPath, '..'); + currentPath = resolve(currentPath, ".."); } return null; @@ -39,7 +39,7 @@ export const checkPluginId = (pluginName: string): null | PackageJSON => { const monorepoRoot = findMonorepoRoot(cwd); // Check in current working directory first - const cwdPluginPath = join(cwd, 'node_modules', pluginName, 'package.json'); + const cwdPluginPath = join(cwd, "node_modules", pluginName, "package.json"); if (existsSync(cwdPluginPath)) { return cwdPluginPath; } @@ -48,9 +48,9 @@ export const checkPluginId = (pluginName: string): null | PackageJSON => { if (monorepoRoot && monorepoRoot !== cwd) { const rootPluginPath = join( monorepoRoot, - 'node_modules', + "node_modules", pluginName, - 'package.json', + "package.json", ); if (existsSync(rootPluginPath)) { return rootPluginPath; @@ -68,7 +68,7 @@ export const checkPluginId = (pluginName: string): null | PackageJSON => { ); } - const content: PackageJSON = JSON.parse(readFileSync(path, 'utf-8')); + const content: PackageJSON = JSON.parse(readFileSync(path, "utf-8")); if (content.name !== pluginName) { throw new Error( `The plugin name in package.json (${content.name}) does not match the requested plugin name (${pluginName}). Please check the plugin name.`, diff --git a/packages/vitnode/src/api/lib/logger-middleware.ts b/packages/vitnode/src/api/lib/logger-middleware.ts index fdfb9fcae..bb4921b8c 100644 --- a/packages/vitnode/src/api/lib/logger-middleware.ts +++ b/packages/vitnode/src/api/lib/logger-middleware.ts @@ -1,9 +1,9 @@ -/* eslint-disable no-console */ -import type { Context } from 'hono'; +/** biome-ignore-all lint/suspicious/noConsole: */ +import type { Context } from "hono"; -import { core_logs } from '@/database/logs'; +import { core_logs } from "@/database/logs"; -type CoreLogsType = 'debug' | 'error' | 'warn'; +type CoreLogsType = "debug" | "error" | "warn"; export interface LoggerMiddlewareType { debug: (content: string) => Promise; @@ -13,11 +13,11 @@ export interface LoggerMiddlewareType { export const loggerMiddleware = (c: Context): LoggerMiddlewareType => { const logToDbAndConsole = async (content: string, type: CoreLogsType) => { - const pluginId = c.get('plugin')?.id ?? 'core'; - const ipAddress = c.get('ipAddress'); + const pluginId = c.get("plugin")?.id ?? "core"; + const ipAddress = c.get("ipAddress"); await c - .get('db') + .get("db") .insert(core_logs) .values({ pluginId, @@ -26,9 +26,9 @@ export const loggerMiddleware = (c: Context): LoggerMiddlewareType => { ipAddress, method: c.req.method.toUpperCase(), path: c.req.path, - userAgent: c.req.header('User-Agent'), + userAgent: c.req.header("User-Agent"), statusCode: c.res.status, - userId: c.get('user')?.id, + userId: c.get("user")?.id, }); const loggers: Record void> = { @@ -50,13 +50,13 @@ export const loggerMiddleware = (c: Context): LoggerMiddlewareType => { return { debug: async (content: string) => { - await logToDbAndConsole(content, 'debug'); + await logToDbAndConsole(content, "debug"); }, error: async (content: string) => { - await logToDbAndConsole(content, 'error'); + await logToDbAndConsole(content, "error"); }, warn: async (content: string) => { - await logToDbAndConsole(content, 'warn'); + await logToDbAndConsole(content, "warn"); }, }; }; diff --git a/packages/vitnode/src/api/lib/module.ts b/packages/vitnode/src/api/lib/module.ts index 9f9297ffc..de0d2c5a8 100644 --- a/packages/vitnode/src/api/lib/module.ts +++ b/packages/vitnode/src/api/lib/module.ts @@ -1,6 +1,6 @@ -import { OpenAPIHono } from '@hono/zod-openapi'; +import { OpenAPIHono } from "@hono/zod-openapi"; -import type { Route } from './route'; +import type { Route } from "./route"; export interface BuildModuleType { plugin: Plugin; diff --git a/packages/vitnode/src/api/lib/plugin.ts b/packages/vitnode/src/api/lib/plugin.ts index 5c1031680..f4829a34d 100644 --- a/packages/vitnode/src/api/lib/plugin.ts +++ b/packages/vitnode/src/api/lib/plugin.ts @@ -1,8 +1,6 @@ -import { OpenAPIHono } from '@hono/zod-openapi'; - -import type { BuildModuleReturn } from './module'; - -import { checkPluginId } from './check-plugin-id'; +import { OpenAPIHono } from "@hono/zod-openapi"; +import { checkPluginId } from "./check-plugin-id"; +import type { BuildModuleReturn } from "./module"; export interface BuildPluginApiReturn { hono: OpenAPIHono; diff --git a/packages/vitnode/src/api/lib/route.ts b/packages/vitnode/src/api/lib/route.ts index b29681fc7..33170e1f4 100644 --- a/packages/vitnode/src/api/lib/route.ts +++ b/packages/vitnode/src/api/lib/route.ts @@ -1,12 +1,12 @@ -import type { RouteConfig, RouteHandler } from '@hono/zod-openapi'; +import type { RouteConfig, RouteHandler } from "@hono/zod-openapi"; -import { createRoute as createRouteHono } from '@hono/zod-openapi'; +import { createRoute as createRouteHono } from "@hono/zod-openapi"; -import { captchaMiddleware } from '../middlewares/captcha.middleware'; +import { captchaMiddleware } from "../middlewares/captcha.middleware"; import { type EnvVitNode, pluginMiddleware, -} from '../middlewares/global.middleware'; +} from "../middlewares/global.middleware"; type RoutingPath

= P extends `${infer Head}/{${infer Param}}${infer Tail}` @@ -20,7 +20,7 @@ type ValidHandler = ( export const buildRoute = < Plugin extends string, P extends string, - R extends Omit & { + R extends Omit & { path: P; withCaptcha?: boolean; }, @@ -37,13 +37,13 @@ export const buildRoute = < handler: H; pluginId: Plugin; route: R & { - getRoutingPath: () => RoutingPath; + getRoutingPath: () => RoutingPath; }; } => { const pluginTag = pluginId .split(/[-_]/) .map(word => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); + .join(" "); const tags = [pluginTag, ...(route.tags ?? [])]; @@ -55,13 +55,14 @@ export const buildRoute = < ...(route.withCaptcha ? [captchaMiddleware()] : []), ...(Array.isArray(route.middleware) ? route.middleware - : route.middleware + : // biome-ignore lint/style/noNestedTernary: + route.middleware ? [route.middleware] : []), ], ...route, }) as R & { - getRoutingPath: () => RoutingPath; + getRoutingPath: () => RoutingPath; }, handler, pluginId, diff --git a/packages/vitnode/src/api/lib/with-pagination.ts b/packages/vitnode/src/api/lib/with-pagination.ts index a7173613c..0ecfc4cef 100644 --- a/packages/vitnode/src/api/lib/with-pagination.ts +++ b/packages/vitnode/src/api/lib/with-pagination.ts @@ -1,19 +1,88 @@ -import type { ColumnBaseConfig, Placeholder, SQL } from 'drizzle-orm'; +import { z } from "@hono/zod-openapi"; +import type { ColumnBaseConfig, Placeholder, SQL } from "drizzle-orm"; +import { and, asc, count, desc, gt, lt } from "drizzle-orm"; import type { PgColumn, PgTable, PgTableWithColumns, TableConfig, -} from 'drizzle-orm/pg-core'; -import type { Context } from 'hono'; +} from "drizzle-orm/pg-core"; +import type { Context } from "hono"; -import { z } from '@hono/zod-openapi'; -import { and, asc, count, desc, gt, lt } from 'drizzle-orm'; +function parsePaginationParams(params: { + query: { cursor?: string; first?: string; last?: string }; +}): { cursor?: number; first?: number; last?: number } { + const cursor = params.query.cursor + ? parseInt(params.query.cursor, 10) + : undefined; + const first = params.query.first + ? Math.min(parseInt(params.query.first, 10), 100) + : undefined; + const last = params.query.last + ? Math.min(parseInt(params.query.last, 10), 100) + : undefined; + + if (first !== undefined && last !== undefined) { + throw new Error("Cannot specify both first and last"); + } + if (first !== undefined && first < 0) { + throw new Error("first must be positive"); + } + if (last !== undefined && last < 0) { + throw new Error("last must be positive"); + } + + return { cursor, first, last }; +} + +function getOrderFn( + isForward: boolean, + order: "asc" | "desc", +): typeof asc | typeof desc { + if (isForward) { + return order === "asc" ? asc : desc; + } + return order === "asc" ? desc : asc; +} + +function buildWhereWithCursor< + Primary extends ColumnBaseConfig<"number", string>, +>( + baseWhere: SQL | undefined, + cursor: number | undefined, + isForward: boolean, + order: "asc" | "desc", + table: PgTable, + primaryCursor: PgColumn, +): SQL | undefined { + if (!cursor) return baseWhere; + + const cursorFilter = + (isForward && order === "asc") || (!isForward && order === "desc") + ? gt + : lt; + + const cursorWhere = cursorFilter(table[primaryCursor.name], cursor); + return baseWhere ? and(baseWhere, cursorWhere) : cursorWhere; +} + +async function fetchTotalCount( + c: Context, + table: PgTable, + where: SQL | undefined, +): Promise { + const [{ count: totalCount }] = await c + .get("db") + .select({ count: count() }) + .from(table) + .where(where); + return totalCount; +} export async function withPagination< QueryMin extends Record, T extends TableConfig, - Primary extends ColumnBaseConfig<'number', string>, + Primary extends ColumnBaseConfig<"number", string>, >({ query, table, @@ -26,7 +95,7 @@ export async function withPagination< c: Context; orderBy: { column: PgColumn; - order: 'asc' | 'desc'; + order: "asc" | "desc"; }; params: { query: { @@ -41,7 +110,7 @@ export async function withPagination< orderBy: SQL; where: SQL | undefined; }) => Promise; - table: Omit, 'enableRLS'>; + table: Omit, "enableRLS">; where?: SQL; }): Promise<{ edges: QueryMin[]; @@ -54,78 +123,39 @@ export async function withPagination< totalCount: number; }; }> { - // Parse and validate pagination parameters - const cursor = params.query.cursor - ? parseInt(params.query.cursor, 10) - : undefined; - const first = params.query.first - ? Math.min(parseInt(params.query.first, 10), 100) - : undefined; - const last = params.query.last - ? Math.min(parseInt(params.query.last, 10), 100) - : undefined; - - if (first !== undefined && last !== undefined) { - throw new Error('Cannot specify both first and last'); - } + const { cursor, first, last } = parsePaginationParams(params); - if (first !== undefined && first < 0) { - throw new Error('first must be positive'); - } - - if (last !== undefined && last < 0) { - throw new Error('last must be positive'); - } - - // Determine sort direction based on pagination parameters const isForward = last === undefined; - const orderFn = isForward - ? orderByFromParams.order === 'asc' - ? asc - : desc - : orderByFromParams.order === 'asc' - ? desc - : asc; - + const orderFn = getOrderFn(isForward, orderByFromParams.order); const orderBy: SQL = orderFn(table[orderByFromParams.column.name]); - // Build where clause with cursor - let where: SQL | undefined = whereFromParams; - if (cursor) { - const cursorFilter = isForward - ? orderByFromParams.order === 'asc' - ? gt - : lt - : orderByFromParams.order === 'asc' - ? lt - : gt; - - const cursorWhere = cursorFilter(table[primaryCursor.name], cursor); - where = where ? and(where, cursorWhere) : cursorWhere; - } - - // Get total count - const [{ count: totalCount }] = await c - .get('db') - .select({ count: count() }) - .from(table as PgTable) - .where(whereFromParams); + const where = buildWhereWithCursor( + whereFromParams, + cursor, + isForward, + orderByFromParams.order, + table, + primaryCursor, + ); + + const totalCount = await fetchTotalCount( + c, + table as PgTable, + whereFromParams, + ); - // Fetch one extra item to determine if there are more pages const limit = (first ?? last ?? 50) + 1; const edges = await query({ limit, where, orderBy }); - // Process results - const hasMore = edges.length > (first ?? last ?? edges.length); - const slicedEdges = edges.slice(0, first ?? last ?? edges.length); + const requested = first ?? last ?? edges.length; + const hasMore = edges.length > requested; + const slicedEdges = edges.slice(0, requested); const finalEdges = isForward ? slicedEdges : slicedEdges.reverse(); - // Prepare cursors const startCursor: null | number = (finalEdges[0]?.[primaryCursor.name] as number) ?? null; - const endCursor: null | number = - (finalEdges[finalEdges.length - 1]?.[primaryCursor.name] as number) ?? null; + (finalEdges.at(-1)?.[primaryCursor.name] as number) ?? null; return { pageInfo: { diff --git a/packages/vitnode/src/api/middlewares/captcha.middleware.ts b/packages/vitnode/src/api/middlewares/captcha.middleware.ts index 9cc1da440..09628d4c5 100644 --- a/packages/vitnode/src/api/middlewares/captcha.middleware.ts +++ b/packages/vitnode/src/api/middlewares/captcha.middleware.ts @@ -1,54 +1,55 @@ -import type { Context, Next } from 'hono'; +import type { Context, Next } from "hono"; -import { HTTPException } from 'hono/http-exception'; +import { HTTPException } from "hono/http-exception"; -import type { VitNodeApiConfig } from '../../vitnode.config'; +import type { VitNodeApiConfig } from "../../vitnode.config"; const getResFromReCaptcha = async ({ token, userIp, captchaConfig, }: { - captchaConfig: NonNullable['captcha']>; + captchaConfig: NonNullable["captcha"]>; token: string; userIp: string; -}): Promise<{ 'error-codes'?: string[]; score: number; success: boolean }> => { - if (captchaConfig.type === 'cloudflare_turnstile') { +}): Promise<{ "error-codes"?: string[]; score: number; success: boolean }> => { + if (captchaConfig.type === "cloudflare_turnstile") { const res = await fetch( - 'https://challenges.cloudflare.com/turnstile/v0/siteverify', + "https://challenges.cloudflare.com/turnstile/v0/siteverify", { - method: 'POST', + method: "POST", body: JSON.stringify({ secret: captchaConfig.secretKey, response: token, remoteip: userIp, }), headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, }, ); const data: { - 'error-codes'?: string[]; + "error-codes"?: string[]; success: boolean; } = await res.json(); return { success: data.success, score: data.success ? 1 : 0, - 'error-codes': data['error-codes'], + "error-codes": data["error-codes"], }; - } else if (captchaConfig.type === 'recaptcha_v3') { + } + if (captchaConfig.type === "recaptcha_v3") { const res = await fetch( `https://www.google.com/recaptcha/api/siteverify?secret=${captchaConfig.secretKey}&response=${token}&remoteip=${userIp}`, { - method: 'POST', + method: "POST", }, ); const data: { - 'error-codes'?: string[]; + "error-codes"?: string[]; score: number; success: boolean; } = await res.json(); @@ -56,7 +57,7 @@ const getResFromReCaptcha = async ({ return { success: data.success, score: data.score ?? 0, - 'error-codes': data['error-codes'], + "error-codes": data["error-codes"], }; } @@ -68,8 +69,8 @@ const getResFromReCaptcha = async ({ export const captchaMiddleware = () => { return async (c: Context, next: Next) => { - const token = c.req.header('x-vitnode-captcha-token'); - const captchaConfig = c.get('core').captcha; + const token = c.req.header("x-vitnode-captcha-token"); + const captchaConfig = c.get("core").captcha; if (!captchaConfig) { await next(); @@ -78,19 +79,19 @@ export const captchaMiddleware = () => { if (!token) { throw new HTTPException(400, { - message: 'Captcha token is required', + message: "Captcha token is required", }); } const res = await getResFromReCaptcha({ token, - userIp: c.get('ipAddress'), + userIp: c.get("ipAddress"), captchaConfig, }); if (!res.success || res.score < 0.5) { throw new HTTPException(400, { - message: 'Captcha validation failed', + message: "Captcha validation failed", }); } diff --git a/packages/vitnode/src/api/middlewares/global.middleware.ts b/packages/vitnode/src/api/middlewares/global.middleware.ts index f051f6462..ec8bf40a1 100644 --- a/packages/vitnode/src/api/middlewares/global.middleware.ts +++ b/packages/vitnode/src/api/middlewares/global.middleware.ts @@ -1,19 +1,19 @@ -import type { Context, Env, Next } from 'hono'; - -import { HTTPException } from 'hono/http-exception'; - -import type { VitNodeApiConfig, VitNodeConfig } from '@/vitnode.config'; - -import { EmailModel, type EmailModelSendArgs } from '@/api/models/email'; -import { SessionModel } from '@/api/models/session'; -import { SessionAdminModel } from '@/api/models/session-admin'; - -import type { SSOApiPlugin } from '../models/sso'; +import type { Context, Env, Next } from "hono"; +import { HTTPException } from "hono/http-exception"; +import { EmailModel, type EmailModelSendArgs } from "@/api/models/email"; +import { SessionModel } from "@/api/models/session"; +import { SessionAdminModel } from "@/api/models/session-admin"; +import type { VitNodeApiConfig, VitNodeConfig } from "@/vitnode.config"; import { - loggerMiddleware, type LoggerMiddlewareType, -} from '../lib/logger-middleware'; + loggerMiddleware, +} from "../lib/logger-middleware"; +import type { SSOApiPlugin } from "../models/sso"; + +declare module "hono" { + interface ContextVariableMap extends EnvVariablesVitNode {} +} export interface EnvVitNode extends Env { Variables: EnvVariablesVitNode; @@ -45,8 +45,8 @@ export interface EnvVariablesVitNode { deviceCookieName: string; ssoAdapters: SSOApiPlugin[]; }; - captcha?: Pick['captcha']; - email?: VitNodeApiConfig['email']; + captcha?: Pick["captcha"]; + email?: VitNodeApiConfig["email"]; metadata: { shortTitle?: string; title: string; @@ -54,7 +54,7 @@ export interface EnvVariablesVitNode { pathToMessages: (path: string) => Promise<{ default: object }>; plugins: { id: string }[]; }; - db: Pick['dbProvider']; + db: Pick["dbProvider"]; email: { send: (args: EmailModelSendArgs) => Promise; }; @@ -77,11 +77,6 @@ export interface EnvVariablesVitNode { }; } -declare module 'hono' { - // eslint-disable-next-line @typescript-eslint/no-empty-object-type - interface ContextVariableMap extends EnvVariablesVitNode {} -} - export const globalMiddleware = ({ authorization, metadata, @@ -92,33 +87,34 @@ export const globalMiddleware = ({ pathToMessages, }: Pick< VitNodeApiConfig, - | 'authorization' - | 'captcha' - | 'dbProvider' - | 'email' - | 'pathToMessages' - | 'plugins' + | "authorization" + | "captcha" + | "dbProvider" + | "email" + | "pathToMessages" + | "plugins" > & - Pick) => { + Pick) => { + // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: return async (c: Context, next: Next) => { // Collect possible IP header keys in order of trust/preference const ipHeaderKeys = [ - 'x-forwarded-for', - 'x-real-ip', - 'cf-connecting-ip', - 'x-client-ip', - 'x-forwarded', - 'x-cluster-client-ip', - 'forwarded-for', - 'forwarded', - 'via', - 'remote-addr', - 'client-ip', - 'ip', - 'x-ip', - 'true-client-ip', - 'fastly-client-ip', - 'x-fastly-client-ip', + "x-forwarded-for", + "x-real-ip", + "cf-connecting-ip", + "x-client-ip", + "x-forwarded", + "x-cluster-client-ip", + "forwarded-for", + "forwarded", + "via", + "remote-addr", + "client-ip", + "ip", + "x-ip", + "true-client-ip", + "fastly-client-ip", + "x-fastly-client-ip", ]; let ipAddress: string | undefined; @@ -144,23 +140,23 @@ export const globalMiddleware = ({ } // Fallback to localhost if nothing found - c.set('ipAddress', ipAddress ?? '127.0.0.1'); - c.set('db', dbProvider); - c.set('email', new EmailModel(c)); + c.set("ipAddress", ipAddress ?? "127.0.0.1"); + c.set("db", dbProvider); + c.set("email", new EmailModel(c)); - c.set('core', { + c.set("core", { pathToMessages, metadata, email, authorization: { - cookieName: authorization?.cookieName ?? 'vitnode_auth', + cookieName: authorization?.cookieName ?? "vitnode_auth", cookie_expires: authorization?.cookieExpires ?? 1000 * 60 * 60 * 24 * 90, // 90 days ssoAdapters: authorization?.ssoAdapters ?? [], - deviceCookieName: authorization?.deviceCookieName ?? 'vitnode_device', + deviceCookieName: authorization?.deviceCookieName ?? "vitnode_device", deviceCookieExpires: authorization?.deviceCookieExpires ?? 1000 * 60 * 60 * 24 * 365, // 1 year, - adminCookieName: authorization?.adminCookieName ?? 'vitnode_auth_admin', + adminCookieName: authorization?.adminCookieName ?? "vitnode_auth_admin", adminCookieExpires: authorization?.adminCookieExpires ?? 1000 * 60 * 60 * 24 * 1, // 1 day cookieSecure: authorization?.cookieSecure ?? true, @@ -172,9 +168,9 @@ export const globalMiddleware = ({ }); const user = await new SessionModel(c).getUser(); - c.set('user', user); - c.set('admin', null); - c.set('log', loggerMiddleware(c)); + c.set("user", user); + c.set("admin", null); + c.set("log", loggerMiddleware(c)); await next(); }; @@ -182,7 +178,7 @@ export const globalMiddleware = ({ export const pluginMiddleware = (pluginId: string) => { return async (c: Context, next: Next) => { - c.set('plugin', { + c.set("plugin", { id: pluginId, }); await next(); @@ -193,7 +189,7 @@ export const globalAdminMiddleware = () => { return async (c: Context, next: Next) => { const user = await new SessionAdminModel(c).getUser(); if (!user) throw new HTTPException(403); - c.set('admin', { + c.set("admin", { user, }); diff --git a/packages/vitnode/src/api/middlewares/rate-limiter.middleware.ts b/packages/vitnode/src/api/middlewares/rate-limiter.middleware.ts index 3b3f1852e..1c4e54055 100644 --- a/packages/vitnode/src/api/middlewares/rate-limiter.middleware.ts +++ b/packages/vitnode/src/api/middlewares/rate-limiter.middleware.ts @@ -1,17 +1,17 @@ -import type { Context, Next } from 'hono'; +import type { Context, Next } from "hono"; import { type IRateLimiterOptions, type RateLimiterAbstract, RateLimiterMemory, -} from 'rate-limiter-flexible'; +} from "rate-limiter-flexible"; -import { CONFIG } from '../../lib/config'; +import { CONFIG } from "../../lib/config"; const createRateLimiter = ({ keyPrefix, ...options -}: Omit & { +}: Omit & { keyPrefix: string; }): RateLimiterAbstract => { // TODO: Add support for Redis or other storage options @@ -25,27 +25,27 @@ const createRateLimiter = ({ }; export const rateLimiterMiddleware = ( - options?: Omit, + options?: Omit, ) => { if (CONFIG.node_development) { // In development, we disable the rate limiter for easier testing - return async (c: Context, next: Next) => { + return async (_c: Context, next: Next) => { await next(); }; } const rateLimiter = createRateLimiter({ ...options, - keyPrefix: 'vitnode-api-rate-limiter', + keyPrefix: "vitnode-api-rate-limiter", }); return async (c: Context, next: Next) => { - const key = c.get('ipAddress'); + const key = c.get("ipAddress"); try { await rateLimiter.consume(key); } catch { - return c.text('Too Many Requests', 429); + return c.text("Too Many Requests", 429); } await next(); diff --git a/packages/vitnode/src/api/models/device.ts b/packages/vitnode/src/api/models/device.ts index 178c514b5..8907d6340 100644 --- a/packages/vitnode/src/api/models/device.ts +++ b/packages/vitnode/src/api/models/device.ts @@ -1,11 +1,10 @@ -import type { Context } from 'hono'; +import { randomBytes } from "node:crypto"; +import { eq } from "drizzle-orm"; +import type { Context } from "hono"; +import { getCookie, setCookie } from "hono/cookie"; -import { randomBytes } from 'crypto'; -import { eq } from 'drizzle-orm'; -import { getCookie, setCookie } from 'hono/cookie'; - -import { core_sessions_known_devices } from '@/database/sessions'; -import { CONFIG } from '@/lib/config'; +import { core_sessions_known_devices } from "@/database/sessions"; +import { CONFIG } from "@/lib/config"; export class DeviceModel { constructor(c: Context) { @@ -14,14 +13,14 @@ export class DeviceModel { protected readonly c: Context; private async createDevice() { - const publicId = randomBytes(16).toString('hex'); + const publicId = randomBytes(16).toString("hex"); const [device] = await this.c - .get('db') + .get("db") .insert(core_sessions_known_devices) .values({ publicId, - ipAddress: this.c.get('ipAddress'), + ipAddress: this.c.get("ipAddress"), userAgent: this.getUserAgent(), }) .returning({ id: core_sessions_known_devices.id }); @@ -32,21 +31,21 @@ export class DeviceModel { } private getUserAgent() { - return this.c.req.header('User-Agent') ?? 'node'; + return this.c.req.header("User-Agent") ?? "node"; } private setCookieDevice(publicDeviceId: string) { setCookie( this.c, - this.c.get('core').authorization.deviceCookieName, + this.c.get("core").authorization.deviceCookieName, publicDeviceId, { httpOnly: true, - secure: this.c.get('core').authorization.cookieSecure, - path: '/', + secure: this.c.get("core").authorization.cookieSecure, + path: "/", domain: CONFIG.web.hostname, expires: new Date( - Date.now() + this.c.get('core').authorization.deviceCookieExpires, + Date.now() + this.c.get("core").authorization.deviceCookieExpires, ), }, ); @@ -55,13 +54,13 @@ export class DeviceModel { async getDeviceId() { const deviceIdFromCookie = getCookie( this.c, - this.c.get('core').authorization.deviceCookieName, + this.c.get("core").authorization.deviceCookieName, ); try { if (deviceIdFromCookie) { const [device] = await this.c - .get('db') + .get("db") .select({ id: core_sessions_known_devices.id, }) @@ -73,10 +72,10 @@ export class DeviceModel { } await this.c - .get('db') + .get("db") .update(core_sessions_known_devices) .set({ - ipAddress: this.c.get('ipAddress'), + ipAddress: this.c.get("ipAddress"), userAgent: this.getUserAgent(), }) .where(eq(core_sessions_known_devices.publicId, deviceIdFromCookie)); diff --git a/packages/vitnode/src/api/models/email.ts b/packages/vitnode/src/api/models/email.ts index 1cbe31fed..80c3b8eb7 100644 --- a/packages/vitnode/src/api/models/email.ts +++ b/packages/vitnode/src/api/models/email.ts @@ -1,22 +1,10 @@ -import type { Context, ContextVariableMap } from 'hono'; -import type React from 'react'; +import { render } from "@react-email/components"; +import type { Context, ContextVariableMap } from "hono"; +import { HTTPException } from "hono/http-exception"; +import type React from "react"; -import { render } from '@react-email/components'; -import { HTTPException } from 'hono/http-exception'; - -import { type DefaultTemplateEmailProps } from '../../emails/default-template'; -import { CONFIG } from '../../lib/config'; - -export interface EmailApiPlugin { - sendEmail: (args: { - html: string; - metadata: ContextVariableMap['core']['metadata']; - replyTo?: string; - subject: string; - text: string; - to: string; - }) => Promise; -} +import type { DefaultTemplateEmailProps } from "../../emails/default-template"; +import { CONFIG } from "../../lib/config"; interface EmailModelSendArgsWithUser { locale?: never; @@ -36,18 +24,28 @@ interface EmailModelSendArgsWithEmail { user?: never; } +export interface EmailApiPlugin { + sendEmail: (args: { + html: string; + metadata: ContextVariableMap["core"]["metadata"]; + replyTo?: string; + subject: string; + text: string; + to: string; + }) => Promise; +} + export type EmailModelSendArgs = { content: ( - props: Omit & - Pick, + props: Omit & + Pick, ) => React.ReactNode; html?: string; locale?: string; replyTo?: string; subject: - | ((props: Pick) => string) + | ((props: Pick) => string) | string; - // eslint-disable-next-line perfectionist/sort-intersection-types } & (EmailModelSendArgsWithEmail | EmailModelSendArgsWithUser); export class EmailModel { @@ -66,18 +64,18 @@ export class EmailModel { content, locale: localeFromArgs, }: EmailModelSendArgs) { - const core = this.c.get('core'); + const core = this.c.get("core"); const provider = core.email?.adapter; if (!provider) { throw new HTTPException(500, { - message: 'Email provider not found', + message: "Email provider not found", }); } - const locale = localeFromArgs ?? user?.language ?? 'en'; + const locale = localeFromArgs ?? user?.language ?? "en"; const pluginIds: string[] = [ - '@vitnode/core', - ...this.c.get('core').plugins.map(plugin => plugin.id), + "@vitnode/core", + ...this.c.get("core").plugins.map(plugin => plugin.id), ]; const messagesPromises = pluginIds.map(async pluginId => { @@ -93,6 +91,7 @@ export class EmailModel { const allMessages = await Promise.all(messagesPromises); const messages = allMessages.reduce( + // biome-ignore lint/performance/noAccumulatingSpread: (acc, curr) => ({ ...acc, ...curr }), {}, ) as Record; @@ -117,7 +116,7 @@ export class EmailModel { const emailTo = user?.email ?? to; if (!emailTo) { throw new HTTPException(400, { - message: 'Email address is required', + message: "Email address is required", }); } @@ -126,7 +125,7 @@ export class EmailModel { html: await render(htmlContent), to: emailTo, subject: - typeof subject === 'function' + typeof subject === "function" ? subject({ i18n: { locale, messages } }) : subject, replyTo, @@ -139,9 +138,9 @@ export class EmailModel { const error = err instanceof Error ? err - : new Error('Unknown error from email provider'); + : new Error("Unknown error from email provider"); - await this.c.get('log').error(`Failed to send email: ${error.message}`); + await this.c.get("log").error(`Failed to send email: ${error.message}`); } } } diff --git a/packages/vitnode/src/api/models/password.ts b/packages/vitnode/src/api/models/password.ts index d84de7cd5..099fa1761 100644 --- a/packages/vitnode/src/api/models/password.ts +++ b/packages/vitnode/src/api/models/password.ts @@ -1,22 +1,22 @@ -import crypto from 'crypto'; +import crypto from "node:crypto"; export class PasswordModel { async encryptPassword(password: string): Promise { - return new Promise((resolve, reject) => { - const salt = crypto.randomBytes(8).toString('hex'); + return await new Promise((resolve, reject) => { + const salt = crypto.randomBytes(8).toString("hex"); crypto.scrypt(password, salt, 64, (err, derivedKey) => { if (err) reject(err); - resolve(salt + ':' + derivedKey.toString('hex')); + resolve(`${salt}:${derivedKey.toString("hex")}`); }); }); } async verifyPassword(password: string, hash: string): Promise { - return new Promise((resolve, reject) => { - const [salt, key] = hash.split(':'); - const keyBuffer = Buffer.from(key, 'hex'); + return await new Promise((resolve, reject) => { + const [salt, key] = hash.split(":"); + const keyBuffer = Buffer.from(key, "hex"); crypto.scrypt(password, salt, 64, (err, derivedKey) => { if (err) reject(err); @@ -35,10 +35,10 @@ export class PasswordModel { export class ForgotPasswordTokenModel { generateResetToken() { - return crypto.randomBytes(32).toString('base64url'); + return crypto.randomBytes(32).toString("base64url"); } hashResetToken(token: string) { - return crypto.createHash('sha256').update(token).digest('hex'); + return crypto.createHash("sha256").update(token).digest("hex"); } } diff --git a/packages/vitnode/src/api/models/session-admin.ts b/packages/vitnode/src/api/models/session-admin.ts index acbb70aa9..0814867f4 100644 --- a/packages/vitnode/src/api/models/session-admin.ts +++ b/packages/vitnode/src/api/models/session-admin.ts @@ -1,14 +1,13 @@ -import type { Context } from 'hono'; +import { and, eq, gt, or } from "drizzle-orm"; // Removed 'or' as it's safer not to use it here +import type { Context } from "hono"; +import { deleteCookie, getCookie, setCookie } from "hono/cookie"; +import { HTTPException } from "hono/http-exception"; -import { and, eq, gt, or } from 'drizzle-orm'; // Removed 'or' as it's safer not to use it here -import { deleteCookie, getCookie, setCookie } from 'hono/cookie'; -import { HTTPException } from 'hono/http-exception'; +import { core_admin_permissions, core_admin_sessions } from "@/database/admins"; +import { CONFIG } from "@/lib/config"; -import { core_admin_permissions, core_admin_sessions } from '@/database/admins'; -import { CONFIG } from '@/lib/config'; - -import { DeviceModel } from './device'; -import { UserModel } from './user'; +import { DeviceModel } from "./device"; +import { UserModel } from "./user"; export class SessionAdminModel { constructor(c: Context) { @@ -19,10 +18,10 @@ export class SessionAdminModel { private async hashToken(token: string): Promise { const encoder = new TextEncoder(); const data = encoder.encode(token); - const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hashBuffer = await crypto.subtle.digest("SHA-256", data); const hashArray = Array.from(new Uint8Array(hashBuffer)); - return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + return hashArray.map(b => b.toString(16).padStart(2, "0")).join(""); } async checkIfUserIsAdmin(userId: number) { @@ -30,7 +29,7 @@ export class SessionAdminModel { if (!user) return false; const [permission] = await this.c - .get('db') + .get("db") .select() .from(core_admin_permissions) .where( @@ -47,36 +46,36 @@ export class SessionAdminModel { async createSessionByUserId(userId: number) { const isAdmin = await this.checkIfUserIsAdmin(userId); if (!isAdmin) { - throw new HTTPException(403, { message: 'Forbidden' }); + throw new HTTPException(403, { message: "Forbidden" }); } const randomBytes = new Uint8Array(64); crypto.getRandomValues(randomBytes); const token = Array.from(randomBytes) - .map(b => b.toString(16).padStart(2, '0')) - .join(''); + .map(b => b.toString(16).padStart(2, "0")) + .join(""); const hashedToken = await this.hashToken(token); const device = await new DeviceModel(this.c).getDeviceId(); await this.c - .get('db') + .get("db") .insert(core_admin_sessions) .values({ token: hashedToken, userId, expiresAt: new Date( - Date.now() + this.c.get('core').authorization.adminCookieExpires, + Date.now() + this.c.get("core").authorization.adminCookieExpires, ), deviceId: device.id, }); - setCookie(this.c, this.c.get('core').authorization.adminCookieName, token, { + setCookie(this.c, this.c.get("core").authorization.adminCookieName, token, { httpOnly: true, - secure: this.c.get('core').authorization.cookieSecure, - path: '/', + secure: this.c.get("core").authorization.cookieSecure, + path: "/", expires: new Date( - Date.now() + this.c.get('core').authorization.adminCookieExpires, + Date.now() + this.c.get("core").authorization.adminCookieExpires, ), domain: CONFIG.web.hostname, }); @@ -87,25 +86,25 @@ export class SessionAdminModel { async deleteSession() { const token = getCookie( this.c, - this.c.get('core').authorization.adminCookieName, + this.c.get("core").authorization.adminCookieName, ); if (!token) return; const hashedToken = await this.hashToken(token); await this.c - .get('db') + .get("db") .delete(core_admin_sessions) .where(eq(core_admin_sessions.token, hashedToken)); - deleteCookie(this.c, this.c.get('core').authorization.adminCookieName, { - path: '/', + deleteCookie(this.c, this.c.get("core").authorization.adminCookieName, { + path: "/", domain: CONFIG.web.hostname, }); } async getUser() { - const { authorization } = this.c.get('core'); + const { authorization } = this.c.get("core"); const token = getCookie(this.c, authorization.adminCookieName); if (!token) return null; @@ -115,7 +114,7 @@ export class SessionAdminModel { const hashedToken = await this.hashToken(token); const [session] = await this.c - .get('db') + .get("db") .select({ userId: core_admin_sessions.userId, }) @@ -130,8 +129,8 @@ export class SessionAdminModel { .limit(1); if (!session) { - deleteCookie(this.c, this.c.get('core').authorization.adminCookieName, { - path: '/', + deleteCookie(this.c, this.c.get("core").authorization.adminCookieName, { + path: "/", domain: CONFIG.web.hostname, }); diff --git a/packages/vitnode/src/api/models/session.ts b/packages/vitnode/src/api/models/session.ts index 8bff250af..251a10a2f 100644 --- a/packages/vitnode/src/api/models/session.ts +++ b/packages/vitnode/src/api/models/session.ts @@ -1,13 +1,12 @@ -import type { Context } from 'hono'; +import { and, eq, gt } from "drizzle-orm"; +import type { Context } from "hono"; +import { deleteCookie, getCookie, setCookie } from "hono/cookie"; -import { and, eq, gt } from 'drizzle-orm'; -import { deleteCookie, getCookie, setCookie } from 'hono/cookie'; +import { core_sessions } from "@/database/sessions"; +import { CONFIG } from "@/lib/config"; -import { core_sessions } from '@/database/sessions'; -import { CONFIG } from '@/lib/config'; - -import { DeviceModel } from './device'; -import { UserModel } from './user'; +import { DeviceModel } from "./device"; +import { UserModel } from "./user"; export class SessionModel { constructor(c: Context) { @@ -18,10 +17,10 @@ export class SessionModel { private async hashToken(token: string): Promise { const encoder = new TextEncoder(); const data = encoder.encode(token); - const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hashBuffer = await crypto.subtle.digest("SHA-256", data); const hashArray = Array.from(new Uint8Array(hashBuffer)); - return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + return hashArray.map(b => b.toString(16).padStart(2, "0")).join(""); } async createSessionByUserId(userId: number) { @@ -29,31 +28,31 @@ export class SessionModel { const randomBytes = new Uint8Array(64); crypto.getRandomValues(randomBytes); const token = Array.from(randomBytes) - .map(b => b.toString(16).padStart(2, '0')) - .join(''); + .map(b => b.toString(16).padStart(2, "0")) + .join(""); const device = await new DeviceModel(this.c).getDeviceId(); const hashedToken = await this.hashToken(token); await this.c - .get('db') + .get("db") .insert(core_sessions) .values({ token: hashedToken, userId, expiresAt: new Date( - Date.now() + this.c.get('core').authorization.cookie_expires, + Date.now() + this.c.get("core").authorization.cookie_expires, ), deviceId: device.id, }); - setCookie(this.c, this.c.get('core').authorization.cookieName, token, { + setCookie(this.c, this.c.get("core").authorization.cookieName, token, { httpOnly: true, - secure: this.c.get('core').authorization.cookieSecure, - path: '/', + secure: this.c.get("core").authorization.cookieSecure, + path: "/", expires: - this.c.get('core').authorization.cookie_expires > 0 + this.c.get("core").authorization.cookie_expires > 0 ? new Date( - Date.now() + this.c.get('core').authorization.cookie_expires, + Date.now() + this.c.get("core").authorization.cookie_expires, ) : undefined, domain: CONFIG.web.hostname, @@ -65,13 +64,13 @@ export class SessionModel { async deleteSession() { const token = getCookie( this.c, - this.c.get('core').authorization.cookieName, + this.c.get("core").authorization.cookieName, ); const device = await new DeviceModel(this.c).getDeviceId(); // Ensure both token and deviceId exist before proceeding - if (!token || !device.id) { - deleteCookie(this.c, this.c.get('core').authorization.cookieName); + if (!(token && device.id)) { + deleteCookie(this.c, this.c.get("core").authorization.cookieName); return; } @@ -79,7 +78,7 @@ export class SessionModel { const hashedToken = await this.hashToken(token); await this.c - .get('db') + .get("db") .delete(core_sessions) // Harden the query to ensure a user can only delete their own device's session .where( @@ -89,13 +88,13 @@ export class SessionModel { ), ); - deleteCookie(this.c, this.c.get('core').authorization.cookieName); + deleteCookie(this.c, this.c.get("core").authorization.cookieName); } async getUser() { const token = getCookie( this.c, - this.c.get('core').authorization.cookieName, + this.c.get("core").authorization.cookieName, ); if (!token) return null; @@ -105,7 +104,7 @@ export class SessionModel { const hashedToken = await this.hashToken(token); const [session] = await this.c - .get('db') + .get("db") .select({ userId: core_sessions.userId, }) @@ -120,7 +119,7 @@ export class SessionModel { .limit(1); if (!session) { - deleteCookie(this.c, this.c.get('core').authorization.cookieName); + deleteCookie(this.c, this.c.get("core").authorization.cookieName); return null; } diff --git a/packages/vitnode/src/api/models/sso.ts b/packages/vitnode/src/api/models/sso.ts index 47bb743d7..09b618096 100644 --- a/packages/vitnode/src/api/models/sso.ts +++ b/packages/vitnode/src/api/models/sso.ts @@ -1,15 +1,14 @@ -import type { Context } from 'hono'; +import crypto from "node:crypto"; +import { and, eq } from "drizzle-orm"; +import type { Context } from "hono"; +import { deleteCookie, getCookie, setCookie } from "hono/cookie"; +import { HTTPException } from "hono/http-exception"; -import crypto from 'crypto'; -import { and, eq } from 'drizzle-orm'; -import { deleteCookie, getCookie, setCookie } from 'hono/cookie'; -import { HTTPException } from 'hono/http-exception'; +import { core_users, core_users_sso } from "@/database/users"; +import { CONFIG } from "@/lib/config"; +import { removeSpecialCharacters } from "@/lib/special-characters"; -import { core_users, core_users_sso } from '@/database/users'; -import { CONFIG } from '@/lib/config'; -import { removeSpecialCharacters } from '@/lib/special-characters'; - -import { UserModel } from './user'; +import { UserModel } from "./user"; export interface SSOApiPlugin { fetchToken: ( @@ -30,7 +29,7 @@ export const getRedirectUri = (code: string) => export class SSOModel { constructor(c: Context) { this.c = c; - this.plugins = c.get('core').authorization.ssoAdapters; + this.plugins = c.get("core").authorization.ssoAdapters; } private readonly c: Context; @@ -58,7 +57,7 @@ export class SSOModel { }, c, ); - await c.get('db').insert(core_users_sso).values({ + await c.get("db").insert(core_users_sso).values({ userId: data.id, providerId: providerId, providerAccountId: user.id, @@ -87,7 +86,7 @@ export class SSOModel { const ssoToken = await provider.fetchToken(code); const userFromSSO = await provider.fetchUser(ssoToken); - return await this.c.get('db').transaction(async tx => { + return await this.c.get("db").transaction(async tx => { const [dataSSOFromDb] = await tx .select({ userId: core_users_sso.userId, @@ -124,17 +123,13 @@ export class SSOModel { // If email exists, throw an error throw new HTTPException(409, { - message: 'Email already exists', + message: "Email already exists", }); // await tx.insert(core_users_sso).values({ // providerId: providerId, // providerAccountId: userFromSSO.id, // userId: userWithEmail.id, // }); - - return { - userId: userWithEmail.id, - }; } return { @@ -144,25 +139,25 @@ export class SSOModel { } async encryptState() { - const state = crypto.randomBytes(8).toString('hex'); + const state = crypto.randomBytes(8).toString("hex"); const encryptedState = await new Promise((resolve, reject) => { - const salt = crypto.randomBytes(4).toString('hex'); + const salt = crypto.randomBytes(4).toString("hex"); crypto.scrypt(state, salt, 16, (err, derivedKey) => { if (err) reject(err); - resolve(salt + ':' + derivedKey.toString('hex')); + resolve(`${salt}:${derivedKey.toString("hex")}`); }); }); setCookie( this.c, - `${this.c.get('core').authorization.cookieName}--state-sso`, + `${this.c.get("core").authorization.cookieName}--state-sso`, encryptedState, { httpOnly: true, - secure: this.c.get('core').authorization.cookieSecure, - path: '/', + secure: this.c.get("core").authorization.cookieSecure, + path: "/", domain: CONFIG.web.hostname, }, ); @@ -182,32 +177,32 @@ export class SSOModel { async verifyState(state: string) { const storedState = getCookie( this.c, - `${this.c.get('core').authorization.cookieName}--state-sso`, + `${this.c.get("core").authorization.cookieName}--state-sso`, ); if (!storedState) { throw new HTTPException(400, { - message: 'Invalid state', + message: "Invalid state", }); } const isValid = await new Promise((resolve, reject) => { - const [salt, storedHash] = storedState.split(':'); + const [salt, storedHash] = storedState.split(":"); crypto.scrypt(state, salt, 16, (err, derivedKey) => { if (err) reject(err); - resolve(storedHash === derivedKey.toString('hex')); + resolve(storedHash === derivedKey.toString("hex")); }); }); if (!isValid) { throw new HTTPException(400, { - message: 'Invalid state', + message: "Invalid state", }); } deleteCookie( this.c, - `${this.c.get('core').authorization.cookieName}--state-sso`, + `${this.c.get("core").authorization.cookieName}--state-sso`, ); } } diff --git a/packages/vitnode/src/api/models/user.ts b/packages/vitnode/src/api/models/user.ts index 06b59190b..e2b8608ff 100644 --- a/packages/vitnode/src/api/models/user.ts +++ b/packages/vitnode/src/api/models/user.ts @@ -1,6 +1,6 @@ -import { getUserById } from './user/get-user-by-id'; -import { signInWithPassword } from './user/sign-in-with-passwords'; -import { signUp } from './user/sign-up'; +import { getUserById } from "./user/get-user-by-id"; +import { signInWithPassword } from "./user/sign-in-with-passwords"; +import { signUp } from "./user/sign-up"; export class UserModel { getUserById = getUserById; diff --git a/packages/vitnode/src/api/models/user/get-user-by-id.ts b/packages/vitnode/src/api/models/user/get-user-by-id.ts index 2f9e37b4d..457a0cef1 100644 --- a/packages/vitnode/src/api/models/user/get-user-by-id.ts +++ b/packages/vitnode/src/api/models/user/get-user-by-id.ts @@ -1,12 +1,11 @@ -import type { Context } from 'hono'; +import { eq } from "drizzle-orm"; +import type { Context } from "hono"; -import { eq } from 'drizzle-orm'; - -import { core_users } from '@/database/users'; +import { core_users } from "@/database/users"; export const getUserById = async ({ id, c }: { c: Context; id: number }) => { const [user] = await c - .get('db') + .get("db") .select({ id: core_users.id, email: core_users.email, diff --git a/packages/vitnode/src/api/models/user/sign-in-with-passwords.ts b/packages/vitnode/src/api/models/user/sign-in-with-passwords.ts index 531598603..a0d3fb549 100644 --- a/packages/vitnode/src/api/models/user/sign-in-with-passwords.ts +++ b/packages/vitnode/src/api/models/user/sign-in-with-passwords.ts @@ -1,11 +1,10 @@ -import type { Context } from 'hono'; +import { eq } from "drizzle-orm"; +import type { Context } from "hono"; +import { HTTPException } from "hono/http-exception"; -import { eq } from 'drizzle-orm'; -import { HTTPException } from 'hono/http-exception'; +import { core_users } from "@/database/users"; -import { core_users } from '@/database/users'; - -import { PasswordModel } from '../password'; +import { PasswordModel } from "../password"; export const signInWithPassword = async ({ email, @@ -17,7 +16,7 @@ export const signInWithPassword = async ({ password: string; }) => { const [user] = await c - .get('db') + .get("db") .select({ id: core_users.id, email: core_users.email, diff --git a/packages/vitnode/src/api/models/user/sign-up.ts b/packages/vitnode/src/api/models/user/sign-up.ts index 47079e125..a67a0eb95 100644 --- a/packages/vitnode/src/api/models/user/sign-up.ts +++ b/packages/vitnode/src/api/models/user/sign-up.ts @@ -1,12 +1,11 @@ -import type { Context } from 'hono'; +import { and, count, eq, or } from "drizzle-orm"; +import type { Context } from "hono"; +import { HTTPException } from "hono/http-exception"; -import { and, count, eq, or } from 'drizzle-orm'; -import { HTTPException } from 'hono/http-exception'; - -import { generateAvatarColor } from '@/api/modules/users/avatar-color'; -import { core_roles } from '@/database/roles'; -import { core_users } from '@/database/users'; -import { removeSpecialCharacters } from '@/lib/special-characters'; +import { generateAvatarColor } from "@/api/modules/users/avatar-color"; +import { core_roles } from "@/database/roles"; +import { core_users } from "@/database/users"; +import { removeSpecialCharacters } from "@/lib/special-characters"; const getDefaultData = async ( c: Context, @@ -15,14 +14,14 @@ const getDefaultData = async ( roleId: number; }> => { const [countUsers] = await c - .get('db') + .get("db") .select({ count: count() }) .from(core_users); // If no users, return root group if (countUsers.count === 0) { const [defaultRole] = await c - .get('db') + .get("db") .select({ id: core_roles.id, }) @@ -32,7 +31,7 @@ const getDefaultData = async ( if (!defaultRole) { throw new HTTPException(400, { - message: 'Default group not found.', + message: "Default group not found.", }); } @@ -43,7 +42,7 @@ const getDefaultData = async ( } const [defaultRole] = await c - .get('db') + .get("db") .select({ id: core_roles.id, }) @@ -53,13 +52,13 @@ const getDefaultData = async ( if (!defaultRole) { throw new HTTPException(400, { - message: 'Default role not found.', + message: "Default role not found.", }); } return { roleId: defaultRole.id, - emailVerified: !c.get('core').email?.adapter, + emailVerified: !c.get("core").email?.adapter, }; }; @@ -79,7 +78,7 @@ export const signUp = async ( ) => { const convertToNameSEO = removeSpecialCharacters(name); const checkIfUserExist = await c - .get('db') + .get("db") .select({ email: core_users.email, name_code: core_users.nameCode, @@ -95,7 +94,7 @@ export const signUp = async ( const findEmail = checkIfUserExist.find(user => user.email === email); if (findEmail) { throw new HTTPException(409, { - message: 'Email already exists', + message: "Email already exists", }); } const findName = checkIfUserExist.find( @@ -103,13 +102,13 @@ export const signUp = async ( ); if (findName) { throw new HTTPException(409, { - message: 'Name already exists', + message: "Name already exists", }); } const { roleId, emailVerified } = await getDefaultData(c); const [data] = await c - .get('db') + .get("db") .insert(core_users) .values({ email, @@ -121,13 +120,12 @@ export const signUp = async ( avatarColor: generateAvatarColor(name), roleId, emailVerified, - ipAddress: c.get('ipAddress'), + ipAddress: c.get("ipAddress"), // TODO: Handle language // language: await this.getLanguage(req), }) .returning(); - // eslint-disable-next-line @typescript-eslint/no-unused-vars const { password: _, ...user } = data; return user; diff --git a/packages/vitnode/src/api/modules/admin/admin.module.ts b/packages/vitnode/src/api/modules/admin/admin.module.ts index 81cc2e8df..1593e9822 100644 --- a/packages/vitnode/src/api/modules/admin/admin.module.ts +++ b/packages/vitnode/src/api/modules/admin/admin.module.ts @@ -1,13 +1,13 @@ -import { buildModule } from '@/api/lib/module'; -import { CONFIG_PLUGIN } from '@/config'; +import { buildModule } from "@/api/lib/module"; +import { CONFIG_PLUGIN } from "@/config"; -import { debugAdminModule } from './debug/debug.admin.module'; -import { sessionAdminRoute } from './routes/session.route'; -import { usersAdminModule } from './users/users.admin.module'; +import { debugAdminModule } from "./debug/debug.admin.module"; +import { sessionAdminRoute } from "./routes/session.route"; +import { usersAdminModule } from "./users/users.admin.module"; export const adminModule = buildModule({ ...CONFIG_PLUGIN, - name: 'admin', + name: "admin", routes: [sessionAdminRoute], modules: [usersAdminModule, debugAdminModule], }); diff --git a/packages/vitnode/src/api/modules/admin/debug/debug.admin.module.ts b/packages/vitnode/src/api/modules/admin/debug/debug.admin.module.ts index 316227e1c..8df6dbfdb 100644 --- a/packages/vitnode/src/api/modules/admin/debug/debug.admin.module.ts +++ b/packages/vitnode/src/api/modules/admin/debug/debug.admin.module.ts @@ -1,9 +1,9 @@ -import { CONFIG_PLUGIN } from '../../../../config'; -import { buildModule } from '../../../lib/module'; -import { logsDebugAdminRoute } from './routes/logs.route'; +import { CONFIG_PLUGIN } from "../../../../config"; +import { buildModule } from "../../../lib/module"; +import { logsDebugAdminRoute } from "./routes/logs.route"; export const debugAdminModule = buildModule({ ...CONFIG_PLUGIN, - name: 'debug', + name: "debug", routes: [logsDebugAdminRoute], }); diff --git a/packages/vitnode/src/api/modules/admin/debug/routes/logs.route.ts b/packages/vitnode/src/api/modules/admin/debug/routes/logs.route.ts index eb3048106..a97843e18 100644 --- a/packages/vitnode/src/api/modules/admin/debug/routes/logs.route.ts +++ b/packages/vitnode/src/api/modules/admin/debug/routes/logs.route.ts @@ -1,39 +1,39 @@ -import { eq } from 'drizzle-orm'; -import { z } from 'zod'; +import { eq } from "drizzle-orm"; +import { z } from "zod"; -import { CONFIG_PLUGIN } from '@/config'; -import { core_logs } from '@/database/logs'; +import { CONFIG_PLUGIN } from "@/config"; +import { core_logs } from "@/database/logs"; -import { core_users } from '../../../../../database/users'; -import { buildRoute } from '../../../../lib/route'; +import { core_users } from "../../../../../database/users"; +import { buildRoute } from "../../../../lib/route"; import { withPagination, zodPaginationPageInfo, zodPaginationQuery, -} from '../../../../lib/with-pagination'; +} from "../../../../lib/with-pagination"; export const logsDebugAdminRoute = buildRoute({ ...CONFIG_PLUGIN, route: { - method: 'get', - description: 'Get Admin Debug Logs', - path: '/logs', + method: "get", + description: "Get Admin Debug Logs", + path: "/logs", request: { query: zodPaginationQuery.extend({ - order: z.enum(['asc', 'desc']).optional(), - orderBy: z.enum(['type', 'createdAt', 'pluginId']).optional(), + order: z.enum(["asc", "desc"]).optional(), + orderBy: z.enum(["type", "createdAt", "pluginId"]).optional(), }), }, responses: { 200: { content: { - 'application/json': { + "application/json": { schema: z.object({ edges: z.array( z.object({ id: z.number(), pluginId: z.string(), - type: z.enum(['warn', 'error', 'debug']), + type: z.enum(["warn", "error", "debug"]), content: z.string(), createdAt: z.date(), ipAddress: z.string(), @@ -54,12 +54,12 @@ export const logsDebugAdminRoute = buildRoute({ }), }, }, - description: 'List of users', + description: "List of users", }, }, }, handler: async c => { - const query = c.req.valid('query'); + const query = c.req.valid("query"); const data = await withPagination({ params: { query, @@ -67,7 +67,7 @@ export const logsDebugAdminRoute = buildRoute({ primaryCursor: core_logs.id, query: async ({ limit, where, orderBy }) => await c - .get('db') + .get("db") .select({ id: core_logs.id, pluginId: core_logs.pluginId, @@ -93,7 +93,7 @@ export const logsDebugAdminRoute = buildRoute({ table: core_logs, orderBy: { column: query.orderBy ? core_logs[query.orderBy] : core_logs.createdAt, - order: query.order ?? 'desc', + order: query.order ?? "desc", }, c, }); diff --git a/packages/vitnode/src/api/modules/admin/routes/session.route.ts b/packages/vitnode/src/api/modules/admin/routes/session.route.ts index 4de23c1eb..a6d861f26 100644 --- a/packages/vitnode/src/api/modules/admin/routes/session.route.ts +++ b/packages/vitnode/src/api/modules/admin/routes/session.route.ts @@ -1,19 +1,19 @@ -import { HTTPException } from 'hono/http-exception'; -import { z } from 'zod'; +import { HTTPException } from "hono/http-exception"; +import { z } from "zod"; -import { buildRoute } from '@/api/lib/route'; -import { CONFIG_PLUGIN } from '@/config'; +import { buildRoute } from "@/api/lib/route"; +import { CONFIG_PLUGIN } from "@/config"; export const sessionAdminRoute = buildRoute({ ...CONFIG_PLUGIN, route: { - method: 'get', - description: 'Verify admin session', - path: '/session', + method: "get", + description: "Verify admin session", + path: "/session", responses: { 200: { content: { - 'application/json': { + "application/json": { schema: z.object({ user: z.object({ id: z.number(), @@ -31,15 +31,15 @@ export const sessionAdminRoute = buildRoute({ }), }, }, - description: 'User', + description: "User", }, 403: { - description: 'Access Denied', + description: "Access Denied", }, }, }, handler: c => { - const user = c.get('admin')?.user; + const user = c.get("admin")?.user; if (!user) throw new HTTPException(403); return c.json({ diff --git a/packages/vitnode/src/api/modules/admin/users/routes/list.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/list.route.ts index c509de84c..64af77bce 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/list.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/list.route.ts @@ -1,30 +1,30 @@ -import { z } from '@hono/zod-openapi'; +import { z } from "@hono/zod-openapi"; -import { buildRoute } from '@/api/lib/route'; +import { buildRoute } from "@/api/lib/route"; import { withPagination, zodPaginationPageInfo, zodPaginationQuery, -} from '@/api/lib/with-pagination'; -import { CONFIG_PLUGIN } from '@/config'; -import { core_users } from '@/database/users'; +} from "@/api/lib/with-pagination"; +import { CONFIG_PLUGIN } from "@/config"; +import { core_users } from "@/database/users"; export const listUsersAdminRoute = buildRoute({ ...CONFIG_PLUGIN, route: { - method: 'get', - description: 'Get list of all users', - path: '/list', + method: "get", + description: "Get list of all users", + path: "/list", request: { query: zodPaginationQuery.extend({ - order: z.enum(['asc', 'desc']).optional(), - orderBy: z.enum(['name', 'createdAt']).optional(), + order: z.enum(["asc", "desc"]).optional(), + orderBy: z.enum(["name", "createdAt"]).optional(), }), }, responses: { 200: { content: { - 'application/json': { + "application/json": { schema: z.object({ edges: z.array( z.object({ @@ -45,12 +45,12 @@ export const listUsersAdminRoute = buildRoute({ }), }, }, - description: 'List of users', + description: "List of users", }, }, }, handler: async c => { - const query = c.req.valid('query'); + const query = c.req.valid("query"); const data = await withPagination({ params: { query, @@ -58,7 +58,7 @@ export const listUsersAdminRoute = buildRoute({ primaryCursor: core_users.id, query: async ({ limit, where, orderBy }) => await c - .get('db') + .get("db") .select({ id: core_users.id, name: core_users.name, @@ -81,7 +81,7 @@ export const listUsersAdminRoute = buildRoute({ column: query.orderBy ? core_users[query.orderBy] : core_users.createdAt, - order: query.order ?? 'desc', + order: query.order ?? "desc", }, c, }); diff --git a/packages/vitnode/src/api/modules/admin/users/routes/users.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/users.route.ts index d88239110..43472246a 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/users.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/users.route.ts @@ -1,30 +1,30 @@ -import { z } from '@hono/zod-openapi'; +import { z } from "@hono/zod-openapi"; -import { buildRoute } from '@/api/lib/route'; +import { buildRoute } from "@/api/lib/route"; import { withPagination, zodPaginationPageInfo, zodPaginationQuery, -} from '@/api/lib/with-pagination'; -import { CONFIG_PLUGIN } from '@/config'; -import { core_users } from '@/database/users'; +} from "@/api/lib/with-pagination"; +import { CONFIG_PLUGIN } from "@/config"; +import { core_users } from "@/database/users"; export const usersAdminRoute = buildRoute({ ...CONFIG_PLUGIN, route: { - method: 'get', - description: 'Get list of all users (Admin only)', - path: '/list', + method: "get", + description: "Get list of all users (Admin only)", + path: "/list", request: { query: zodPaginationQuery.extend({ - order: z.enum(['asc', 'desc']).optional(), - orderBy: z.enum(['id', 'name', 'email', 'createdAt']).optional(), + order: z.enum(["asc", "desc"]).optional(), + orderBy: z.enum(["id", "name", "email", "createdAt"]).optional(), }), }, responses: { 200: { content: { - 'application/json': { + "application/json": { schema: z.object({ edges: z.array( z.object({ @@ -45,15 +45,15 @@ export const usersAdminRoute = buildRoute({ }), }, }, - description: 'List of users', + description: "List of users", }, 403: { - description: 'Access Denied', + description: "Access Denied", }, }, }, handler: async c => { - const query = c.req.valid('query'); + const query = c.req.valid("query"); const data = await withPagination({ params: { query, @@ -61,7 +61,7 @@ export const usersAdminRoute = buildRoute({ primaryCursor: core_users.id, query: async ({ limit, where, orderBy }) => await c - .get('db') + .get("db") .select({ id: core_users.id, name: core_users.name, @@ -84,7 +84,7 @@ export const usersAdminRoute = buildRoute({ column: query.orderBy ? core_users[query.orderBy] : core_users.createdAt, - order: query.order ?? 'desc', + order: query.order ?? "desc", }, c, }); diff --git a/packages/vitnode/src/api/modules/admin/users/users.admin.module.ts b/packages/vitnode/src/api/modules/admin/users/users.admin.module.ts index 6584983a7..3b31ce8db 100644 --- a/packages/vitnode/src/api/modules/admin/users/users.admin.module.ts +++ b/packages/vitnode/src/api/modules/admin/users/users.admin.module.ts @@ -1,10 +1,10 @@ -import { buildModule } from '@/api/lib/module'; -import { CONFIG_PLUGIN } from '@/config'; +import { buildModule } from "@/api/lib/module"; +import { CONFIG_PLUGIN } from "@/config"; -import { listUsersAdminRoute } from './routes/list.route'; +import { listUsersAdminRoute } from "./routes/list.route"; export const usersAdminModule = buildModule({ ...CONFIG_PLUGIN, - name: 'users', + name: "users", routes: [listUsersAdminRoute], }); diff --git a/packages/vitnode/src/api/modules/middleware/middleware.module.ts b/packages/vitnode/src/api/modules/middleware/middleware.module.ts index 9b229b429..0158f4472 100644 --- a/packages/vitnode/src/api/modules/middleware/middleware.module.ts +++ b/packages/vitnode/src/api/modules/middleware/middleware.module.ts @@ -1,10 +1,10 @@ -import { buildModule } from '@/api/lib/module'; -import { CONFIG_PLUGIN } from '@/config'; +import { buildModule } from "@/api/lib/module"; +import { CONFIG_PLUGIN } from "@/config"; -import { routeMiddleware } from './route'; +import { routeMiddleware } from "./route"; export const middlewareModule = buildModule({ ...CONFIG_PLUGIN, - name: 'middleware', + name: "middleware", routes: [routeMiddleware], }); diff --git a/packages/vitnode/src/api/modules/middleware/route.ts b/packages/vitnode/src/api/modules/middleware/route.ts index 2f25bd22b..202054dd2 100644 --- a/packages/vitnode/src/api/modules/middleware/route.ts +++ b/packages/vitnode/src/api/modules/middleware/route.ts @@ -1,7 +1,7 @@ -import { z } from 'zod'; +import { z } from "zod"; -import { buildRoute } from '@/api/lib/route'; -import { CONFIG_PLUGIN } from '@/config'; +import { buildRoute } from "@/api/lib/route"; +import { CONFIG_PLUGIN } from "@/config"; export const routeMiddlewareSchema = z.object({ sso: z.array(z.object({ id: z.string(), name: z.string() })), @@ -9,7 +9,7 @@ export const routeMiddlewareSchema = z.object({ captcha: z .object({ siteKey: z.string(), - type: z.enum(['cloudflare_turnstile', 'recaptcha_v3']), + type: z.enum(["cloudflare_turnstile", "recaptcha_v3"]), }) .optional(), }); @@ -17,31 +17,31 @@ export const routeMiddlewareSchema = z.object({ export const routeMiddleware = buildRoute({ ...CONFIG_PLUGIN, route: { - path: '/', - method: 'get', - description: 'Middleware route with user authentication', + path: "/", + method: "get", + description: "Middleware route with user authentication", responses: { 200: { content: { - 'application/json': { + "application/json": { schema: routeMiddlewareSchema, }, }, - description: 'Middleware route', + description: "Middleware route", }, }, }, handler: c => { - const sso = c.get('core').authorization.ssoAdapters; + const sso = c.get("core").authorization.ssoAdapters; return c.json( { - isEmail: !!c.get('core').email?.adapter, + isEmail: !!c.get("core").email?.adapter, sso: sso.map(s => ({ id: s.id, name: s.name })), - captcha: c.get('core').captcha + captcha: c.get("core").captcha ? { - siteKey: c.get('core').captcha?.siteKey ?? '', - type: c.get('core').captcha?.type ?? 'cloudflare_turnstile', + siteKey: c.get("core").captcha?.siteKey ?? "", + type: c.get("core").captcha?.type ?? "cloudflare_turnstile", } : undefined, }, diff --git a/packages/vitnode/src/api/modules/users/avatar-color.ts b/packages/vitnode/src/api/modules/users/avatar-color.ts index 0c715993d..99b355957 100644 --- a/packages/vitnode/src/api/modules/users/avatar-color.ts +++ b/packages/vitnode/src/api/modules/users/avatar-color.ts @@ -1,4 +1,4 @@ -import { convertColor } from '@/lib/colors'; +import { convertColor } from "@/lib/colors"; const getHashOfString = (str: string) => { let hash = 0; diff --git a/packages/vitnode/src/api/modules/users/routes/change-password.route.ts b/packages/vitnode/src/api/modules/users/routes/change-password.route.ts index a42d0a0d1..532cd0747 100644 --- a/packages/vitnode/src/api/modules/users/routes/change-password.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/change-password.route.ts @@ -1,31 +1,31 @@ -import { and, eq, gt } from 'drizzle-orm'; -import { HTTPException } from 'hono/http-exception'; -import { z } from 'zod'; +import { and, eq, gt } from "drizzle-orm"; +import { HTTPException } from "hono/http-exception"; +import { z } from "zod"; -import { buildRoute } from '@/api/lib/route'; -import { PasswordModel } from '@/api/models/password'; -import { CONFIG_PLUGIN } from '@/config'; -import { core_users, core_users_forgot_password } from '@/database/users'; +import { buildRoute } from "@/api/lib/route"; +import { PasswordModel } from "@/api/models/password"; +import { CONFIG_PLUGIN } from "@/config"; +import { core_users, core_users_forgot_password } from "@/database/users"; export const zodChangePasswordSchema = z.object({ password: z.string().min(8).openapi({ - example: 'Test123!', + example: "Test123!", }), userId: z.number().openapi({ example: 123456 }), - token: z.string().openapi({ example: 'abcdefg12345' }), + token: z.string().openapi({ example: "abcdefg12345" }), }); export const changePasswordRoute = buildRoute({ ...CONFIG_PLUGIN, route: { - method: 'post', - description: 'Change user password', - path: '/change-password', + method: "post", + description: "Change user password", + path: "/change-password", request: { body: { required: true, content: { - 'application/json': { + "application/json": { schema: zodChangePasswordSchema, }, }, @@ -33,15 +33,15 @@ export const changePasswordRoute = buildRoute({ }, responses: { 201: { - description: 'Password changed', + description: "Password changed", }, }, }, handler: async c => { - const { password, userId, token } = c.req.valid('json'); + const { password, userId, token } = c.req.valid("json"); const [user] = await c - .get('db') + .get("db") .select() .from(core_users_forgot_password) .where( @@ -54,22 +54,22 @@ export const changePasswordRoute = buildRoute({ .limit(1); if (!user) { - throw new HTTPException(400, { message: 'Invalid token' }); + throw new HTTPException(400, { message: "Invalid token" }); } const hashPassword = await new PasswordModel().encryptPassword(password); await Promise.all([ c - .get('db') + .get("db") .update(core_users) .set({ password: hashPassword }) .where(eq(core_users.id, userId)), c - .get('db') + .get("db") .delete(core_users_forgot_password) .where(eq(core_users_forgot_password.id, user.id)), ]); - return c.text('Password changed', 201); + return c.text("Password changed", 201); }, }); diff --git a/packages/vitnode/src/api/modules/users/routes/reset-passowrd.route.ts b/packages/vitnode/src/api/modules/users/routes/reset-passowrd.route.ts index 001a10169..108b4f7ce 100644 --- a/packages/vitnode/src/api/modules/users/routes/reset-passowrd.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/reset-passowrd.route.ts @@ -1,29 +1,29 @@ -import { eq } from 'drizzle-orm'; -import { createTranslator } from 'use-intl'; -import { z } from 'zod'; +import { eq } from "drizzle-orm"; +import { createTranslator } from "use-intl"; +import { z } from "zod"; -import { buildRoute } from '@/api/lib/route'; -import { ForgotPasswordTokenModel } from '@/api/models/password'; -import { CONFIG_PLUGIN } from '@/config'; -import { core_users, core_users_forgot_password } from '@/database/users'; -import ResetPasswordEmailTemplate from '@/emails/reset-password'; -import { CONFIG } from '@/lib/config'; +import { buildRoute } from "@/api/lib/route"; +import { ForgotPasswordTokenModel } from "@/api/models/password"; +import { CONFIG_PLUGIN } from "@/config"; +import { core_users, core_users_forgot_password } from "@/database/users"; +import ResetPasswordEmailTemplate from "@/emails/reset-password"; +import { CONFIG } from "@/lib/config"; export const resetPasswordRoute = buildRoute({ ...CONFIG_PLUGIN, route: { - method: 'post', - description: 'Request a password reset', - path: '/reset-password', + method: "post", + description: "Request a password reset", + path: "/reset-password", withCaptcha: true, request: { body: { required: true, content: { - 'application/json': { + "application/json": { schema: z.object({ email: z.email().toLowerCase().openapi({ - example: 'test@test.com', + example: "test@test.com", }), }), }, @@ -32,15 +32,15 @@ export const resetPasswordRoute = buildRoute({ }, responses: { 201: { - description: 'Email sent', + description: "Email sent", }, }, }, handler: async c => { - const RESPONSE_TEXT = c.text('Email sent', 201); - const { email } = c.req.valid('json'); + const RESPONSE_TEXT = c.text("Email sent", 201); + const { email } = c.req.valid("json"); const [findUser] = await c - .get('db') + .get("db") .select({ email: core_users.email, id: core_users.id, @@ -57,7 +57,7 @@ export const resetPasswordRoute = buildRoute({ const hashToken = new ForgotPasswordTokenModel().generateResetToken(); const [findLastRecord] = await c - .get('db') + .get("db") .select() .from(core_users_forgot_password) .where(eq(core_users_forgot_password.userId, findUser.id)) @@ -72,22 +72,22 @@ export const resetPasswordRoute = buildRoute({ if (findLastRecord) { await c - .get('db') + .get("db") .update(core_users_forgot_password) .set({ createdAt: new Date(), expiresAt: EXPIRES_AT, token: hashToken, - ipAddress: c.get('ipAddress'), + ipAddress: c.get("ipAddress"), }) .where(eq(core_users_forgot_password.id, findLastRecord.id)); } else { await c - .get('db') + .get("db") .insert(core_users_forgot_password) .values({ token: hashToken, - ipAddress: c.get('ipAddress'), + ipAddress: c.get("ipAddress"), userId: findUser.id, expiresAt: EXPIRES_AT, }); @@ -99,7 +99,7 @@ export const resetPasswordRoute = buildRoute({ CONFIG.web.href, ); - await c.get('email').send({ + await c.get("email").send({ user: { id: findUser.id, email: findUser.email, @@ -110,12 +110,12 @@ export const resetPasswordRoute = buildRoute({ ...props, resetUrl: resetUrlNative.href, expiryDate: EXPIRES_AT, - userIpAddress: c.get('ipAddress'), + userIpAddress: c.get("ipAddress"), }), subject: ({ i18n }) => { const t = createTranslator(i18n); - return t('core.auth.reset_password.email.subject'); + return t("core.auth.reset_password.email.subject"); }, }); diff --git a/packages/vitnode/src/api/modules/users/routes/session.route.ts b/packages/vitnode/src/api/modules/users/routes/session.route.ts index e48b5bbe3..e9d0066e2 100644 --- a/packages/vitnode/src/api/modules/users/routes/session.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/session.route.ts @@ -1,19 +1,19 @@ -import { z } from 'zod'; +import { z } from "zod"; -import { buildRoute } from '@/api/lib/route'; -import { SessionAdminModel } from '@/api/models/session-admin'; -import { CONFIG_PLUGIN } from '@/config'; +import { buildRoute } from "@/api/lib/route"; +import { SessionAdminModel } from "@/api/models/session-admin"; +import { CONFIG_PLUGIN } from "@/config"; export const sessionRoute = buildRoute({ ...CONFIG_PLUGIN, route: { - method: 'get', - description: 'Verify session', - path: '/session', + method: "get", + description: "Verify session", + path: "/session", responses: { 200: { content: { - 'application/json': { + "application/json": { schema: z.object({ user: z .object({ @@ -33,12 +33,12 @@ export const sessionRoute = buildRoute({ }), }, }, - description: 'User', + description: "User", }, }, }, handler: async c => { - const user = c.get('user'); + const user = c.get("user"); const admin = new SessionAdminModel(c); return c.json({ diff --git a/packages/vitnode/src/api/modules/users/routes/sign-in.route.ts b/packages/vitnode/src/api/modules/users/routes/sign-in.route.ts index 8fad0b330..6d3c2b681 100644 --- a/packages/vitnode/src/api/modules/users/routes/sign-in.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/sign-in.route.ts @@ -1,17 +1,17 @@ -import { z } from 'zod'; +import { z } from "zod"; -import { buildRoute } from '@/api/lib/route'; -import { SessionModel } from '@/api/models/session'; -import { SessionAdminModel } from '@/api/models/session-admin'; -import { UserModel } from '@/api/models/user'; -import { CONFIG_PLUGIN } from '@/config'; +import { buildRoute } from "@/api/lib/route"; +import { SessionModel } from "@/api/models/session"; +import { SessionAdminModel } from "@/api/models/session-admin"; +import { UserModel } from "@/api/models/user"; +import { CONFIG_PLUGIN } from "@/config"; export const zodSignInSchema = z.object({ email: z.email().toLowerCase().openapi({ - example: 'test@test.com', + example: "test@test.com", }), password: z.string().openapi({ - example: 'Test123!', + example: "Test123!", }), isAdmin: z.boolean().optional().openapi({ example: false, @@ -21,14 +21,14 @@ export const zodSignInSchema = z.object({ export const signInRoute = buildRoute({ ...CONFIG_PLUGIN, route: { - method: 'post', - description: 'Sign in with email and password', - path: '/sign_in', + method: "post", + description: "Sign in with email and password", + path: "/sign_in", request: { body: { required: true, content: { - 'application/json': { + "application/json": { schema: zodSignInSchema, }, }, @@ -36,23 +36,23 @@ export const signInRoute = buildRoute({ }, responses: { 403: { - description: 'Access Denied', + description: "Access Denied", }, 201: { content: { - 'application/json': { + "application/json": { schema: z.object({ id: z.number(), token: z.string(), }), }, }, - description: 'User signed in', + description: "User signed in", }, }, }, handler: async c => { - const { password, isAdmin, email } = c.req.valid('json'); + const { password, isAdmin, email } = c.req.valid("json"); const data = await new UserModel().signInWithPassword({ password, email, diff --git a/packages/vitnode/src/api/modules/users/routes/sign-out.route.ts b/packages/vitnode/src/api/modules/users/routes/sign-out.route.ts index f26e76912..ef2b46ce4 100644 --- a/packages/vitnode/src/api/modules/users/routes/sign-out.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/sign-out.route.ts @@ -1,20 +1,20 @@ -import { z } from '@hono/zod-openapi'; +import { z } from "@hono/zod-openapi"; -import { buildRoute } from '@/api/lib/route'; -import { SessionModel } from '@/api/models/session'; -import { SessionAdminModel } from '@/api/models/session-admin'; -import { CONFIG_PLUGIN } from '@/config'; +import { buildRoute } from "@/api/lib/route"; +import { SessionModel } from "@/api/models/session"; +import { SessionAdminModel } from "@/api/models/session-admin"; +import { CONFIG_PLUGIN } from "@/config"; export const signOutRoute = buildRoute({ ...CONFIG_PLUGIN, route: { - method: 'delete', - description: 'Sign out the current admin', - path: '/sign_out', + method: "delete", + description: "Sign out the current admin", + path: "/sign_out", request: { body: { content: { - 'application/json': { + "application/json": { schema: z.object({ isAdmin: z.boolean().optional().openapi({ example: false, @@ -26,15 +26,15 @@ export const signOutRoute = buildRoute({ }, responses: { 200: { - description: 'User signed out', + description: "User signed out", }, 403: { - description: 'Access Denied', + description: "Access Denied", }, }, }, handler: async c => { - const { isAdmin } = c.req.valid('json'); + const { isAdmin } = c.req.valid("json"); if (isAdmin) { await new SessionAdminModel(c).deleteSession(); diff --git a/packages/vitnode/src/api/modules/users/routes/sign-up.route.ts b/packages/vitnode/src/api/modules/users/routes/sign-up.route.ts index 57c7a1f12..a1b2f9f7e 100644 --- a/packages/vitnode/src/api/modules/users/routes/sign-up.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/sign-up.route.ts @@ -1,27 +1,27 @@ -import { z } from 'zod'; +import { z } from "zod"; -import { buildRoute } from '@/api/lib/route'; -import { PasswordModel } from '@/api/models/password'; -import { UserModel } from '@/api/models/user'; -import { CONFIG_PLUGIN } from '@/config'; +import { buildRoute } from "@/api/lib/route"; +import { PasswordModel } from "@/api/models/password"; +import { UserModel } from "@/api/models/user"; +import { CONFIG_PLUGIN } from "@/config"; -import { SessionModel } from '../../../models/session'; +import { SessionModel } from "../../../models/session"; const nameRegex = /^(?!.* {2})[\p{L}\p{N}._@ -]*$/u; export const zodSignUpSchema = z.object({ email: z.email().toLowerCase().openapi({ - example: 'test@test.com', + example: "test@test.com", }), name: z .string() - .openapi({ example: 'test' }) + .openapi({ example: "test" }) .min(3) .refine(val => nameRegex.test(val), { - message: 'Invalid name', + message: "Invalid name", }), password: z.string().min(8).openapi({ - example: 'Test123!', + example: "Test123!", }), newsletter: z.boolean().default(false).optional(), }); @@ -29,15 +29,15 @@ export const zodSignUpSchema = z.object({ export const signUpRoute = buildRoute({ ...CONFIG_PLUGIN, route: { - method: 'post', - description: 'Create a new user', - path: '/sign_up', + method: "post", + description: "Create a new user", + path: "/sign_up", withCaptcha: true, request: { body: { required: true, content: { - 'application/json': { + "application/json": { schema: zodSignUpSchema, }, }, @@ -46,7 +46,7 @@ export const signUpRoute = buildRoute({ responses: { 201: { content: { - 'application/json': { + "application/json": { schema: z.object({ id: z.number(), emailVerified: z.boolean(), @@ -54,22 +54,22 @@ export const signUpRoute = buildRoute({ }), }, }, - description: 'User created', + description: "User created", }, 400: { - description: 'Bad Request', + description: "Bad Request", }, 409: { - description: 'Email or name already exists', + description: "Email or name already exists", }, }, }, handler: async c => { const hashedPassword = await new PasswordModel().encryptPassword( - c.req.valid('json').password, + c.req.valid("json").password, ); const data = await new UserModel().signUp( - { ...c.req.valid('json'), hashedPassword }, + { ...c.req.valid("json"), hashedPassword }, c, ); diff --git a/packages/vitnode/src/api/modules/users/routes/test.route.ts b/packages/vitnode/src/api/modules/users/routes/test.route.ts index 6bc597294..489c74bb3 100644 --- a/packages/vitnode/src/api/modules/users/routes/test.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/test.route.ts @@ -1,36 +1,36 @@ -import { z } from 'zod'; +import { z } from "zod"; -import { buildRoute } from '@/api/lib/route'; -import { CONFIG_PLUGIN } from '@/config'; +import { buildRoute } from "@/api/lib/route"; +import { CONFIG_PLUGIN } from "@/config"; export const testRoute = buildRoute({ ...CONFIG_PLUGIN, route: { - method: 'post', - description: 'Test route', - path: '/test', + method: "post", + description: "Test route", + path: "/test", responses: { 200: { content: { - 'text/plain': { + "text/plain": { schema: z.string(), }, }, - description: 'User', + description: "User", }, 201: { content: { - 'text/plain': { + "text/plain": { schema: z.string(), }, }, - description: 'User', + description: "User", }, }, }, handler: async c => { - await c.get('log').warn('This is a test warn log'); + await c.get("log").warn("This is a test warn log"); - return c.text('test'); + return c.text("test"); }, }); diff --git a/packages/vitnode/src/api/modules/users/sso/routes/callback.route.ts b/packages/vitnode/src/api/modules/users/sso/routes/callback.route.ts index de9d91fff..ab928594f 100644 --- a/packages/vitnode/src/api/modules/users/sso/routes/callback.route.ts +++ b/packages/vitnode/src/api/modules/users/sso/routes/callback.route.ts @@ -1,16 +1,16 @@ -import { z } from 'zod'; +import { z } from "zod"; -import { buildRoute } from '@/api/lib/route'; -import { SessionModel } from '@/api/models/session'; -import { SSOModel } from '@/api/models/sso'; -import { CONFIG_PLUGIN } from '@/config'; +import { buildRoute } from "@/api/lib/route"; +import { SessionModel } from "@/api/models/session"; +import { SSOModel } from "@/api/models/sso"; +import { CONFIG_PLUGIN } from "@/config"; export const callbackRoute = buildRoute({ ...CONFIG_PLUGIN, route: { - method: 'get', - description: 'SSO Callback', - path: '/{providerId}/callback', + method: "get", + description: "SSO Callback", + path: "/{providerId}/callback", request: { params: z.object({ providerId: z.string(), @@ -23,23 +23,23 @@ export const callbackRoute = buildRoute({ responses: { 200: { content: { - 'application/json': { + "application/json": { schema: z.object({ id: z.number(), token: z.string(), }), }, }, - description: 'URL', + description: "URL", }, 409: { - description: 'Email already exists', + description: "Email already exists", }, }, }, handler: async c => { - const { providerId } = c.req.valid('param'); - const { code, state } = c.req.valid('query'); + const { providerId } = c.req.valid("param"); + const { code, state } = c.req.valid("query"); const sso = await new SSOModel(c).callback({ providerId, code, state }); const { token } = await new SessionModel(c).createSessionByUserId( sso.userId, diff --git a/packages/vitnode/src/api/modules/users/sso/routes/create-url.route.ts b/packages/vitnode/src/api/modules/users/sso/routes/create-url.route.ts index 4c4ea3b67..32deb959a 100644 --- a/packages/vitnode/src/api/modules/users/sso/routes/create-url.route.ts +++ b/packages/vitnode/src/api/modules/users/sso/routes/create-url.route.ts @@ -1,15 +1,15 @@ -import { z } from 'zod'; +import { z } from "zod"; -import { buildRoute } from '@/api/lib/route'; -import { SSOModel } from '@/api/models/sso'; -import { CONFIG_PLUGIN } from '@/config'; +import { buildRoute } from "@/api/lib/route"; +import { SSOModel } from "@/api/models/sso"; +import { CONFIG_PLUGIN } from "@/config"; export const createUrlRoute = buildRoute({ ...CONFIG_PLUGIN, route: { - method: 'post', - description: 'Generate SSO URL', - path: '/{providerId}', + method: "post", + description: "Generate SSO URL", + path: "/{providerId}", request: { params: z.object({ providerId: z.string(), @@ -18,16 +18,16 @@ export const createUrlRoute = buildRoute({ responses: { 200: { content: { - 'application/json': { + "application/json": { schema: z.object({ url: z.string() }), }, }, - description: 'URL', + description: "URL", }, }, }, handler: async c => { - const { providerId } = c.req.valid('param'); + const { providerId } = c.req.valid("param"); const url = await new SSOModel(c).getUrl(providerId); return c.json({ diff --git a/packages/vitnode/src/api/modules/users/sso/sso.module.ts b/packages/vitnode/src/api/modules/users/sso/sso.module.ts index 0d4196ed7..dd9df7e54 100644 --- a/packages/vitnode/src/api/modules/users/sso/sso.module.ts +++ b/packages/vitnode/src/api/modules/users/sso/sso.module.ts @@ -1,11 +1,11 @@ -import { buildModule } from '@/api/lib/module'; -import { CONFIG_PLUGIN } from '@/config'; +import { buildModule } from "@/api/lib/module"; +import { CONFIG_PLUGIN } from "@/config"; -import { callbackRoute } from './routes/callback.route'; -import { createUrlRoute } from './routes/create-url.route'; +import { callbackRoute } from "./routes/callback.route"; +import { createUrlRoute } from "./routes/create-url.route"; export const ssoUserModule = buildModule({ ...CONFIG_PLUGIN, - name: 'sso', + name: "sso", routes: [callbackRoute, createUrlRoute], }); diff --git a/packages/vitnode/src/api/modules/users/users.module.ts b/packages/vitnode/src/api/modules/users/users.module.ts index c6f1293f9..b3e4e0223 100644 --- a/packages/vitnode/src/api/modules/users/users.module.ts +++ b/packages/vitnode/src/api/modules/users/users.module.ts @@ -1,18 +1,18 @@ -import { buildModule } from '@/api/lib/module'; -import { CONFIG_PLUGIN } from '@/config'; +import { buildModule } from "@/api/lib/module"; +import { CONFIG_PLUGIN } from "@/config"; -import { changePasswordRoute } from './routes/change-password.route'; -import { resetPasswordRoute } from './routes/reset-passowrd.route'; -import { sessionRoute } from './routes/session.route'; -import { signInRoute } from './routes/sign-in.route'; -import { signOutRoute } from './routes/sign-out.route'; -import { signUpRoute } from './routes/sign-up.route'; -import { testRoute } from './routes/test.route'; -import { ssoUserModule } from './sso/sso.module'; +import { changePasswordRoute } from "./routes/change-password.route"; +import { resetPasswordRoute } from "./routes/reset-passowrd.route"; +import { sessionRoute } from "./routes/session.route"; +import { signInRoute } from "./routes/sign-in.route"; +import { signOutRoute } from "./routes/sign-out.route"; +import { signUpRoute } from "./routes/sign-up.route"; +import { testRoute } from "./routes/test.route"; +import { ssoUserModule } from "./sso/sso.module"; export const usersModule = buildModule({ ...CONFIG_PLUGIN, - name: 'users', + name: "users", routes: [ sessionRoute, signInRoute, diff --git a/packages/vitnode/src/api/plugin.ts b/packages/vitnode/src/api/plugin.ts index 54e6bb0fd..70f15100a 100644 --- a/packages/vitnode/src/api/plugin.ts +++ b/packages/vitnode/src/api/plugin.ts @@ -1,9 +1,9 @@ -import { CONFIG_PLUGIN } from '@/config'; +import { CONFIG_PLUGIN } from "@/config"; -import { buildApiPlugin } from './lib/plugin'; -import { adminModule } from './modules/admin/admin.module'; -import { middlewareModule } from './modules/middleware/middleware.module'; -import { usersModule } from './modules/users/users.module'; +import { buildApiPlugin } from "./lib/plugin"; +import { adminModule } from "./modules/admin/admin.module"; +import { middlewareModule } from "./modules/middleware/middleware.module"; +import { usersModule } from "./modules/users/users.module"; export const newBuildPluginApiCore = buildApiPlugin({ ...CONFIG_PLUGIN, diff --git a/packages/vitnode/src/app/login/page.tsx b/packages/vitnode/src/app/login/page.tsx index 3493152a8..66b7cf78a 100644 --- a/packages/vitnode/src/app/login/page.tsx +++ b/packages/vitnode/src/app/login/page.tsx @@ -1,8 +1,8 @@ -import type { Metadata } from 'next/dist/types'; +import type { Metadata } from "next/dist/types"; -import { getTranslations } from 'next-intl/server'; +import { getTranslations } from "next-intl/server"; -import { SignInView } from '../../views/auth/sign-in/sign-in-view'; +import { SignInView } from "../../views/auth/sign-in/sign-in-view"; export const generateMetadata = async ({ params, @@ -10,10 +10,10 @@ export const generateMetadata = async ({ params: Promise<{ locale: string }>; }): Promise => { const { locale } = await params; - const t = await getTranslations({ locale, namespace: 'core.global' }); + const t = await getTranslations({ locale, namespace: "core.global" }); return { - title: t('login'), + title: t("login"), }; }; diff --git a/packages/vitnode/src/app/login/reset-password/page.tsx b/packages/vitnode/src/app/login/reset-password/page.tsx index e363ee2a5..e9f49711e 100644 --- a/packages/vitnode/src/app/login/reset-password/page.tsx +++ b/packages/vitnode/src/app/login/reset-password/page.tsx @@ -1,8 +1,8 @@ -import type { Metadata } from 'next/dist/types'; +import type { Metadata } from "next/dist/types"; -import { getTranslations } from 'next-intl/server'; +import { getTranslations } from "next-intl/server"; -import { PasswordResetView } from '@/views/auth/password-reset/password-reset-view'; +import { PasswordResetView } from "@/views/auth/password-reset/password-reset-view"; export const generateMetadata = async ({ params, @@ -12,11 +12,11 @@ export const generateMetadata = async ({ const { locale } = await params; const t = await getTranslations({ locale, - namespace: 'core.auth.reset_password', + namespace: "core.auth.reset_password", }); return { - title: t('title'), + title: t("title"), }; }; diff --git a/packages/vitnode/src/app/login/sso/[providerId]/page.tsx b/packages/vitnode/src/app/login/sso/[providerId]/page.tsx index 9e8b6aa07..b9d1b7895 100644 --- a/packages/vitnode/src/app/login/sso/[providerId]/page.tsx +++ b/packages/vitnode/src/app/login/sso/[providerId]/page.tsx @@ -1,4 +1,4 @@ -import { CallbackSSOView } from '@/views/auth/sso/callback/callback-sso-view'; +import { CallbackSSOView } from "@/views/auth/sso/callback/callback-sso-view"; export default async function Page({ params, diff --git a/packages/vitnode/src/app/register/page.tsx b/packages/vitnode/src/app/register/page.tsx index 8ba66aec9..004422387 100644 --- a/packages/vitnode/src/app/register/page.tsx +++ b/packages/vitnode/src/app/register/page.tsx @@ -1,8 +1,8 @@ -import type { Metadata } from 'next/dist/types'; +import type { Metadata } from "next/dist/types"; -import { getTranslations } from 'next-intl/server'; +import { getTranslations } from "next-intl/server"; -import { SignUpView } from '../../views/auth/sign-up/sign-up-view'; +import { SignUpView } from "../../views/auth/sign-up/sign-up-view"; export const generateMetadata = async ({ params, @@ -10,10 +10,10 @@ export const generateMetadata = async ({ params: Promise<{ locale: string }>; }): Promise => { const { locale } = await params; - const t = await getTranslations({ locale, namespace: 'core.global' }); + const t = await getTranslations({ locale, namespace: "core.global" }); return { - title: t('register'), + title: t("register"), }; }; diff --git a/packages/vitnode/src/app_admin/core/debug/page.tsx b/packages/vitnode/src/app_admin/core/debug/page.tsx index 92d64acd3..fa46ae5fe 100644 --- a/packages/vitnode/src/app_admin/core/debug/page.tsx +++ b/packages/vitnode/src/app_admin/core/debug/page.tsx @@ -1,14 +1,14 @@ -import { getTranslations } from 'next-intl/server'; -import dynamic from 'next/dynamic'; -import React from 'react'; +import dynamic from "next/dynamic"; +import { getTranslations } from "next-intl/server"; +import React from "react"; -import { I18nProvider } from '@/components/i18n-provider'; -import { DataTableSkeleton } from '@/components/table/data-table'; -import { HeaderContent } from '@/components/ui/header-content'; -import { ClearCacheAction } from '@/views/admin/views/core/debug/actions/clear-cache/clear-cache'; +import { I18nProvider } from "@/components/i18n-provider"; +import { DataTableSkeleton } from "@/components/table/data-table"; +import { HeaderContent } from "@/components/ui/header-content"; +import { ClearCacheAction } from "@/views/admin/views/core/debug/actions/clear-cache/clear-cache"; const SystemLogsView = dynamic(async () => - import('@/views/admin/views/core/debug/system-logs/system-logs-view').then( + import("@/views/admin/views/core/debug/system-logs/system-logs-view").then( module => ({ default: module.SystemLogsView, }), @@ -16,27 +16,27 @@ const SystemLogsView = dynamic(async () => ); export const generateMetadata = async () => { - const t = await getTranslations('admin.debug'); + const t = await getTranslations("admin.debug"); return { - title: t('title'), - description: t('desc'), + title: t("title"), + description: t("desc"), }; }; export default async function Page( props: React.ComponentProps, ) { - const t = await getTranslations('admin.debug'); + const t = await getTranslations("admin.debug"); return (

- + - + }> diff --git a/packages/vitnode/src/app_admin/core/page.tsx b/packages/vitnode/src/app_admin/core/page.tsx index 331370fe7..5a81f0e88 100644 --- a/packages/vitnode/src/app_admin/core/page.tsx +++ b/packages/vitnode/src/app_admin/core/page.tsx @@ -1,4 +1,4 @@ -import { DashboardAdminView } from '../../views/admin/views/core/dashboard/dashboard-admin-view'; +import { DashboardAdminView } from "../../views/admin/views/core/dashboard/dashboard-admin-view"; export default function Page() { return ; diff --git a/packages/vitnode/src/app_admin/core/test/page.tsx b/packages/vitnode/src/app_admin/core/test/page.tsx index 0f9a918b8..b34e7eeae 100644 --- a/packages/vitnode/src/app_admin/core/test/page.tsx +++ b/packages/vitnode/src/app_admin/core/test/page.tsx @@ -1,4 +1,4 @@ -import { TestView } from '../../../views/admin/views/core/test'; +import { TestView } from "../../../views/admin/views/core/test"; export default function Page() { return ; diff --git a/packages/vitnode/src/app_admin/core/users/page.tsx b/packages/vitnode/src/app_admin/core/users/page.tsx index 5e5b7cf50..504c12bb2 100644 --- a/packages/vitnode/src/app_admin/core/users/page.tsx +++ b/packages/vitnode/src/app_admin/core/users/page.tsx @@ -1,23 +1,22 @@ -import type { Metadata } from 'next/dist/types'; +import type { Metadata } from "next/dist/types"; +import dynamic from "next/dynamic"; +import { getTranslations } from "next-intl/server"; +import React from "react"; -import { getTranslations } from 'next-intl/server'; -import dynamic from 'next/dynamic'; -import React from 'react'; - -import { DataTableSkeleton } from '@/components/table/data-table'; -import { HeaderContent } from '@/components/ui/header-content'; +import { DataTableSkeleton } from "@/components/table/data-table"; +import { HeaderContent } from "@/components/ui/header-content"; const UsersAdminView = dynamic(async () => - import('@/views/admin/views/core/users/users-admin-view').then(module => ({ + import("@/views/admin/views/core/users/users-admin-view").then(module => ({ default: module.UsersAdminView, })), ); export const generateMetadata = async (): Promise => { - const t = await getTranslations('admin.global.nav.users'); + const t = await getTranslations("admin.global.nav.users"); return { - title: t('list'), + title: t("list"), }; }; @@ -25,13 +24,13 @@ export default async function Page( props: React.ComponentProps, ) { const [t, tNav] = await Promise.all([ - getTranslations('admin.user.list'), - getTranslations('admin.global.nav.users'), + getTranslations("admin.user.list"), + getTranslations("admin.global.nav.users"), ]); return (
- + }> diff --git a/packages/vitnode/src/components/avatar.tsx b/packages/vitnode/src/components/avatar.tsx index 684b69b66..4d0e3dceb 100644 --- a/packages/vitnode/src/components/avatar.tsx +++ b/packages/vitnode/src/components/avatar.tsx @@ -1,6 +1,6 @@ -import Image from 'next/image'; +import Image from "next/image"; -import { cn } from '@/lib/utils'; +import { cn } from "@/lib/utils"; const generateLetterPhoto = (letter: string, color: string) => `data:image/svg+xml,${encodeURIComponent( @@ -14,7 +14,7 @@ export const Avatar = ({ ...props }: Omit< React.ComponentProps, - 'alt' | 'height' | 'src' | 'width' + "alt" | "height" | "src" | "width" > & { size: number; user: { avatarColor: string; name: string; nameCode: string }; @@ -22,7 +22,7 @@ export const Avatar = ({ return ( {name} - import('./content').then(module => ({ + import("./content").then(module => ({ default: module.ContentConfirmAction, })), ); @@ -27,13 +27,13 @@ export const ConfirmActionAlertDialog = ({ textSubmit, onSubmit, ...props -}: Omit, 'children'> & +}: Omit, "children"> & React.ComponentProps & { children: React.ReactNode; description?: React.ReactNode; title?: React.ReactNode; }) => { - const t = useTranslations('core.global.confirm_action'); + const t = useTranslations("core.global.confirm_action"); return ( @@ -41,9 +41,9 @@ export const ConfirmActionAlertDialog = ({ - {title ?? t('title')} + {title ?? t("title")} - {description ?? t('desc')} + {description ?? t("desc")} diff --git a/packages/vitnode/src/components/confirm-action/content.tsx b/packages/vitnode/src/components/confirm-action/content.tsx index ea7424fa4..882df8ca3 100644 --- a/packages/vitnode/src/components/confirm-action/content.tsx +++ b/packages/vitnode/src/components/confirm-action/content.tsx @@ -1,12 +1,12 @@ -import { useTranslations } from 'next-intl'; -import React from 'react'; +import { useTranslations } from "next-intl"; +import React from "react"; import { AlertDialogCancel, AlertDialogFooter, useAlertDialog, -} from '../ui/alert-dialog'; -import { Button } from '../ui/button'; +} from "../ui/alert-dialog"; +import { Button } from "../ui/button"; export const ContentConfirmAction = ({ onSubmit, @@ -15,10 +15,9 @@ export const ContentConfirmAction = ({ onSubmit: (props: { onClose: () => void }) => Promise | void; textSubmit?: string; }) => { - const t = useTranslations('core.global.confirm_action'); + const t = useTranslations("core.global.confirm_action"); const { setOpen } = useAlertDialog(); - // eslint-disable-next-line @typescript-eslint/no-unused-vars const [_, formAction, isLoading] = React.useActionState(async () => { await onSubmit({ onClose: () => setOpen?.(false) }); }, null); @@ -26,9 +25,9 @@ export const ContentConfirmAction = ({ return (
- {t('cancel')} + {t("cancel")}
diff --git a/packages/vitnode/src/components/date-format.tsx b/packages/vitnode/src/components/date-format.tsx index 039c0480f..0e8431d1b 100644 --- a/packages/vitnode/src/components/date-format.tsx +++ b/packages/vitnode/src/components/date-format.tsx @@ -1,13 +1,13 @@ -'use client'; +"use client"; -import { useFormatter, useNow } from 'next-intl'; +import { useFormatter, useNow } from "next-intl"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, -} from './ui/tooltip'; +} from "./ui/tooltip"; export const DateFormat = ({ date, @@ -18,22 +18,22 @@ export const DateFormat = ({ showFullDate?: boolean; updateInterval?: number; }) => { - const dateToFormat = typeof date === 'string' ? new Date(date) : date; + const dateToFormat = typeof date === "string" ? new Date(date) : date; const format = useFormatter(); const now = useNow({ updateInterval: updateInterval ?? // Update it every 1 minute if the date is from today, otherwise don't update - (new Date().getTime() - dateToFormat.getTime() < 86400000 ? 60000 : 0), + (Date.now() - dateToFormat.getTime() < 86400000 ? 60000 : 0), }); const fullDate = format.dateTime(dateToFormat, { year: - dateToFormat.getFullYear() === now.getFullYear() ? undefined : 'numeric', - month: 'short', - day: 'numeric', - hour: 'numeric', - minute: 'numeric', + dateToFormat.getFullYear() === now.getFullYear() ? undefined : "numeric", + month: "short", + day: "numeric", + hour: "numeric", + minute: "numeric", }); if (showFullDate) { diff --git a/packages/vitnode/src/components/form/auto-form.tsx b/packages/vitnode/src/components/form/auto-form.tsx index b7a7a227c..cd3e0a1a5 100644 --- a/packages/vitnode/src/components/form/auto-form.tsx +++ b/packages/vitnode/src/components/form/auto-form.tsx @@ -1,43 +1,26 @@ -'use client'; +"use client"; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useTranslations } from 'next-intl'; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useTranslations } from "next-intl"; import { type ControllerRenderProps, type FieldPath, type FieldValues, type Mode, - useForm, type UseFormReturn, -} from 'react-hook-form'; -import * as z from 'zod'; - -import type { routeMiddlewareSchema } from '../../api/modules/middleware/route'; - -import { useCaptcha } from '../../hooks/use-captcha'; + useForm, +} from "react-hook-form"; +import z from "zod"; +import type { routeMiddlewareSchema } from "../../api/modules/middleware/route"; +import { useCaptcha } from "../../hooks/use-captcha"; import { getDefaults, getNestedParam, getZodInputParams, -} from '../../lib/helpers/auto-form'; -import { Button } from '../ui/button'; -import { DialogClose, DialogFooter, useDialog } from '../ui/dialog'; -import { Form, FormField } from '../ui/form'; - -export interface ItemAutoFormComponentProps { - description?: React.ReactNode; - field: ControllerRenderProps; - label?: React.ReactNode; - labelRight?: React.ReactNode; - otherProps: { - enum?: string[]; - isOptional?: boolean; - maxLength?: number; - minLength?: number; - pattern?: string; - type?: string; - }; -} +} from "../../lib/helpers/auto-form"; +import { Button } from "../ui/button"; +import { DialogClose, DialogFooter, useDialog } from "../ui/dialog"; +import { Form, FormField } from "../ui/form"; type ItemAutoFormProps< T extends z.ZodObject = z.ZodObject, @@ -54,6 +37,21 @@ type ItemAutoFormProps< label?: React.ReactNode; }; +export interface ItemAutoFormComponentProps { + description?: React.ReactNode; + field: ControllerRenderProps; + label?: React.ReactNode; + labelRight?: React.ReactNode; + otherProps: { + enum?: string[]; + isOptional?: boolean; + maxLength?: number; + minLength?: number; + pattern?: string; + type?: string; + }; +} + export type AutoFormOnSubmit< T extends z.ZodObject, TContext = unknown, @@ -77,15 +75,15 @@ export function AutoForm< submitButtonProps, children, ...props -}: Omit, 'onSubmit'> & { - captcha?: z.infer['captcha']; +}: Omit, "onSubmit"> & { + captcha?: z.infer["captcha"]; fields: ItemAutoFormProps[]; formSchema: T; mode?: Mode; onSubmit?: AutoFormOnSubmit; submitButtonProps?: Omit< React.ComponentProps, - 'isLoading' | 'type' + "isLoading" | "type" >; }) { const { @@ -94,7 +92,7 @@ export function AutoForm< onReset: onResetCaptcha, } = useCaptcha(captcha); const { setIsDirty } = useDialog(); - const t = useTranslations('core.global'); + const t = useTranslations("core.global"); const jsonSchema: z.core.JSONSchema.JSONSchema = z.toJSONSchema(formSchema); const inputParams = getZodInputParams(jsonSchema); const form = useForm, TContext, z.core.output>({ @@ -107,7 +105,7 @@ export function AutoForm< const parsedValues = formSchema.safeParse(values); if (parsedValues.success) { await onSubmitProp?.(parsedValues.data, form, { - captchaToken: captcha ? await getTokenCaptcha() : '', + captchaToken: captcha ? await getTokenCaptcha() : "", }); if (captcha) { @@ -125,10 +123,10 @@ export function AutoForm< } isLoading={form.formState.isSubmitting} {...submitButtonProps} - aria-label={submitButtonProps?.['aria-label'] ?? t('submit')} + aria-label={submitButtonProps?.["aria-label"] ?? t("submit")} type="submit" > - {submitButtonProps?.children ?? t('submit')} + {submitButtonProps?.children ?? t("submit")} ); @@ -161,34 +159,35 @@ export function AutoForm< render={({ field }) => { return ( <> {item.component({ field, description: - typeof params.description === 'string' + typeof params.description === "string" ? params.description - : '', + : "", otherProps: { isOptional: !params.required, enum: Array.isArray(params.enum) ? params.enum : undefined, maxLength: - typeof params.maxLength === 'number' + typeof params.maxLength === "number" ? params.maxLength : undefined, minLength: - typeof params.minLength === 'number' + typeof params.minLength === "number" ? params.minLength : undefined, pattern: - typeof params.pattern === 'string' + typeof params.pattern === "string" ? params.pattern : undefined, type: - typeof params.type === 'string' + typeof params.type === "string" ? params.type : undefined, }, @@ -206,7 +205,7 @@ export function AutoForm< {setIsDirty ? ( - + {submitButton} diff --git a/packages/vitnode/src/components/form/common/desc.tsx b/packages/vitnode/src/components/form/common/desc.tsx index af6c19cce..167a48fed 100644 --- a/packages/vitnode/src/components/form/common/desc.tsx +++ b/packages/vitnode/src/components/form/common/desc.tsx @@ -1,12 +1,12 @@ -import { cn } from '@/lib/utils'; +import { cn } from "@/lib/utils"; export const AutoFormDesc = ({ children, className, ...props -}: React.ComponentProps<'p'>) => { +}: React.ComponentProps<"p">) => { return ( -

+

{children}

); diff --git a/packages/vitnode/src/components/form/common/label.tsx b/packages/vitnode/src/components/form/common/label.tsx index 01257defd..7a37e5d6e 100644 --- a/packages/vitnode/src/components/form/common/label.tsx +++ b/packages/vitnode/src/components/form/common/label.tsx @@ -1,5 +1,5 @@ -import { FormLabel } from '@/components/ui/form'; -import { cn } from '@/lib/utils'; +import { FormLabel } from "@/components/ui/form"; +import { cn } from "@/lib/utils"; export const AutoFormLabel = ({ children, @@ -13,7 +13,7 @@ export const AutoFormLabel = ({ , 'checked'>) => { + Omit, "checked">) => { return ( diff --git a/packages/vitnode/src/components/form/fields/combobox-async.tsx b/packages/vitnode/src/components/form/fields/combobox-async.tsx index 3df0449cf..3ea0e0033 100644 --- a/packages/vitnode/src/components/form/fields/combobox-async.tsx +++ b/packages/vitnode/src/components/form/fields/combobox-async.tsx @@ -1,10 +1,10 @@ -import { useQuery } from '@tanstack/react-query'; -import { Check, ChevronsUpDown } from 'lucide-react'; -import { useTranslations } from 'next-intl'; -import React from 'react'; -import { useDebouncedCallback } from 'use-debounce'; +import { useQuery } from "@tanstack/react-query"; +import { Check, ChevronsUpDown } from "lucide-react"; +import { useTranslations } from "next-intl"; +import React from "react"; +import { useDebouncedCallback } from "use-debounce"; -import { Button } from '@/components/ui/button'; +import { Button } from "@/components/ui/button"; import { Command, CommandEmpty, @@ -12,20 +12,18 @@ import { CommandInput, CommandItem, CommandList, -} from '@/components/ui/command'; -import { FormControl, FormItem, FormMessage } from '@/components/ui/form'; +} from "@/components/ui/command"; +import { FormControl, FormItem, FormMessage } from "@/components/ui/form"; import { Popover, PopoverContent, PopoverTrigger, -} from '@/components/ui/popover'; -import { cn } from '@/lib/utils'; - -import type { ItemAutoFormComponentProps } from '../auto-form'; - -import { Skeleton } from '../../ui/skeleton'; -import { AutoFormDesc } from '../common/desc'; -import { AutoFormLabel } from '../common/label'; +} from "@/components/ui/popover"; +import { cn } from "@/lib/utils"; +import { Skeleton } from "../../ui/skeleton"; +import type { ItemAutoFormComponentProps } from "../auto-form"; +import { AutoFormDesc } from "../common/desc"; +import { AutoFormLabel } from "../common/label"; export const AutoFormComboboxAsync = ({ label, @@ -40,7 +38,7 @@ export const AutoFormComboboxAsync = ({ fetchData, ...props }: ItemAutoFormComponentProps & - Omit, 'role' | 'variant'> & { + Omit, "role" | "variant"> & { fetchData: (params: { search: string }) => | Promise< { @@ -56,8 +54,8 @@ export const AutoFormComboboxAsync = ({ placeholder?: string; searchPlaceholder?: string; }) => { - const t = useTranslations('core.global'); - const [search, setSearch] = React.useState(''); + const t = useTranslations("core.global"); + const [search, setSearch] = React.useState(""); const { data, isLoading } = useQuery({ queryKey: [id, { search }], queryFn: async () => { @@ -83,15 +81,15 @@ export const AutoFormComboboxAsync = ({ @@ -103,47 +101,51 @@ export const AutoFormComboboxAsync = ({ onChangeCapture={e => { handleChangeSearch(e.currentTarget.value); }} - placeholder={searchPlaceholder ?? t('search_placeholder')} + placeholder={searchPlaceholder ?? t("search_placeholder")} /> - {isLoading ? ( -
- - -
- ) : ( - <> - {data?.length === 0 ? ( - {t('results_not_found')} - ) : ( - - {(data ?? []).map(({ label, value }) => ( - { - field.onChange({ - label, - value, - }); - }} - value={label} - > - {label} - - - ))} - - )} - - )} + {(() => { + if (isLoading) { + return ( +
+ + +
+ ); + } + + if ((data ?? []).length === 0) { + return {t("results_not_found")}; + } + + return ( + + {(data ?? []).map(({ label, value }) => ( + { + field.onChange({ + label, + value, + }); + }} + value={label} + > + {label} + + + ))} + + ); + })()}
diff --git a/packages/vitnode/src/components/form/fields/combobox.tsx b/packages/vitnode/src/components/form/fields/combobox.tsx index d44e6e5b7..5b1705365 100644 --- a/packages/vitnode/src/components/form/fields/combobox.tsx +++ b/packages/vitnode/src/components/form/fields/combobox.tsx @@ -1,8 +1,8 @@ -import { Check, ChevronsUpDown } from 'lucide-react'; -import { useTranslations } from 'next-intl'; -import React from 'react'; +import { Check, ChevronsUpDown } from "lucide-react"; +import { useTranslations } from "next-intl"; +import type React from "react"; -import { Button } from '@/components/ui/button'; +import { Button } from "@/components/ui/button"; import { Command, CommandEmpty, @@ -10,19 +10,19 @@ import { CommandInput, CommandItem, CommandList, -} from '@/components/ui/command'; -import { FormControl, FormItem, FormMessage } from '@/components/ui/form'; +} from "@/components/ui/command"; +import { FormControl, FormItem, FormMessage } from "@/components/ui/form"; import { Popover, PopoverContent, PopoverTrigger, -} from '@/components/ui/popover'; -import { cn } from '@/lib/utils'; +} from "@/components/ui/popover"; +import { cn } from "@/lib/utils"; -import type { ItemAutoFormComponentProps } from '../auto-form'; +import type { ItemAutoFormComponentProps } from "../auto-form"; -import { AutoFormDesc } from '../common/desc'; -import { AutoFormLabel } from '../common/label'; +import { AutoFormDesc } from "../common/desc"; +import { AutoFormLabel } from "../common/label"; export const AutoFormCombobox = ({ label, @@ -36,12 +36,12 @@ export const AutoFormCombobox = ({ searchPlaceholder, ...props }: ItemAutoFormComponentProps & - Omit, 'role' | 'variant'> & { + Omit, "role" | "variant"> & { labels?: { label: string; value: string }[]; placeholder?: string; searchPlaceholder?: string; }) => { - const t = useTranslations('core.global'); + const t = useTranslations("core.global"); const values: { label: string; value: string }[] = enumValues.map(value => { const label = labels.find(l => l.value === value)?.label; @@ -66,8 +66,8 @@ export const AutoFormCombobox = ({ @@ -85,10 +85,10 @@ export const AutoFormCombobox = ({ - {t('results_not_found')} + {t("results_not_found")} {values.map(({ label, value }) => ( diff --git a/packages/vitnode/src/components/form/fields/input.tsx b/packages/vitnode/src/components/form/fields/input.tsx index 3e9beed8f..e194a385d 100644 --- a/packages/vitnode/src/components/form/fields/input.tsx +++ b/packages/vitnode/src/components/form/fields/input.tsx @@ -1,9 +1,8 @@ -import type { ItemAutoFormComponentProps } from '../auto-form'; - -import { FormControl, FormItem, FormMessage } from '../../ui/form'; -import { Input } from '../../ui/input'; -import { AutoFormDesc } from '../common/desc'; -import { AutoFormLabel } from '../common/label'; +import { FormControl, FormItem, FormMessage } from "../../ui/form"; +import { Input } from "../../ui/input"; +import type { ItemAutoFormComponentProps } from "../auto-form"; +import { AutoFormDesc } from "../common/desc"; +import { AutoFormLabel } from "../common/label"; export const AutoFormInput = ({ label, @@ -13,7 +12,7 @@ export const AutoFormInput = ({ field, ...props }: ItemAutoFormComponentProps & - Omit, 'value'>) => { + Omit, "value">) => { return ( {label && ( @@ -35,8 +34,8 @@ export const AutoFormInput = ({ props.onChange?.(e); }} pattern={pattern} - type={type ?? 'text'} - value={field.value ?? ''} + type={type ?? "text"} + value={field.value ?? ""} {...props} /> diff --git a/packages/vitnode/src/components/form/fields/radio-group.tsx b/packages/vitnode/src/components/form/fields/radio-group.tsx index ffb742c8f..7301dbaba 100644 --- a/packages/vitnode/src/components/form/fields/radio-group.tsx +++ b/packages/vitnode/src/components/form/fields/radio-group.tsx @@ -1,17 +1,17 @@ -import React from 'react'; +import type React from "react"; import { FormControl, FormItem, FormLabel, FormMessage, -} from '@/components/ui/form'; -import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +} from "@/components/ui/form"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; -import type { ItemAutoFormComponentProps } from '../auto-form'; +import type { ItemAutoFormComponentProps } from "../auto-form"; -import { AutoFormDesc } from '../common/desc'; -import { AutoFormLabel } from '../common/label'; +import { AutoFormDesc } from "../common/desc"; +import { AutoFormLabel } from "../common/label"; export const AutoFormRadioGroup = ({ label, @@ -22,7 +22,7 @@ export const AutoFormRadioGroup = ({ labels = [], ...props }: ItemAutoFormComponentProps & - Omit, 'value'> & { + Omit, "value"> & { labels?: { label: string; value: string }[]; }) => { const values: { label: string; value: string }[] = enumValues.map(value => { diff --git a/packages/vitnode/src/components/form/fields/select.tsx b/packages/vitnode/src/components/form/fields/select.tsx index c21008117..fb0f28cd4 100644 --- a/packages/vitnode/src/components/form/fields/select.tsx +++ b/packages/vitnode/src/components/form/fields/select.tsx @@ -1,19 +1,19 @@ -import { useTranslations } from 'next-intl'; -import React from 'react'; +import { useTranslations } from "next-intl"; +import type React from "react"; -import { FormControl, FormItem, FormMessage } from '@/components/ui/form'; +import { FormControl, FormItem, FormMessage } from "@/components/ui/form"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from '@/components/ui/select'; +} from "@/components/ui/select"; -import type { ItemAutoFormComponentProps } from '../auto-form'; +import type { ItemAutoFormComponentProps } from "../auto-form"; -import { AutoFormDesc } from '../common/desc'; -import { AutoFormLabel } from '../common/label'; +import { AutoFormDesc } from "../common/desc"; +import { AutoFormLabel } from "../common/label"; export const AutoFormSelect = ({ label, @@ -25,11 +25,11 @@ export const AutoFormSelect = ({ labels = [], ...props }: ItemAutoFormComponentProps & - Omit, 'value'> & { + Omit, "value"> & { labels?: { label: string; value: string }[]; placeholder?: string; }) => { - const t = useTranslations('core.global'); + const t = useTranslations("core.global"); const values: { label: string; value: string }[] = enumValues.map(value => { const label = labels.find(l => l.value === value)?.label; @@ -41,7 +41,7 @@ export const AutoFormSelect = ({ const currentPlaceholder = (values ?? labels).find(l => l.value === field.value)?.label ?? - t('select_option'); + t("select_option"); return ( diff --git a/packages/vitnode/src/components/form/fields/switch.tsx b/packages/vitnode/src/components/form/fields/switch.tsx index 1b9d9ccd0..05de62c25 100644 --- a/packages/vitnode/src/components/form/fields/switch.tsx +++ b/packages/vitnode/src/components/form/fields/switch.tsx @@ -1,10 +1,10 @@ -import { FormControl, FormItem } from '@/components/ui/form'; -import { Switch } from '@/components/ui/switch'; +import { FormControl, FormItem } from "@/components/ui/form"; +import { Switch } from "@/components/ui/switch"; -import type { ItemAutoFormComponentProps } from '../auto-form'; +import type { ItemAutoFormComponentProps } from "../auto-form"; -import { AutoFormDesc } from '../common/desc'; -import { AutoFormLabel } from '../common/label'; +import { AutoFormDesc } from "../common/desc"; +import { AutoFormLabel } from "../common/label"; export const AutoFormSwitch = ({ label, @@ -14,10 +14,9 @@ export const AutoFormSwitch = ({ description, ...props }: ItemAutoFormComponentProps & - Omit, 'checked'>) => { + Omit, "checked">) => { return ( - {/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */} {(label || description) && (
{label && ( diff --git a/packages/vitnode/src/components/form/fields/textarea.tsx b/packages/vitnode/src/components/form/fields/textarea.tsx index 6ee42e7b6..8bfb8df63 100644 --- a/packages/vitnode/src/components/form/fields/textarea.tsx +++ b/packages/vitnode/src/components/form/fields/textarea.tsx @@ -1,12 +1,12 @@ -import React from 'react'; +import type React from "react"; -import { FormControl, FormItem, FormMessage } from '@/components/ui/form'; -import { Textarea } from '@/components/ui/textarea'; +import { FormControl, FormItem, FormMessage } from "@/components/ui/form"; +import { Textarea } from "@/components/ui/textarea"; -import type { ItemAutoFormComponentProps } from '../auto-form'; +import type { ItemAutoFormComponentProps } from "../auto-form"; -import { AutoFormDesc } from '../common/desc'; -import { AutoFormLabel } from '../common/label'; +import { AutoFormDesc } from "../common/desc"; +import { AutoFormLabel } from "../common/label"; export const AutoFormTextarea = ({ label, @@ -16,7 +16,7 @@ export const AutoFormTextarea = ({ field, ...props }: ItemAutoFormComponentProps & - Omit, 'value'> & { + Omit, "value"> & { description?: React.ReactNode; label?: React.ReactNode; }) => { @@ -40,7 +40,7 @@ export const AutoFormTextarea = ({ field.onChange(e); props.onChange?.(e); }} - value={field.value ?? ''} + value={field.value ?? ""} {...props} /> diff --git a/packages/vitnode/src/components/i18n-provider.tsx b/packages/vitnode/src/components/i18n-provider.tsx index a49aea6a8..a049d3b61 100644 --- a/packages/vitnode/src/components/i18n-provider.tsx +++ b/packages/vitnode/src/components/i18n-provider.tsx @@ -1,18 +1,18 @@ -import type { Messages, NamespaceKeys, NestedKeyOf } from 'next-intl'; +import type { Messages, NamespaceKeys, NestedKeyOf } from "next-intl"; -import { NextIntlClientProvider } from 'next-intl'; -import { getLocale, getMessages } from 'next-intl/server'; -import 'server-only'; +import { NextIntlClientProvider } from "next-intl"; +import { getLocale, getMessages } from "next-intl/server"; +import "server-only"; const pick = (obj: object, paths: string[]) => { const result = {}; for (const path of paths) { - const keys = path.split('.'); + const keys = path.split("."); let src: object | undefined = obj; let dest = result; for (let i = 0; i < keys.length; i++) { const key = keys[i]; - if (src && Object.prototype.hasOwnProperty.call(src, key)) { + if (src && Object.hasOwn(src, key)) { if (i === keys.length - 1) { dest[key] = src[key]; } else { @@ -41,7 +41,7 @@ export async function I18nProvider< const locale = await getLocale(); const messagesInit: object = await getMessages({ locale }); const messages = pick(messagesInit, [ - 'core.global', + "core.global", ...(Array.isArray(namespaces) ? namespaces : [namespaces]), ]); diff --git a/packages/vitnode/src/components/logo-vitnode.tsx b/packages/vitnode/src/components/logo-vitnode.tsx index abb0ed17d..0c8d12afd 100644 --- a/packages/vitnode/src/components/logo-vitnode.tsx +++ b/packages/vitnode/src/components/logo-vitnode.tsx @@ -1,22 +1,23 @@ -import { cn } from '@/lib/utils'; +import { cn } from "@/lib/utils"; export const LogoVitNode = ({ className, small, ...props -}: React.ComponentProps<'svg'> & { +}: React.ComponentProps<"svg"> & { small?: boolean; }) => { if (small) return ( + Logo VitNode + Logo VitNode { const currentLocale = useLocale(); const [isPending, startTransition] = React.useTransition(); const { replace } = useRouter(); - const t = useTranslations('core.global'); + const t = useTranslations("core.global"); const pathname = usePathname(); return (
@@ -58,13 +56,13 @@ export function ContentDataTable({ allData: edges, row, }) ?? - (column.id === 'actions' ? '' : String(row[column.id])); + (column.id === "actions" ? "" : String(row[column.id])); return ( @@ -86,10 +84,10 @@ export function ContentDataTable({

- {t('no_results.title')} + {t("no_results.title")}

- {t('no_results.desc')} + {t("no_results.desc")}

diff --git a/packages/vitnode/src/components/table/data-table.tsx b/packages/vitnode/src/components/table/data-table.tsx index 061d93b0c..050be810d 100644 --- a/packages/vitnode/src/components/table/data-table.tsx +++ b/packages/vitnode/src/components/table/data-table.tsx @@ -1,18 +1,15 @@ -/* eslint-disable @eslint-react/no-array-index-key */ -import React from 'react'; - -import type { PaginationDataTable } from './pagination'; - -import { ErrorView } from '../../views/error/error-view'; -import { Skeleton } from '../ui/skeleton'; +import React from "react"; +import { ErrorView } from "../../views/error/error-view"; +import { Skeleton } from "../ui/skeleton"; import { Table, TableBody, TableHead, TableHeader, TableRow, -} from '../ui/table'; -import { ContentDataTable } from './content'; +} from "../ui/table"; +import { ContentDataTable } from "./content"; +import type { PaginationDataTable } from "./pagination"; export interface DataTableTMin { id: number; @@ -22,50 +19,71 @@ export interface SearchParamsDataTable { cursor?: string; first?: string; last?: string; - order?: 'asc' | 'desc'; + order?: "asc" | "desc"; orderBy?: keyof T; } -export const DataTableSkeleton = ({ columns }: { columns: number }) => ( -
-
- - - - {Array.from({ length: columns }).map((_, i) => ( - - - - ))} - - - - {Array.from({ length: 6 }).map((_, i) => ( - - {Array.from({ length: columns }).map((_, j) => ( - +export const DataTableSkeleton = ({ columns }: { columns: number }) => { + const headerIds = React.useMemo( + () => + Array.from({ length: columns }).map( + () => `s-head-${Math.random().toString(36).slice(2, 9)}`, + ), + [columns], + ); + + const rowIds = React.useMemo( + () => + Array.from({ length: 6 }).map( + () => `s-row-${Math.random().toString(36).slice(2, 9)}`, + ), + [], + ); + + return ( +
+
+
- -
+ + + {headerIds.map(hid => ( + + + ))} - ))} - -
-
+ + + {rowIds.map(rid => ( + + {headerIds.map((_, j) => { + const cellId = `s-cell-${rid}-${j}`; + return ( + + + + ); + })} + + ))} + + +
-
- +
+ +
-
-); + ); +}; export function DataTable( - props: Omit, 'columns'> & + props: Omit, "columns"> & React.ComponentProps & { columns: { cell?: (data: { allData: T[]; row: T }) => React.ReactNode; className?: string; - id: 'actions' | keyof T; + id: "actions" | keyof T; label: string; }[]; customNotFoundComponent?: React.ReactNode; @@ -74,12 +92,12 @@ export function DataTable( columns?: (keyof T)[]; defaultOrder: { column: keyof T; - order: 'asc' | 'desc'; + order: "asc" | "desc"; }; }; }, ) { - if (!props.edges || !props.pageInfo) { + if (!(props.edges && props.pageInfo)) { return ; } diff --git a/packages/vitnode/src/components/table/order-table-head.tsx b/packages/vitnode/src/components/table/order-table-head.tsx index ed67db5d6..fa69aba78 100644 --- a/packages/vitnode/src/components/table/order-table-head.tsx +++ b/packages/vitnode/src/components/table/order-table-head.tsx @@ -1,23 +1,21 @@ -'use client'; +"use client"; -import { ArrowDown, ArrowUp, ChevronsUpDown } from 'lucide-react'; -import { useSearchParams } from 'next/navigation'; -import React from 'react'; +import { ArrowDown, ArrowUp, ChevronsUpDown } from "lucide-react"; +import { useSearchParams } from "next/navigation"; +import React from "react"; -import { usePathname, useRouter } from '@/lib/navigation'; - -import type { DataTable, DataTableTMin } from './data-table'; - -import { Button } from '../ui/button'; -import { Loader } from '../ui/loader'; +import { usePathname, useRouter } from "@/lib/navigation"; +import { Button } from "../ui/button"; +import { Loader } from "../ui/loader"; +import type { DataTable, DataTableTMin } from "./data-table"; export function OrderTableHeadDataTable({ id, children, order: { defaultOrder }, -}: Pick>, 'order'> & { +}: Pick>, "order"> & { children: React.ReactNode; - id: React.ComponentProps>['columns'][0]['id']; + id: React.ComponentProps>["columns"][0]["id"]; }) { const [isPending, startTransition] = React.useTransition(); const searchParams = useSearchParams(); @@ -25,11 +23,11 @@ export function OrderTableHeadDataTable({ const { push } = useRouter(); const currentOrderBy = - searchParams.get('orderBy') ?? defaultOrder.column.toString(); - const currentOrder = searchParams.get('order') ?? defaultOrder.order; + searchParams.get("orderBy") ?? defaultOrder.column.toString(); + const currentOrder = searchParams.get("order") ?? defaultOrder.order; const isActive = currentOrderBy === id.toString(); - const nextOrder = isActive && currentOrder === 'asc' ? 'desc' : 'asc'; + const nextOrder = isActive && currentOrder === "asc" ? "desc" : "asc"; return ( ); } diff --git a/packages/vitnode/src/components/table/pagination.tsx b/packages/vitnode/src/components/table/pagination.tsx index 02e85d901..fc88a1407 100644 --- a/packages/vitnode/src/components/table/pagination.tsx +++ b/packages/vitnode/src/components/table/pagination.tsx @@ -1,21 +1,21 @@ -'use client'; +"use client"; -import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'; -import { useTranslations } from 'next-intl'; -import { useSearchParams } from 'next/navigation'; -import React from 'react'; +import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; +import { useSearchParams } from "next/navigation"; +import { useTranslations } from "next-intl"; +import React from "react"; -import { usePathname, useRouter } from '@/lib/navigation'; +import { usePathname, useRouter } from "@/lib/navigation"; -import { Button } from '../ui/button'; +import { Button } from "../ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from '../ui/select'; -import { Skeleton } from '../ui/skeleton'; +} from "../ui/select"; +import { Skeleton } from "../ui/skeleton"; const PAGE_SIZE_OPTIONS = [10, 20, 30, 40]; @@ -31,15 +31,15 @@ export const PaginationDataTable = ({ totalCount: number; }; }) => { - const t = useTranslations('core.global'); + const t = useTranslations("core.global"); const { push } = useRouter(); const [isPending, startTransition] = React.useTransition(); const pathname = usePathname(); const searchParams = useSearchParams(); const pagination = { - first: searchParams.get('first'), - last: searchParams.get('last'), - cursor: searchParams.get('cursor'), + first: searchParams.get("first"), + last: searchParams.get("last"), + cursor: searchParams.get("cursor"), }; const pageSize = pagination.first ?? pagination.last ?? 10; @@ -51,9 +51,9 @@ export const PaginationDataTable = ({ onValueChange={value => { startTransition(() => { const params = new URLSearchParams(searchParams.toString()); - params.set('first', value); - params.delete('last'); - params.delete('cursor'); + params.set("first", value); + params.delete("last"); + params.delete("cursor"); push(`${pathname}?${params.toString()}`, { scroll: false, }); @@ -82,19 +82,19 @@ export const PaginationDataTable = ({ ) : (