Feat: added database

This commit is contained in:
Harshith Mullapudi 2025-05-27 13:10:08 +05:30
parent d3f81404db
commit 72edab887e
38 changed files with 728 additions and 1665 deletions

View File

@ -0,0 +1,30 @@
import { z } from "zod";
import { isValidDatabaseUrl } from "./utils/db";
const EnvironmentSchema = z.object({
NODE_ENV: z.union([z.literal("development"), z.literal("production"), z.literal("test")]),
DATABASE_URL: z
.string()
.refine(
isValidDatabaseUrl,
"DATABASE_URL is invalid, for details please check the additional output above this message."
),
DATABASE_CONNECTION_LIMIT: z.coerce.number().int().default(10),
DATABASE_POOL_TIMEOUT: z.coerce.number().int().default(60),
DATABASE_CONNECTION_TIMEOUT: z.coerce.number().int().default(20),
DIRECT_URL: z
.string()
.refine(
isValidDatabaseUrl,
"DIRECT_URL is invalid, for details please check the additional output above this message."
),
DATABASE_READ_REPLICA_URL: z.string().optional(),
SESSION_SECRET: z.string(),
APP_ENV: z.string().default(process.env.NODE_ENV),
APP_ORIGIN: z.string().default("http://localhost:5173"),
POSTHOG_PROJECT_KEY: z.string().default(""),
});
export type Environment = z.infer<typeof EnvironmentSchema>;
export const env = EnvironmentSchema.parse(process.env);

View File

@ -0,0 +1,177 @@
import { json, type Session , createCookieSessionStorage } from "@remix-run/node";
import { redirect } from "remix-typedjson";
import { env } from "~/env.server";
export type ToastMessage = {
message: string;
type: "success" | "error";
options: Required<ToastMessageOptions>;
};
export type ToastMessageOptions = {
/** Ephemeral means it disappears after a delay, defaults to true */
ephemeral?: boolean;
};
const ONE_YEAR = 1000 * 60 * 60 * 24 * 365;
export const { commitSession, getSession } = createCookieSessionStorage({
cookie: {
name: "__message",
path: "/",
httpOnly: true,
sameSite: "lax",
secrets: [env.SESSION_SECRET],
secure: env.NODE_ENV === "production",
},
});
export function setSuccessMessage(
session: Session,
message: string,
options?: ToastMessageOptions
) {
session.flash("toastMessage", {
message,
type: "success",
options: {
ephemeral: options?.ephemeral ?? true,
},
} as ToastMessage);
}
export function setErrorMessage(session: Session, message: string, options?: ToastMessageOptions) {
session.flash("toastMessage", {
message,
type: "error",
options: {
ephemeral: options?.ephemeral ?? true,
},
} as ToastMessage);
}
export async function setRequestErrorMessage(
request: Request,
message: string,
options?: ToastMessageOptions
) {
const session = await getSession(request.headers.get("cookie"));
setErrorMessage(session, message, options);
return session;
}
export async function setRequestSuccessMessage(
request: Request,
message: string,
options?: ToastMessageOptions
) {
const session = await getSession(request.headers.get("cookie"));
setSuccessMessage(session, message, options);
return session;
}
export async function setToastMessageCookie(session: Session) {
return {
"Set-Cookie": await commitSession(session, {
expires: new Date(Date.now() + ONE_YEAR),
}),
};
}
export async function jsonWithSuccessMessage(
data: any,
request: Request,
message: string,
options?: ToastMessageOptions
) {
const session = await getSession(request.headers.get("cookie"));
setSuccessMessage(session, message, options);
return json(data, {
headers: {
"Set-Cookie": await commitSession(session, {
expires: new Date(Date.now() + ONE_YEAR),
}),
},
});
}
export async function jsonWithErrorMessage(
data: any,
request: Request,
message: string,
options?: ToastMessageOptions
) {
const session = await getSession(request.headers.get("cookie"));
setErrorMessage(session, message, options);
return json(data, {
headers: {
"Set-Cookie": await commitSession(session, {
expires: new Date(Date.now() + ONE_YEAR),
}),
},
});
}
export async function redirectWithSuccessMessage(
path: string,
request: Request,
message: string,
options?: ToastMessageOptions
) {
const session = await getSession(request.headers.get("cookie"));
setSuccessMessage(session, message, options);
return redirect(path, {
headers: {
"Set-Cookie": await commitSession(session, {
expires: new Date(Date.now() + ONE_YEAR),
}),
},
});
}
export async function redirectWithErrorMessage(
path: string,
request: Request,
message: string,
options?: ToastMessageOptions
) {
const session = await getSession(request.headers.get("cookie"));
setErrorMessage(session, message, options);
return redirect(path, {
headers: {
"Set-Cookie": await commitSession(session, {
expires: new Date(Date.now() + ONE_YEAR),
}),
},
});
}
export async function redirectBackWithErrorMessage(
request: Request,
message: string,
options?: ToastMessageOptions
) {
const url = new URL(request.url);
return redirectWithErrorMessage(url.pathname, request, message, options);
}
export async function redirectBackWithSuccessMessage(
request: Request,
message: string,
options?: ToastMessageOptions
) {
const url = new URL(request.url);
return redirectWithSuccessMessage(url.pathname, request, message, options);
}

