From ffd072b82b2d465ce505cb1ebe6ccf204dd8504c Mon Sep 17 00:00:00 2001 From: Vincenzo Fanizza Date: Mon, 23 Dec 2024 01:50:38 +0100 Subject: [PATCH 01/12] added todos --- TODO.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..c133cf1 --- /dev/null +++ b/TODO.md @@ -0,0 +1,23 @@ +# TODOs + +- decide design +- write 1st version of docs +- set up npm package +- landing page + +- add at least 3 components before making a video about it: + - auth + - landing page + - stripe + +auth implementation: +- next auth +- providers + - email, password + - google +- auth wrapper + - if the app is just initialized, save in index.tsx + - otherwise log and inform the user you're saving them in another file + +landing page: +- design it for our website and then use a very simple version to release \ No newline at end of file From afb0eddf66668d5f1ff01923d05f53b7fd1e413e Mon Sep 17 00:00:00 2001 From: Vincenzo Fanizza Date: Sun, 12 Jan 2025 18:21:14 +0100 Subject: [PATCH 02/12] cli --- .gitignore | 3 +- packages/cli/package-lock.json | 308 ++++++++++++++++++++++++ packages/cli/package.json | 25 ++ packages/cli/src/commands/add.ts | 13 + packages/cli/src/commands/init.ts | 35 +++ packages/cli/src/index.ts | 17 ++ packages/cli/src/utils/add-component.ts | 94 ++++++++ packages/cli/src/utils/handle-error.ts | 11 + packages/cli/src/utils/highlighter.ts | 7 + packages/cli/src/utils/init-setup.ts | 214 ++++++++++++++++ packages/cli/src/utils/logger.ts | 11 + packages/cli/src/utils/spinner.ts | 19 ++ packages/cli/tsconfig.json | 15 ++ 13 files changed, 771 insertions(+), 1 deletion(-) create mode 100644 packages/cli/package-lock.json create mode 100644 packages/cli/package.json create mode 100644 packages/cli/src/commands/add.ts create mode 100644 packages/cli/src/commands/init.ts create mode 100644 packages/cli/src/index.ts create mode 100644 packages/cli/src/utils/add-component.ts create mode 100644 packages/cli/src/utils/handle-error.ts create mode 100644 packages/cli/src/utils/highlighter.ts create mode 100644 packages/cli/src/utils/init-setup.ts create mode 100644 packages/cli/src/utils/logger.ts create mode 100644 packages/cli/src/utils/spinner.ts create mode 100644 packages/cli/tsconfig.json diff --git a/.gitignore b/.gitignore index 674c0aa..0c3b936 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ !.env.example # dependencies -/node_modules +**/node_modules /.pnp .pnp.* .yarn/* @@ -21,6 +21,7 @@ # production /build +**/dist/ # misc .DS_Store diff --git a/packages/cli/package-lock.json b/packages/cli/package-lock.json new file mode 100644 index 0000000..56d2f9b --- /dev/null +++ b/packages/cli/package-lock.json @@ -0,0 +1,308 @@ +{ + "name": "@libui-next/cli", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@libui-next/cli", + "version": "1.0.0", + "dependencies": { + "chalk": "^4.1.2", + "commander": "^13.0.0", + "nanospinner": "^1.1.0" + }, + "bin": { + "libui-next": "dist/index.js" + }, + "devDependencies": { + "@types/node": "^20.4.2", + "ts-node": "^10.9.1", + "typescript": "^5.1.6" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.17.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.12.tgz", + "integrity": "sha512-vo/wmBgMIiEA23A/knMfn/cf37VnuF52nZh5ZoW0GWt4e4sxNquibrMRJ7UQsA06+MBx9r/H1jsI9grYjQCQlw==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/commander": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.0.0.tgz", + "integrity": "sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ==", + "engines": { + "node": ">=18" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/nanospinner": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/nanospinner/-/nanospinner-1.2.2.tgz", + "integrity": "sha512-Zt/AmG6qRU3e+WnzGGLuMCEAO/dAu45stNbHY223tUxldaDAeE+FxSPsd9Q+j+paejmm0ZbrNVs5Sraqy3dRxA==", + "dependencies": { + "picocolors": "^1.1.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 0000000..922e807 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,25 @@ +{ + "name": "@libui-next/cli", + "version": "1.0.0", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "build:cli": "tsc && chmod +x dist/index.js", + "start": "ts-node src/index.ts", + "prepare": "npm run build" + }, + "bin": { + "libui-next": "dist/index.js" + }, + "dependencies": { + "chalk": "^4.1.2", + "commander": "^13.0.0", + "nanospinner": "^1.1.0" + }, + "devDependencies": { + "@types/node": "^20.4.2", + "ts-node": "^10.9.1", + "typescript": "^5.1.6" + } +} diff --git a/packages/cli/src/commands/add.ts b/packages/cli/src/commands/add.ts new file mode 100644 index 0000000..b4c6043 --- /dev/null +++ b/packages/cli/src/commands/add.ts @@ -0,0 +1,13 @@ +import { Command } from 'commander'; +import { addComponent } from '../utils/add-component'; + +export const addCommand = new Command('add') + .description('add a new component') + .argument('', 'Name of the component to add') + .action(async (componentName: string) => { + if (componentName.toLowerCase() !== 'auth') { + console.error('Only the "auth" component can be added at this time.'); + process.exit(1); + } + await addComponent(); + }); \ No newline at end of file diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts new file mode 100644 index 0000000..741fd2a --- /dev/null +++ b/packages/cli/src/commands/init.ts @@ -0,0 +1,35 @@ +import { Command } from 'commander'; +import { runInit } from '../utils/init-setup'; +import { logger } from '../utils/logger'; +import { handleError } from '../utils/handle-error'; +import path from 'path'; +import { promises as fs } from 'fs'; + +export const initCommand = new Command('init') + .description('Initialize a new Next.js project with TypeScript and Tailwind CSS') + .action(async () => { + try { + const projectDir = process.cwd(); + const configPath = path.join(projectDir, 'components.json'); + + if (await exists(configPath)) { + logger.error('components.json already exists in this directory. Initialization aborted.'); + process.exit(1); + } + + await runInit(projectDir); + + logger.log('Project initialization completed successfully.'); + } catch (error) { + handleError(error); + } + }); + +async function exists(path: string): Promise { + try { + await fs.access(path); + return true; + } catch { + return false; + } +} \ No newline at end of file diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts new file mode 100644 index 0000000..e86f0a6 --- /dev/null +++ b/packages/cli/src/index.ts @@ -0,0 +1,17 @@ +#!/usr/bin/env node + +import { Command } from 'commander'; +import { initCommand } from './commands/init'; +import { addCommand } from './commands/add'; + +const program = new Command(); + +program + .name('libui-next') + .description('add full-stack components to your project') + .version('1.0.0'); + +program.addCommand(initCommand); +program.addCommand(addCommand); + +program.parse(process.argv); \ No newline at end of file diff --git a/packages/cli/src/utils/add-component.ts b/packages/cli/src/utils/add-component.ts new file mode 100644 index 0000000..a8e62d2 --- /dev/null +++ b/packages/cli/src/utils/add-component.ts @@ -0,0 +1,94 @@ +import path from 'path'; +import fs from 'fs/promises'; +import axios from 'axios'; +import { handleError } from './handle-error'; +import { logger } from './logger'; +import { spinner } from './spinner'; + +interface LibUIConfig { + aliases: Record; +} + +export async function addComponent() { + const spin = spinner('Adding auth component...'); + try { + spin.start(); + + const configPath = path.join(process.cwd(), 'libuiconfig.json'); + const configData = await fs.readFile(configPath, 'utf-8'); + const libConfig: LibUIConfig = JSON.parse(configData); + + const aliases = libConfig.aliases; + + const remoteRepoBaseURL = 'https://raw.githubusercontent.com/nizzyabi/lib-ui/main/'; + + // Define component files with their relative paths + const componentFiles = [ + 'actions/admin.ts', + 'actions/login.ts', + 'actions/logout.ts', + 'actions/new-password.ts', + 'actions/new-verification.ts', + 'actions/register.ts', + 'actions/reset.ts', + 'actions/settings.ts', + 'app/api/admin/route.ts', + 'app/api/auth/[...nextauth]/route.ts', + 'auth.config.ts', + 'auth.ts', + 'components/auth/auth.tsx', + 'components/auth/logout-button.tsx', + 'components/auth/user-button.tsx', + 'components/ui/avatar.tsx', + 'components/ui/examples/auth-example.tsx', + 'data/account.ts', + 'data/password-reset-token.ts', + 'data/two-factor-confirmation.ts', + 'data/two-factor-token.ts', + 'data/user.ts', + 'data/verification-token.ts', + 'hooks/use-current-role.ts', + 'hooks/use-current-user.ts', + 'lib/auth.ts', + 'lib/mail.ts', + 'lib/token.ts', + 'lib/utils.ts', + 'next-auth.d.ts', + 'prisma/schema.prisma', + 'routes.ts', + 'schemas/index.ts', + ]; + + for (const file of componentFiles) { + const remoteFileURL = `${remoteRepoBaseURL}${file}`; + const localFilePath = path.join(process.cwd(), file); + + console.log(`Downloading from: ${remoteFileURL}`); + console.log(`Saving to: ${localFilePath}`); + + // Ensure the local directory exists + const directory = path.dirname(localFilePath); + await fs.mkdir(directory, { recursive: true }); + + // Download the file from the remote repository + let response; + try { + response = await axios.get(remoteFileURL, { responseType: 'text' }); + + if (response.data) { + await fs.writeFile(localFilePath, response.data, 'utf-8'); + logger.log(`Added ${file} to ${localFilePath}`); + } else { + throw new Error('Failed to download file'); + } + } catch { + throw new Error(`Failed to download ${file} from ${remoteFileURL}`); + } + } + + spin.succeed('Auth component added successfully'); + } catch (error) { + spin.fail('Failed to add auth component'); + handleError(error); + } +} \ No newline at end of file diff --git a/packages/cli/src/utils/handle-error.ts b/packages/cli/src/utils/handle-error.ts new file mode 100644 index 0000000..6491e1c --- /dev/null +++ b/packages/cli/src/utils/handle-error.ts @@ -0,0 +1,11 @@ +import { logger } from './logger'; +import { highlighter } from './highlighter'; + +export function handleError(error: unknown) { + if (error instanceof Error) { + logger.error(highlighter.error(error.message)); + } else { + logger.error(highlighter.error('An unknown error occurred.')); + } + process.exit(1); +} \ No newline at end of file diff --git a/packages/cli/src/utils/highlighter.ts b/packages/cli/src/utils/highlighter.ts new file mode 100644 index 0000000..76776ff --- /dev/null +++ b/packages/cli/src/utils/highlighter.ts @@ -0,0 +1,7 @@ +import chalk from 'chalk'; + +export const highlighter = { + error: (str: string) => chalk.red(str), + success: (str: string) => chalk.green(str), + info: (str: string) => chalk.blue(str), +}; \ No newline at end of file diff --git a/packages/cli/src/utils/init-setup.ts b/packages/cli/src/utils/init-setup.ts new file mode 100644 index 0000000..7367d41 --- /dev/null +++ b/packages/cli/src/utils/init-setup.ts @@ -0,0 +1,214 @@ +import { promises as fs } from 'fs'; +import path from 'path'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import { logger } from './logger'; +import { highlighter } from './highlighter'; +import { spinner } from './spinner'; + +const execPromise = promisify(exec); + +export async function runInit(projectDir: string) { + // Check if directory is empty + const dirContents = await fs.readdir(projectDir); + if (dirContents.length > 0) { + logger.error('Directory is not empty. Please run this command in an empty directory.'); + process.exit(1); + } + + // Initialize Next.js project with TypeScript + const nextInitSpinner = spinner('Initializing Next.js project with TypeScript...').start(); + try { + await execPromise('npx create-next-app@latest . --typescript --use-npm --no-eslint --no-tailwind --no-src-dir --no-experimental-app --yes', + { + cwd: projectDir, + env: { ...process.env, FORCE_COLOR: '1' }, + } + ); + nextInitSpinner.success('Next.js project initialized with TypeScript.'); + } catch (error) { + nextInitSpinner.error('Failed to initialize Next.js project.'); + if (error instanceof Error) { + logger.error(`Error details: ${error.message}`); + } + throw error; + } + + // Install Tailwind CSS + const tailwindSpinner = spinner('Installing Tailwind CSS...').start(); + try { + await execPromise('npm install -D tailwindcss postcss autoprefixer', { cwd: projectDir }); + await execPromise('npx tailwindcss init -p', { cwd: projectDir }); + tailwindSpinner.success('Tailwind CSS installed.'); + + // Configure Tailwind CSS + const tailwindConfigPath = path.join(projectDir, 'tailwind.config.js'); + const tailwindConfig = `/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + './pages/**/*.{js,ts,jsx,tsx,mdx}', + './components/**/*.{js,ts,jsx,tsx,mdx}', + './app/**/*.{js,ts,jsx,tsx,mdx}', + ], + theme: { + extend: {}, + }, + plugins: [], +}; +`; + await fs.writeFile(tailwindConfigPath, tailwindConfig, 'utf8'); + + // Check for app directory structure + const appDirExists = await exists(path.join(projectDir, 'app')); + const globalCssPath = appDirExists + ? path.join(projectDir, 'app', 'globals.css') + : path.join(projectDir, 'styles', 'globals.css'); + + // Create directory if it doesn't exist + await fs.mkdir(path.dirname(globalCssPath), { recursive: true }); + + // Add Tailwind directives to globals.css + const globalsCssContent = `@tailwind base; +@tailwind components; +@tailwind utilities; + +/* Add your custom styles here */ +`; + await fs.writeFile(globalCssPath, globalsCssContent, 'utf8'); + logger.log(highlighter.success('Tailwind CSS configured successfully.')); + } catch (error) { + tailwindSpinner.error('Failed to install Tailwind CSS.'); + throw error; + } + + // Install Prisma + const prismaSpinner = spinner('Setting up Prisma...').start(); + try { + await execPromise('npm install -D prisma @prisma/client', { cwd: projectDir }); + await fs.mkdir(path.join(projectDir, 'prisma'), { recursive: true }); + + const prismaSchema = `generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// Add your models here +`; + await fs.writeFile(path.join(projectDir, 'prisma', 'schema.prisma'), prismaSchema, 'utf8'); + prismaSpinner.success('Prisma setup completed.'); + } catch (error) { + prismaSpinner.error('Failed to setup Prisma.'); + throw error; + } + + // Create API directory structure + const apiSpinner = spinner('Creating API structure...').start(); + try { + // Create all necessary directories + await fs.mkdir(path.join(projectDir, 'app', 'api', 'hello'), { recursive: true }); + + // Create example API route + const exampleApiRoute = `import { NextResponse } from 'next/server' + +export async function GET() { + return NextResponse.json({ message: 'Hello from libui API!' }) +} +`; + await fs.writeFile(path.join(projectDir, 'app', 'api', 'hello', 'route.ts'), exampleApiRoute, 'utf8'); + apiSpinner.success('API structure created.'); + } catch (error) { + apiSpinner.error('Failed to create API structure.'); + throw error; + } + + // Create both .env and .env.sample with the same content + const envSpinner = spinner('Creating environment file...').start(); + try { + const envContent = `# This file will contain all the required environment variables for the application +# Make sure to update these values in your .env file + +# Database Configuration +DATABASE_URL="postgresql://user:password@localhost:5432/dbname?schema=public" + +# API Configuration +API_BASE_URL="http://localhost:3000/api" +`; + await fs.writeFile(path.join(projectDir, '.env'), envContent, 'utf8'); + envSpinner.success('Environment file created.'); + } catch (error) { + envSpinner.error('Failed to create environment file.'); + throw error; + } + + // Create libuiconfig.json (expanded version) + const config = { + tsx: true, + tailwind: { + config: 'tailwind.config.js', + css: 'styles/globals.css', + baseColor: 'blue', + cssVariables: true, + prefix: '', + }, + rsc: false, + database: { + provider: 'prisma', + schema: 'prisma/schema.prisma', + url: 'DATABASE_URL', + }, + api: { + baseUrl: '/api', + version: 'v1', + cors: { + enabled: true, + origins: ['http://localhost:3000'], + }, + }, + aliases: { + utils: 'utils', + components: 'components', + lib: 'lib', + hooks: 'hooks', + api: 'app/api', + prisma: 'prisma', + }, + }; + + const configPath = path.join(projectDir, 'libuiconfig.json'); + try { + const configSpinner = spinner('Writing libuiconfig.json...').start(); + await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8'); + configSpinner.success('libuiconfig.json written successfully.'); + } catch (error) { + logger.error('Failed to write libuiconfig.json.'); + throw error; + } + + // Install additional dependencies + const dependenciesSpinner = spinner('Installing additional dependencies...').start(); + try { + await execPromise('npm install react react-dom next @prisma/client', { cwd: projectDir }); + dependenciesSpinner.success('Additional dependencies installed.'); + } catch (error) { + dependenciesSpinner.error('Failed to install additional dependencies.'); + throw error; + } + + logger.log(highlighter.success('Initialization complete! You can now:')); + logger.log('1. Update your .env file with your database credentials'); + logger.log('2. Run `npx prisma generate` to generate the Prisma Client'); + logger.log('3. Start your development server with `npm run dev`'); +} + +async function exists(path: string): Promise { + try { + await fs.access(path); + return true; + } catch { + return false; + } +} \ No newline at end of file diff --git a/packages/cli/src/utils/logger.ts b/packages/cli/src/utils/logger.ts new file mode 100644 index 0000000..8d019b0 --- /dev/null +++ b/packages/cli/src/utils/logger.ts @@ -0,0 +1,11 @@ +import chalk from 'chalk'; + +export const logger = { + log: (message: string) => { + console.log(message); + }, + error: (message: string) => { + console.error(chalk.red(message)); + }, + // Add other methods if necessary +}; \ No newline at end of file diff --git a/packages/cli/src/utils/spinner.ts b/packages/cli/src/utils/spinner.ts new file mode 100644 index 0000000..3613e1f --- /dev/null +++ b/packages/cli/src/utils/spinner.ts @@ -0,0 +1,19 @@ +import { createSpinner } from 'nanospinner' + +export function spinner(message: string) { + const sp = createSpinner(message); + return { + start: () => { + sp.start(); + return sp; + }, + succeed: (text?: string) => { + sp.success({ text }); + return sp; + }, + fail: (text?: string) => { + sp.error({ text }); + return sp; + }, + }; +} \ No newline at end of file diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 0000000..12a98ef --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "moduleResolution": "node", + "skipLibCheck": true + }, + "include": ["src/**/*"] +} From cbdc7507b2a40b63b07ee594f38b3aec50e83e6f Mon Sep 17 00:00:00 2001 From: Vincenzo Fanizza Date: Thu, 23 Jan 2025 19:53:17 +0100 Subject: [PATCH 03/12] update gitignore --- .../cache/content-collection-config.mjs | 394 ------------------ ...00dc33ec1ab4a63f4d06cff0d778ce08cd63.cache | 1 - ...a550903d80e0f006a6896cebb95a748dc444.cache | 1 - ...146922446204bfda4c3dd05301e0dd25220c.cache | 1 - .content-collections/cache/mapping.json | 1 - .content-collections/generated/allDocs.js | 72 ---- .content-collections/generated/allPages.js | 2 - .../generated/allShowcases.js | 2 - .content-collections/generated/index.d.ts | 13 - .content-collections/generated/index.js | 7 - .gitignore | 1 + 11 files changed, 1 insertion(+), 494 deletions(-) delete mode 100644 .content-collections/cache/content-collection-config.mjs delete mode 100644 .content-collections/cache/doc/docs/36c920da13347438bad62a209abb00dc33ec1ab4a63f4d06cff0d778ce08cd63.cache delete mode 100644 .content-collections/cache/doc/docs_cli/06eb5a14822b1ac9891f9460e471a550903d80e0f006a6896cebb95a748dc444.cache delete mode 100644 .content-collections/cache/doc/docs_installation/afdc9fa72addad13f3d0cec80a80146922446204bfda4c3dd05301e0dd25220c.cache delete mode 100644 .content-collections/cache/mapping.json delete mode 100644 .content-collections/generated/allDocs.js delete mode 100644 .content-collections/generated/allPages.js delete mode 100644 .content-collections/generated/allShowcases.js delete mode 100644 .content-collections/generated/index.d.ts delete mode 100644 .content-collections/generated/index.js diff --git a/.content-collections/cache/content-collection-config.mjs b/.content-collections/cache/content-collection-config.mjs deleted file mode 100644 index b929b80..0000000 --- a/.content-collections/cache/content-collection-config.mjs +++ /dev/null @@ -1,394 +0,0 @@ -// content-collections.ts -import { defineCollection, defineConfig } from "@content-collections/core"; -import { compileMDX } from "@content-collections/mdx"; -import rehypeAutolinkHeadings from "rehype-autolink-headings"; -import rehypePrettyCode from "rehype-pretty-code"; -import rehypeSlug from "rehype-slug"; -import { codeImport } from "remark-code-import"; -import remarkGfm from "remark-gfm"; -import { createHighlighter } from "shiki"; -import { visit as visit3 } from "unist-util-visit"; - -// lib/rehype-component.ts -import fs from "fs"; -import path from "path"; -import { u } from "unist-builder"; -import { visit } from "unist-util-visit"; - -// __registry__/index.tsx -var Index = {}; - -// components/ui/registry/registry-styles.ts -var styles = [ - { - name: "default", - label: "Default" - } -]; - -// lib/rehype-component.ts -function rehypeComponent() { - return async (tree) => { - visit(tree, (node) => { - const { value: srcPath } = getNodeAttributeByName(node, "src") || {}; - if (node.name === "ComponentSource") { - const name = getNodeAttributeByName(node, "name")?.value; - const fileName = getNodeAttributeByName(node, "fileName")?.value; - if (!name && !srcPath) { - return null; - } - try { - for (const style of styles) { - let src; - if (srcPath) { - src = srcPath; - } else { - const component = Index[style.name][name]; - src = fileName ? component.files.find((file) => { - return file.endsWith(`${fileName}.tsx`) || file.endsWith(`${fileName}.ts`); - }) || component.files[0] : component.files[0]; - } - const filePath = path.join(process.cwd(), src); - let source = fs.readFileSync(filePath, "utf8"); - source = source.replaceAll( - `@/registry/${style.name}/`, - "@/components/" - ); - source = source.replaceAll("export default", "export"); - node.children?.push( - u("element", { - tagName: "pre", - properties: { - __src__: src - }, - attributes: [ - { - name: "styleName", - type: "mdxJsxAttribute", - value: style.name - } - ], - children: [ - u("element", { - tagName: "code", - properties: { - className: ["language-tsx"] - }, - data: { - meta: `event="copy_source_code"` - }, - children: [ - { - type: "text", - value: source - } - ] - }) - ] - }) - ); - } - } catch (error) { - console.error(error); - } - } - if (node.name === "ComponentPreview" || node.name === "BlockPreview") { - const name = getNodeAttributeByName(node, "name")?.value; - if (!name) { - return null; - } - try { - for (const style of styles) { - const component = Index[style.name][name]; - const src = component.files[0]; - const filePath = path.join(process.cwd(), src); - let source = fs.readFileSync(filePath, "utf8"); - source = source.replaceAll( - `@/registry/${style.name}/`, - "@/components/" - ); - source = source.replaceAll("export default", "export"); - node.children?.push( - u("element", { - tagName: "pre", - properties: { - __src__: src - }, - children: [ - u("element", { - tagName: "code", - properties: { - className: ["language-tsx"] - }, - data: { - meta: `event="copy_usage_code"` - }, - children: [ - { - type: "text", - value: source - } - ] - }) - ] - }) - ); - } - } catch (error) { - console.error(error); - } - } - }); - }; -} -function getNodeAttributeByName(node, name) { - return node.attributes?.find((attribute) => attribute.name === name); -} - -// lib/rehype-npm-command.ts -import { visit as visit2 } from "unist-util-visit"; -function rehypeNpmCommand() { - return (tree) => { - visit2(tree, (node) => { - if (node.type !== "element" || node?.tagName !== "pre") { - return; - } - if (node.properties?.["__rawString__"]?.startsWith("npm install")) { - const npmCommand = node.properties?.["__rawString__"]; - node.properties["__npmCommand__"] = npmCommand; - node.properties["__yarnCommand__"] = npmCommand.replace( - "npm install", - "yarn add" - ); - node.properties["__pnpmCommand__"] = npmCommand.replace( - "npm install", - "pnpm add" - ); - node.properties["__bunCommand__"] = npmCommand.replace( - "npm install", - "bun add" - ); - } - if (node.properties?.["__rawString__"]?.startsWith("npx create-")) { - const npmCommand = node.properties?.["__rawString__"]; - node.properties["__npmCommand__"] = npmCommand; - node.properties["__yarnCommand__"] = npmCommand.replace( - "npx create-", - "yarn create " - ); - node.properties["__pnpmCommand__"] = npmCommand.replace( - "npx create-", - "pnpm create " - ); - node.properties["__bunCommand__"] = npmCommand.replace( - "npx", - "bunx --bun" - ); - } - if (node.properties?.["__rawString__"]?.startsWith("npx") && !node.properties?.["__rawString__"]?.startsWith("npx create-")) { - const npmCommand = node.properties?.["__rawString__"]; - node.properties["__npmCommand__"] = npmCommand; - node.properties["__yarnCommand__"] = npmCommand; - node.properties["__pnpmCommand__"] = npmCommand.replace( - "npx", - "pnpm dlx" - ); - node.properties["__bunCommand__"] = npmCommand.replace( - "npx", - "bunx --bun" - ); - } - if (node.properties?.["__rawString__"]?.startsWith("npm create")) { - const npmCommand = node.properties?.["__rawString__"]; - node.properties["__npmCommand__"] = npmCommand; - node.properties["__yarnCommand__"] = npmCommand.replace( - "npm create", - "yarn create" - ); - node.properties["__pnpmCommand__"] = npmCommand.replace( - "npm create", - "pnpm create" - ); - node.properties["__bunCommand__"] = npmCommand.replace( - "npm create", - "bun create" - ); - } - }); - }; -} - -// content-collections.ts -var prettyCodeOptions = { - theme: "github-dark", - getHighlighter: (options) => createHighlighter({ - ...options - }), - onVisitLine(node) { - if (node.children.length === 0) { - node.children = [{ type: "text", value: " " }]; - } - }, - onVisitHighlightedLine(node) { - if (!node.properties.className) { - node.properties.className = []; - } - node.properties.className.push("line--highlighted"); - }, - onVisitHighlightedChars(node) { - if (!node.properties.className) { - node.properties.className = []; - } - node.properties.className = ["word--highlighted"]; - } -}; -var showcase = defineCollection({ - name: "Showcase", - directory: "content/showcase", - include: "**/*.mdx", - schema: (z) => ({ - title: z.string(), - description: z.string(), - image: z.string(), - href: z.string(), - affiliation: z.string(), - featured: z.boolean().optional().default(false) - }), - transform: async (document, context) => { - const body = await compileMDX(context, document, { - remarkPlugins: [codeImport, remarkGfm] - }); - return { - ...document, - slug: `/showcase/${document._meta.path}`, - slugAsParams: document._meta.path, - body: { - raw: document.content, - code: body - } - }; - } -}); -var pages = defineCollection({ - name: "Page", - directory: "content/pages", - include: "**/*.mdx", - schema: (z) => ({ - title: z.string(), - description: z.string() - }), - transform: async (document, context) => { - const body = await compileMDX(context, document, { - remarkPlugins: [codeImport, remarkGfm] - }); - return { - ...document, - slug: `/${document._meta.path}`, - slugAsParams: document._meta.path, - body: { - raw: document.content, - code: body - } - }; - } -}); -var documents = defineCollection({ - name: "Doc", - directory: "content", - include: "**/*.mdx", - schema: (z) => ({ - title: z.string(), - description: z.string(), - published: z.boolean().default(true), - date: z.string().optional(), - links: z.object({ - doc: z.string().optional(), - api: z.string().optional() - }).optional(), - featured: z.boolean().optional().default(false), - component: z.boolean().optional().default(false), - toc: z.boolean().optional().default(true), - image: z.string().optional() - }), - transform: async (document, context) => { - const body = await compileMDX(context, document, { - remarkPlugins: [codeImport, remarkGfm], - rehypePlugins: [ - rehypeSlug, - rehypeComponent, - () => (tree) => { - visit3(tree, (node) => { - if (node?.type === "element" && node?.tagName === "pre") { - const [codeEl] = node.children; - if (codeEl.tagName !== "code") { - return; - } - if (codeEl.data?.meta) { - const regex = /event="([^"]*)"/; - const match = codeEl.data?.meta.match(regex); - if (match) { - node.__event__ = match ? match[1] : null; - codeEl.data.meta = codeEl.data.meta.replace(regex, ""); - } - } - node.__rawString__ = codeEl.children?.[0].value; - node.__src__ = node.properties?.__src__; - node.__style__ = node.properties?.__style__; - } - }); - }, - [rehypePrettyCode, prettyCodeOptions], - () => (tree) => { - visit3(tree, (node) => { - if (node?.type === "element" && node?.tagName === "figure") { - if (!("data-rehype-pretty-code-figure" in node.properties)) { - return; - } - const preElement = node.children.at(-1); - if (preElement.tagName !== "pre") { - return; - } - preElement.properties["__withMeta__"] = node.children.at(0).tagName === "div"; - preElement.properties["__rawString__"] = node.__rawString__; - if (node.__src__) { - preElement.properties["__src__"] = node.__src__; - } - if (node.__event__) { - preElement.properties["__event__"] = node.__event__; - } - if (node.__style__) { - preElement.properties["__style__"] = node.__style__; - } - } - }); - }, - rehypeNpmCommand, - [ - rehypeAutolinkHeadings, - { - properties: { - className: ["subheading-anchor"], - ariaLabel: "Link to section" - } - } - ] - ] - }); - return { - ...document, - image: `${process.env.NEXT_PUBLIC_APP_URL}/og?title=${encodeURI(document.title)}`, - slug: `/${document._meta.path}`, - slugAsParams: document._meta.path.split("/").slice(1).join("/"), - body: { - raw: document.content, - code: body - } - }; - } -}); -var content_collections_default = defineConfig({ - collections: [documents, pages, showcase] -}); -export { - content_collections_default as default -}; diff --git a/.content-collections/cache/doc/docs/36c920da13347438bad62a209abb00dc33ec1ab4a63f4d06cff0d778ce08cd63.cache b/.content-collections/cache/doc/docs/36c920da13347438bad62a209abb00dc33ec1ab4a63f4d06cff0d778ce08cd63.cache deleted file mode 100644 index e9cee9e..0000000 --- a/.content-collections/cache/doc/docs/36c920da13347438bad62a209abb00dc33ec1ab4a63f4d06cff0d778ce08cd63.cache +++ /dev/null @@ -1 +0,0 @@ -"var Component=(()=>{var u=Object.create;var l=Object.defineProperty;var p=Object.getOwnPropertyDescriptor;var m=Object.getOwnPropertyNames;var g=Object.getPrototypeOf,b=Object.prototype.hasOwnProperty;var f=(i,n)=>()=>(n||i((n={exports:{}}).exports,n),n.exports),y=(i,n)=>{for(var o in n)l(i,o,{get:n[o],enumerable:!0})},r=(i,n,o,a)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let t of m(n))!b.call(i,t)&&t!==o&&l(i,t,{get:()=>n[t],enumerable:!(a=p(n,t))||a.enumerable});return i};var k=(i,n,o)=>(o=i!=null?u(g(i)):{},r(n||!i||!i.__esModule?l(o,\"default\",{value:i,enumerable:!0}):o,i)),w=i=>r(l({},\"__esModule\",{value:!0}),i);var s=f((I,c)=>{c.exports=_jsx_runtime});var x={};y(x,{default:()=>h});var e=k(s());function d(i){let n={a:\"a\",h2:\"h2\",h3:\"h3\",li:\"li\",p:\"p\",span:\"span\",strong:\"strong\",ul:\"ul\",...i.components};return(0,e.jsxs)(e.Fragment,{children:[(0,e.jsx)(n.p,{children:\"Lib UI is a revolutionary full-stack component library that combines the power of frontend components with backend functionality. Build production-ready applications faster than ever before.\"}),`\n`,(0,e.jsxs)(n.h2,{id:\"why-lib-ui\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#why-lib-ui\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Why Lib UI?\"]}),`\n`,(0,e.jsx)(n.p,{children:\"In today's JavaScript ecosystem, we have countless libraries for frontend components and backend tools. But what if you could have both in one unified solution?\"}),`\n`,(0,e.jsx)(n.p,{children:\"Lib UI bridges this gap by offering:\"}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Full-stack Components\"}),\": Get both UI and logic in a single package\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Ready-to-use Solutions\"}),\": Implement complex features like authentication and payments with one command\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Customizable Building Blocks\"}),\": Maintain full control while leveraging pre-built functionality\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Developer Experience\"}),\": Focus on building your product, not wrestling with documentation\"]}),`\n`]}),`\n`,(0,e.jsxs)(n.h3,{id:\"real-world-examples\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#real-world-examples\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Real-world Examples\"]}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Authentication\"}),\": Implement secure user authentication with both UI components and backend logic using a single command\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Payment Integration\"}),\": Add Stripe payments to your app without spending hours reading documentation\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Admin Dashboards\"}),\": Deploy fully functional admin interfaces that connect directly to your data\"]}),`\n`]}),`\n`,(0,e.jsxs)(n.h2,{id:\"our-mission\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#our-mission\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Our Mission\"]}),`\n`,(0,e.jsx)(n.p,{children:\"We believe developers should spend more time building unique features for their applications and less time implementing common functionality. Lib UI provides pre-built, customizable full-stack components that handle both frontend and backend concerns, allowing you to:\"}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsx)(n.li,{children:\"\\u{1F680} Ship products faster\"}),`\n`,(0,e.jsx)(n.li,{children:\"\\u{1F6E0}\\uFE0F Reduce boilerplate code\"}),`\n`,(0,e.jsx)(n.li,{children:\"\\u26A1 Focus on core business logic\"}),`\n`,(0,e.jsx)(n.li,{children:\"\\u{1F3A8} Maintain design flexibility\"}),`\n`]}),`\n`,(0,e.jsxs)(n.h2,{id:\"inspiration\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#inspiration\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Inspiration\"]}),`\n`,(0,e.jsx)(n.p,{children:\"This project stands on the shoulders of those before us. We're grateful for the incredible open-source work of:\"}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.a,{href:\"https://ui.shadcn.com/\",children:\"shadcn/ui\"}),\" - For pioneering component architecture\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.a,{href:\"https://magicui.design/\",children:\"MagicUI\"}),\" - For innovative design patterns\"]}),`\n`]})]})}function h(i={}){let{wrapper:n}=i.components||{};return n?(0,e.jsx)(n,{...i,children:(0,e.jsx)(d,{...i})}):d(i)}return w(x);})();\n;return Component;" \ No newline at end of file diff --git a/.content-collections/cache/doc/docs_cli/06eb5a14822b1ac9891f9460e471a550903d80e0f006a6896cebb95a748dc444.cache b/.content-collections/cache/doc/docs_cli/06eb5a14822b1ac9891f9460e471a550903d80e0f006a6896cebb95a748dc444.cache deleted file mode 100644 index e9cee9e..0000000 --- a/.content-collections/cache/doc/docs_cli/06eb5a14822b1ac9891f9460e471a550903d80e0f006a6896cebb95a748dc444.cache +++ /dev/null @@ -1 +0,0 @@ -"var Component=(()=>{var u=Object.create;var l=Object.defineProperty;var p=Object.getOwnPropertyDescriptor;var m=Object.getOwnPropertyNames;var g=Object.getPrototypeOf,b=Object.prototype.hasOwnProperty;var f=(i,n)=>()=>(n||i((n={exports:{}}).exports,n),n.exports),y=(i,n)=>{for(var o in n)l(i,o,{get:n[o],enumerable:!0})},r=(i,n,o,a)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let t of m(n))!b.call(i,t)&&t!==o&&l(i,t,{get:()=>n[t],enumerable:!(a=p(n,t))||a.enumerable});return i};var k=(i,n,o)=>(o=i!=null?u(g(i)):{},r(n||!i||!i.__esModule?l(o,\"default\",{value:i,enumerable:!0}):o,i)),w=i=>r(l({},\"__esModule\",{value:!0}),i);var s=f((I,c)=>{c.exports=_jsx_runtime});var x={};y(x,{default:()=>h});var e=k(s());function d(i){let n={a:\"a\",h2:\"h2\",h3:\"h3\",li:\"li\",p:\"p\",span:\"span\",strong:\"strong\",ul:\"ul\",...i.components};return(0,e.jsxs)(e.Fragment,{children:[(0,e.jsx)(n.p,{children:\"Lib UI is a revolutionary full-stack component library that combines the power of frontend components with backend functionality. Build production-ready applications faster than ever before.\"}),`\n`,(0,e.jsxs)(n.h2,{id:\"why-lib-ui\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#why-lib-ui\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Why Lib UI?\"]}),`\n`,(0,e.jsx)(n.p,{children:\"In today's JavaScript ecosystem, we have countless libraries for frontend components and backend tools. But what if you could have both in one unified solution?\"}),`\n`,(0,e.jsx)(n.p,{children:\"Lib UI bridges this gap by offering:\"}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Full-stack Components\"}),\": Get both UI and logic in a single package\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Ready-to-use Solutions\"}),\": Implement complex features like authentication and payments with one command\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Customizable Building Blocks\"}),\": Maintain full control while leveraging pre-built functionality\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Developer Experience\"}),\": Focus on building your product, not wrestling with documentation\"]}),`\n`]}),`\n`,(0,e.jsxs)(n.h3,{id:\"real-world-examples\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#real-world-examples\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Real-world Examples\"]}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Authentication\"}),\": Implement secure user authentication with both UI components and backend logic using a single command\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Payment Integration\"}),\": Add Stripe payments to your app without spending hours reading documentation\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Admin Dashboards\"}),\": Deploy fully functional admin interfaces that connect directly to your data\"]}),`\n`]}),`\n`,(0,e.jsxs)(n.h2,{id:\"our-mission\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#our-mission\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Our Mission\"]}),`\n`,(0,e.jsx)(n.p,{children:\"We believe developers should spend more time building unique features for their applications and less time implementing common functionality. Lib UI provides pre-built, customizable full-stack components that handle both frontend and backend concerns, allowing you to:\"}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsx)(n.li,{children:\"\\u{1F680} Ship products faster\"}),`\n`,(0,e.jsx)(n.li,{children:\"\\u{1F6E0}\\uFE0F Reduce boilerplate code\"}),`\n`,(0,e.jsx)(n.li,{children:\"\\u26A1 Focus on core business logic\"}),`\n`,(0,e.jsx)(n.li,{children:\"\\u{1F3A8} Maintain design flexibility\"}),`\n`]}),`\n`,(0,e.jsxs)(n.h2,{id:\"inspiration\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#inspiration\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Inspiration\"]}),`\n`,(0,e.jsx)(n.p,{children:\"This project stands on the shoulders of those before us. We're grateful for the incredible open-source work of:\"}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.a,{href:\"https://ui.shadcn.com/\",children:\"shadcn/ui\"}),\" - For pioneering component architecture\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.a,{href:\"https://magicui.design/\",children:\"MagicUI\"}),\" - For innovative design patterns\"]}),`\n`]})]})}function h(i={}){let{wrapper:n}=i.components||{};return n?(0,e.jsx)(n,{...i,children:(0,e.jsx)(d,{...i})}):d(i)}return w(x);})();\n;return Component;" \ No newline at end of file diff --git a/.content-collections/cache/doc/docs_installation/afdc9fa72addad13f3d0cec80a80146922446204bfda4c3dd05301e0dd25220c.cache b/.content-collections/cache/doc/docs_installation/afdc9fa72addad13f3d0cec80a80146922446204bfda4c3dd05301e0dd25220c.cache deleted file mode 100644 index 091782b..0000000 --- a/.content-collections/cache/doc/docs_installation/afdc9fa72addad13f3d0cec80a80146922446204bfda4c3dd05301e0dd25220c.cache +++ /dev/null @@ -1 +0,0 @@ -"var Component=(()=>{var u=Object.create;var l=Object.defineProperty;var g=Object.getOwnPropertyDescriptor;var m=Object.getOwnPropertyNames;var y=Object.getPrototypeOf,_=Object.prototype.hasOwnProperty;var x=(a,n)=>()=>(n||a((n={exports:{}}).exports,n),n.exports),b=(a,n)=>{for(var t in n)l(a,t,{get:n[t],enumerable:!0})},o=(a,n,t,r)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let i of m(n))!_.call(a,i)&&i!==t&&l(a,i,{get:()=>n[i],enumerable:!(r=g(n,i))||r.enumerable});return a};var E=(a,n,t)=>(t=a!=null?u(y(a)):{},o(n||!a||!a.__esModule?l(t,\"default\",{value:a,enumerable:!0}):t,a)),f=a=>o(l({},\"__esModule\",{value:!0}),a);var c=x((j,d)=>{d.exports=_jsx_runtime});var F={};b(F,{default:()=>p});var e=E(c());function s(a){let n={a:\"a\",code:\"code\",figure:\"figure\",h3:\"h3\",p:\"p\",pre:\"pre\",span:\"span\",strong:\"strong\",...a.components},{Callout:t,Steps:r}=n;return t||h(\"Callout\",!0),r||h(\"Steps\",!0),(0,e.jsxs)(e.Fragment,{children:[(0,e.jsx)(t,{children:(0,e.jsx)(n.p,{children:(0,e.jsx)(n.strong,{children:\"We are currently only available on nextjs, stay tuned for further updates.\"})})}),`\n`,(0,e.jsxs)(r,{children:[(0,e.jsxs)(n.h3,{id:\"create-project\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#create-project\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Create project\"]}),(0,e.jsxs)(n.p,{children:[\"Run the \",(0,e.jsx)(n.code,{children:\"init\"}),\" command to create a new Next.js project or to setup an existing one:\"]}),(0,e.jsx)(n.figure,{\"data-rehype-pretty-code-figure\":\"\",children:(0,e.jsx)(n.pre,{style:{backgroundColor:\"#24292e\",color:\"#e1e4e8\"},tabIndex:\"0\",\"data-language\":\"bash\",\"data-theme\":\"github-dark\",__rawString__:`npx libui-next init\n`,__npmCommand__:`npx libui-next init\n`,__yarnCommand__:`npx libui-next init\n`,__pnpmCommand__:`pnpm dlx libui-next init\n`,__bunCommand__:`bunx --bun libui-next init\n`,children:(0,e.jsx)(n.code,{\"data-language\":\"bash\",\"data-theme\":\"github-dark\",style:{display:\"grid\"},children:(0,e.jsxs)(n.span,{\"data-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#B392F0\"},children:\"npx\"}),(0,e.jsx)(n.span,{style:{color:\"#9ECBFF\"},children:\" libui-next\"}),(0,e.jsx)(n.span,{style:{color:\"#9ECBFF\"},children:\" init\"})]})})})}),(0,e.jsxs)(n.h3,{id:\"start-coding\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#start-coding\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Start coding\"]}),(0,e.jsx)(n.p,{children:\"You can now start adding full stack components to your project!\"}),(0,e.jsx)(n.figure,{\"data-rehype-pretty-code-figure\":\"\",children:(0,e.jsx)(n.pre,{style:{backgroundColor:\"#24292e\",color:\"#e1e4e8\"},tabIndex:\"0\",\"data-language\":\"bash\",\"data-theme\":\"github-dark\",__rawString__:`npx libui-next add authjs\n`,__npmCommand__:`npx libui-next add authjs\n`,__yarnCommand__:`npx libui-next add authjs\n`,__pnpmCommand__:`pnpm dlx libui-next add authjs\n`,__bunCommand__:`bunx --bun libui-next add authjs\n`,children:(0,e.jsx)(n.code,{\"data-language\":\"bash\",\"data-theme\":\"github-dark\",style:{display:\"grid\"},children:(0,e.jsxs)(n.span,{\"data-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#B392F0\"},children:\"npx\"}),(0,e.jsx)(n.span,{style:{color:\"#9ECBFF\"},children:\" libui-next\"}),(0,e.jsx)(n.span,{style:{color:\"#9ECBFF\"},children:\" add\"}),(0,e.jsx)(n.span,{style:{color:\"#9ECBFF\"},children:\" authjs\"})]})})})}),(0,e.jsxs)(n.p,{children:[\"This will add all the configuration of \",(0,e.jsx)(n.code,{children:\"Auth.js\"}),\" to your project.\"]}),(0,e.jsx)(n.figure,{\"data-rehype-pretty-code-figure\":\"\",children:(0,e.jsx)(n.pre,{style:{backgroundColor:\"#24292e\",color:\"#e1e4e8\"},tabIndex:\"0\",\"data-language\":\"tsx\",\"data-theme\":\"github-dark\",__rawString__:`import { SignUpForm } from '@/components/auth/sign-up-form'\n\nexport default function SignUp() {\n return (\n
\n \n
\n )\n}\n`,children:(0,e.jsxs)(n.code,{\"data-line-numbers\":\"\",\"data-language\":\"tsx\",\"data-theme\":\"github-dark\",style:{display:\"grid\"},\"data-line-numbers-max-digits\":\"1\",children:[(0,e.jsxs)(n.span,{className:\"line--highlighted\",\"data-line\":\"\",\"data-highlighted-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#F97583\"},children:\"import\"}),(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\" { SignUpForm } \"}),(0,e.jsx)(n.span,{style:{color:\"#F97583\"},children:\"from\"}),(0,e.jsx)(n.span,{style:{color:\"#9ECBFF\"},children:\" '@/components/auth/sign-up-form'\"})]}),`\n`,(0,e.jsx)(n.span,{\"data-line\":\"\",children:\" \"}),`\n`,(0,e.jsxs)(n.span,{\"data-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#F97583\"},children:\"export\"}),(0,e.jsx)(n.span,{style:{color:\"#F97583\"},children:\" default\"}),(0,e.jsx)(n.span,{style:{color:\"#F97583\"},children:\" function\"}),(0,e.jsx)(n.span,{style:{color:\"#B392F0\"},children:\" SignUp\"}),(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\"() {\"})]}),`\n`,(0,e.jsxs)(n.span,{\"data-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#F97583\"},children:\" return\"}),(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\" (\"})]}),`\n`,(0,e.jsxs)(n.span,{\"data-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\" <\"}),(0,e.jsx)(n.span,{style:{color:\"#85E89D\"},children:\"div\"}),(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\">\"})]}),`\n`,(0,e.jsxs)(n.span,{className:\"line--highlighted\",\"data-line\":\"\",\"data-highlighted-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\" <\"}),(0,e.jsx)(n.span,{style:{color:\"#79B8FF\"},children:\"SignUpForm\"}),(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\" />\"})]}),`\n`,(0,e.jsxs)(n.span,{\"data-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\" \"})]}),`\n`,(0,e.jsx)(n.span,{\"data-line\":\"\",children:(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\" )\"})}),`\n`,(0,e.jsx)(n.span,{\"data-line\":\"\",children:(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\"}\"})})]})})})]})]})}function p(a={}){let{wrapper:n}=a.components||{};return n?(0,e.jsx)(n,{...a,children:(0,e.jsx)(s,{...a})}):s(a)}function h(a,n){throw new Error(\"Expected \"+(n?\"component\":\"object\")+\" `\"+a+\"` to be defined: you likely forgot to import, pass, or provide it.\")}return f(F);})();\n;return Component;" \ No newline at end of file diff --git a/.content-collections/cache/mapping.json b/.content-collections/cache/mapping.json deleted file mode 100644 index c7612ab..0000000 --- a/.content-collections/cache/mapping.json +++ /dev/null @@ -1 +0,0 @@ -{"Doc":{"docs":["36c920da13347438bad62a209abb00dc33ec1ab4a63f4d06cff0d778ce08cd63"],"docs/cli":["06eb5a14822b1ac9891f9460e471a550903d80e0f006a6896cebb95a748dc444"],"docs/installation":["afdc9fa72addad13f3d0cec80a80146922446204bfda4c3dd05301e0dd25220c"]}} \ No newline at end of file diff --git a/.content-collections/generated/allDocs.js b/.content-collections/generated/allDocs.js deleted file mode 100644 index 3d818fc..0000000 --- a/.content-collections/generated/allDocs.js +++ /dev/null @@ -1,72 +0,0 @@ - -export default [ - { - "content": "Lib UI is a revolutionary full-stack component library that combines the power of frontend components with backend functionality. Build production-ready applications faster than ever before.\n\n## Why Lib UI?\n\nIn today's JavaScript ecosystem, we have countless libraries for frontend components and backend tools. But what if you could have both in one unified solution?\n\nLib UI bridges this gap by offering:\n\n- **Full-stack Components**: Get both UI and logic in a single package\n- **Ready-to-use Solutions**: Implement complex features like authentication and payments with one command\n- **Customizable Building Blocks**: Maintain full control while leveraging pre-built functionality\n- **Developer Experience**: Focus on building your product, not wrestling with documentation\n\n### Real-world Examples\n\n- **Authentication**: Implement secure user authentication with both UI components and backend logic using a single command\n- **Payment Integration**: Add Stripe payments to your app without spending hours reading documentation\n- **Admin Dashboards**: Deploy fully functional admin interfaces that connect directly to your data\n\n## Our Mission\n\nWe believe developers should spend more time building unique features for their applications and less time implementing common functionality. Lib UI provides pre-built, customizable full-stack components that handle both frontend and backend concerns, allowing you to:\n\n- 🚀 Ship products faster\n- 🛠️ Reduce boilerplate code\n- ⚡ Focus on core business logic\n- 🎨 Maintain design flexibility\n\n## Inspiration\n\nThis project stands on the shoulders of those before us. We're grateful for the incredible open-source work of:\n\n- [shadcn/ui](https://ui.shadcn.com/) - For pioneering component architecture\n- [MagicUI](https://magicui.design/) - For innovative design patterns", - "title": "CLI", - "description": "Build full-stack applications faster with pre-built, customizable components", - "published": true, - "featured": false, - "component": false, - "toc": true, - "_meta": { - "filePath": "docs/cli.mdx", - "fileName": "cli.mdx", - "directory": "docs", - "extension": "mdx", - "path": "docs/cli" - }, - "image": "http://localhost:3000/og?title=CLI", - "slug": "/docs/cli", - "slugAsParams": "cli", - "body": { - "raw": "Lib UI is a revolutionary full-stack component library that combines the power of frontend components with backend functionality. Build production-ready applications faster than ever before.\n\n## Why Lib UI?\n\nIn today's JavaScript ecosystem, we have countless libraries for frontend components and backend tools. But what if you could have both in one unified solution?\n\nLib UI bridges this gap by offering:\n\n- **Full-stack Components**: Get both UI and logic in a single package\n- **Ready-to-use Solutions**: Implement complex features like authentication and payments with one command\n- **Customizable Building Blocks**: Maintain full control while leveraging pre-built functionality\n- **Developer Experience**: Focus on building your product, not wrestling with documentation\n\n### Real-world Examples\n\n- **Authentication**: Implement secure user authentication with both UI components and backend logic using a single command\n- **Payment Integration**: Add Stripe payments to your app without spending hours reading documentation\n- **Admin Dashboards**: Deploy fully functional admin interfaces that connect directly to your data\n\n## Our Mission\n\nWe believe developers should spend more time building unique features for their applications and less time implementing common functionality. Lib UI provides pre-built, customizable full-stack components that handle both frontend and backend concerns, allowing you to:\n\n- 🚀 Ship products faster\n- 🛠️ Reduce boilerplate code\n- ⚡ Focus on core business logic\n- 🎨 Maintain design flexibility\n\n## Inspiration\n\nThis project stands on the shoulders of those before us. We're grateful for the incredible open-source work of:\n\n- [shadcn/ui](https://ui.shadcn.com/) - For pioneering component architecture\n- [MagicUI](https://magicui.design/) - For innovative design patterns", - "code": "var Component=(()=>{var u=Object.create;var l=Object.defineProperty;var p=Object.getOwnPropertyDescriptor;var m=Object.getOwnPropertyNames;var g=Object.getPrototypeOf,b=Object.prototype.hasOwnProperty;var f=(i,n)=>()=>(n||i((n={exports:{}}).exports,n),n.exports),y=(i,n)=>{for(var o in n)l(i,o,{get:n[o],enumerable:!0})},r=(i,n,o,a)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let t of m(n))!b.call(i,t)&&t!==o&&l(i,t,{get:()=>n[t],enumerable:!(a=p(n,t))||a.enumerable});return i};var k=(i,n,o)=>(o=i!=null?u(g(i)):{},r(n||!i||!i.__esModule?l(o,\"default\",{value:i,enumerable:!0}):o,i)),w=i=>r(l({},\"__esModule\",{value:!0}),i);var s=f((I,c)=>{c.exports=_jsx_runtime});var x={};y(x,{default:()=>h});var e=k(s());function d(i){let n={a:\"a\",h2:\"h2\",h3:\"h3\",li:\"li\",p:\"p\",span:\"span\",strong:\"strong\",ul:\"ul\",...i.components};return(0,e.jsxs)(e.Fragment,{children:[(0,e.jsx)(n.p,{children:\"Lib UI is a revolutionary full-stack component library that combines the power of frontend components with backend functionality. Build production-ready applications faster than ever before.\"}),`\n`,(0,e.jsxs)(n.h2,{id:\"why-lib-ui\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#why-lib-ui\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Why Lib UI?\"]}),`\n`,(0,e.jsx)(n.p,{children:\"In today's JavaScript ecosystem, we have countless libraries for frontend components and backend tools. But what if you could have both in one unified solution?\"}),`\n`,(0,e.jsx)(n.p,{children:\"Lib UI bridges this gap by offering:\"}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Full-stack Components\"}),\": Get both UI and logic in a single package\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Ready-to-use Solutions\"}),\": Implement complex features like authentication and payments with one command\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Customizable Building Blocks\"}),\": Maintain full control while leveraging pre-built functionality\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Developer Experience\"}),\": Focus on building your product, not wrestling with documentation\"]}),`\n`]}),`\n`,(0,e.jsxs)(n.h3,{id:\"real-world-examples\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#real-world-examples\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Real-world Examples\"]}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Authentication\"}),\": Implement secure user authentication with both UI components and backend logic using a single command\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Payment Integration\"}),\": Add Stripe payments to your app without spending hours reading documentation\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Admin Dashboards\"}),\": Deploy fully functional admin interfaces that connect directly to your data\"]}),`\n`]}),`\n`,(0,e.jsxs)(n.h2,{id:\"our-mission\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#our-mission\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Our Mission\"]}),`\n`,(0,e.jsx)(n.p,{children:\"We believe developers should spend more time building unique features for their applications and less time implementing common functionality. Lib UI provides pre-built, customizable full-stack components that handle both frontend and backend concerns, allowing you to:\"}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsx)(n.li,{children:\"\\u{1F680} Ship products faster\"}),`\n`,(0,e.jsx)(n.li,{children:\"\\u{1F6E0}\\uFE0F Reduce boilerplate code\"}),`\n`,(0,e.jsx)(n.li,{children:\"\\u26A1 Focus on core business logic\"}),`\n`,(0,e.jsx)(n.li,{children:\"\\u{1F3A8} Maintain design flexibility\"}),`\n`]}),`\n`,(0,e.jsxs)(n.h2,{id:\"inspiration\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#inspiration\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Inspiration\"]}),`\n`,(0,e.jsx)(n.p,{children:\"This project stands on the shoulders of those before us. We're grateful for the incredible open-source work of:\"}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.a,{href:\"https://ui.shadcn.com/\",children:\"shadcn/ui\"}),\" - For pioneering component architecture\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.a,{href:\"https://magicui.design/\",children:\"MagicUI\"}),\" - For innovative design patterns\"]}),`\n`]})]})}function h(i={}){let{wrapper:n}=i.components||{};return n?(0,e.jsx)(n,{...i,children:(0,e.jsx)(d,{...i})}):d(i)}return w(x);})();\n;return Component;" - } - }, - { - "content": "Lib UI is a revolutionary full-stack component library that combines the power of frontend components with backend functionality. Build production-ready applications faster than ever before.\n\n## Why Lib UI?\n\nIn today's JavaScript ecosystem, we have countless libraries for frontend components and backend tools. But what if you could have both in one unified solution?\n\nLib UI bridges this gap by offering:\n\n- **Full-stack Components**: Get both UI and logic in a single package\n- **Ready-to-use Solutions**: Implement complex features like authentication and payments with one command\n- **Customizable Building Blocks**: Maintain full control while leveraging pre-built functionality\n- **Developer Experience**: Focus on building your product, not wrestling with documentation\n\n### Real-world Examples\n\n- **Authentication**: Implement secure user authentication with both UI components and backend logic using a single command\n- **Payment Integration**: Add Stripe payments to your app without spending hours reading documentation\n- **Admin Dashboards**: Deploy fully functional admin interfaces that connect directly to your data\n\n## Our Mission\n\nWe believe developers should spend more time building unique features for their applications and less time implementing common functionality. Lib UI provides pre-built, customizable full-stack components that handle both frontend and backend concerns, allowing you to:\n\n- 🚀 Ship products faster\n- 🛠️ Reduce boilerplate code\n- ⚡ Focus on core business logic\n- 🎨 Maintain design flexibility\n\n## Inspiration\n\nThis project stands on the shoulders of those before us. We're grateful for the incredible open-source work of:\n\n- [shadcn/ui](https://ui.shadcn.com/) - For pioneering component architecture\n- [MagicUI](https://magicui.design/) - For innovative design patterns", - "title": "Introduction", - "description": "Build full-stack applications faster with pre-built, customizable components", - "published": true, - "featured": false, - "component": false, - "toc": true, - "_meta": { - "filePath": "docs/index.mdx", - "fileName": "index.mdx", - "directory": "docs", - "extension": "mdx", - "path": "docs" - }, - "image": "http://localhost:3000/og?title=Introduction", - "slug": "/docs", - "slugAsParams": "", - "body": { - "raw": "Lib UI is a revolutionary full-stack component library that combines the power of frontend components with backend functionality. Build production-ready applications faster than ever before.\n\n## Why Lib UI?\n\nIn today's JavaScript ecosystem, we have countless libraries for frontend components and backend tools. But what if you could have both in one unified solution?\n\nLib UI bridges this gap by offering:\n\n- **Full-stack Components**: Get both UI and logic in a single package\n- **Ready-to-use Solutions**: Implement complex features like authentication and payments with one command\n- **Customizable Building Blocks**: Maintain full control while leveraging pre-built functionality\n- **Developer Experience**: Focus on building your product, not wrestling with documentation\n\n### Real-world Examples\n\n- **Authentication**: Implement secure user authentication with both UI components and backend logic using a single command\n- **Payment Integration**: Add Stripe payments to your app without spending hours reading documentation\n- **Admin Dashboards**: Deploy fully functional admin interfaces that connect directly to your data\n\n## Our Mission\n\nWe believe developers should spend more time building unique features for their applications and less time implementing common functionality. Lib UI provides pre-built, customizable full-stack components that handle both frontend and backend concerns, allowing you to:\n\n- 🚀 Ship products faster\n- 🛠️ Reduce boilerplate code\n- ⚡ Focus on core business logic\n- 🎨 Maintain design flexibility\n\n## Inspiration\n\nThis project stands on the shoulders of those before us. We're grateful for the incredible open-source work of:\n\n- [shadcn/ui](https://ui.shadcn.com/) - For pioneering component architecture\n- [MagicUI](https://magicui.design/) - For innovative design patterns", - "code": "var Component=(()=>{var u=Object.create;var l=Object.defineProperty;var p=Object.getOwnPropertyDescriptor;var m=Object.getOwnPropertyNames;var g=Object.getPrototypeOf,b=Object.prototype.hasOwnProperty;var f=(i,n)=>()=>(n||i((n={exports:{}}).exports,n),n.exports),y=(i,n)=>{for(var o in n)l(i,o,{get:n[o],enumerable:!0})},r=(i,n,o,a)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let t of m(n))!b.call(i,t)&&t!==o&&l(i,t,{get:()=>n[t],enumerable:!(a=p(n,t))||a.enumerable});return i};var k=(i,n,o)=>(o=i!=null?u(g(i)):{},r(n||!i||!i.__esModule?l(o,\"default\",{value:i,enumerable:!0}):o,i)),w=i=>r(l({},\"__esModule\",{value:!0}),i);var s=f((I,c)=>{c.exports=_jsx_runtime});var x={};y(x,{default:()=>h});var e=k(s());function d(i){let n={a:\"a\",h2:\"h2\",h3:\"h3\",li:\"li\",p:\"p\",span:\"span\",strong:\"strong\",ul:\"ul\",...i.components};return(0,e.jsxs)(e.Fragment,{children:[(0,e.jsx)(n.p,{children:\"Lib UI is a revolutionary full-stack component library that combines the power of frontend components with backend functionality. Build production-ready applications faster than ever before.\"}),`\n`,(0,e.jsxs)(n.h2,{id:\"why-lib-ui\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#why-lib-ui\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Why Lib UI?\"]}),`\n`,(0,e.jsx)(n.p,{children:\"In today's JavaScript ecosystem, we have countless libraries for frontend components and backend tools. But what if you could have both in one unified solution?\"}),`\n`,(0,e.jsx)(n.p,{children:\"Lib UI bridges this gap by offering:\"}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Full-stack Components\"}),\": Get both UI and logic in a single package\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Ready-to-use Solutions\"}),\": Implement complex features like authentication and payments with one command\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Customizable Building Blocks\"}),\": Maintain full control while leveraging pre-built functionality\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Developer Experience\"}),\": Focus on building your product, not wrestling with documentation\"]}),`\n`]}),`\n`,(0,e.jsxs)(n.h3,{id:\"real-world-examples\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#real-world-examples\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Real-world Examples\"]}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Authentication\"}),\": Implement secure user authentication with both UI components and backend logic using a single command\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Payment Integration\"}),\": Add Stripe payments to your app without spending hours reading documentation\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.strong,{children:\"Admin Dashboards\"}),\": Deploy fully functional admin interfaces that connect directly to your data\"]}),`\n`]}),`\n`,(0,e.jsxs)(n.h2,{id:\"our-mission\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#our-mission\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Our Mission\"]}),`\n`,(0,e.jsx)(n.p,{children:\"We believe developers should spend more time building unique features for their applications and less time implementing common functionality. Lib UI provides pre-built, customizable full-stack components that handle both frontend and backend concerns, allowing you to:\"}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsx)(n.li,{children:\"\\u{1F680} Ship products faster\"}),`\n`,(0,e.jsx)(n.li,{children:\"\\u{1F6E0}\\uFE0F Reduce boilerplate code\"}),`\n`,(0,e.jsx)(n.li,{children:\"\\u26A1 Focus on core business logic\"}),`\n`,(0,e.jsx)(n.li,{children:\"\\u{1F3A8} Maintain design flexibility\"}),`\n`]}),`\n`,(0,e.jsxs)(n.h2,{id:\"inspiration\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#inspiration\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Inspiration\"]}),`\n`,(0,e.jsx)(n.p,{children:\"This project stands on the shoulders of those before us. We're grateful for the incredible open-source work of:\"}),`\n`,(0,e.jsxs)(n.ul,{children:[`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.a,{href:\"https://ui.shadcn.com/\",children:\"shadcn/ui\"}),\" - For pioneering component architecture\"]}),`\n`,(0,e.jsxs)(n.li,{children:[(0,e.jsx)(n.a,{href:\"https://magicui.design/\",children:\"MagicUI\"}),\" - For innovative design patterns\"]}),`\n`]})]})}function h(i={}){let{wrapper:n}=i.components||{};return n?(0,e.jsx)(n,{...i,children:(0,e.jsx)(d,{...i})}):d(i)}return w(x);})();\n;return Component;" - } - }, - { - "content": "\n\n**We are currently only available on nextjs, stay tuned for further updates.**\n\n\n\n\n\n### Create project\n\nRun the `init` command to create a new Next.js project or to setup an existing one:\n\n```bash\nnpx libui-next init\n```\n\n### Start coding\n\nYou can now start adding full stack components to your project!\n\n```bash\nnpx libui-next add authjs\n```\n\nThis will add all the configuration of `Auth.js` to your project.\n\n```tsx {1,6} showLineNumbers\nimport { SignUpForm } from '@/components/auth/sign-up-form'\n\nexport default function SignUp() {\n return (\n
\n \n
\n )\n}\n```\n\n
", - "title": "Installation (example)", - "description": "Install and configure Next.js.", - "published": true, - "featured": false, - "component": false, - "toc": true, - "_meta": { - "filePath": "docs/installation.mdx", - "fileName": "installation.mdx", - "directory": "docs", - "extension": "mdx", - "path": "docs/installation" - }, - "image": "http://localhost:3000/og?title=Installation%20(example)", - "slug": "/docs/installation", - "slugAsParams": "installation", - "body": { - "raw": "\n\n**We are currently only available on nextjs, stay tuned for further updates.**\n\n\n\n\n\n### Create project\n\nRun the `init` command to create a new Next.js project or to setup an existing one:\n\n```bash\nnpx libui-next init\n```\n\n### Start coding\n\nYou can now start adding full stack components to your project!\n\n```bash\nnpx libui-next add authjs\n```\n\nThis will add all the configuration of `Auth.js` to your project.\n\n```tsx {1,6} showLineNumbers\nimport { SignUpForm } from '@/components/auth/sign-up-form'\n\nexport default function SignUp() {\n return (\n
\n \n
\n )\n}\n```\n\n
", - "code": "var Component=(()=>{var u=Object.create;var l=Object.defineProperty;var g=Object.getOwnPropertyDescriptor;var m=Object.getOwnPropertyNames;var y=Object.getPrototypeOf,_=Object.prototype.hasOwnProperty;var x=(a,n)=>()=>(n||a((n={exports:{}}).exports,n),n.exports),b=(a,n)=>{for(var t in n)l(a,t,{get:n[t],enumerable:!0})},o=(a,n,t,r)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let i of m(n))!_.call(a,i)&&i!==t&&l(a,i,{get:()=>n[i],enumerable:!(r=g(n,i))||r.enumerable});return a};var E=(a,n,t)=>(t=a!=null?u(y(a)):{},o(n||!a||!a.__esModule?l(t,\"default\",{value:a,enumerable:!0}):t,a)),f=a=>o(l({},\"__esModule\",{value:!0}),a);var c=x((j,d)=>{d.exports=_jsx_runtime});var F={};b(F,{default:()=>p});var e=E(c());function s(a){let n={a:\"a\",code:\"code\",figure:\"figure\",h3:\"h3\",p:\"p\",pre:\"pre\",span:\"span\",strong:\"strong\",...a.components},{Callout:t,Steps:r}=n;return t||h(\"Callout\",!0),r||h(\"Steps\",!0),(0,e.jsxs)(e.Fragment,{children:[(0,e.jsx)(t,{children:(0,e.jsx)(n.p,{children:(0,e.jsx)(n.strong,{children:\"We are currently only available on nextjs, stay tuned for further updates.\"})})}),`\n`,(0,e.jsxs)(r,{children:[(0,e.jsxs)(n.h3,{id:\"create-project\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#create-project\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Create project\"]}),(0,e.jsxs)(n.p,{children:[\"Run the \",(0,e.jsx)(n.code,{children:\"init\"}),\" command to create a new Next.js project or to setup an existing one:\"]}),(0,e.jsx)(n.figure,{\"data-rehype-pretty-code-figure\":\"\",children:(0,e.jsx)(n.pre,{style:{backgroundColor:\"#24292e\",color:\"#e1e4e8\"},tabIndex:\"0\",\"data-language\":\"bash\",\"data-theme\":\"github-dark\",__rawString__:`npx libui-next init\n`,__npmCommand__:`npx libui-next init\n`,__yarnCommand__:`npx libui-next init\n`,__pnpmCommand__:`pnpm dlx libui-next init\n`,__bunCommand__:`bunx --bun libui-next init\n`,children:(0,e.jsx)(n.code,{\"data-language\":\"bash\",\"data-theme\":\"github-dark\",style:{display:\"grid\"},children:(0,e.jsxs)(n.span,{\"data-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#B392F0\"},children:\"npx\"}),(0,e.jsx)(n.span,{style:{color:\"#9ECBFF\"},children:\" libui-next\"}),(0,e.jsx)(n.span,{style:{color:\"#9ECBFF\"},children:\" init\"})]})})})}),(0,e.jsxs)(n.h3,{id:\"start-coding\",children:[(0,e.jsx)(n.a,{className:\"subheading-anchor\",\"aria-label\":\"Link to section\",href:\"#start-coding\",children:(0,e.jsx)(n.span,{className:\"icon icon-link\"})}),\"Start coding\"]}),(0,e.jsx)(n.p,{children:\"You can now start adding full stack components to your project!\"}),(0,e.jsx)(n.figure,{\"data-rehype-pretty-code-figure\":\"\",children:(0,e.jsx)(n.pre,{style:{backgroundColor:\"#24292e\",color:\"#e1e4e8\"},tabIndex:\"0\",\"data-language\":\"bash\",\"data-theme\":\"github-dark\",__rawString__:`npx libui-next add authjs\n`,__npmCommand__:`npx libui-next add authjs\n`,__yarnCommand__:`npx libui-next add authjs\n`,__pnpmCommand__:`pnpm dlx libui-next add authjs\n`,__bunCommand__:`bunx --bun libui-next add authjs\n`,children:(0,e.jsx)(n.code,{\"data-language\":\"bash\",\"data-theme\":\"github-dark\",style:{display:\"grid\"},children:(0,e.jsxs)(n.span,{\"data-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#B392F0\"},children:\"npx\"}),(0,e.jsx)(n.span,{style:{color:\"#9ECBFF\"},children:\" libui-next\"}),(0,e.jsx)(n.span,{style:{color:\"#9ECBFF\"},children:\" add\"}),(0,e.jsx)(n.span,{style:{color:\"#9ECBFF\"},children:\" authjs\"})]})})})}),(0,e.jsxs)(n.p,{children:[\"This will add all the configuration of \",(0,e.jsx)(n.code,{children:\"Auth.js\"}),\" to your project.\"]}),(0,e.jsx)(n.figure,{\"data-rehype-pretty-code-figure\":\"\",children:(0,e.jsx)(n.pre,{style:{backgroundColor:\"#24292e\",color:\"#e1e4e8\"},tabIndex:\"0\",\"data-language\":\"tsx\",\"data-theme\":\"github-dark\",__rawString__:`import { SignUpForm } from '@/components/auth/sign-up-form'\n\nexport default function SignUp() {\n return (\n
\n \n
\n )\n}\n`,children:(0,e.jsxs)(n.code,{\"data-line-numbers\":\"\",\"data-language\":\"tsx\",\"data-theme\":\"github-dark\",style:{display:\"grid\"},\"data-line-numbers-max-digits\":\"1\",children:[(0,e.jsxs)(n.span,{className:\"line--highlighted\",\"data-line\":\"\",\"data-highlighted-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#F97583\"},children:\"import\"}),(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\" { SignUpForm } \"}),(0,e.jsx)(n.span,{style:{color:\"#F97583\"},children:\"from\"}),(0,e.jsx)(n.span,{style:{color:\"#9ECBFF\"},children:\" '@/components/auth/sign-up-form'\"})]}),`\n`,(0,e.jsx)(n.span,{\"data-line\":\"\",children:\" \"}),`\n`,(0,e.jsxs)(n.span,{\"data-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#F97583\"},children:\"export\"}),(0,e.jsx)(n.span,{style:{color:\"#F97583\"},children:\" default\"}),(0,e.jsx)(n.span,{style:{color:\"#F97583\"},children:\" function\"}),(0,e.jsx)(n.span,{style:{color:\"#B392F0\"},children:\" SignUp\"}),(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\"() {\"})]}),`\n`,(0,e.jsxs)(n.span,{\"data-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#F97583\"},children:\" return\"}),(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\" (\"})]}),`\n`,(0,e.jsxs)(n.span,{\"data-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\" <\"}),(0,e.jsx)(n.span,{style:{color:\"#85E89D\"},children:\"div\"}),(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\">\"})]}),`\n`,(0,e.jsxs)(n.span,{className:\"line--highlighted\",\"data-line\":\"\",\"data-highlighted-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\" <\"}),(0,e.jsx)(n.span,{style:{color:\"#79B8FF\"},children:\"SignUpForm\"}),(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\" />\"})]}),`\n`,(0,e.jsxs)(n.span,{\"data-line\":\"\",children:[(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\" \"})]}),`\n`,(0,e.jsx)(n.span,{\"data-line\":\"\",children:(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\" )\"})}),`\n`,(0,e.jsx)(n.span,{\"data-line\":\"\",children:(0,e.jsx)(n.span,{style:{color:\"#E1E4E8\"},children:\"}\"})})]})})})]})]})}function p(a={}){let{wrapper:n}=a.components||{};return n?(0,e.jsx)(n,{...a,children:(0,e.jsx)(s,{...a})}):s(a)}function h(a,n){throw new Error(\"Expected \"+(n?\"component\":\"object\")+\" `\"+a+\"` to be defined: you likely forgot to import, pass, or provide it.\")}return f(F);})();\n;return Component;" - } - } -] \ No newline at end of file diff --git a/.content-collections/generated/allPages.js b/.content-collections/generated/allPages.js deleted file mode 100644 index 4b90925..0000000 --- a/.content-collections/generated/allPages.js +++ /dev/null @@ -1,2 +0,0 @@ - -export default [] \ No newline at end of file diff --git a/.content-collections/generated/allShowcases.js b/.content-collections/generated/allShowcases.js deleted file mode 100644 index 4b90925..0000000 --- a/.content-collections/generated/allShowcases.js +++ /dev/null @@ -1,2 +0,0 @@ - -export default [] \ No newline at end of file diff --git a/.content-collections/generated/index.d.ts b/.content-collections/generated/index.d.ts deleted file mode 100644 index 9c6e024..0000000 --- a/.content-collections/generated/index.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -import configuration from "../../content-collections.ts"; -import { GetTypeByName } from "@content-collections/core"; - -export type Doc = GetTypeByName; -export declare const allDocs: Array; - -export type Page = GetTypeByName; -export declare const allPages: Array; - -export type Showcase = GetTypeByName; -export declare const allShowcases: Array; - -export {}; diff --git a/.content-collections/generated/index.js b/.content-collections/generated/index.js deleted file mode 100644 index bdae159..0000000 --- a/.content-collections/generated/index.js +++ /dev/null @@ -1,7 +0,0 @@ -// generated by content-collections at Wed Jan 08 2025 13:32:25 GMT-0500 (Eastern Standard Time) - -import allDocs from "./allDocs.js"; -import allPages from "./allPages.js"; -import allShowcases from "./allShowcases.js"; - -export { allDocs, allPages, allShowcases }; diff --git a/.gitignore b/.gitignore index 0c3b936..94de876 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ # misc .DS_Store *.pem +.content-collections/ # debug npm-debug.log* From 67f124eea7295ed58c661bf1c0dbd21fd023a4e4 Mon Sep 17 00:00:00 2001 From: Vincenzo Fanizza Date: Thu, 23 Jan 2025 20:06:56 +0100 Subject: [PATCH 04/12] store auth files in a dedicated folder --- core/auth/actions/admin.ts | 14 + core/auth/actions/login.ts | 102 +++ core/auth/actions/logout.ts | 7 + core/auth/actions/new-password.ts | 57 ++ core/auth/actions/new-verification.ts | 38 ++ core/auth/actions/register.ts | 60 ++ core/auth/actions/reset.ts | 32 + core/auth/actions/settings.ts | 89 +++ core/auth/app/api/admin/route.ts | 13 + core/auth/app/api/auth/[...nextauth]/route.ts | 1 + core/auth/auth.config.ts | 39 ++ core/auth/auth.ts | 80 +++ core/auth/components/auth/auth.tsx | 601 ++++++++++++++++++ core/auth/components/auth/logout-button.tsx | 19 + core/auth/components/auth/user-button.tsx | 43 ++ core/auth/components/ui/avatar.tsx | 50 ++ .../components/ui/examples/auth-example.tsx | 182 ++++++ core/auth/data/account.ts | 13 + core/auth/data/password-reset-token.ts | 25 + core/auth/data/two-factor-confirmation.ts | 15 + core/auth/data/two-factor-token.ts | 25 + core/auth/data/user.ts | 21 + core/auth/data/verification-token.ts | 29 + core/auth/hooks/use-current-role.ts | 7 + core/auth/hooks/use-current-user.ts | 7 + core/auth/lib/auth.ts | 13 + core/auth/lib/mail.ts | 36 ++ core/auth/lib/token.ts | 80 +++ core/auth/lib/utils.ts | 6 + core/auth/metadata.json | 15 + core/auth/next-auth.d.ts | 14 + core/auth/prisma/schema.prisma | 82 +++ core/auth/routes.ts | 35 + core/auth/schemas/index.ts | 65 ++ 34 files changed, 1915 insertions(+) create mode 100644 core/auth/actions/admin.ts create mode 100644 core/auth/actions/login.ts create mode 100644 core/auth/actions/logout.ts create mode 100644 core/auth/actions/new-password.ts create mode 100644 core/auth/actions/new-verification.ts create mode 100644 core/auth/actions/register.ts create mode 100644 core/auth/actions/reset.ts create mode 100644 core/auth/actions/settings.ts create mode 100644 core/auth/app/api/admin/route.ts create mode 100644 core/auth/app/api/auth/[...nextauth]/route.ts create mode 100644 core/auth/auth.config.ts create mode 100644 core/auth/auth.ts create mode 100644 core/auth/components/auth/auth.tsx create mode 100644 core/auth/components/auth/logout-button.tsx create mode 100644 core/auth/components/auth/user-button.tsx create mode 100644 core/auth/components/ui/avatar.tsx create mode 100644 core/auth/components/ui/examples/auth-example.tsx create mode 100644 core/auth/data/account.ts create mode 100644 core/auth/data/password-reset-token.ts create mode 100644 core/auth/data/two-factor-confirmation.ts create mode 100644 core/auth/data/two-factor-token.ts create mode 100644 core/auth/data/user.ts create mode 100644 core/auth/data/verification-token.ts create mode 100644 core/auth/hooks/use-current-role.ts create mode 100644 core/auth/hooks/use-current-user.ts create mode 100644 core/auth/lib/auth.ts create mode 100644 core/auth/lib/mail.ts create mode 100644 core/auth/lib/token.ts create mode 100644 core/auth/lib/utils.ts create mode 100644 core/auth/metadata.json create mode 100644 core/auth/next-auth.d.ts create mode 100644 core/auth/prisma/schema.prisma create mode 100644 core/auth/routes.ts create mode 100644 core/auth/schemas/index.ts diff --git a/core/auth/actions/admin.ts b/core/auth/actions/admin.ts new file mode 100644 index 0000000..7c2011d --- /dev/null +++ b/core/auth/actions/admin.ts @@ -0,0 +1,14 @@ +"use server"; + +import { currentRole } from "@/lib/auth"; +import { UserRole } from "@prisma/client"; + +export const admin = async () => { + const role = await currentRole(); + + if (role === UserRole.ADMIN) { + return { success: "Allowed Server Action!" }; + } + + return { error: "Forbidden Server Action!" } +}; \ No newline at end of file diff --git a/core/auth/actions/login.ts b/core/auth/actions/login.ts new file mode 100644 index 0000000..919438b --- /dev/null +++ b/core/auth/actions/login.ts @@ -0,0 +1,102 @@ +"use server"; + +import * as z from "zod"; +import * as bcrypt from "bcrypt"; + +import { db } from "@/lib/db"; +import { signIn } from "@/auth"; +import { LoginSchema } from "@/schemas"; +import { getUserByEmail } from "@/data/user"; +import { getTwoFactorTokenByEmail } from "@/data/two-factor-token"; +import { sendVerificationEmail, sendTwoFactorTokenEmail } from "@/lib/mail"; +import { DEFAULT_LOGIN_REDIRECT } from "../routes"; +import { + generateVerificationToken, + generateTwoFactorToken, +} from "../lib/token"; +import { getTwoFactorConfirmationByUserId } from "@/data/two-factor-confirmation"; + +export const login = async ( + values: z.infer, + callbackUrl?: string | null, + twoFactorEnabled?: boolean +) => { + const validatedFields = LoginSchema.safeParse(values); + + if (!validatedFields.success) { + return { error: "Invalid fields!" }; + } + + const { email, password, code } = validatedFields.data; + + const existingUser = await getUserByEmail(email); + + if (!existingUser || !existingUser.email || !existingUser.password) { + return { error: "Email does not exist!" }; + } + + const passwordsMatch = await bcrypt.compare(password, existingUser.password); + + if (!passwordsMatch) { + return { error: "Invalid credentials!" }; + } + + if (!existingUser.emailVerified) { + const verificationToken = await generateVerificationToken( + existingUser.email + ); + await sendVerificationEmail( + verificationToken.email, + verificationToken.token + ); + return { success: "Confirmation email sent!" }; + } + + // Only proceed with 2FA if enabled via prop + if (twoFactorEnabled) { + if (code) { + const twoFactorToken = await getTwoFactorTokenByEmail(existingUser.email); + + if (!twoFactorToken || twoFactorToken.token !== code) { + return { error: "Invalid 2FA code!" }; + } + + const hasExpired = new Date(twoFactorToken.expires) < new Date(); + + if (hasExpired) { + return { error: "2FA code has expired!" }; + } + + await db.twoFactorToken.delete({ + where: { id: twoFactorToken.id }, + }); + + await db.twoFactorConfirmation.upsert({ + where: { + userId: existingUser.id, + }, + update: {}, + create: { + userId: existingUser.id, + }, + }); + } else { + const twoFactorToken = await generateTwoFactorToken(existingUser.email); + await sendTwoFactorTokenEmail(twoFactorToken.email, twoFactorToken.token); + return { twoFactor: true }; + } + } + + try { + await signIn("credentials", { + email, + password, + redirect: false, + }); + + return { success: "Logged in successfully!" }; + } catch (error) { + console.error("Login error:", error); + return { error: "Something went wrong!" }; + } +}; diff --git a/core/auth/actions/logout.ts b/core/auth/actions/logout.ts new file mode 100644 index 0000000..252bc7f --- /dev/null +++ b/core/auth/actions/logout.ts @@ -0,0 +1,7 @@ +"use server"; + +import { signOut } from "@/auth"; + +export const logout = async () => { + await signOut(); +}; \ No newline at end of file diff --git a/core/auth/actions/new-password.ts b/core/auth/actions/new-password.ts new file mode 100644 index 0000000..5509b60 --- /dev/null +++ b/core/auth/actions/new-password.ts @@ -0,0 +1,57 @@ +"use server"; + +import * as z from "zod"; +import bcrypt from "bcryptjs"; + +import { NewPasswordSchema } from "@/schemas"; +import { getPasswordResetTokenByToken } from "@/data/password-reset-token"; +import { getUserByEmail } from "@/data/user"; +import { db } from "@/lib/db"; + +export const newPassword = async ( + values: z.infer , + token?: string | null, +) => { + if (!token) { + return { error: "Missing token!" }; + } + + const validatedFields = NewPasswordSchema.safeParse(values); + + if (!validatedFields.success) { + return { error: "Invalid fields!" }; + } + + const { password } = validatedFields.data; + + const existingToken = await getPasswordResetTokenByToken(token); + + if (!existingToken) { + return { error: "Invalid token!" }; + } + + const hasExpired = new Date(existingToken.expires) < new Date(); + + if (hasExpired) { + return { error: "Token has expired!" }; + } + + const existingUser = await getUserByEmail(existingToken.email); + + if (!existingUser) { + return { error: "Email does not exist!" } + } + + const hashedPassword = await bcrypt.hash(password, 10); + + await db.user.update({ + where: { id: existingUser.id }, + data: { password: hashedPassword }, + }); + + await db.passwordResetToken.delete({ + where: { id: existingToken.id } + }); + + return { success: "Password updated!" }; +}; \ No newline at end of file diff --git a/core/auth/actions/new-verification.ts b/core/auth/actions/new-verification.ts new file mode 100644 index 0000000..2b6b21e --- /dev/null +++ b/core/auth/actions/new-verification.ts @@ -0,0 +1,38 @@ +"use server"; + +import { db } from "@/lib/db"; +import { getUserByEmail } from "@/data/user"; +import { getVerificationTokenByToken } from "../data/verification-token"; + +export const newVerification = async (token: string) => { + const existingToken = await getVerificationTokenByToken(token); + + if (!existingToken) { + return { error: "Token does not exist!" }; + } + + const hasExpired = new Date(existingToken.expires) < new Date(); + + if (hasExpired) { + return { error: "Token has expired!" }; + } + + const existingUser = await getUserByEmail(existingToken.email); + + if (!existingUser) { + return { error: "Email does not exist!" }; + } + + await db.user.update({ + where: { id: existingUser.id }, + data: { + emailVerified: new Date(), + email: existingToken.email, + } + }); + await db.verificationToken.delete({ + where: { id: existingToken.id } + }); + + return { success: "Email verified!" }; +}; \ No newline at end of file diff --git a/core/auth/actions/register.ts b/core/auth/actions/register.ts new file mode 100644 index 0000000..3c55c76 --- /dev/null +++ b/core/auth/actions/register.ts @@ -0,0 +1,60 @@ +"use server"; + +import * as z from "zod"; +import bcrypt from "bcryptjs"; + +import { db } from "@/lib/db"; +import { RegisterSchema } from "@/schemas"; +import { getUserByEmail } from "@/data/user"; +import { sendVerificationEmail } from "@/lib/mail"; +import { generateVerificationToken } from "../lib/token"; +import { signIn } from "@/auth"; + +export const register = async (values: z.infer) => { + const validatedFields = RegisterSchema.safeParse(values); + + if (!validatedFields.success) { + return { error: "Invalid fields!" }; + } + + const { email, password, name } = validatedFields.data; + const hashedPassword = await bcrypt.hash(password, 10); + + const existingUser = await getUserByEmail(email); + + if (existingUser) { + return { error: "Email already in use!" }; + } + + await db.user.create({ + data: { + name, + email, + password: hashedPassword, + }, + }); + + const verificationToken = await generateVerificationToken(email); + await sendVerificationEmail( + verificationToken.email, + verificationToken.token, + ); + + try { + const signInResult = await signIn("credentials", { + email, + password, + redirect: false, + }); + + console.log(password) + + if (signInResult?.error) { + return { error: "Something went wrong!" }; + } + } catch (error) { + return { error: "Something went wrong!" }; + } + + return { success: "Confirmation email sent!" }; +}; \ No newline at end of file diff --git a/core/auth/actions/reset.ts b/core/auth/actions/reset.ts new file mode 100644 index 0000000..c84fd66 --- /dev/null +++ b/core/auth/actions/reset.ts @@ -0,0 +1,32 @@ +"use server"; + +import * as z from "zod"; + +import { ResetSchema } from "@/schemas"; +import { getUserByEmail } from "@/data/user"; +import { sendPasswordResetEmail } from "@/lib/mail"; +import { generatePasswordResetToken } from "../lib/token"; + +export const reset = async (values: z.infer) => { + const validatedFields = ResetSchema.safeParse(values); + + if (!validatedFields.success) { + return { error: "Invalid emaiL!" }; + } + + const { email } = validatedFields.data; + + const existingUser = await getUserByEmail(email); + + if (!existingUser) { + return { error: "Email not found!" }; + } + + const passwordResetToken = await generatePasswordResetToken(email); + await sendPasswordResetEmail( + passwordResetToken.email, + passwordResetToken.token, + ); + + return { success: "Reset email sent!" }; +} \ No newline at end of file diff --git a/core/auth/actions/settings.ts b/core/auth/actions/settings.ts new file mode 100644 index 0000000..f7373e0 --- /dev/null +++ b/core/auth/actions/settings.ts @@ -0,0 +1,89 @@ +"use server"; + +import * as z from "zod"; +import bcrypt from "bcryptjs"; + +import { update } from "@/auth"; +import { db } from "@/lib/db"; +import { SettingsSchema } from "@/schemas"; +import { getUserByEmail, getUserById } from "@/data/user"; +import { currentUser } from "@/lib/auth"; +import { generateVerificationToken } from "@/lib/token"; +import { sendVerificationEmail } from "@/lib/mail"; + +export const settings = async ( + values: z.infer + ) => { + const user = await currentUser(); + + if (!user) { + return { error: "Unauthorized" } + } + + const dbUser = await getUserById(user.id); + + if (!dbUser) { + return { error: "Unauthorized" } + } + + if (user.isOAuth) { + values.email = undefined; + values.password = undefined; + values.newPassword = undefined; + values.isTwoFactorEnabled = undefined; + } + + if (values.email && values.email !== user.email) { + const existingUser = await getUserByEmail(values.email); + + if (existingUser && existingUser.id !== user.id) { + return { error: "Email already in use!" } + } + + const verificationToken = await generateVerificationToken( + values.email + ); + await sendVerificationEmail( + verificationToken.email, + verificationToken.token, + ); + + return { success: "Verification email sent!" }; + } + + if (values.password && values.newPassword && dbUser.password) { + const passwordsMatch = await bcrypt.compare( + values.password, + dbUser.password, + ); + + if (!passwordsMatch) { + return { error: "Incorrect password!" }; + } + + const hashedPassword = await bcrypt.hash( + values.newPassword, + 10, + ); + values.password = hashedPassword; + values.newPassword = undefined; + } + + const updatedUser = await db.user.update({ + where: { id: dbUser.id }, + data: { + ...values, + } + }); + + update({ + user: { + name: updatedUser.name, + email: updatedUser.email, + isTwoFactorEnabled: updatedUser.isTwoFactorEnabled, + role: updatedUser.role, + } + }); + + return { success: "Settings Updated!" } + } \ No newline at end of file diff --git a/core/auth/app/api/admin/route.ts b/core/auth/app/api/admin/route.ts new file mode 100644 index 0000000..7a460e9 --- /dev/null +++ b/core/auth/app/api/admin/route.ts @@ -0,0 +1,13 @@ +import { currentRole } from "@/lib/auth"; +import { UserRole } from "@prisma/client"; +import { NextResponse } from "next/server"; + +export async function GET() { + const role = await currentRole(); + + if (role === UserRole.ADMIN) { + return new NextResponse(null, { status: 200 }); + } + + return new NextResponse(null, { status: 403 }); +} diff --git a/core/auth/app/api/auth/[...nextauth]/route.ts b/core/auth/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..5acc419 --- /dev/null +++ b/core/auth/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1 @@ +export { GET, POST } from "@/auth"; \ No newline at end of file diff --git a/core/auth/auth.config.ts b/core/auth/auth.config.ts new file mode 100644 index 0000000..dc443a4 --- /dev/null +++ b/core/auth/auth.config.ts @@ -0,0 +1,39 @@ +import bcrypt from 'bcryptjs'; +import type { NextAuthConfig } from 'next-auth'; +import Credentials from 'next-auth/providers/credentials'; +import Github from 'next-auth/providers/github'; +import Google from 'next-auth/providers/google'; + +import { LoginSchema } from '@/schemas'; +import { getUserByEmail } from '@/data/user'; + +export default { + providers: [ + Google({ + clientId: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + }), + Github({ + clientId: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET, + }), + Credentials({ + async authorize(credentials) { + const validatedFields = LoginSchema.safeParse(credentials); + + if (validatedFields.success) { + const { email, password } = validatedFields.data; + + const user = await getUserByEmail(email); + if (!user || !user.password) return null; + + const passwordsMatch = await bcrypt.compare(password, user.password); + + if (passwordsMatch) return user; + } + + return null; + }, + }), + ], +} satisfies NextAuthConfig; diff --git a/core/auth/auth.ts b/core/auth/auth.ts new file mode 100644 index 0000000..b3a11bc --- /dev/null +++ b/core/auth/auth.ts @@ -0,0 +1,80 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import NextAuth from "next-auth"; +import { UserRole } from "@prisma/client"; +import { PrismaAdapter } from "@auth/prisma-adapter"; + +import { db } from "@/lib/db"; +import authConfig from "@/auth.config"; +import { getUserById } from "@/data/user"; +import { getTwoFactorConfirmationByUserId } from "@/data/two-factor-confirmation"; +import { getAccountByUserId } from "./data/account"; + +export const { + handlers: { GET, POST }, + auth, + signIn, + signOut, + update, +} = NextAuth({ + pages: { + signIn: "/auth/login", + error: "/auth/error", + }, + events: { + async linkAccount({ user }: any) { + await db.user.update({ + where: { id: user.id }, + data: { emailVerified: new Date() }, + }); + }, + }, + callbacks: { + async signIn({ account }: any) { + + if (account?.provider !== "credentials") return true; + + return true; + }, + async session({ token, session }: any) { + if (token.sub && session.user) { + session.user.id = token.sub; + } + + if (token.role && session.user) { + session.user.role = token.role as UserRole; + } + + if (session.user) { + session.user.isTwoFactorEnabled = token.isTwoFactorEnabled as boolean; + } + + if (session.user) { + session.user.name = token.name; + session.user.email = token.email; + session.user.isOAuth = token.isOAuth as boolean; + } + + return session; + }, + async jwt({ token }: any) { + if (!token.sub) return token; + + const existingUser = await getUserById(token.sub); + + if (!existingUser) return token; + + const existingAccount = await getAccountByUserId(existingUser.id); + + token.isOAuth = !!existingAccount; + token.name = existingUser.name; + token.email = existingUser.email; + token.role = existingUser.role; + token.isTwoFactorEnabled = existingUser.isTwoFactorEnabled; + + return token; + }, + }, + adapter: PrismaAdapter(db), + session: { strategy: "jwt" }, + ...authConfig, +}); diff --git a/core/auth/components/auth/auth.tsx b/core/auth/components/auth/auth.tsx new file mode 100644 index 0000000..4eb0969 --- /dev/null +++ b/core/auth/components/auth/auth.tsx @@ -0,0 +1,601 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +"use client"; + +import * as z from "zod"; +import { useCallback, useEffect, useState, useTransition } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; + +import { + LoginSchema, + NewPasswordSchema, + RegisterSchema, + ResetSchema, +} from "@/schemas"; +import { Input } from "@/components/ui/input"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Button } from "@/components/ui/button"; +import { register } from "@/actions/register"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { useSearchParams } from "next/navigation"; +import { DEFAULT_LOGIN_REDIRECT } from "@/routes"; +import { signIn } from "@/auth"; +import { FaGithub, FaGoogle, FaUser } from "react-icons/fa"; +import { login } from "@/actions/login"; +import Link from "next/link"; +import { reset } from "@/actions/reset"; +import { newVerification } from "@/actions/new-verification"; +import { BeatLoader } from "react-spinners"; +import { newPassword } from "@/actions/new-password"; +import { useCurrentUser } from "@/hooks/use-current-user"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; +import { logout } from "@/actions/logout"; +import { toast } from "sonner"; +import { useSession } from "next-auth/react"; + +export const SignUp = ({ + google, + github, +}: { + google?: boolean; + github?: boolean; +}) => { + const [error, setError] = useState(""); + const [success, setSuccess] = useState(""); + const [isPending, startTransition] = useTransition(); + + const form = useForm>({ + resolver: zodResolver(RegisterSchema), + defaultValues: { + email: "", + password: "", + name: "", + }, + }); + + const onSubmit = (values: z.infer) => { + setError(""); + setSuccess(""); + + startTransition(() => { + register(values).then((data) => { + if (data.error) { + setError(data.error); + toast.error(data.error); + } + if (data.success) { + form.reset(); + setSuccess(data.success); + toast.success(data.success); + } + }); + }); + }; + + return ( + + + Create an account + Enter your email to access. + + +
+ +
+ ( + + Name + + + + + )} + /> + ( + + Email + + + + + )} + /> + ( + + Password + + + + + )} + /> +
+ +
+ +
+ {(google || github) && ( +
+
+ +
+
+ + Or continue with + +
+
+ )} + {(google || github) && ( + + + + )} +
+ ); +}; + +export const Login = ({ + google, + github, + twoFactor, +}: { + google?: boolean; + github?: boolean; + twoFactor?: boolean; +}) => { + const searchParams = useSearchParams(); + const callbackUrl = searchParams.get("callbackUrl"); + const session = useSession(); + + const [error, setError] = useState(""); + const [success, setSuccess] = useState(""); + const [isPending, startTransition] = useTransition(); + const [showTwoFactor, setShowTwoFactor] = useState(false); + + const form = useForm>({ + resolver: zodResolver(LoginSchema), + defaultValues: { + email: "", + password: "", + code: "", + }, + }); + + const onSubmit = (values: z.infer) => { + setError(""); + setSuccess(""); + + startTransition(() => { + login(values, callbackUrl, twoFactor) + .then((data) => { + console.log("Login response:", data); + if (data?.error) { + setError(data.error); + toast.error(data.error); + } + + if (data?.twoFactor) { + setShowTwoFactor(true); + toast.success("2FA Code sent to your email!"); + } + + if (data?.success) { + form.reset(); + setSuccess(data.success); + toast.success(data.success); + window.location.href = callbackUrl || DEFAULT_LOGIN_REDIRECT; + } + }) + .catch((error) => { + console.error("Login error:", error); + toast.error("Something went wrong with login"); + }); + }); + }; + + return ( + + + + {showTwoFactor ? "Two-Factor Authentication" : "Login"} + + + {showTwoFactor + ? "Enter the code sent to your email" + : "Enter your information to login."} + + + +
+ +
+ {showTwoFactor ? ( + ( + + 2FA Code + + + + + + )} + /> + ) : ( + <> + ( + + Email + + + + + + )} + /> + ( + + Password + + + + + + + )} + /> + + )} +
+ +
+ +
+ {(google || github) && ( +
+
+ +
+
+ + Or continue with + +
+
+ )} + {(google || github) && ( + + + + )} +
+ ); +}; + +export const OAuth = ({ + google, + github, +}: { + google?: boolean; + github?: boolean; +}) => { + const searchParams = useSearchParams(); + const callbackUrl = searchParams.get("callbackUrl"); + + const onClick = (provider: "google" | "github") => { + signIn(provider, { + callbackUrl: callbackUrl || DEFAULT_LOGIN_REDIRECT, + }); + }; + return ( +
+ {google && ( + + )} + {github && ( + + )} +
+ ); +}; + +export const VerifyEmail = () => { + const [error, setError] = useState(); + const [success, setSuccess] = useState(); + + const searchParams = useSearchParams(); + + const token = searchParams.get("token"); + + const onSubmit = useCallback(() => { + if (success || error) return; + + if (!token) { + setError("Missing token!"); + return; + } + + newVerification(token) + .then((data) => { + setSuccess(data.success); + setError(data.error); + }) + .catch(() => { + setError("Something went wrong!"); + }); + }, [token, success, error]); + + useEffect(() => { + onSubmit(); + }, [onSubmit]); + + return ( + + + Verify your email + + This page is used to verify your email address. + + + +
+ {!success && !error && } + {success && toast.success(success)} + {error && toast.error(error)} +
+
+ ); +}; + +export const ResetPassword = () => { + const [error, setError] = useState(""); + const [success, setSuccess] = useState(""); + const [isPending, startTransition] = useTransition(); + + const form = useForm>({ + resolver: zodResolver(ResetSchema), + defaultValues: { + email: "", + }, + }); + + const onSubmit = (values: z.infer) => { + setError(""); + setSuccess(""); + + startTransition(() => { + reset(values).then((data) => { + toast.error(data?.error); + toast.success(data?.success); + }); + }); + }; + + return ( + + + Forgot your password? + + Enter your email to reset your password. + + + +
+ +
+ ( + + Email + + + + + + )} + /> +
+ +
+ +
+
+ ); +}; + +export const NewPassword = () => { + const searchParams = useSearchParams(); + const token = searchParams.get("token"); + + const [error, setError] = useState(""); + const [success, setSuccess] = useState(""); + const [isPending, startTransition] = useTransition(); + + const form = useForm>({ + resolver: zodResolver(NewPasswordSchema), + defaultValues: { + password: "", + }, + }); + + const onSubmit = (values: z.infer) => { + setError(""); + setSuccess(""); + + startTransition(() => { + newPassword(values, token).then((data) => { + toast.error(data?.error); + toast.success(data?.success); + }); + }); + }; + + return ( + + + Enter a new password + + This page is used to enter a new password. + + + +
+ +
+ ( + + Password + + + + + + )} + /> +
+ + +
+ +
+ ); +}; + +export const UserButton = () => { + const user = useCurrentUser(); + + const onClick = () => { + logout(); + }; + + return ( + + + + + + + + + + + + + + ); +}; diff --git a/core/auth/components/auth/logout-button.tsx b/core/auth/components/auth/logout-button.tsx new file mode 100644 index 0000000..8d7b4d9 --- /dev/null +++ b/core/auth/components/auth/logout-button.tsx @@ -0,0 +1,19 @@ +"use client"; + +import { logout } from "@/actions/logout"; + +interface LogoutButtonProps { + children?: React.ReactNode; +} + +export const LogoutButton = ({ children }: LogoutButtonProps) => { + const onClick = () => { + logout(); + }; + + return ( + + {children} + + ); +}; diff --git a/core/auth/components/auth/user-button.tsx b/core/auth/components/auth/user-button.tsx new file mode 100644 index 0000000..4c0cf02 --- /dev/null +++ b/core/auth/components/auth/user-button.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { FaUser } from "react-icons/fa"; +import { ExitIcon } from "@radix-ui/react-icons" + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Avatar, + AvatarImage, + AvatarFallback, +} from "@/components/ui/avatar"; +import { useCurrentUser } from "@/hooks/use-current-user"; +import { LogoutButton } from "@/components/auth/logout-button"; + +export const UserButton = () => { + const user = useCurrentUser(); + + return ( + + + + + + + + + + + + + + Logout + + + + + ); +}; diff --git a/core/auth/components/ui/avatar.tsx b/core/auth/components/ui/avatar.tsx new file mode 100644 index 0000000..51e507b --- /dev/null +++ b/core/auth/components/ui/avatar.tsx @@ -0,0 +1,50 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/core/auth/components/ui/examples/auth-example.tsx b/core/auth/components/ui/examples/auth-example.tsx new file mode 100644 index 0000000..568743f --- /dev/null +++ b/core/auth/components/ui/examples/auth-example.tsx @@ -0,0 +1,182 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +"use client"; + +import { Button } from "@/components/ui/button"; +import { signIn, signOut, useSession } from "next-auth/react"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Icons } from "@/components/ui/icons/icons"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { toast } from "sonner"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import axios from "axios"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; + +export function Auth() { + const router = useRouter(); + const [error, setError] = useState(null); + const session = useSession(); + + const formSchema = z.object({ + name: z.string().min(1, { message: "Name is required" }), + email: z.string().email(), + password: z.string().min(1, { message: "Password is required" }), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: "", + email: "", + password: "", + }, + }); + + const handleGitHubSignIn = () => { + signIn("github", { callbackUrl: "/" }); + toast.success("Successfully signed in with GitHub!"); + }; + + const handleGoogleSignIn = () => { + signIn("google", { callbackUrl: "/" }); + }; + + const onSubmit = async (values: z.infer) => { + if (session.status === "authenticated") { + toast.error("You are already logged in!"); + return; + } + try { + event?.preventDefault(); + + await axios.post("/api/auth/signup", { + email: values.email, + password: values.password, + name: values.name, + }); + + const result = await signIn("credentials", { + email: values.email, + password: values.password, + redirect: false, + }); + + if (result?.error) { + throw new Error(result.error); + } + + form.reset(); + toast.success("Successfully created account!"); + router.refresh(); + } catch (error) { + const errorMessage = axios.isAxiosError(error) + ? error.response?.data || error.message + : "An error occurred"; + setError(errorMessage); + toast.error(errorMessage); + } + }; + + return ( + + + Create an account + Enter your email to access. + + +
+ +
+ ( + + Name + + + + + + )} + /> + ( + + Email + + + + + + )} + /> +
+ ( + + Password + + + + + + )} + /> + + + +
+
+
+ +
+
+ + Or continue with + +
+
+ +
+ + +
+
+ {/* if logged in card should show persons name and saying hello */} +
+ ); +} diff --git a/core/auth/data/account.ts b/core/auth/data/account.ts new file mode 100644 index 0000000..49427b3 --- /dev/null +++ b/core/auth/data/account.ts @@ -0,0 +1,13 @@ +import { db } from "@/lib/db"; + +export const getAccountByUserId = async (userId: string) => { + try { + const account = await db.account.findFirst({ + where: { userId } + }); + + return account; + } catch { + return null; + } +}; \ No newline at end of file diff --git a/core/auth/data/password-reset-token.ts b/core/auth/data/password-reset-token.ts new file mode 100644 index 0000000..677c730 --- /dev/null +++ b/core/auth/data/password-reset-token.ts @@ -0,0 +1,25 @@ +import { db } from "@/lib/db"; + +export const getPasswordResetTokenByToken = async (token: string) => { + try { + const passwordResetToken = await db.passwordResetToken.findUnique({ + where: { token } + }); + + return passwordResetToken; + } catch { + return null; + } +}; + +export const getPasswordResetTokenByEmail = async (email: string) => { + try { + const passwordResetToken = await db.passwordResetToken.findFirst({ + where: { email } + }); + + return passwordResetToken; + } catch { + return null; + } +}; \ No newline at end of file diff --git a/core/auth/data/two-factor-confirmation.ts b/core/auth/data/two-factor-confirmation.ts new file mode 100644 index 0000000..e3eb4f6 --- /dev/null +++ b/core/auth/data/two-factor-confirmation.ts @@ -0,0 +1,15 @@ +import { db } from "@/lib/db"; + +export const getTwoFactorConfirmationByUserId = async ( + userId: string +) => { + try { + const twoFactorConfirmation = await db.twoFactorConfirmation.findUnique({ + where: { userId } + }); + + return twoFactorConfirmation; + } catch { + return null; + } +}; \ No newline at end of file diff --git a/core/auth/data/two-factor-token.ts b/core/auth/data/two-factor-token.ts new file mode 100644 index 0000000..48f9e00 --- /dev/null +++ b/core/auth/data/two-factor-token.ts @@ -0,0 +1,25 @@ +import { db } from "@/lib/db"; + +export const getTwoFactorTokenByToken = async (token: string) => { + try { + const twoFactorToken = await db.twoFactorToken.findUnique({ + where: { token } + }); + + return twoFactorToken; + } catch { + return null; + } +}; + +export const getTwoFactorTokenByEmail = async (email: string) => { + try { + const twoFactorToken = await db.twoFactorToken.findFirst({ + where: { email } + }); + + return twoFactorToken; + } catch { + return null; + } +}; \ No newline at end of file diff --git a/core/auth/data/user.ts b/core/auth/data/user.ts new file mode 100644 index 0000000..0710961 --- /dev/null +++ b/core/auth/data/user.ts @@ -0,0 +1,21 @@ +import { db } from "@/lib/db"; + +export const getUserByEmail = async (email: string) => { + try { + const user = await db.user.findUnique({ where: { email } }); + + return user; + } catch { + return null; + } +}; + +export const getUserById = async (id: string) => { + try { + const user = await db.user.findUnique({ where: { id } }); + + return user; + } catch { + return null; + } +}; \ No newline at end of file diff --git a/core/auth/data/verification-token.ts b/core/auth/data/verification-token.ts new file mode 100644 index 0000000..2db9ad2 --- /dev/null +++ b/core/auth/data/verification-token.ts @@ -0,0 +1,29 @@ +import { db } from "@/lib/db"; + +export const getVerificationTokenByToken = async ( + token: string +) => { + try { + const verificationToken = await db.verificationToken.findUnique({ + where: { token } + }); + + return verificationToken; + } catch { + return null; + } +} + +export const getVerificationTokenByEmail = async ( + email: string +) => { + try { + const verificationToken = await db.verificationToken.findFirst({ + where: { email } + }); + + return verificationToken; + } catch { + return null; + } +} \ No newline at end of file diff --git a/core/auth/hooks/use-current-role.ts b/core/auth/hooks/use-current-role.ts new file mode 100644 index 0000000..dd485c1 --- /dev/null +++ b/core/auth/hooks/use-current-role.ts @@ -0,0 +1,7 @@ +import { useSession } from "next-auth/react"; + +export const useCurrentRole = () => { + const session = useSession(); + + return session.data?.user?.role ; +}; \ No newline at end of file diff --git a/core/auth/hooks/use-current-user.ts b/core/auth/hooks/use-current-user.ts new file mode 100644 index 0000000..7a328da --- /dev/null +++ b/core/auth/hooks/use-current-user.ts @@ -0,0 +1,7 @@ +import { useSession } from "next-auth/react"; + +export const useCurrentUser = () => { + const session = useSession(); + + return session.data?.user; +}; \ No newline at end of file diff --git a/core/auth/lib/auth.ts b/core/auth/lib/auth.ts new file mode 100644 index 0000000..3c95273 --- /dev/null +++ b/core/auth/lib/auth.ts @@ -0,0 +1,13 @@ +import { auth } from "@/auth"; + +export const currentUser = async () => { + const session = await auth(); + + return session?.user; +}; + +export const currentRole = async () => { + const session = await auth(); + + return session?.user?.role; +}; \ No newline at end of file diff --git a/core/auth/lib/mail.ts b/core/auth/lib/mail.ts new file mode 100644 index 0000000..80431e5 --- /dev/null +++ b/core/auth/lib/mail.ts @@ -0,0 +1,36 @@ +import { Resend } from 'resend'; + +const resend = new Resend(process.env.RESEND_API_KEY); + +const domain = process.env.NEXT_PUBLIC_APP_URL; + +export const sendTwoFactorTokenEmail = async (email: string, token: string) => { + await resend.emails.send({ + from: 'Acme ', + to: email, + subject: '2FA Code', + html: `

Your 2FA code: ${token}

`, + }); +}; + +export const sendPasswordResetEmail = async (email: string, token: string) => { + const resetLink = `${domain}/auth/new-password?token=${token}`; + + await resend.emails.send({ + from: 'Acme ', + to: email, + subject: 'Reset your password', + html: `

Click here to reset password.

`, + }); +}; + +export const sendVerificationEmail = async (email: string, token: string) => { + const confirmLink = `${domain}/auth/new-verification?token=${token}`; + + await resend.emails.send({ + from: 'Acme ', + to: email, + subject: 'Confirm your email', + html: `

Click here to confirm email.

`, + }); +}; \ No newline at end of file diff --git a/core/auth/lib/token.ts b/core/auth/lib/token.ts new file mode 100644 index 0000000..537f52e --- /dev/null +++ b/core/auth/lib/token.ts @@ -0,0 +1,80 @@ +import crypto from "crypto"; +import { v4 as uuidv4 } from "uuid"; + +import { db } from "@/lib/db"; +import { getVerificationTokenByEmail } from "../data/verification-token"; +import { getPasswordResetTokenByEmail } from "../data/password-reset-token"; +import { getTwoFactorTokenByEmail } from "../data/two-factor-token"; + +export const generateTwoFactorToken = async (email: string) => { + const token = crypto.randomInt(100_000, 1_000_000).toString(); + const expires = new Date(new Date().getTime() + 5 * 60 * 1000); + + const existingToken = await getTwoFactorTokenByEmail(email); + + if (existingToken) { + await db.twoFactorToken.delete({ + where: { + id: existingToken.id, + }, + }); + } + + const twoFactorToken = await db.twoFactorToken.create({ + data: { + email, + token, + expires, + }, + }); + + return twoFactorToken; +}; + +export const generatePasswordResetToken = async (email: string) => { + const token = uuidv4(); + const expires = new Date(new Date().getTime() + 3600 * 1000); + + const existingToken = await getPasswordResetTokenByEmail(email); + + if (existingToken) { + await db.passwordResetToken.delete({ + where: { id: existingToken.id }, + }); + } + + const passwordResetToken = await db.passwordResetToken.create({ + data: { + email, + token, + expires, + }, + }); + + return passwordResetToken; +}; + +export const generateVerificationToken = async (email: string) => { + const token = uuidv4(); + const expires = new Date(new Date().getTime() + 3600 * 1000); + + const existingToken = await getVerificationTokenByEmail(email); + + if (existingToken) { + await db.verificationToken.delete({ + where: { + id: existingToken.id, + }, + }); + } + + const verificationToken = await db.verificationToken.create({ + data: { + email, + token, + expires, + }, + }); + + return verificationToken; +}; diff --git a/core/auth/lib/utils.ts b/core/auth/lib/utils.ts new file mode 100644 index 0000000..1a860ee --- /dev/null +++ b/core/auth/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} \ No newline at end of file diff --git a/core/auth/metadata.json b/core/auth/metadata.json new file mode 100644 index 0000000..3ac1bc0 --- /dev/null +++ b/core/auth/metadata.json @@ -0,0 +1,15 @@ +{ + "dependencies": { + "@auth/prisma-adapter": "^1.0.12", + "@hookform/resolvers": "^3.9.1", + "@prisma/client": "^6.1.0", + "bcrypt": "^5.1.1", + "next-auth": "^5.0.0-beta.4", + "zod": "^3.24.1" + }, + "devDependencies": { + "@types/bcrypt": "^5.0.2", + "prisma": "^6.1.0", + "typescript": "^5" + } +} \ No newline at end of file diff --git a/core/auth/next-auth.d.ts b/core/auth/next-auth.d.ts new file mode 100644 index 0000000..7246b9f --- /dev/null +++ b/core/auth/next-auth.d.ts @@ -0,0 +1,14 @@ +import { UserRole } from '@prisma/client'; +import NextAuth, { type DefaultSession } from 'next-auth'; + +export type ExtendedUser = DefaultSession['user'] & { + role: UserRole; + isTwoFactorEnabled: boolean; + isOAuth: boolean; +}; + +declare module 'next-auth' { + interface Session { + user: ExtendedUser; + } +} diff --git a/core/auth/prisma/schema.prisma b/core/auth/prisma/schema.prisma new file mode 100644 index 0000000..2a051f8 --- /dev/null +++ b/core/auth/prisma/schema.prisma @@ -0,0 +1,82 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + + +enum UserRole { + ADMIN + USER +} + +model User { + id String @id @default(cuid()) + name String? + email String? @unique + emailVerified DateTime? + image String? + password String? + role UserRole @default(USER) + accounts Account[] + isTwoFactorEnabled Boolean @default(false) + twoFactorConfirmation TwoFactorConfirmation? +} + +model Account { + id String @id @default(cuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? @db.Text + access_token String? @db.Text + expires_at Int? + token_type String? + scope String? + id_token String? @db.Text + session_state String? + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) +} + +model VerificationToken { + id String @id @default(cuid()) + email String + token String @unique + expires DateTime + + @@unique([email, token]) +} + +model PasswordResetToken { + id String @id @default(cuid()) + email String + token String @unique + expires DateTime + + @@unique([email, token]) +} + +model TwoFactorToken { + id String @id @default(cuid()) + email String + token String @unique + expires DateTime + + @@unique([email, token]) +} + +model TwoFactorConfirmation { + id String @id @default(cuid()) + + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([userId]) +} \ No newline at end of file diff --git a/core/auth/routes.ts b/core/auth/routes.ts new file mode 100644 index 0000000..9d513ac --- /dev/null +++ b/core/auth/routes.ts @@ -0,0 +1,35 @@ +/** + * An array of routes that are accessible to the public + * These routes do not require authentication + * @type {string[]} + */ +export const publicRoutes = [ + "/", + "/auth/new-verification" + ]; + + /** + * An array of routes that are used for authentication + * These routes will redirect logged in users to /settings + * @type {string[]} + */ + export const authRoutes = [ + "/auth/login", + "/auth/register", + "/auth/error", + "/auth/reset", + "/auth/new-password" + ]; + + /** + * The prefix for API authentication routes + * Routes that start with this prefix are used for API authentication purposes + * @type {string} + */ + export const apiAuthPrefix = "/api/auth"; + + /** + * The default redirect path after logging in + * @type {string} + */ + export const DEFAULT_LOGIN_REDIRECT = "/settings"; \ No newline at end of file diff --git a/core/auth/schemas/index.ts b/core/auth/schemas/index.ts new file mode 100644 index 0000000..1a586a9 --- /dev/null +++ b/core/auth/schemas/index.ts @@ -0,0 +1,65 @@ +import * as z from "zod"; +import { UserRole } from "@prisma/client"; + +export const SettingsSchema = z.object({ + name: z.optional(z.string()), + isTwoFactorEnabled: z.optional(z.boolean()), + role: z.enum([UserRole.ADMIN, UserRole.USER]), + email: z.optional(z.string().email()), + password: z.optional(z.string().min(6)), + newPassword: z.optional(z.string().min(6)), +}) + .refine((data) => { + if (data.password && !data.newPassword) { + return false; + } + + return true; + }, { + message: "New password is required!", + path: ["newPassword"] + }) + .refine((data) => { + if (data.newPassword && !data.password) { + return false; + } + + return true; + }, { + message: "Password is required!", + path: ["password"] + }) + +export const NewPasswordSchema = z.object({ + password: z.string().min(6, { + message: "Minimum of 6 characters required", + }), +}); + +export const ResetSchema = z.object({ + email: z.string().email({ + message: "Email is required", + }), +}); + +export const LoginSchema = z.object({ + email: z.string().email({ + message: "Email is required", + }), + password: z.string().min(1, { + message: "Password is required", + }), + code: z.optional(z.string()), +}); + +export const RegisterSchema = z.object({ + email: z.string().email({ + message: "Email is required", + }), + password: z.string().min(6, { + message: "Minimum 6 characters required", + }), + name: z.string().min(1, { + message: "Name is required", + }), +}); \ No newline at end of file From fec1e4c891d43b1318a8150c81a906ff4a9f0b2d Mon Sep 17 00:00:00 2001 From: Vincenzo Fanizza Date: Thu, 23 Jan 2025 20:24:53 +0100 Subject: [PATCH 05/12] added component metadata --- core/auth/metadata.json | 36 ++++++++ packages/cli/src/commands/add.ts | 4 +- packages/cli/src/utils/add-component.ts | 112 ++++++++++-------------- 3 files changed, 82 insertions(+), 70 deletions(-) diff --git a/core/auth/metadata.json b/core/auth/metadata.json index 3ac1bc0..efdfe73 100644 --- a/core/auth/metadata.json +++ b/core/auth/metadata.json @@ -1,4 +1,40 @@ { + "name": "auth", + "files": [ + "actions/admin.ts", + "actions/login.ts", + "actions/logout.ts", + "actions/new-password.ts", + "actions/new-verification.ts", + "actions/register.ts", + "actions/reset.ts", + "actions/settings.ts", + "app/api/admin/route.ts", + "app/api/auth/[...nextauth]/route.ts", + "auth.config.ts", + "auth.ts", + "components/auth/auth.tsx", + "components/auth/logout-button.tsx", + "components/auth/user-button.tsx", + "components/ui/avatar.tsx", + "components/ui/examples/auth-example.tsx", + "data/account.ts", + "data/password-reset-token.ts", + "data/two-factor-confirmation.ts", + "data/two-factor-token.ts", + "data/user.ts", + "data/verification-token.ts", + "hooks/use-current-role.ts", + "hooks/use-current-user.ts", + "lib/auth.ts", + "lib/mail.ts", + "lib/token.ts", + "lib/utils.ts", + "next-auth.d.ts", + "prisma/schema.prisma", + "routes.ts", + "schemas/index.ts" + ], "dependencies": { "@auth/prisma-adapter": "^1.0.12", "@hookform/resolvers": "^3.9.1", diff --git a/packages/cli/src/commands/add.ts b/packages/cli/src/commands/add.ts index b4c6043..ad1686a 100644 --- a/packages/cli/src/commands/add.ts +++ b/packages/cli/src/commands/add.ts @@ -5,9 +5,9 @@ export const addCommand = new Command('add') .description('add a new component') .argument('', 'Name of the component to add') .action(async (componentName: string) => { - if (componentName.toLowerCase() !== 'auth') { + if (componentName !== 'auth') { console.error('Only the "auth" component can be added at this time.'); process.exit(1); } - await addComponent(); + await addComponent(componentName); }); \ No newline at end of file diff --git a/packages/cli/src/utils/add-component.ts b/packages/cli/src/utils/add-component.ts index a8e62d2..efc468f 100644 --- a/packages/cli/src/utils/add-component.ts +++ b/packages/cli/src/utils/add-component.ts @@ -5,90 +5,66 @@ import { handleError } from './handle-error'; import { logger } from './logger'; import { spinner } from './spinner'; -interface LibUIConfig { - aliases: Record; +interface Metadata { + files: string[]; + dependencies: Record; + devDependencies: Record; } -export async function addComponent() { - const spin = spinner('Adding auth component...'); +export async function addComponent(componentName: string) { + const spin = spinner(`Adding ${componentName} component...`); try { spin.start(); - const configPath = path.join(process.cwd(), 'libuiconfig.json'); - const configData = await fs.readFile(configPath, 'utf-8'); - const libConfig: LibUIConfig = JSON.parse(configData); + const remoteRepoBaseURL = 'https://raw.githubusercontent.com/nizzyabi/lib-ui/main'; + const basePath = `core/${componentName}`; + const metadata: Metadata = await fs.readFile(`${basePath}/metadata.json`, 'utf-8').then(JSON.parse); - const aliases = libConfig.aliases; + // First, validate that none of the target directories exist + for (const file of metadata.files) { + const localFilePath = path.join(process.cwd(), file); + const directory = path.dirname(localFilePath); - const remoteRepoBaseURL = 'https://raw.githubusercontent.com/nizzyabi/lib-ui/main/'; + if (await fs.access(directory).then(() => true).catch(() => false)) { + throw new Error(`Directory ${directory} already exists`); + } + } - // Define component files with their relative paths - const componentFiles = [ - 'actions/admin.ts', - 'actions/login.ts', - 'actions/logout.ts', - 'actions/new-password.ts', - 'actions/new-verification.ts', - 'actions/register.ts', - 'actions/reset.ts', - 'actions/settings.ts', - 'app/api/admin/route.ts', - 'app/api/auth/[...nextauth]/route.ts', - 'auth.config.ts', - 'auth.ts', - 'components/auth/auth.tsx', - 'components/auth/logout-button.tsx', - 'components/auth/user-button.tsx', - 'components/ui/avatar.tsx', - 'components/ui/examples/auth-example.tsx', - 'data/account.ts', - 'data/password-reset-token.ts', - 'data/two-factor-confirmation.ts', - 'data/two-factor-token.ts', - 'data/user.ts', - 'data/verification-token.ts', - 'hooks/use-current-role.ts', - 'hooks/use-current-user.ts', - 'lib/auth.ts', - 'lib/mail.ts', - 'lib/token.ts', - 'lib/utils.ts', - 'next-auth.d.ts', - 'prisma/schema.prisma', - 'routes.ts', - 'schemas/index.ts', - ]; + // Download all files first + const downloadedFiles = await Promise.all( + metadata.files.map(async (file) => { + const remoteFileURL = `${remoteRepoBaseURL}/${file}`; + logger.log(`Downloading file from ${remoteFileURL}`); + + try { + const response = await axios.get(remoteFileURL, { responseType: 'text' }); + if (!response.data) { + throw new Error('Failed to download file'); + } + return { file, content: response.data }; + } catch { + throw new Error(`Failed to download ${file} from ${remoteFileURL}`); + } + }) + ); - for (const file of componentFiles) { - const remoteFileURL = `${remoteRepoBaseURL}${file}`; + // Create directories and write files + for (const { file, content } of downloadedFiles) { const localFilePath = path.join(process.cwd(), file); - - console.log(`Downloading from: ${remoteFileURL}`); - console.log(`Saving to: ${localFilePath}`); - - // Ensure the local directory exists const directory = path.dirname(localFilePath); - await fs.mkdir(directory, { recursive: true }); - // Download the file from the remote repository - let response; - try { - response = await axios.get(remoteFileURL, { responseType: 'text' }); + logger.log(`Creating directory ${directory}`); + await fs.mkdir(directory, { recursive: true }); - if (response.data) { - await fs.writeFile(localFilePath, response.data, 'utf-8'); - logger.log(`Added ${file} to ${localFilePath}`); - } else { - throw new Error('Failed to download file'); - } - } catch { - throw new Error(`Failed to download ${file} from ${remoteFileURL}`); - } + await fs.writeFile(localFilePath, content, 'utf-8'); + logger.log(`Added ${file} to ${localFilePath}`); } - spin.succeed('Auth component added successfully'); + // TODO Install dependencies + + spin.succeed(`${componentName} component added successfully`); } catch (error) { - spin.fail('Failed to add auth component'); + spin.fail(`Failed to add ${componentName} component`); handleError(error); } } \ No newline at end of file From 99dd07f0e2f84ada5a20556efa1623c7be4bc7df Mon Sep 17 00:00:00 2001 From: Vincenzo Fanizza Date: Sun, 26 Jan 2025 13:28:42 +0100 Subject: [PATCH 06/12] update add-component command --- packages/cli/package.json | 1 - packages/cli/src/commands/init.ts | 1 - packages/cli/src/utils/add-component.ts | 12 ++--- packages/cli/src/utils/init-setup.ts | 72 +++---------------------- packages/cli/src/utils/logger.ts | 1 - 5 files changed, 14 insertions(+), 73 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 922e807..875e16c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -4,7 +4,6 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { - "build": "tsc", "build:cli": "tsc && chmod +x dist/index.js", "start": "ts-node src/index.ts", "prepare": "npm run build" diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 741fd2a..3dd17d8 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -19,7 +19,6 @@ export const initCommand = new Command('init') await runInit(projectDir); - logger.log('Project initialization completed successfully.'); } catch (error) { handleError(error); } diff --git a/packages/cli/src/utils/add-component.ts b/packages/cli/src/utils/add-component.ts index efc468f..ae93c11 100644 --- a/packages/cli/src/utils/add-component.ts +++ b/packages/cli/src/utils/add-component.ts @@ -20,7 +20,7 @@ export async function addComponent(componentName: string) { const basePath = `core/${componentName}`; const metadata: Metadata = await fs.readFile(`${basePath}/metadata.json`, 'utf-8').then(JSON.parse); - // First, validate that none of the target directories exist + // Validate that none of the target directories exist for (const file of metadata.files) { const localFilePath = path.join(process.cwd(), file); const directory = path.dirname(localFilePath); @@ -39,11 +39,11 @@ export async function addComponent(componentName: string) { try { const response = await axios.get(remoteFileURL, { responseType: 'text' }); if (!response.data) { - throw new Error('Failed to download file'); + throw new Error(`No content retrieved from ${remoteFileURL}`); } return { file, content: response.data }; - } catch { - throw new Error(`Failed to download ${file} from ${remoteFileURL}`); + } catch (error) { + throw new Error(`Failed to download remote file from ${remoteFileURL}: ${error}`); } }) ); @@ -60,11 +60,11 @@ export async function addComponent(componentName: string) { logger.log(`Added ${file} to ${localFilePath}`); } - // TODO Install dependencies + // TODO Add dependencies to package.json spin.succeed(`${componentName} component added successfully`); } catch (error) { spin.fail(`Failed to add ${componentName} component`); handleError(error); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/packages/cli/src/utils/init-setup.ts b/packages/cli/src/utils/init-setup.ts index 7367d41..f016fca 100644 --- a/packages/cli/src/utils/init-setup.ts +++ b/packages/cli/src/utils/init-setup.ts @@ -16,16 +16,16 @@ export async function runInit(projectDir: string) { process.exit(1); } - // Initialize Next.js project with TypeScript - const nextInitSpinner = spinner('Initializing Next.js project with TypeScript...').start(); + // Initialize Next.js project with TypeScript and Tailwind + const nextInitSpinner = spinner('Initializing Next.js project with TypeScript and Tailwind...').start(); try { - await execPromise('npx create-next-app@latest . --typescript --use-npm --no-eslint --no-tailwind --no-src-dir --no-experimental-app --yes', + await execPromise('npx create-next-app@latest . --typescript --use-npm --no-eslint --no-src-dir --no-experimental-app --yes', { cwd: projectDir, env: { ...process.env, FORCE_COLOR: '1' }, } ); - nextInitSpinner.success('Next.js project initialized with TypeScript.'); + nextInitSpinner.success('Next.js project initialized with TypeScript and Tailwind.'); } catch (error) { nextInitSpinner.error('Failed to initialize Next.js project.'); if (error instanceof Error) { @@ -34,53 +34,6 @@ export async function runInit(projectDir: string) { throw error; } - // Install Tailwind CSS - const tailwindSpinner = spinner('Installing Tailwind CSS...').start(); - try { - await execPromise('npm install -D tailwindcss postcss autoprefixer', { cwd: projectDir }); - await execPromise('npx tailwindcss init -p', { cwd: projectDir }); - tailwindSpinner.success('Tailwind CSS installed.'); - - // Configure Tailwind CSS - const tailwindConfigPath = path.join(projectDir, 'tailwind.config.js'); - const tailwindConfig = `/** @type {import('tailwindcss').Config} */ -module.exports = { - content: [ - './pages/**/*.{js,ts,jsx,tsx,mdx}', - './components/**/*.{js,ts,jsx,tsx,mdx}', - './app/**/*.{js,ts,jsx,tsx,mdx}', - ], - theme: { - extend: {}, - }, - plugins: [], -}; -`; - await fs.writeFile(tailwindConfigPath, tailwindConfig, 'utf8'); - - // Check for app directory structure - const appDirExists = await exists(path.join(projectDir, 'app')); - const globalCssPath = appDirExists - ? path.join(projectDir, 'app', 'globals.css') - : path.join(projectDir, 'styles', 'globals.css'); - - // Create directory if it doesn't exist - await fs.mkdir(path.dirname(globalCssPath), { recursive: true }); - - // Add Tailwind directives to globals.css - const globalsCssContent = `@tailwind base; -@tailwind components; -@tailwind utilities; - -/* Add your custom styles here */ -`; - await fs.writeFile(globalCssPath, globalsCssContent, 'utf8'); - logger.log(highlighter.success('Tailwind CSS configured successfully.')); - } catch (error) { - tailwindSpinner.error('Failed to install Tailwind CSS.'); - throw error; - } - // Install Prisma const prismaSpinner = spinner('Setting up Prisma...').start(); try { @@ -115,7 +68,7 @@ datasource db { const exampleApiRoute = `import { NextResponse } from 'next/server' export async function GET() { - return NextResponse.json({ message: 'Hello from libui API!' }) + return NextResponse.json({ message: 'Hello from the libui API!' }) } `; await fs.writeFile(path.join(projectDir, 'app', 'api', 'hello', 'route.ts'), exampleApiRoute, 'utf8'); @@ -199,16 +152,7 @@ API_BASE_URL="http://localhost:3000/api" } logger.log(highlighter.success('Initialization complete! You can now:')); - logger.log('1. Update your .env file with your database credentials'); - logger.log('2. Run `npx prisma generate` to generate the Prisma Client'); - logger.log('3. Start your development server with `npm run dev`'); + logger.log(highlighter.success('1. Update your .env file with your database credentials')); + logger.log(highlighter.success('2. Run `npx prisma generate` to generate the Prisma Client')); + logger.log(highlighter.success('3. Start your development server with `npm run dev`')); } - -async function exists(path: string): Promise { - try { - await fs.access(path); - return true; - } catch { - return false; - } -} \ No newline at end of file diff --git a/packages/cli/src/utils/logger.ts b/packages/cli/src/utils/logger.ts index 8d019b0..05f0c66 100644 --- a/packages/cli/src/utils/logger.ts +++ b/packages/cli/src/utils/logger.ts @@ -7,5 +7,4 @@ export const logger = { error: (message: string) => { console.error(chalk.red(message)); }, - // Add other methods if necessary }; \ No newline at end of file From cd88652db5fc149f162f4b534d48ac9fc8e4d2c2 Mon Sep 17 00:00:00 2001 From: Vincenzo Fanizza Date: Sun, 26 Jan 2025 16:19:44 +0100 Subject: [PATCH 07/12] env variables --- core/auth/metadata.json | 9 +++ packages/cli/src/commands/add.ts | 2 +- packages/cli/src/utils/add-component.ts | 96 ++++++++++++++++++++++--- packages/cli/src/utils/init-setup.ts | 54 ++------------ packages/cli/src/utils/logger.ts | 8 ++- 5 files changed, 109 insertions(+), 60 deletions(-) diff --git a/core/auth/metadata.json b/core/auth/metadata.json index efdfe73..571b346 100644 --- a/core/auth/metadata.json +++ b/core/auth/metadata.json @@ -35,6 +35,15 @@ "routes.ts", "schemas/index.ts" ], + "envVariables": [ + "DATABASE_URL", + "NEXTAUTH_SECRET", + "NEXTAUTH_URL", + "GOOGLE_CLIENT_ID", + "GOOGLE_CLIENT_SECRET", + "GITHUB_CLIENT_ID", + "GITHUB_CLIENT_SECRET" + ], "dependencies": { "@auth/prisma-adapter": "^1.0.12", "@hookform/resolvers": "^3.9.1", diff --git a/packages/cli/src/commands/add.ts b/packages/cli/src/commands/add.ts index ad1686a..d0a7df4 100644 --- a/packages/cli/src/commands/add.ts +++ b/packages/cli/src/commands/add.ts @@ -10,4 +10,4 @@ export const addCommand = new Command('add') process.exit(1); } await addComponent(componentName); - }); \ No newline at end of file + }); diff --git a/packages/cli/src/utils/add-component.ts b/packages/cli/src/utils/add-component.ts index ae93c11..0cf2264 100644 --- a/packages/cli/src/utils/add-component.ts +++ b/packages/cli/src/utils/add-component.ts @@ -4,29 +4,47 @@ import axios from 'axios'; import { handleError } from './handle-error'; import { logger } from './logger'; import { spinner } from './spinner'; +import { highlighter } from './highlighter'; interface Metadata { files: string[]; + envVariables: string[]; dependencies: Record; devDependencies: Record; } export async function addComponent(componentName: string) { - const spin = spinner(`Adding ${componentName} component...`); + const spin = spinner(`Adding ${componentName} component...`); try { + const configPath = path.join(process.cwd(), 'libui.config.json'); + if (!(await fs.access(configPath).then(() => true).catch(() => false))) { + throw new Error('libui.config.json not found. Please run `libui-next init` first.'); + } + + const config = JSON.parse(await fs.readFile(configPath, 'utf8')); + if (componentName in config.addedComponents) { + throw new Error(`${componentName} component already added.`); + } + spin.start(); - const remoteRepoBaseURL = 'https://raw.githubusercontent.com/nizzyabi/lib-ui/main'; + const remoteRepoBaseURL = 'https://raw.githubusercontent.com/nizzyabi/lib-ui/cli'; const basePath = `core/${componentName}`; - const metadata: Metadata = await fs.readFile(`${basePath}/metadata.json`, 'utf-8').then(JSON.parse); + logger.log(`Fetching component metadata...`); + const response = await axios.get(`${remoteRepoBaseURL}/${basePath}/metadata.json`); + if (!response.data) { + throw new Error(`Failed to fetch metadata.json`); + } + const metadata: Metadata = response.data; + // Validate that none of the target directories exist for (const file of metadata.files) { + logger.log(`Checking if file ${file} exists...`); const localFilePath = path.join(process.cwd(), file); - const directory = path.dirname(localFilePath); - if (await fs.access(directory).then(() => true).catch(() => false)) { - throw new Error(`Directory ${directory} already exists`); + if (await fs.access(localFilePath).then(() => true).catch(() => false)) { + throw new Error(`File ${localFilePath} already exists`); } } @@ -43,7 +61,7 @@ export async function addComponent(componentName: string) { } return { file, content: response.data }; } catch (error) { - throw new Error(`Failed to download remote file from ${remoteFileURL}: ${error}`); + throw new Error(`Failed to download remote file from ${remoteFileURL}: `); } }) ); @@ -60,9 +78,67 @@ export async function addComponent(componentName: string) { logger.log(`Added ${file} to ${localFilePath}`); } - // TODO Add dependencies to package.json - - spin.succeed(`${componentName} component added successfully`); + // Validate package.json exists + const packageJsonPath = path.join(process.cwd(), 'package.json'); + if (!await fs.access(packageJsonPath).then(() => true).catch(() => false)) { + throw new Error('package.json not found in the current directory'); + } + + // Add dependencies to package.json + const packageJson = await fs.readFile(packageJsonPath, 'utf-8'); + const packageJsonObject = JSON.parse(packageJson); + + // Handle dependencies + packageJsonObject.dependencies = packageJsonObject.dependencies || {}; + for (const [dep, version] of Object.entries(metadata.dependencies)) { + if (packageJsonObject.dependencies[dep]) { + if (packageJsonObject.dependencies[dep] === version) { + logger.info(`Dependency ${dep}@${version} already exists in package.json`); + } else { + logger.warn(`Dependency ${dep} exists with different version: ${packageJsonObject.dependencies[dep]} (wanted: ${version})`); + } + } else { + packageJsonObject.dependencies[dep] = version; + } + } + + // Handle devDependencies + packageJsonObject.devDependencies = packageJsonObject.devDependencies || {}; + for (const [dep, version] of Object.entries(metadata.devDependencies)) { + if (packageJsonObject.devDependencies[dep]) { + if (packageJsonObject.devDependencies[dep] === version) { + logger.info(`DevDependency ${dep}@${version} already exists in package.json`); + } else { + logger.warn(`DevDependency ${dep} exists with different version: ${packageJsonObject.devDependencies[dep]} (wanted: ${version}). Skipping...`); + } + } else { + packageJsonObject.devDependencies[dep] = version; + } + } + + // Add env variables + const envFilePath = path.join(process.cwd(), '.env'); + const envFile = await fs.readFile(envFilePath, 'utf-8'); + for (const envVar of metadata.envVariables) { + const regex = new RegExp(`^${envVar}\\s*=\\s*`, 'm'); + if (regex.test(envFile)) { + logger.warn(`Env variable ${envVar} already exists in .env file`); + } else { + await fs.writeFile(envFilePath, `\n${envVar}=...\n`, { flag: 'a' }); + } + } + + await fs.writeFile(packageJsonPath, JSON.stringify(packageJsonObject, null, 2), 'utf-8'); + + // Add the component to the config + config.addedComponents.push(componentName); + await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8'); + + + spin.succeed('Files and dependencies added successfully'); + logger.log(highlighter.success(`The ${componentName} component has been added successfully`)); + logger.log(highlighter.success('1. Run `npm install` to install the new dependencies')); + logger.log(highlighter.success('2. Run `npx prisma generate` to update the Prisma Client')); } catch (error) { spin.fail(`Failed to add ${componentName} component`); handleError(error); diff --git a/packages/cli/src/utils/init-setup.ts b/packages/cli/src/utils/init-setup.ts index f016fca..6383eef 100644 --- a/packages/cli/src/utils/init-setup.ts +++ b/packages/cli/src/utils/init-setup.ts @@ -39,19 +39,6 @@ export async function runInit(projectDir: string) { try { await execPromise('npm install -D prisma @prisma/client', { cwd: projectDir }); await fs.mkdir(path.join(projectDir, 'prisma'), { recursive: true }); - - const prismaSchema = `generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -// Add your models here -`; - await fs.writeFile(path.join(projectDir, 'prisma', 'schema.prisma'), prismaSchema, 'utf8'); prismaSpinner.success('Prisma setup completed.'); } catch (error) { prismaSpinner.error('Failed to setup Prisma.'); @@ -97,47 +84,18 @@ API_BASE_URL="http://localhost:3000/api" throw error; } - // Create libuiconfig.json (expanded version) + // Create libui.config.json const config = { - tsx: true, - tailwind: { - config: 'tailwind.config.js', - css: 'styles/globals.css', - baseColor: 'blue', - cssVariables: true, - prefix: '', - }, - rsc: false, - database: { - provider: 'prisma', - schema: 'prisma/schema.prisma', - url: 'DATABASE_URL', - }, - api: { - baseUrl: '/api', - version: 'v1', - cors: { - enabled: true, - origins: ['http://localhost:3000'], - }, - }, - aliases: { - utils: 'utils', - components: 'components', - lib: 'lib', - hooks: 'hooks', - api: 'app/api', - prisma: 'prisma', - }, + addedComponents: [], }; - const configPath = path.join(projectDir, 'libuiconfig.json'); + const configPath = path.join(projectDir, 'libui.config.json'); try { - const configSpinner = spinner('Writing libuiconfig.json...').start(); + const configSpinner = spinner('Writing libui.config.json...').start(); await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8'); - configSpinner.success('libuiconfig.json written successfully.'); + configSpinner.success('libui.config.json written successfully.'); } catch (error) { - logger.error('Failed to write libuiconfig.json.'); + logger.error('Failed to write libui.config.json.'); throw error; } diff --git a/packages/cli/src/utils/logger.ts b/packages/cli/src/utils/logger.ts index 05f0c66..45ac50a 100644 --- a/packages/cli/src/utils/logger.ts +++ b/packages/cli/src/utils/logger.ts @@ -2,7 +2,13 @@ import chalk from 'chalk'; export const logger = { log: (message: string) => { - console.log(message); + console.log(chalk.grey(message)); + }, + info: (message: string) => { + console.info(chalk.blue(message)); + }, + warn: (message: string) => { + console.warn(chalk.yellow(message)); }, error: (message: string) => { console.error(chalk.red(message)); From 93349248c9830e355f29e932ae121b4a21f449a7 Mon Sep 17 00:00:00 2001 From: Vincenzo Fanizza Date: Sun, 26 Jan 2025 17:13:31 +0100 Subject: [PATCH 08/12] improved logging and code structure --- packages/cli/src/commands/add.ts | 26 ++++-- packages/cli/src/commands/init.ts | 31 +++---- packages/cli/src/types/metadata.ts | 6 ++ .../utils/{add-component.ts => add-utils.ts} | 80 ++++++++----------- packages/cli/src/utils/errors.ts | 10 +++ packages/cli/src/utils/files.ts | 12 +++ packages/cli/src/utils/handle-error.ts | 11 --- packages/cli/src/utils/highlighter.ts | 7 -- .../utils/{init-setup.ts => init-utils.ts} | 75 ++++++++--------- packages/cli/src/utils/logger.ts | 16 ---- packages/cli/src/utils/spinner.ts | 19 ----- packages/cli/src/utils/visuals.ts | 38 +++++++++ 12 files changed, 163 insertions(+), 168 deletions(-) create mode 100644 packages/cli/src/types/metadata.ts rename packages/cli/src/utils/{add-component.ts => add-utils.ts} (66%) create mode 100644 packages/cli/src/utils/errors.ts create mode 100644 packages/cli/src/utils/files.ts delete mode 100644 packages/cli/src/utils/handle-error.ts delete mode 100644 packages/cli/src/utils/highlighter.ts rename packages/cli/src/utils/{init-setup.ts => init-utils.ts} (61%) delete mode 100644 packages/cli/src/utils/logger.ts delete mode 100644 packages/cli/src/utils/spinner.ts create mode 100644 packages/cli/src/utils/visuals.ts diff --git a/packages/cli/src/commands/add.ts b/packages/cli/src/commands/add.ts index d0a7df4..778e1a6 100644 --- a/packages/cli/src/commands/add.ts +++ b/packages/cli/src/commands/add.ts @@ -1,13 +1,27 @@ import { Command } from 'commander'; -import { addComponent } from '../utils/add-component'; +import { addComponent } from '../utils/add-utils'; +import { logger } from '../utils/visuals'; +import { handleError } from '../utils/errors'; +import { exists } from '../utils/files'; export const addCommand = new Command('add') - .description('add a new component') + .description('Add a new libui component to your project') .argument('', 'Name of the component to add') .action(async (componentName: string) => { - if (componentName !== 'auth') { - console.error('Only the "auth" component can be added at this time.'); - process.exit(1); + try { + // Initial validation + if (componentName !== 'auth') { + logger.error('Only the "auth" component can be added at this time.'); + process.exit(1); + } + if (!(await exists('libui.config.json'))) { + throw new Error('libui.config.json not found. Please run `libui-next init` first.'); + } + + // Run core command logic + await addComponent(componentName); + + } catch (error) { + handleError(error); } - await addComponent(componentName); }); diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 3dd17d8..7297d3d 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -1,34 +1,23 @@ import { Command } from 'commander'; -import { runInit } from '../utils/init-setup'; -import { logger } from '../utils/logger'; -import { handleError } from '../utils/handle-error'; -import path from 'path'; -import { promises as fs } from 'fs'; +import { runInit } from '../utils/init-utils'; +import { logger } from '../utils/visuals'; +import { handleError } from '../utils/errors'; +import { exists } from '../utils/files'; export const initCommand = new Command('init') - .description('Initialize a new Next.js project with TypeScript and Tailwind CSS') + .description('Setup libui in your project') .action(async () => { try { - const projectDir = process.cwd(); - const configPath = path.join(projectDir, 'components.json'); - - if (await exists(configPath)) { - logger.error('components.json already exists in this directory. Initialization aborted.'); + // Initial validation + if (await exists('libui.config.json')) { + logger.error('libui.config.json already exists in this directory. Initialization aborted.'); process.exit(1); } - await runInit(projectDir); + // Run core command logic + await runInit(); } catch (error) { handleError(error); } }); - -async function exists(path: string): Promise { - try { - await fs.access(path); - return true; - } catch { - return false; - } -} \ No newline at end of file diff --git a/packages/cli/src/types/metadata.ts b/packages/cli/src/types/metadata.ts new file mode 100644 index 0000000..ce13ed4 --- /dev/null +++ b/packages/cli/src/types/metadata.ts @@ -0,0 +1,6 @@ +export interface Metadata { + files: string[]; + envVariables: string[]; + dependencies: Record; + devDependencies: Record; +} \ No newline at end of file diff --git a/packages/cli/src/utils/add-component.ts b/packages/cli/src/utils/add-utils.ts similarity index 66% rename from packages/cli/src/utils/add-component.ts rename to packages/cli/src/utils/add-utils.ts index 0cf2264..11a65cc 100644 --- a/packages/cli/src/utils/add-component.ts +++ b/packages/cli/src/utils/add-utils.ts @@ -1,34 +1,33 @@ import path from 'path'; import fs from 'fs/promises'; import axios from 'axios'; -import { handleError } from './handle-error'; -import { logger } from './logger'; -import { spinner } from './spinner'; -import { highlighter } from './highlighter'; - -interface Metadata { - files: string[]; - envVariables: string[]; - dependencies: Record; - devDependencies: Record; -} +import { logger, spinner } from './visuals'; +import { Metadata } from '../types/metadata'; +import { exists } from './files'; export async function addComponent(componentName: string) { - const spin = spinner(`Adding ${componentName} component...`); + const spin = spinner(`Adding ${componentName} component...`); + const projectDir = process.cwd(); try { - const configPath = path.join(process.cwd(), 'libui.config.json'); - if (!(await fs.access(configPath).then(() => true).catch(() => false))) { - throw new Error('libui.config.json not found. Please run `libui-next init` first.'); + // Validate libui.config.json exists and the component is not already added + const configPath = path.join(projectDir, 'libui.config.json'); + if (!await exists(configPath)) { + throw new Error('libui.config.json not found in the current directory'); } - const config = JSON.parse(await fs.readFile(configPath, 'utf8')); if (componentName in config.addedComponents) { throw new Error(`${componentName} component already added.`); } - + + // Validate package.json exists + const packageJsonPath = path.join(projectDir, 'package.json'); + if (!await exists(packageJsonPath)) { + throw new Error('package.json not found in the current directory'); + } + spin.start(); - const remoteRepoBaseURL = 'https://raw.githubusercontent.com/nizzyabi/lib-ui/cli'; + const remoteRepoBaseURL = 'https://raw.githubusercontent.com/nizzyabi/lib-ui/main'; const basePath = `core/${componentName}`; logger.log(`Fetching component metadata...`); @@ -39,11 +38,10 @@ export async function addComponent(componentName: string) { const metadata: Metadata = response.data; // Validate that none of the target directories exist + logger.log('Checking if files already exist...'); for (const file of metadata.files) { - logger.log(`Checking if file ${file} exists...`); - const localFilePath = path.join(process.cwd(), file); - - if (await fs.access(localFilePath).then(() => true).catch(() => false)) { + const localFilePath = path.join(projectDir, file); + if (await exists(localFilePath)) { throw new Error(`File ${localFilePath} already exists`); } } @@ -52,38 +50,26 @@ export async function addComponent(componentName: string) { const downloadedFiles = await Promise.all( metadata.files.map(async (file) => { const remoteFileURL = `${remoteRepoBaseURL}/${file}`; - logger.log(`Downloading file from ${remoteFileURL}`); - - try { - const response = await axios.get(remoteFileURL, { responseType: 'text' }); - if (!response.data) { - throw new Error(`No content retrieved from ${remoteFileURL}`); - } - return { file, content: response.data }; - } catch (error) { - throw new Error(`Failed to download remote file from ${remoteFileURL}: `); + + logger.log(`Downloading file from ${remoteFileURL}`); + const response = await axios.get(remoteFileURL, { responseType: 'text' }); + if (!response.data) { + throw new Error(`No content retrieved from ${remoteFileURL}`); } + return { file, content: response.data }; }) ); // Create directories and write files for (const { file, content } of downloadedFiles) { - const localFilePath = path.join(process.cwd(), file); + const localFilePath = path.join(projectDir, file); const directory = path.dirname(localFilePath); - logger.log(`Creating directory ${directory}`); await fs.mkdir(directory, { recursive: true }); - await fs.writeFile(localFilePath, content, 'utf-8'); logger.log(`Added ${file} to ${localFilePath}`); } - // Validate package.json exists - const packageJsonPath = path.join(process.cwd(), 'package.json'); - if (!await fs.access(packageJsonPath).then(() => true).catch(() => false)) { - throw new Error('package.json not found in the current directory'); - } - // Add dependencies to package.json const packageJson = await fs.readFile(packageJsonPath, 'utf-8'); const packageJsonObject = JSON.parse(packageJson); @@ -117,7 +103,7 @@ export async function addComponent(componentName: string) { } // Add env variables - const envFilePath = path.join(process.cwd(), '.env'); + const envFilePath = path.join(projectDir, '.env'); const envFile = await fs.readFile(envFilePath, 'utf-8'); for (const envVar of metadata.envVariables) { const regex = new RegExp(`^${envVar}\\s*=\\s*`, 'm'); @@ -133,14 +119,14 @@ export async function addComponent(componentName: string) { // Add the component to the config config.addedComponents.push(componentName); await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8'); - spin.succeed('Files and dependencies added successfully'); - logger.log(highlighter.success(`The ${componentName} component has been added successfully`)); - logger.log(highlighter.success('1. Run `npm install` to install the new dependencies')); - logger.log(highlighter.success('2. Run `npx prisma generate` to update the Prisma Client')); + logger.success(`The ${componentName} component has been added successfully`); + logger.success('1. Update the .env file with the correct values'); + logger.success('2. Run `npm install` to install the new dependencies'); + logger.success('3. Run `npx prisma generate` to update the Prisma Client'); } catch (error) { spin.fail(`Failed to add ${componentName} component`); - handleError(error); + throw error; } } \ No newline at end of file diff --git a/packages/cli/src/utils/errors.ts b/packages/cli/src/utils/errors.ts new file mode 100644 index 0000000..e4e83ad --- /dev/null +++ b/packages/cli/src/utils/errors.ts @@ -0,0 +1,10 @@ +import { logger } from './visuals'; + +export function handleError(error: unknown) { + if (error instanceof Error) { + logger.error(error.message); + } else { + logger.error('An unknown error occurred.'); + } + process.exit(1); +} \ No newline at end of file diff --git a/packages/cli/src/utils/files.ts b/packages/cli/src/utils/files.ts new file mode 100644 index 0000000..af3c6d5 --- /dev/null +++ b/packages/cli/src/utils/files.ts @@ -0,0 +1,12 @@ +import fs from 'fs/promises'; +import path from 'path'; + +export async function exists(relativePath: string): Promise { + const configPath = path.join(process.cwd(), relativePath); + try { + await fs.access(configPath); + return true; + } catch { + return false; + } +} diff --git a/packages/cli/src/utils/handle-error.ts b/packages/cli/src/utils/handle-error.ts deleted file mode 100644 index 6491e1c..0000000 --- a/packages/cli/src/utils/handle-error.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { logger } from './logger'; -import { highlighter } from './highlighter'; - -export function handleError(error: unknown) { - if (error instanceof Error) { - logger.error(highlighter.error(error.message)); - } else { - logger.error(highlighter.error('An unknown error occurred.')); - } - process.exit(1); -} \ No newline at end of file diff --git a/packages/cli/src/utils/highlighter.ts b/packages/cli/src/utils/highlighter.ts deleted file mode 100644 index 76776ff..0000000 --- a/packages/cli/src/utils/highlighter.ts +++ /dev/null @@ -1,7 +0,0 @@ -import chalk from 'chalk'; - -export const highlighter = { - error: (str: string) => chalk.red(str), - success: (str: string) => chalk.green(str), - info: (str: string) => chalk.blue(str), -}; \ No newline at end of file diff --git a/packages/cli/src/utils/init-setup.ts b/packages/cli/src/utils/init-utils.ts similarity index 61% rename from packages/cli/src/utils/init-setup.ts rename to packages/cli/src/utils/init-utils.ts index 6383eef..ffb1f92 100644 --- a/packages/cli/src/utils/init-setup.ts +++ b/packages/cli/src/utils/init-utils.ts @@ -2,22 +2,36 @@ import { promises as fs } from 'fs'; import path from 'path'; import { exec } from 'child_process'; import { promisify } from 'util'; -import { logger } from './logger'; -import { highlighter } from './highlighter'; -import { spinner } from './spinner'; +import { logger, spinner } from './visuals'; const execPromise = promisify(exec); -export async function runInit(projectDir: string) { +const helloApiRouteContent = `import { NextResponse } from 'next/server' + +export async function GET() { + return NextResponse.json({ message: 'Hello from the libui API!' }) +} +`; + +const envFileContent = `# This file will contain all the required environment variables for the application +# Make sure to update these values in your .env file + +DATABASE_URL="postgresql://user:password@localhost:5432/dbname?schema=public" + +API_BASE_URL="http://localhost:3000/api" +`; + +export async function runInit() { // Check if directory is empty + const projectDir = process.cwd(); const dirContents = await fs.readdir(projectDir); if (dirContents.length > 0) { logger.error('Directory is not empty. Please run this command in an empty directory.'); process.exit(1); } - // Initialize Next.js project with TypeScript and Tailwind - const nextInitSpinner = spinner('Initializing Next.js project with TypeScript and Tailwind...').start(); + // 1. Create Next.js project + const nextInitSpinner = spinner('Creating Next.js project...').start(); try { await execPromise('npx create-next-app@latest . --typescript --use-npm --no-eslint --no-src-dir --no-experimental-app --yes', { @@ -25,16 +39,13 @@ export async function runInit(projectDir: string) { env: { ...process.env, FORCE_COLOR: '1' }, } ); - nextInitSpinner.success('Next.js project initialized with TypeScript and Tailwind.'); + nextInitSpinner.success('Next.js project created.'); } catch (error) { - nextInitSpinner.error('Failed to initialize Next.js project.'); - if (error instanceof Error) { - logger.error(`Error details: ${error.message}`); - } + nextInitSpinner.error('Failed to create Next.js project.'); throw error; } - // Install Prisma + // 2. Install Prisma const prismaSpinner = spinner('Setting up Prisma...').start(); try { await execPromise('npm install -D prisma @prisma/client', { cwd: projectDir }); @@ -45,46 +56,28 @@ export async function runInit(projectDir: string) { throw error; } - // Create API directory structure + // 3. Create API directory structure const apiSpinner = spinner('Creating API structure...').start(); try { - // Create all necessary directories - await fs.mkdir(path.join(projectDir, 'app', 'api', 'hello'), { recursive: true }); - - // Create example API route - const exampleApiRoute = `import { NextResponse } from 'next/server' - -export async function GET() { - return NextResponse.json({ message: 'Hello from the libui API!' }) -} -`; - await fs.writeFile(path.join(projectDir, 'app', 'api', 'hello', 'route.ts'), exampleApiRoute, 'utf8'); + await fs.mkdir(path.join(projectDir, 'app/api/hello'), { recursive: true }); + await fs.writeFile(path.join(projectDir, 'app/api/hello/route.ts'), helloApiRouteContent, 'utf8'); apiSpinner.success('API structure created.'); } catch (error) { apiSpinner.error('Failed to create API structure.'); throw error; } - // Create both .env and .env.sample with the same content + // 4. Create .env file const envSpinner = spinner('Creating environment file...').start(); try { - const envContent = `# This file will contain all the required environment variables for the application -# Make sure to update these values in your .env file - -# Database Configuration -DATABASE_URL="postgresql://user:password@localhost:5432/dbname?schema=public" - -# API Configuration -API_BASE_URL="http://localhost:3000/api" -`; - await fs.writeFile(path.join(projectDir, '.env'), envContent, 'utf8'); + await fs.writeFile(path.join(projectDir, '.env'), envFileContent, 'utf8'); envSpinner.success('Environment file created.'); } catch (error) { envSpinner.error('Failed to create environment file.'); throw error; } - // Create libui.config.json + // 5. Create libui.config.json const config = { addedComponents: [], }; @@ -99,7 +92,7 @@ API_BASE_URL="http://localhost:3000/api" throw error; } - // Install additional dependencies + // 6. Install additional dependencies const dependenciesSpinner = spinner('Installing additional dependencies...').start(); try { await execPromise('npm install react react-dom next @prisma/client', { cwd: projectDir }); @@ -109,8 +102,8 @@ API_BASE_URL="http://localhost:3000/api" throw error; } - logger.log(highlighter.success('Initialization complete! You can now:')); - logger.log(highlighter.success('1. Update your .env file with your database credentials')); - logger.log(highlighter.success('2. Run `npx prisma generate` to generate the Prisma Client')); - logger.log(highlighter.success('3. Start your development server with `npm run dev`')); + logger.success('Initialization complete! You can now:'); + logger.success('1. Update your .env file with your database credentials'); + logger.success('2. Run `npx prisma generate` to generate the Prisma Client'); + logger.success('3. Start your development server with `npm run dev`'); } diff --git a/packages/cli/src/utils/logger.ts b/packages/cli/src/utils/logger.ts deleted file mode 100644 index 45ac50a..0000000 --- a/packages/cli/src/utils/logger.ts +++ /dev/null @@ -1,16 +0,0 @@ -import chalk from 'chalk'; - -export const logger = { - log: (message: string) => { - console.log(chalk.grey(message)); - }, - info: (message: string) => { - console.info(chalk.blue(message)); - }, - warn: (message: string) => { - console.warn(chalk.yellow(message)); - }, - error: (message: string) => { - console.error(chalk.red(message)); - }, -}; \ No newline at end of file diff --git a/packages/cli/src/utils/spinner.ts b/packages/cli/src/utils/spinner.ts deleted file mode 100644 index 3613e1f..0000000 --- a/packages/cli/src/utils/spinner.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { createSpinner } from 'nanospinner' - -export function spinner(message: string) { - const sp = createSpinner(message); - return { - start: () => { - sp.start(); - return sp; - }, - succeed: (text?: string) => { - sp.success({ text }); - return sp; - }, - fail: (text?: string) => { - sp.error({ text }); - return sp; - }, - }; -} \ No newline at end of file diff --git a/packages/cli/src/utils/visuals.ts b/packages/cli/src/utils/visuals.ts new file mode 100644 index 0000000..9fb42e6 --- /dev/null +++ b/packages/cli/src/utils/visuals.ts @@ -0,0 +1,38 @@ +import chalk from 'chalk'; +import { createSpinner } from 'nanospinner' + +export const logger = { + log: (message: string) => { + console.log(chalk.grey(message)); + }, + info: (message: string) => { + console.info(chalk.blue(message)); + }, + warn: (message: string) => { + console.warn(chalk.yellow(message)); + }, + error: (message: string) => { + console.error(chalk.red(message)); + }, + success: (message: string) => { + console.log(chalk.green(message)); + }, +}; + +export function spinner(message: string) { + const sp = createSpinner(message); + return { + start: () => { + sp.start(); + return sp; + }, + succeed: (text?: string) => { + sp.success({ text }); + return sp; + }, + fail: (text?: string) => { + sp.error({ text }); + return sp; + }, + }; +} \ No newline at end of file From b74a9efc9df1378591fa78945b6c77133bad6a4b Mon Sep 17 00:00:00 2001 From: Vincenzo Fanizza Date: Sun, 26 Jan 2025 18:08:26 +0100 Subject: [PATCH 09/12] updated files --- TODO.md | 23 --------------- core/auth/metadata.json | 6 ++-- packages/cli/src/utils/add-utils.ts | 38 +++++++++++------------- packages/cli/src/utils/init-utils.ts | 43 +++++++++++++++++----------- 4 files changed, 47 insertions(+), 63 deletions(-) delete mode 100644 TODO.md diff --git a/TODO.md b/TODO.md deleted file mode 100644 index c133cf1..0000000 --- a/TODO.md +++ /dev/null @@ -1,23 +0,0 @@ -# TODOs - -- decide design -- write 1st version of docs -- set up npm package -- landing page - -- add at least 3 components before making a video about it: - - auth - - landing page - - stripe - -auth implementation: -- next auth -- providers - - email, password - - google -- auth wrapper - - if the app is just initialized, save in index.tsx - - otherwise log and inform the user you're saving them in another file - -landing page: -- design it for our website and then use a very simple version to release \ No newline at end of file diff --git a/core/auth/metadata.json b/core/auth/metadata.json index 571b346..e7dc1ff 100644 --- a/core/auth/metadata.json +++ b/core/auth/metadata.json @@ -27,9 +27,9 @@ "hooks/use-current-role.ts", "hooks/use-current-user.ts", "lib/auth.ts", + "lib/db.ts", "lib/mail.ts", "lib/token.ts", - "lib/utils.ts", "next-auth.d.ts", "prisma/schema.prisma", "routes.ts", @@ -50,10 +50,12 @@ "@prisma/client": "^6.1.0", "bcrypt": "^5.1.1", "next-auth": "^5.0.0-beta.4", + "resend": "^4.1.1", + "uuid": "^11.0.5", "zod": "^3.24.1" }, "devDependencies": { - "@types/bcrypt": "^5.0.2", + "@types/bcryptjs": "^2.4.6", "prisma": "^6.1.0", "typescript": "^5" } diff --git a/packages/cli/src/utils/add-utils.ts b/packages/cli/src/utils/add-utils.ts index 11a65cc..690c5bc 100644 --- a/packages/cli/src/utils/add-utils.ts +++ b/packages/cli/src/utils/add-utils.ts @@ -6,28 +6,25 @@ import { Metadata } from '../types/metadata'; import { exists } from './files'; export async function addComponent(componentName: string) { - const spin = spinner(`Adding ${componentName} component...`); - const projectDir = process.cwd(); + const spin = spinner(`Adding ${componentName} component...\n`); try { // Validate libui.config.json exists and the component is not already added - const configPath = path.join(projectDir, 'libui.config.json'); - if (!await exists(configPath)) { + if (!await exists('libui.config.json')) { throw new Error('libui.config.json not found in the current directory'); } - const config = JSON.parse(await fs.readFile(configPath, 'utf8')); + const config = JSON.parse(await fs.readFile('libui.config.json', 'utf8')); if (componentName in config.addedComponents) { throw new Error(`${componentName} component already added.`); } // Validate package.json exists - const packageJsonPath = path.join(projectDir, 'package.json'); - if (!await exists(packageJsonPath)) { + if (!await exists('package.json')) { throw new Error('package.json not found in the current directory'); } spin.start(); - const remoteRepoBaseURL = 'https://raw.githubusercontent.com/nizzyabi/lib-ui/main'; + const remoteRepoBaseURL = 'https://raw.githubusercontent.com/nizzyabi/lib-ui/cli'; const basePath = `core/${componentName}`; logger.log(`Fetching component metadata...`); @@ -40,9 +37,8 @@ export async function addComponent(componentName: string) { // Validate that none of the target directories exist logger.log('Checking if files already exist...'); for (const file of metadata.files) { - const localFilePath = path.join(projectDir, file); - if (await exists(localFilePath)) { - throw new Error(`File ${localFilePath} already exists`); + if (await exists(file)) { + throw new Error(`File ${file} already exists`); } } @@ -62,16 +58,15 @@ export async function addComponent(componentName: string) { // Create directories and write files for (const { file, content } of downloadedFiles) { - const localFilePath = path.join(projectDir, file); - const directory = path.dirname(localFilePath); + const directory = path.dirname(file); await fs.mkdir(directory, { recursive: true }); - await fs.writeFile(localFilePath, content, 'utf-8'); - logger.log(`Added ${file} to ${localFilePath}`); + await fs.writeFile(file, content, 'utf-8'); + logger.log(`Added ${file} to ${file}`); } // Add dependencies to package.json - const packageJson = await fs.readFile(packageJsonPath, 'utf-8'); + const packageJson = await fs.readFile('package.json', 'utf-8'); const packageJsonObject = JSON.parse(packageJson); // Handle dependencies @@ -103,24 +98,23 @@ export async function addComponent(componentName: string) { } // Add env variables - const envFilePath = path.join(projectDir, '.env'); - const envFile = await fs.readFile(envFilePath, 'utf-8'); + const envFile = await fs.readFile('.env', 'utf-8'); for (const envVar of metadata.envVariables) { const regex = new RegExp(`^${envVar}\\s*=\\s*`, 'm'); if (regex.test(envFile)) { logger.warn(`Env variable ${envVar} already exists in .env file`); } else { - await fs.writeFile(envFilePath, `\n${envVar}=...\n`, { flag: 'a' }); + await fs.writeFile('.env', `\n${envVar}=...\n`, { flag: 'a' }); } } - await fs.writeFile(packageJsonPath, JSON.stringify(packageJsonObject, null, 2), 'utf-8'); + await fs.writeFile('package.json', JSON.stringify(packageJsonObject, null, 2), 'utf-8'); // Add the component to the config config.addedComponents.push(componentName); - await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8'); + await fs.writeFile('libui.config.json', JSON.stringify(config, null, 2), 'utf-8'); - spin.succeed('Files and dependencies added successfully'); + spin.succeed('Project updated successfully'); logger.success(`The ${componentName} component has been added successfully`); logger.success('1. Update the .env file with the correct values'); logger.success('2. Run `npm install` to install the new dependencies'); diff --git a/packages/cli/src/utils/init-utils.ts b/packages/cli/src/utils/init-utils.ts index ffb1f92..c95f7e3 100644 --- a/packages/cli/src/utils/init-utils.ts +++ b/packages/cli/src/utils/init-utils.ts @@ -1,5 +1,4 @@ import { promises as fs } from 'fs'; -import path from 'path'; import { exec } from 'child_process'; import { promisify } from 'util'; import { logger, spinner } from './visuals'; @@ -14,11 +13,15 @@ export async function GET() { `; const envFileContent = `# This file will contain all the required environment variables for the application -# Make sure to update these values in your .env file +# Make sure to update them every time you add a new component +`; -DATABASE_URL="postgresql://user:password@localhost:5432/dbname?schema=public" +const utilsContent = `import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" -API_BASE_URL="http://localhost:3000/api" +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} `; export async function runInit() { @@ -46,21 +49,20 @@ export async function runInit() { } // 2. Install Prisma - const prismaSpinner = spinner('Setting up Prisma...').start(); + const prismaSpinner = spinner('Installing Prisma...').start(); try { await execPromise('npm install -D prisma @prisma/client', { cwd: projectDir }); - await fs.mkdir(path.join(projectDir, 'prisma'), { recursive: true }); - prismaSpinner.success('Prisma setup completed.'); + prismaSpinner.success('Prisma installed.'); } catch (error) { - prismaSpinner.error('Failed to setup Prisma.'); + prismaSpinner.error('Failed to install Prisma.'); throw error; } // 3. Create API directory structure const apiSpinner = spinner('Creating API structure...').start(); try { - await fs.mkdir(path.join(projectDir, 'app/api/hello'), { recursive: true }); - await fs.writeFile(path.join(projectDir, 'app/api/hello/route.ts'), helloApiRouteContent, 'utf8'); + await fs.mkdir('app/api/hello', { recursive: true }); + await fs.writeFile('app/api/hello/route.ts', helloApiRouteContent, 'utf8'); apiSpinner.success('API structure created.'); } catch (error) { apiSpinner.error('Failed to create API structure.'); @@ -70,7 +72,7 @@ export async function runInit() { // 4. Create .env file const envSpinner = spinner('Creating environment file...').start(); try { - await fs.writeFile(path.join(projectDir, '.env'), envFileContent, 'utf8'); + await fs.writeFile('.env', envFileContent, 'utf8'); envSpinner.success('Environment file created.'); } catch (error) { envSpinner.error('Failed to create environment file.'); @@ -82,10 +84,9 @@ export async function runInit() { addedComponents: [], }; - const configPath = path.join(projectDir, 'libui.config.json'); try { const configSpinner = spinner('Writing libui.config.json...').start(); - await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8'); + await fs.writeFile('libui.config.json', JSON.stringify(config, null, 2), 'utf8'); configSpinner.success('libui.config.json written successfully.'); } catch (error) { logger.error('Failed to write libui.config.json.'); @@ -95,15 +96,25 @@ export async function runInit() { // 6. Install additional dependencies const dependenciesSpinner = spinner('Installing additional dependencies...').start(); try { - await execPromise('npm install react react-dom next @prisma/client', { cwd: projectDir }); + await execPromise('npm install react react-dom next @prisma/client clsx tailwind-merge', { cwd: projectDir }); dependenciesSpinner.success('Additional dependencies installed.'); } catch (error) { dependenciesSpinner.error('Failed to install additional dependencies.'); throw error; } + // 7. Create utils directory and cn helper + const utilsSpinner = spinner('Creating utils directory and helpers...').start(); + try { + await fs.mkdir('lib', { recursive: true }); + await fs.writeFile('lib/utils.ts', utilsContent, 'utf8'); + utilsSpinner.success('Utils directory and helpers created.'); + } catch (error) { + utilsSpinner.error('Failed to create utils directory and helpers.'); + throw error; + } + logger.success('Initialization complete! You can now:'); logger.success('1. Update your .env file with your database credentials'); - logger.success('2. Run `npx prisma generate` to generate the Prisma Client'); - logger.success('3. Start your development server with `npm run dev`'); + logger.success('2. Start your development server with `npm run dev`'); } From 6f3bdbcc0e51a62239f4d5c7c1f420ac37285820 Mon Sep 17 00:00:00 2001 From: Vincenzo Fanizza Date: Sun, 26 Jan 2025 18:11:37 +0100 Subject: [PATCH 10/12] prisma version --- core/auth/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/auth/metadata.json b/core/auth/metadata.json index e7dc1ff..4dc16b5 100644 --- a/core/auth/metadata.json +++ b/core/auth/metadata.json @@ -47,7 +47,7 @@ "dependencies": { "@auth/prisma-adapter": "^1.0.12", "@hookform/resolvers": "^3.9.1", - "@prisma/client": "^6.1.0", + "@prisma/client": "^6.2.1", "bcrypt": "^5.1.1", "next-auth": "^5.0.0-beta.4", "resend": "^4.1.1", From ab05237328ca4a9581442a466b7ab969146aa394 Mon Sep 17 00:00:00 2001 From: Vincenzo Fanizza Date: Sun, 26 Jan 2025 18:12:49 +0100 Subject: [PATCH 11/12] prisma version --- core/auth/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/auth/metadata.json b/core/auth/metadata.json index 4dc16b5..3de88d6 100644 --- a/core/auth/metadata.json +++ b/core/auth/metadata.json @@ -56,7 +56,7 @@ }, "devDependencies": { "@types/bcryptjs": "^2.4.6", - "prisma": "^6.1.0", + "prisma": "^6.2.1", "typescript": "^5" } } \ No newline at end of file From 5425e804a9a6ebd3d53f856e1332092e1c90f77d Mon Sep 17 00:00:00 2001 From: Vincenzo Fanizza Date: Sun, 26 Jan 2025 18:16:45 +0100 Subject: [PATCH 12/12] refactored init --- core/auth/metadata.json | 6 +----- packages/cli/src/utils/init-utils.ts | 3 +++ 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/core/auth/metadata.json b/core/auth/metadata.json index 3de88d6..e260817 100644 --- a/core/auth/metadata.json +++ b/core/auth/metadata.json @@ -36,7 +36,6 @@ "schemas/index.ts" ], "envVariables": [ - "DATABASE_URL", "NEXTAUTH_SECRET", "NEXTAUTH_URL", "GOOGLE_CLIENT_ID", @@ -47,7 +46,6 @@ "dependencies": { "@auth/prisma-adapter": "^1.0.12", "@hookform/resolvers": "^3.9.1", - "@prisma/client": "^6.2.1", "bcrypt": "^5.1.1", "next-auth": "^5.0.0-beta.4", "resend": "^4.1.1", @@ -55,8 +53,6 @@ "zod": "^3.24.1" }, "devDependencies": { - "@types/bcryptjs": "^2.4.6", - "prisma": "^6.2.1", - "typescript": "^5" + "@types/bcryptjs": "^2.4.6" } } \ No newline at end of file diff --git a/packages/cli/src/utils/init-utils.ts b/packages/cli/src/utils/init-utils.ts index c95f7e3..59dd849 100644 --- a/packages/cli/src/utils/init-utils.ts +++ b/packages/cli/src/utils/init-utils.ts @@ -14,6 +14,9 @@ export async function GET() { const envFileContent = `# This file will contain all the required environment variables for the application # Make sure to update them every time you add a new component + +# This is a sample database url, you can change it to your own +DATABASE_URL="postgresql://user:password@localhost:5432/dbname?schema=public" `; const utilsContent = `import { type ClassValue, clsx } from "clsx"