View File

@ -1,15 +1,11 @@
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
import type { LinksFunction } from "@remix-run/node";
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "@remix-run/react";
import type { LinksFunction, LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
import { type UseDataFunctionReturn, typedjson, useTypedLoaderData } from "remix-typedjson";
import tailwindStylesheetUrl from "~/tailwind.css";
import { appEnvTitleTag } from "./utils";
import { commitSession, getSession, type ToastMessage } from "./models/message.server";
import { env } from "./env.server";
export const links: LinksFunction = () => [
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
@ -22,9 +18,26 @@ export const links: LinksFunction = () => [
rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
},
{ rel: "stylesheet", href: tailwindStylesheetUrl }
{ rel: "stylesheet", href: tailwindStylesheetUrl },
];
export const loader = async ({ request }: LoaderFunctionArgs) => {
const session = await getSession(request.headers.get("cookie"));
const toastMessage = session.get("toastMessage") as ToastMessage;
const posthogProjectKey = env.POSTHOG_PROJECT_KEY;
return typedjson(
{
user: await getUser(request),
toastMessage,
posthogProjectKey,
appEnv: env.APP_ENV,
appOrigin: env.APP_ORIGIN,
},
{ headers: { "Set-Cookie": await commitSession(session) } }
);
};
export const meta: MetaFunction = ({ data }) => {
const typedData = data as UseDataFunctionReturn<typeof loader>;
return [
@ -36,7 +49,7 @@ export const meta: MetaFunction = ({ data }) => {
{
name: "robots",
content:
typeof window === "undefined" || window.location.hostname !== "cloud.trigger.dev"
typeof window === "undefined" || window.location.hostname !== "echo.mysigma.ai"
? "noindex, nofollow"
: "index, follow",
},

View File

@ -96,7 +96,6 @@ export function titleCase(original: string): string {
.join(" ");
}
// Takes an api key (either trigger_live_xxxx or trigger_development_xxxx) and returns trigger_live_********
export const obfuscateApiKey = (apiKey: string) => {
const [prefix, slug, secretPart] = apiKey.split("_");
return `${prefix}_${slug}_${"*".repeat(secretPart.length)}`;

View File

@ -0,0 +1,9 @@
import { z } from "zod";
export const BoolEnv = z.preprocess((val) => {
if (typeof val !== "string") {
return val;
}
return ["true", "1"].includes(val.toLowerCase().trim());
}, z.boolean());

View File

@ -0,0 +1,18 @@
export function isValidDatabaseUrl(url: string) {
try {
const databaseUrl = new URL(url);
const schemaFromSearchParam = databaseUrl.searchParams.get("schema");
if (schemaFromSearchParam === "") {
console.error(
"Invalid Database URL: The schema search param can't have an empty value. To use the `public` schema, either omit the schema param entirely or specify it in full: `?schema=public`"
);
return false;
}
return true;
} catch (err) {
console.error(err);
return false;
}
}

View File

@ -0,0 +1,8 @@
export function isValidRegex(regex: string) {
try {
new RegExp(regex);
return true;
} catch (err) {
return false;
}
}

View File

@ -15,6 +15,7 @@
"@remix-run/react": "^2.16.7",
"@remix-run/serve": "^2.16.7",
"@tailwindcss/container-queries": "^0.1.1",
"@echo/database": "workspace:*",
"class-variance-authority": "^0.7.1",
"remix-typedjson": "0.3.1",
"clsx": "^2.1.1",
@ -26,7 +27,10 @@
"tailwind-scrollbar-hide": "^1.1.7",
"tailwindcss-animate": "^1.0.5",
"tailwindcss-textshadow": "^2.1.3",
"non.geist": "^1.0.2"
"non.geist": "^1.0.2",
"zod": "3.23.8",
"zod-error": "1.5.0",
"zod-validation-error": "^1.5.0"
},
"devDependencies": {
"@remix-run/dev": "^2.16.7",

View File

@ -2,7 +2,7 @@
"exclude": [],
"include": ["remix.env.d.ts", "global.d.ts", "**/*.ts", "**/*.tsx"],
"compilerOptions": {
"types": ["vitest/globals"],
"types": [],
"lib": ["DOM", "DOM.Iterable", "DOM.AsyncIterable", "ES2020"],
"isolatedModules": true,
"esModuleInterop": true,
@ -22,6 +22,6 @@
"~/*": ["./app/*"],
"@/*": ["./*"]
},
"noEmit": true,
"noEmit": true
}
}

View File

@ -9,7 +9,12 @@
"dev": "turbo run dev",
"lint": "turbo run lint",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"check-types": "turbo run check-types"
"check-types": "turbo run check-types",
"db:migrate": "turbo run db:migrate:deploy generate",
"db:seed": "turbo run db:seed",
"db:studio": "turbo run db:studio",
"db:populate": "turbo run db:populate",
"generate": "turbo run generate"
},
"devDependencies": {
"prettier": "^3.5.3",

1
packages/database/.env Normal file
View File

@ -0,0 +1 @@
../../.env

3
packages/database/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
# Ensure the .env symlink is not removed by accident
!.env

View File

@ -0,0 +1,26 @@
{
"name": "@echo/database",
"private": true,
"version": "0.0.1",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"dependencies": {
"@prisma/client": "5.4.1"
},
"devDependencies": {
"prisma": "5.4.1",
"rimraf": "6.0.1"
},
"scripts": {
"clean": "rimraf dist",
"generate": "prisma generate",
"db:migrate:dev:create": "prisma migrate dev --create-only",
"db:migrate:deploy": "prisma migrate deploy",
"db:push": "prisma db push",
"db:studio": "prisma studio",
"db:reset": "prisma migrate reset",
"typecheck": "tsc --noEmit",
"build": "pnpm run clean && tsc --noEmit false --outDir dist --declaration",
"dev": "tsc --noEmit false --outDir dist --declaration --watch"
}
}

View File

@ -0,0 +1,92 @@
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "debian-openssl-1.1.x"]
previewFeatures = ["tracing"]
}
model User {
id String @id @default(cuid())
email String @unique
authenticationMethod AuthenticationMethod
authenticationProfile Json?
authenticationExtraParams Json?
authIdentifier String? @unique
displayName String?
name String?
avatarUrl String?
admin Boolean @default(false)
/// Preferences for the dashboard
dashboardPreferences Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
/// @deprecated
isOnCloudWaitlist Boolean @default(false)
/// @deprecated
featureCloud Boolean @default(false)
/// @deprecated
isOnHostedRepoWaitlist Boolean @default(false)
marketingEmails Boolean @default(true)
confirmedBasicDetails Boolean @default(false)
referralSource String?
personalAccessTokens PersonalAccessToken[]
}
enum AuthenticationMethod {
GOOGLE
}
/// Used to generate PersonalAccessTokens, they're one-time use
model AuthorizationCode {
id String @id @default(cuid())
code String @unique
personalAccessToken PersonalAccessToken? @relation(fields: [personalAccessTokenId], references: [id], onDelete: Cascade, onUpdate: Cascade)
personalAccessTokenId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// Used by User's to perform API actions
model PersonalAccessToken {
id String @id @default(cuid())
/// If generated by the CLI this will be "cli", otherwise user-provided
name String
/// This is the token encrypted using the ENCRYPTION_KEY
encryptedToken Json
/// This is shown in the UI, with ********
obfuscatedToken String
/// This is used to find the token in the database
hashedToken String @unique
user User @relation(fields: [userId], references: [id])
userId String
revokedAt DateTime?
lastAccessedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
authorizationCodes AuthorizationCode[]
}

View File

@ -0,0 +1,2 @@
export * from "@prisma/client";
export * from "./transaction";

View File

@ -0,0 +1,131 @@
import { Prisma, PrismaClient } from "@prisma/client";
export type PrismaTransactionClient = Omit<
PrismaClient,
"$connect" | "$disconnect" | "$on" | "$transaction" | "$use" | "$extends"
>;
export type PrismaClientOrTransaction = PrismaClient | PrismaTransactionClient;
export type PrismaReplicaClient = Omit<PrismaClient, "$transaction">;
function isTransactionClient(prisma: PrismaClientOrTransaction): prisma is PrismaTransactionClient {
return !("$transaction" in prisma);
}
export function isPrismaKnownError(error: unknown): error is Prisma.PrismaClientKnownRequestError {
return (
typeof error === "object" && error !== null && "code" in error && typeof error.code === "string"
);
}
/*
P2024: Connection timeout errors
P2028: Transaction timeout errors
P2034: Transaction deadlock/conflict errors
*/
const retryCodes = ["P2024", "P2028", "P2034"];
export function isPrismaRetriableError(error: unknown): boolean {
if (!isPrismaKnownError(error)) {
return false;
}
return retryCodes.includes(error.code);
}
/*
P2025: Record not found errors (in race conditions) [not included for now]
*/
export function isPrismaRaceConditionError(error: unknown): boolean {
if (!isPrismaKnownError(error)) {
return false;
}
return error.code === "P2025";
}
export type PrismaTransactionOptions = {
/** The maximum amount of time (in ms) Prisma Client will wait to acquire a transaction from the database. The default value is 2000ms. */
maxWait?: number;
/** The maximum amount of time (in ms) the interactive transaction can run before being canceled and rolled back. The default value is 5000ms. */
timeout?: number;
/** Sets the transaction isolation level. By default this is set to the value currently configured in your database. */
isolationLevel?: Prisma.TransactionIsolationLevel;
swallowPrismaErrors?: boolean;
/**
* The maximum number of times the transaction will be retried in case of a serialization failure. The default value is 0.
*
* See https://www.prisma.io/docs/orm/prisma-client/queries/transactions#transaction-timing-issues
*/
maxRetries?: number;
};
export async function $transaction<R>(
prisma: PrismaClientOrTransaction,
fn: (prisma: PrismaTransactionClient) => Promise<R>,
prismaError: (error: Prisma.PrismaClientKnownRequestError) => void,
options?: PrismaTransactionOptions,
attempt = 0
): Promise<R | undefined> {
if (isTransactionClient(prisma)) {
return fn(prisma);
}
try {
return await (prisma as PrismaClient).$transaction(fn, options);
} catch (error) {
if (isPrismaKnownError(error)) {
if (
retryCodes.includes(error.code) &&
typeof options?.maxRetries === "number" &&
attempt < options.maxRetries
) {
return $transaction(prisma, fn, prismaError, options, attempt + 1);
}
prismaError(error);
if (options?.swallowPrismaErrors) {
return;
}
}
throw error;
}
}
export function isUniqueConstraintError<T extends readonly string[]>(
error: unknown,
columns: T
): boolean {
if (!isPrismaKnownError(error)) {
return false;
}
if (error.code !== "P2002") {
return false;
}
const target = error.meta?.target;
if (!Array.isArray(target)) {
return false;
}
if (target.length !== columns.length) {
return false;
}
for (let i = 0; i < columns.length; i++) {
if (target[i] !== columns[i]) {
return false;
}
}
return true;
}

View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "es2016",
"lib": ["es2016", "dom"],
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"moduleResolution": "node",
"preserveWatchOutput": true,
"skipLibCheck": true,
"strict": true,
"noEmit": false,
"outDir": "./dist",
"declaration": true,
"declarationDir": "./dist"
},
"exclude": ["node_modules", "dist"]
}

View File

@ -1,3 +0,0 @@
# `@turbo/eslint-config`
Collection of internal eslint configurations.

View File

@ -1,32 +0,0 @@
import js from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier";
import turboPlugin from "eslint-plugin-turbo";
import tseslint from "typescript-eslint";
import onlyWarn from "eslint-plugin-only-warn";
/**
* A shared ESLint configuration for the repository.
*
* @type {import("eslint").Linter.Config[]}
* */
export const config = [
js.configs.recommended,
eslintConfigPrettier,
...tseslint.configs.recommended,
{
plugins: {
turbo: turboPlugin,
},
rules: {
"turbo/no-undeclared-env-vars": "warn",
},
},
{
plugins: {
onlyWarn,
},
},
{
ignores: ["dist/**"],
},
];

View File

@ -1,49 +0,0 @@
import js from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier";
import tseslint from "typescript-eslint";
import pluginReactHooks from "eslint-plugin-react-hooks";
import pluginReact from "eslint-plugin-react";
import globals from "globals";
import pluginNext from "@next/eslint-plugin-next";
import { config as baseConfig } from "./base.js";
/**
* A custom ESLint configuration for libraries that use Next.js.
*
* @type {import("eslint").Linter.Config[]}
* */
export const nextJsConfig = [
...baseConfig,
js.configs.recommended,
eslintConfigPrettier,
...tseslint.configs.recommended,
{
...pluginReact.configs.flat.recommended,
languageOptions: {
...pluginReact.configs.flat.recommended.languageOptions,
globals: {
...globals.serviceworker,
},
},
},
{
plugins: {
"@next/next": pluginNext,
},
rules: {
...pluginNext.configs.recommended.rules,
...pluginNext.configs["core-web-vitals"].rules,
},
},
{
plugins: {
"react-hooks": pluginReactHooks,
},
settings: { react: { version: "detect" } },
rules: {
...pluginReactHooks.configs.recommended.rules,
// React scope no longer necessary with new JSX transform.
"react/react-in-jsx-scope": "off",
},
},
];

View File

@ -1,24 +0,0 @@
{
"name": "@repo/eslint-config",
"version": "0.0.0",
"type": "module",
"private": true,
"exports": {
"./base": "./base.js",
"./next-js": "./next.js",
"./react-internal": "./react-internal.js"
},
"devDependencies": {
"@eslint/js": "^9.27.0",
"@next/eslint-plugin-next": "^15.3.0",
"eslint": "^9.27.0",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-only-warn": "^1.1.0",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-turbo": "^2.5.0",
"globals": "^16.1.0",
"typescript": "^5.8.2",
"typescript-eslint": "^8.32.0"
}
}

View File

@ -1,39 +0,0 @@
import js from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier";
import tseslint from "typescript-eslint";
import pluginReactHooks from "eslint-plugin-react-hooks";
import pluginReact from "eslint-plugin-react";
import globals from "globals";
import { config as baseConfig } from "./base.js";
/**
* A custom ESLint configuration for libraries that use React.
*
* @type {import("eslint").Linter.Config[]} */
export const config = [
...baseConfig,
js.configs.recommended,
eslintConfigPrettier,
...tseslint.configs.recommended,
pluginReact.configs.flat.recommended,
{
languageOptions: {
...pluginReact.configs.flat.recommended.languageOptions,
globals: {
...globals.serviceworker,
...globals.browser,
},
},
},
{
plugins: {
"react-hooks": pluginReactHooks,
},
settings: { react: { version: "detect" } },
rules: {
...pluginReactHooks.configs.recommended.rules,
// React scope no longer necessary with new JSX transform.
"react/react-in-jsx-scope": "off",
},
},
];

View File

@ -1,19 +0,0 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"incremental": false,
"isolatedModules": true,
"lib": ["es2022", "DOM", "DOM.Iterable"],
"module": "NodeNext",
"moduleDetection": "force",
"moduleResolution": "NodeNext",
"noUncheckedIndexedAccess": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "ES2022"
}
}

View File

@ -1,12 +0,0 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"plugins": [{ "name": "next" }],
"module": "ESNext",
"moduleResolution": "Bundler",
"allowJs": true,
"jsx": "preserve",
"noEmit": true
}
}

View File

@ -1,9 +0,0 @@
{
"name": "@repo/typescript-config",
"version": "0.0.0",
"private": true,
"license": "MIT",
"publishConfig": {
"access": "public"
}
}

View File

@ -1,7 +0,0 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"jsx": "react-jsx"
}
}

View File

@ -1,4 +0,0 @@
import { config } from "@repo/eslint-config/react-internal";
/** @type {import("eslint").Linter.Config} */
export default config;

View File

@ -1,27 +0,0 @@
{
"name": "@repo/ui",
"version": "0.0.0",
"private": true,
"exports": {
"./*": "./src/*.tsx"
},
"scripts": {
"lint": "eslint . --max-warnings 0",
"generate:component": "turbo gen react-component",
"check-types": "tsc --noEmit"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@turbo/gen": "^2.5.0",
"@types/node": "^22.15.3",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.1",
"eslint": "^9.27.0",
"typescript": "5.8.2"
},
"dependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0"
}
}

View File

@ -1,20 +0,0 @@
"use client";
import { ReactNode } from "react";
interface ButtonProps {
children: ReactNode;
className?: string;
appName: string;
}
export const Button = ({ children, className, appName }: ButtonProps) => {
return (
<button
className={className}
onClick={() => alert(`Hello from your ${appName} app!`)}
>
{children}
</button>
);
};

View File

@ -1,27 +0,0 @@
import { type JSX } from "react";
export function Card({
className,
title,
children,
href,
}: {
className?: string;
title: string;
children: React.ReactNode;
href: string;
}): JSX.Element {
return (
<a
className={className}
href={`${href}?utm_source=create-turbo&utm_medium=basic&utm_campaign=create-turbo"`}
rel="noopener noreferrer"
target="_blank"
>
<h2>
{title} <span>-&gt;</span>
</h2>
<p>{children}</p>
</a>
);
}

View File

@ -1,11 +0,0 @@
import { type JSX } from "react";
export function Code({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}): JSX.Element {
return <code className={className}>{children}</code>;
}

View File

@ -1,8 +0,0 @@
{
"extends": "@repo/typescript-config/react-library.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@ -1,30 +0,0 @@
import type { PlopTypes } from "@turbo/gen";
// Learn more about Turborepo Generators at https://turborepo.com/docs/guides/generating-code
export default function generator(plop: PlopTypes.NodePlopAPI): void {
// A simple generator to add a new React component to the internal UI library
plop.setGenerator("react-component", {
description: "Adds a new react component",
prompts: [
{
type: "input",
name: "name",
message: "What is the name of the component?",
},
],
actions: [
{
type: "add",
path: "src/{{kebabCase name}}.tsx",
templateFile: "templates/component.hbs",
},
{
type: "append",
path: "package.json",
pattern: /"exports": {(?<insertion>)/g,
template: ' "./{{kebabCase name}}": "./src/{{kebabCase name}}.tsx",',
},
],
});
}

View File

@ -1,8 +0,0 @@
export const {{ pascalCase name }} = ({ children }: { children: React.ReactNode }) => {
return (
<div>
<h1>{{ pascalCase name }} Component</h1>
{children}
</div>
);
};

1482
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,20 @@
"dev": {
"cache": false,
"persistent": true
},
"db:generate": {
"cache": false
},
"db:migrate:deploy": {
"cache": false
},
"db:studio": {
"cache": false
},
"generate": {
"dependsOn": [
"^generate"
]
}
}
}