├── prettierrc.json
├── src
├── templateRoot
│ ├── app
│ │ ├── globals.css
│ │ ├── icon.png
│ │ ├── favicon.ico
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ ├── not-found.tsx
│ │ ├── rootGroupRouteHome.tsx
│ │ ├── groupRouteHome.tsx
│ │ ├── nexquikTemplateModel
│ │ │ ├── create
│ │ │ │ └── page.tsx
│ │ │ ├── [id]
│ │ │ │ ├── edit
│ │ │ │ │ └── page.tsx
│ │ │ │ └── page.tsx
│ │ │ └── page.tsx
│ │ └── rootGroupRouteLayout.tsx
│ ├── public
│ │ ├── logo.png
│ │ └── backdrop.png
│ ├── postcss.config.js
│ ├── next.config.js
│ ├── tailwind.config.js
│ ├── lib
│ │ └── prisma.ts
│ ├── .env
│ ├── prisma
│ │ └── schema.prisma
│ ├── README.md
│ ├── .gitignore
│ ├── tsconfig.json
│ └── package.json
├── index.ts
├── prismaGenerator.ts
├── modelTree.ts
├── cli.ts
├── helpers.ts
└── generators.ts
├── .devcontainer
├── Dockerfile
└── devcontainer.json
├── babel.config.js
├── .gitignore
├── example
├── package.json
└── schema.prisma
├── .npmignore
├── docker-compose.yml
├── renovate.json
├── .codesandbox
├── template.json
└── tasks.json
├── __tests__
├── prisma-schemas
│ ├── minimal-examples
│ │ ├── many-to-many.prisma
│ │ ├── enums.prisma
│ │ ├── one-to-many.prisma
│ │ ├── composite-id-relation.prisma
│ │ └── composite-types.prisma
│ ├── not-working
│ │ └── multiple-related-fields.prisma
│ └── schemas.test.ts
├── core-functionality-tests
│ ├── schema.prisma
│ ├── prisma-generator.test.ts
│ └── cli-arg.test.ts
└── utils.ts
├── tsconfig.json
├── .github
├── dependabot.yml
└── workflows
│ └── publish.yml
├── prisma
└── dev.prisma
├── release.config.js
├── .gitattributes
├── CONTRIBUTING.md
├── template.eslintrc.json
├── package.json
├── jest.config.js
├── README.md
├── LICENSE
└── CHANGELOG.md
/prettierrc.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/src/templateRoot/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/.devcontainer/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:20-bullseye
2 |
3 | # The workspace will be mounted under /workspace
--------------------------------------------------------------------------------
/src/templateRoot/app/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bcanfield/nexquik/HEAD/src/templateRoot/app/icon.png
--------------------------------------------------------------------------------
/src/templateRoot/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bcanfield/nexquik/HEAD/src/templateRoot/app/favicon.ico
--------------------------------------------------------------------------------
/src/templateRoot/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bcanfield/nexquik/HEAD/src/templateRoot/public/logo.png
--------------------------------------------------------------------------------
/src/templateRoot/public/backdrop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bcanfield/nexquik/HEAD/src/templateRoot/public/backdrop.png
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Devcontainer",
3 | "build": {
4 | "dockerfile": "./Dockerfile"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/templateRoot/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env node
2 | import chalk from "chalk";
3 | import { run } from "./cli";
4 | console.log(chalk.gray("Running Nexquik"));
5 | run();
6 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | ["@babel/preset-env", { targets: { node: "current" } }],
4 | "@babel/preset-typescript",
5 | ],
6 | };
7 |
--------------------------------------------------------------------------------
/src/templateRoot/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | experimental: {
4 | appDir: true,
5 | serverActions: true,
6 | },
7 | };
8 |
9 | module.exports = nextConfig;
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 | *.tsbuildinfo
4 |
5 | # Development
6 | devNexquikApp/
7 | nexquik.tgz
8 |
9 | # Testing
10 | testOutputDirectory
11 |
12 | # Allows for local testing using a private schema
13 | private-schema.prisma
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nexquik-live-demo",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "init": "nexquik --init group --name UserManagement --include Organization"
6 | },
7 | "dependencies": {
8 | "nexquik": "*"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src/**/*
2 | node_modules/
3 | prisma/
4 | prisma_large/
5 | prisma_non_working/
6 | devNexquikApp/
7 | templateRoot/
8 | .github/
9 | __tests__/
10 | .gitattributes
11 | babel.config.js
12 | jest.config.js
13 | prettierrc.json
14 | release.config.js
15 | schema.prisma
16 | example/
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 |
3 | services:
4 | db:
5 | container_name: nexquik-db
6 | image: postgres:11
7 | restart: always
8 | ports:
9 | - 5444:5432
10 | environment:
11 | POSTGRES_PASSWORD: password
12 | POSTGRES_USER: nexquik
13 | POSTGRES_DB: nexquik-db
14 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:recommended"
5 | ],
6 | "packageRules": [
7 | {
8 | "matchUpdateTypes": ["minor", "patch"],
9 | "matchCurrentVersion": "!/^0/",
10 | "automerge": true
11 | }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/src/templateRoot/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Roboto } from "next/font/google";
2 |
3 | // const roboto = Roboto({
4 | // weight: "400",
5 | // subsets: ["latin"],
6 | // display: "swap",
7 | // });
8 | export default function RootLayout({
9 | children,
10 | }: {
11 | children: React.ReactNode;
12 | }) {
13 | return <>{children}>;
14 | }
15 |
--------------------------------------------------------------------------------
/src/templateRoot/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from "next/navigation";
2 |
3 | export default async function Home() {
4 | // @nexquik homeRedirect start
5 | redirect("/gen");
6 | // @nexquik homeRedirect stop
7 | return (
8 |
9 | APP DIR Home Page
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/src/templateRoot/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ["./app/**/*.{js,ts,jsx,tsx,mdx}"],
4 | theme: {
5 | extend: {
6 | fontFamily: {
7 | sans: ["Roboto", "ui-sans-serif", "system-ui"],
8 | },
9 | maxWidth: {
10 | "8xl": "90rem",
11 | },
12 | },
13 | },
14 | plugins: [require("@tailwindcss/forms")],
15 | };
16 |
--------------------------------------------------------------------------------
/.codesandbox/template.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Nexquik Live Demo",
3 | "description": "This is an example of Nexquik generating a full app from only a Prisma Schema. Then it will re-generate your components in your app directory as you change your Schema.",
4 | "tags": [
5 | "nextjs",
6 | "prisma",
7 | "typescript",
8 | "react",
9 | "app-router",
10 | "tailwindcss",
11 | "nexquik"
12 | ],
13 | "published": true
14 | }
15 |
--------------------------------------------------------------------------------
/__tests__/prisma-schemas/minimal-examples/many-to-many.prisma:
--------------------------------------------------------------------------------
1 | datasource db {
2 | provider = "postgresql"
3 | url = env("DATABASE_URL")
4 | }
5 |
6 | generator client {
7 | provider = "prisma-client-js"
8 | }
9 |
10 | model User {
11 | id Int @id @default(autoincrement())
12 | name String
13 | tags Tag[]
14 | }
15 |
16 | model Tag {
17 | id Int @id @default(autoincrement())
18 | name String
19 | users User[]
20 | }
21 |
--------------------------------------------------------------------------------
/src/templateRoot/lib/prisma.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | let prisma: PrismaClient;
4 |
5 | if (process.env.NODE_ENV === "production") {
6 | prisma = new PrismaClient();
7 | } else {
8 | const globalWithPrisma = global as typeof globalThis & {
9 | prisma: PrismaClient;
10 | };
11 | if (!globalWithPrisma.prisma) {
12 | globalWithPrisma.prisma = new PrismaClient();
13 | }
14 | prisma = globalWithPrisma.prisma;
15 | }
16 |
17 | export default prisma;
18 |
--------------------------------------------------------------------------------
/__tests__/prisma-schemas/minimal-examples/enums.prisma:
--------------------------------------------------------------------------------
1 | datasource db {
2 | provider = "postgresql"
3 | url = env("DATABASE_URL")
4 | }
5 |
6 | generator client {
7 | provider = "prisma-client-js"
8 | }
9 |
10 | enum Status {
11 | TODO
12 | IN_PROGRESS
13 | DONE
14 | }
15 |
16 | model Task {
17 | id Int @id @default(autoincrement())
18 | title String
19 | status Status
20 | createdAt DateTime @default(now())
21 | updatedAt DateTime @updatedAt
22 | }
23 |
--------------------------------------------------------------------------------
/src/prismaGenerator.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import { generatorHandler } from "@prisma/generator-helper";
3 | import { defaultOutputDirectory, run } from "./cli";
4 | import chalk from "chalk";
5 |
6 | console.log(chalk.gray("Running Nexquik as Prisma Generator"));
7 | generatorHandler({
8 | onManifest() {
9 | return {
10 | prettyName: "Nexquik",
11 | version: require("../package.json").version,
12 | defaultOutput: defaultOutputDirectory,
13 | };
14 | },
15 | onGenerate: run,
16 | });
17 |
--------------------------------------------------------------------------------
/src/templateRoot/.env:
--------------------------------------------------------------------------------
1 | # Environment variables declared in this file are automatically made available to Prisma.
2 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
3 |
4 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
5 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings
6 |
7 | DATABASE_URL=postgresql://nexquik:password@0.0.0.0:5444/nexquik-db?schema=public
8 |
--------------------------------------------------------------------------------
/__tests__/prisma-schemas/minimal-examples/one-to-many.prisma:
--------------------------------------------------------------------------------
1 | datasource db {
2 | provider = "postgresql"
3 | url = env("DATABASE_URL")
4 | }
5 |
6 | generator client {
7 | provider = "prisma-client-js"
8 | }
9 |
10 | model User {
11 | id Int @id @default(autoincrement())
12 | name String
13 | posts Post[]
14 | }
15 |
16 | model Post {
17 | id Int @id @default(autoincrement())
18 | title String
19 | content String
20 | userId Int
21 | user User @relation(fields: [userId], references: [id])
22 | }
23 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "rootDir": "src",
4 | "outDir": "dist",
5 | "strict": true,
6 | "target": "es6",
7 | "module": "commonjs",
8 | "sourceMap": true,
9 | "esModuleInterop": true,
10 | "moduleResolution": "node",
11 | "types": ["node"],
12 | "plugins": [
13 | {
14 | "name": "next"
15 | }
16 | ]
17 | },
18 | "exclude": [
19 | "dist/*",
20 | "src/templateRoot/*",
21 | "devNexquikApp/*",
22 | "example/*",
23 | "nexquikApp/*",
24 | "__tests__"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/src/templateRoot/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 | datasource db {
5 | provider = "sqlite"
6 | url = env("DATABASE_URL")
7 | }
8 |
9 | generator client {
10 | provider = "prisma-client-js"
11 | }
12 |
13 | model Product {
14 | id String @id
15 | name String
16 | foo Foo @relation(fields: [fooId], references: [id])
17 | fooId String
18 | }
19 |
20 | model Foo {
21 | id String @id
22 | bar String
23 | Product Product[]
24 | }
25 |
--------------------------------------------------------------------------------
/src/templateRoot/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project generated with [`Nexquik`](https://github.com/bcanfield/nexquik).
2 |
3 | ## Getting Started
4 |
5 | Change `DATABASE_URL` in your .env file to point to your desired database.
6 | ```zsh
7 | DATABASE_URL="YOUR DB HERE"
8 | ```
9 |
10 | Install Dependencies
11 | ```zsh
12 | npm install
13 | ```
14 |
15 | Run Prisma Generate
16 | ```bash
17 | npm run generate
18 | ```
19 |
20 | Start the dev server
21 | ```bash
22 | npm run dev
23 | ```
24 |
25 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "npm" # See documentation for possible values
9 | directory: "/src/templateRoot" # Location of package manifests
10 | schedule:
11 | interval: "daily"
12 | versioning-strategy: increase-if-necessary
13 |
--------------------------------------------------------------------------------
/__tests__/prisma-schemas/not-working/multiple-related-fields.prisma:
--------------------------------------------------------------------------------
1 | datasource db {
2 | provider = "postgresql"
3 | url = env("DATABASE_URL")
4 | }
5 |
6 | generator client {
7 | provider = "prisma-client-js"
8 | }
9 |
10 | model Child {
11 | id Int @id @default(autoincrement())
12 | Parent Parent? @relation(fields: [ParentName, ParentType], references: [name, type], onDelete: Cascade)
13 | ParentName String?
14 | ParentType String?
15 | }
16 |
17 | model Parent {
18 | name String @id @default(cuid())
19 | type String
20 | Children Child[]
21 |
22 | @@unique([name, type])
23 | }
24 |
--------------------------------------------------------------------------------
/__tests__/prisma-schemas/minimal-examples/composite-id-relation.prisma:
--------------------------------------------------------------------------------
1 | datasource db {
2 | provider = "postgresql"
3 | url = env("DATABASE_URL")
4 | }
5 |
6 | generator client {
7 | provider = "prisma-client-js"
8 | }
9 |
10 | model Post {
11 | postId1 Int
12 | postId2 Int
13 | name String
14 | bookings User @relation(fields: [postId1, postId2], references: [userId1, userId2], onDelete: Cascade)
15 | userId1 Int
16 | userId2 Int
17 |
18 | @@id([postId1, postId2])
19 | }
20 |
21 | model User {
22 | userId1 Int
23 | userId2 Int
24 | name String
25 | posts Post[]
26 |
27 | @@id([userId1, userId2])
28 | }
29 |
--------------------------------------------------------------------------------
/__tests__/prisma-schemas/minimal-examples/composite-types.prisma:
--------------------------------------------------------------------------------
1 | datasource db {
2 | provider = "mongodb"
3 | url = env("DATABASE_URL")
4 | }
5 |
6 | generator client {
7 | provider = "prisma-client-js"
8 | }
9 |
10 | model Product {
11 | id String @id @default(auto()) @map("_id") @db.ObjectId
12 | name String
13 | photos Photo[]
14 | foo Foo @relation(fields: [fooId], references: [id])
15 | fooId String @db.ObjectId
16 | }
17 |
18 | type Photo {
19 | height Int
20 | width Int
21 | url String
22 | }
23 |
24 | model Foo {
25 | id String @id @default(auto()) @map("_id") @db.ObjectId
26 | bar String
27 | Product Product[]
28 | }
29 |
--------------------------------------------------------------------------------
/src/templateRoot/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
37 | # prisma sqlite db
38 | dev.db
39 |
40 | # prevent package lock from being tracked because it will change every time we build a new local package
41 | package-lock.json
--------------------------------------------------------------------------------
/prisma/dev.prisma:
--------------------------------------------------------------------------------
1 | datasource db {
2 | provider = "postgresql"
3 | url = env("DATABASE_URL")
4 | }
5 |
6 | generator client {
7 | provider = "prisma-client-js"
8 | }
9 |
10 | model User {
11 | id Int @id @default(autoincrement())
12 | username String @unique
13 | email String @unique
14 | posts Post[]
15 | }
16 |
17 | model Post {
18 | id Int @id @default(autoincrement())
19 | title String
20 | content String
21 | status Status // Enum field
22 | author User @relation(fields: [authorId], references: [id])
23 | authorId Int
24 | }
25 |
26 | // Define the 'Status' enum
27 | enum Status {
28 | PUBLISHED
29 | DRAFT
30 | ARCHIVED
31 | }
32 |
--------------------------------------------------------------------------------
/release.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | branches: [
3 | "main",
4 | {
5 | name: "beta",
6 | prerelease: true,
7 | },
8 | ],
9 | plugins: [
10 | "@semantic-release/commit-analyzer",
11 | "@semantic-release/release-notes-generator",
12 | [
13 | "@semantic-release/changelog",
14 | {
15 | changelogFile: "CHANGELOG.md",
16 | },
17 | ],
18 | "@semantic-release/npm",
19 | "@semantic-release/github",
20 | [
21 | "@semantic-release/git",
22 | {
23 | assets: ["CHANGELOG.md", "package.json"],
24 | message:
25 | "chore(release): set `package.json` to ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}",
26 | },
27 | ],
28 | ],
29 | };
30 |
--------------------------------------------------------------------------------
/__tests__/core-functionality-tests/schema.prisma:
--------------------------------------------------------------------------------
1 | datasource db {
2 | provider = "sqlite"
3 | url = env("DATABASE_URL")
4 | }
5 |
6 | generator client {
7 | provider = "prisma-client-js"
8 | }
9 |
10 | generator nexquik {
11 | provider = "node ./dist/prismaGenerator.js"
12 | command = "group --name Group1 --include Product,Foo --name Group2 --include Product,Foo --init --output __tests__/core-functionality-tests/testOutputDirectory"
13 | disabled = env("DISABLE_NEXQUIK")
14 | }
15 |
16 | model Product {
17 | id String @id
18 | name String
19 | foo Foo @relation(fields: [fooId], references: [id])
20 | fooId String
21 | }
22 |
23 | model Foo {
24 | id String @id
25 | bar String
26 | Product Product[]
27 | }
28 |
--------------------------------------------------------------------------------
/src/templateRoot/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ],
22 | "paths": {
23 | "@/*": ["./*"]
24 | }
25 | },
26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
27 | "exclude": ["node_modules"]
28 | }
29 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Git attributes
2 | # https://git-scm.com/docs/gitattributes
3 | # https://git-scm.com/book/en/v2/Customizing-Git-Git-Attributes
4 |
5 | # Normalize line endings for all files that git determines to be text.
6 | # https://git-scm.com/docs/gitattributes#gitattributes-Settostringvalueauto
7 | * text=auto
8 |
9 | # Normalize line endings to LF on checkin, and do NOT convert to CRLF when checking-out on Windows.
10 | # https://git-scm.com/docs/gitattributes#gitattributes-Settostringvaluelf
11 | *.txt text eol=lf
12 | *.html text eol=lf
13 | *.md text eol=lf
14 | *.css text eol=lf
15 | *.scss text eol=lf
16 | *.map text eol=lf
17 | *.js text eol=lf
18 | *.jsx text eol=lf
19 | *.ts text eol=lf
20 | *.tsx text eol=lf
21 | *.json text eol=lf
22 | *.yml text eol=lf
23 | *.yaml text eol=lf
24 | *.xml text eol=lf
25 | *.svg text eol=lf
26 |
27 | *.js linguist-detectable=false
--------------------------------------------------------------------------------
/src/templateRoot/app/not-found.tsx:
--------------------------------------------------------------------------------
1 | export default async function NotFound() {
2 | return (
3 |
4 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/src/templateRoot/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nexquik-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "reset": "prisma migrate reset",
8 | "push": "prisma db push --accept-data-loss",
9 | "build": "next build",
10 | "start": "next start",
11 | "lint": "next lint",
12 | "typecheck": "tsc",
13 | "generate": "prisma generate"
14 | },
15 | "dependencies": {
16 | "@prisma/client": "5.4.2",
17 | "clsx": "^2.0.0",
18 | "next": "^13.5.4",
19 | "prisma": "5.4.2",
20 | "react": "18.2.0",
21 | "react-dom": "18.2.0",
22 | "typescript": "5.2.2"
23 | },
24 | "devDependencies": {
25 | "@tailwindcss/forms": "^0.5.6",
26 | "@types/node": "20.8.5",
27 | "@types/react": "18.2.28",
28 | "@types/react-dom": "18.2.13",
29 | "autoprefixer": "^10.4.16",
30 | "nexquik": "*",
31 | "postcss": "^8.4.31",
32 | "tailwindcss": "^3.3.3"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## How to contribute to Nexquik
2 |
3 | #### **Developing with Nexquik is Easy!**
4 |
5 | - To build and run locally, run:
6 | ```
7 | npm run dev
8 | ```
9 | - This command will watch for file changes under the `src` and `templateApp` directories and spin up a local next.js project that is using the output from Nexquik.
10 | - Check out the `dev` folder for the details of the local next.js project
11 | - To run tests, run:
12 | ```
13 | npm run test
14 | ```
15 |
16 | #### **Did you find a bug?**
17 |
18 | - **Submit the bug** on [Github](https://github.com/bcanfield/nexquik/issues/new).
19 | - At the bare minimum - please include your prisma schema or a minimal example.
20 |
21 | #### **Did you write a patch that fixes a bug?**
22 |
23 | - Open a new GitHub pull request with the patch.
24 |
25 | - Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable.
26 |
27 | Thank you!
28 |
29 | Nexquik Team
30 |
--------------------------------------------------------------------------------
/__tests__/core-functionality-tests/prisma-generator.test.ts:
--------------------------------------------------------------------------------
1 | import * as child_process from "child_process";
2 | import { isDirectoryNotEmpty } from "../utils";
3 |
4 | const testOutputDirectory =
5 | "__tests__/core-functionality-tests/testOutputDirectory";
6 |
7 | const prismaMain = "./node_modules/prisma/build/index.js";
8 |
9 | test("prisma-generator", async () => {
10 | child_process.execSync(`rm -rf ${testOutputDirectory}`);
11 | child_process.execSync(
12 | `prisma generate --schema __tests__/core-functionality-tests/schema.prisma`,
13 | {
14 | stdio: "inherit",
15 | }
16 | );
17 | expect(isDirectoryNotEmpty(testOutputDirectory)).toBeTruthy();
18 | try {
19 | // Run npm install
20 | child_process.execSync("npm install --quiet", {
21 | cwd: testOutputDirectory,
22 | });
23 | } catch (error) {
24 | console.error("TypeScript compilation error:", error.message);
25 | throw error;
26 | } finally {
27 | child_process.execSync(`rm -rf ${testOutputDirectory}`);
28 | }
29 | });
30 |
--------------------------------------------------------------------------------
/src/templateRoot/app/rootGroupRouteHome.tsx:
--------------------------------------------------------------------------------
1 | export default async function Home() {
2 | return (
3 |
4 |
18 |
19 | {/* @nexquik routeGroupList start */}
20 | Route
21 | {/* @nexquik routeGroupList stop */}
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/templateRoot/app/groupRouteHome.tsx:
--------------------------------------------------------------------------------
1 | export default async function Home() {
2 | return (
3 |
4 |
19 |
20 | {/* @nexquik routeList start */}
21 | Route
22 | {/* @nexquik routeList stop */}
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/.codesandbox/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // These tasks will run in order when initializing your CodeSandbox project.
3 | "setupTasks": [
4 | {
5 | "name": "Install Nexquik",
6 | "command": "cd example && npm install"
7 | },
8 | {
9 | "name": "Generate App with Nexquik",
10 | "command": "cd example && npm run init"
11 | },
12 | {
13 | "name": "Install Dependencies in new App",
14 | "command": "cd example && npm i"
15 | },
16 | {
17 | "name": "Set DATABASE_URL environment variable",
18 | "command": "cd example && rm -f .env && echo 'DATABASE_URL=file:./dev.db' > .env"
19 | },
20 | {
21 | "name": "Push Schema to Database",
22 | "command": "cd example && npx prisma db push"
23 | }
24 | ],
25 |
26 | // These tasks can be run from CodeSandbox. Running one will open a log in the app.
27 | "tasks": {
28 | "dev": {
29 | "name": "dev",
30 | "command": "cd example && npm run dev",
31 | "runAtStart": true,
32 | "preview": {
33 | "port": 3000
34 | }
35 | },
36 | "generate": {
37 | "name": "generate",
38 | "command": "cd example && npx prisma db push && npm run generate",
39 | "restartOn": { "files": ["**/*.schema"] }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/example/schema.prisma:
--------------------------------------------------------------------------------
1 | // https://pris.ly/d/prisma-schema
2 |
3 | generator nexquik {
4 | provider = "prisma-generator-nexquik"
5 | command = "group --name UserManagement --include Organization"
6 | }
7 |
8 | generator client {
9 | provider = "prisma-client-js"
10 | }
11 |
12 | datasource db {
13 | provider = "sqlite"
14 | url = env("DATABASE_URL")
15 | }
16 |
17 | model Organization {
18 | id Int @id @default(autoincrement())
19 | name String
20 | User User[]
21 | }
22 |
23 | model User {
24 | id Int @id @default(autoincrement())
25 | email String
26 | username String
27 | password String
28 | roles Role[]
29 | organization Organization @relation(fields: [organizationId], references: [id])
30 | organizationId Int
31 | tasks Task[]
32 | createdAt DateTime @default(now())
33 | }
34 |
35 | model Role {
36 | id Int @id @default(autoincrement())
37 | name String
38 | users User[]
39 | }
40 |
41 | model Task {
42 | id Int @id @default(autoincrement())
43 | name String
44 | description String?
45 | createdAt DateTime @default(now())
46 | User User? @relation(fields: [userId], references: [id])
47 | userId Int?
48 | }
49 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: "Build / Publish"
2 |
3 | on:
4 | push:
5 | branches: [main, beta]
6 | pull_request:
7 | branches: [main, beta]
8 |
9 | jobs:
10 | release:
11 | name: Release
12 | runs-on: ubuntu-latest
13 |
14 | permissions:
15 | packages: write
16 | contents: write
17 |
18 | steps:
19 | - name: Checkout
20 | uses: actions/checkout@v2
21 | with:
22 | fetch-depth: 0
23 | - name: Setup Node.js
24 | uses: actions/setup-node@v2
25 | with:
26 | node-version: 18.x
27 | - name: Install dependencies
28 | run: npx ci
29 | - name: Install semantic-release extra plugins
30 | run: npm install --save-dev @semantic-release/changelog @semantic-release/git
31 | - name: Build
32 | run: npm run build
33 | - name: Test
34 | run: npm run test
35 | - name: Create .npmrc file
36 | run: |
37 | echo registry=https://registry.npmjs.org/ > .npmrc
38 | echo @uzenith360:registry=https://npm.pkg.github.com/ > .npmrc
39 | echo '//npm.pkg.github.com/:_authToken=${{ secrets.GH_TOKEN }}' >> .npmrc
40 | - name: Release
41 | env:
42 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
43 | NPM_TOKEN: ${{ secrets.NPMAUTHTOKEN }}
44 | run: npx semantic-release
45 |
--------------------------------------------------------------------------------
/__tests__/utils.ts:
--------------------------------------------------------------------------------
1 | // expect(isDirectoryNotEmpty(testOutputDirectory)).toBeTruthy();
2 | import * as child_process from "child_process";
3 | import { readdirSync } from "fs";
4 |
5 | // export const validateOutputDirectory ()
6 | export const isDirectoryNotEmpty = (path: string) => {
7 | try {
8 | const files = readdirSync(path);
9 | return files.length > 0;
10 | } catch (err) {
11 | if (err.code === "ENOENT") {
12 | // Directory doesn't exist
13 | return false;
14 | } else {
15 | // Other error occurred
16 | throw err;
17 | }
18 | }
19 | };
20 |
21 | export function validateOutputDirectory(
22 | outputDirectory: string,
23 | runGenerate?: boolean
24 | ) {
25 | try {
26 | // Run npm install
27 | child_process.execSync("npm install --quiet", {
28 | cwd: outputDirectory,
29 | });
30 | // // Run prisma generate
31 | if (runGenerate) {
32 | child_process.execSync(`npm run generate`, {
33 | stdio: "inherit",
34 | cwd: outputDirectory,
35 | });
36 | }
37 | // Run type check
38 | child_process.execSync("npm run typecheck", {
39 | stdio: "inherit",
40 | cwd: outputDirectory,
41 | });
42 | } catch (error) {
43 | console.error("TypeScript compilation error:", error.message);
44 | throw error;
45 | } finally {
46 | child_process.execSync(`rm -rf ${outputDirectory}`);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/template.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "parserOptions": {
4 | "ecmaFeatures": {
5 | "jsx": true
6 | },
7 | "ecmaVersion": 2020,
8 | "sourceType": "module"
9 | },
10 | "extends": ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"],
11 | "plugins": ["prettier", "@typescript-eslint", "unused-imports"],
12 | "rules": {
13 | "react/react-in-jsx-scope": "off",
14 | // all prettier file errors
15 | // "prettier/prettier": "error",
16 | // "@typescript-eslint/explicit-module-boundary-types": 0,
17 | // "@typescript-eslint/ban-ts-ignore": 0,
18 | // "@typescript-eslint/ban-ts-comment": 0,
19 | // "@typescript-eslint/no-unused-vars": 2,
20 | // "@typescript-eslint/no-explicit-any": 1,
21 | // "no-console": 1,
22 | "unused-imports/no-unused-imports": "error",
23 | "unused-imports/no-unused-vars": [
24 | "error",
25 | { "vars": "all", "args": "after-used", "ignoreRestSiblings": false }
26 | ],
27 | "import/no-unused-modules": [
28 | "error",
29 | {
30 | "missingExports": true,
31 | "unusedExports": true
32 | }
33 | ],
34 | "no-unused-vars": [
35 | "warn",
36 | { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_" }
37 | ]
38 | },
39 | // "ignorePatterns": ["src/server/*.js"],
40 | "globals": {
41 | "React": "writable"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/__tests__/prisma-schemas/schemas.test.ts:
--------------------------------------------------------------------------------
1 | import * as child_process from "child_process";
2 | import { readdirSync } from "fs";
3 | import path from "path";
4 | import { isDirectoryNotEmpty } from "../utils";
5 |
6 | const nexquikMain = "./dist/index.js";
7 | const prismaSchemaDirectory = path.join(
8 | "__tests__",
9 | "prisma-schemas",
10 | "minimal-examples"
11 | );
12 | const prismaMain = "./node_modules/prisma/build/index.js";
13 |
14 | // Create an array of schema paths
15 | const schemaPaths = readdirSync(prismaSchemaDirectory);
16 |
17 | // Define a function to run a single schema test asynchronously
18 | async function runSchemaTest(schemaPath) {
19 | const testOutputDirectory = path.join(
20 | "__tests__",
21 | "prisma-schemas",
22 | `${schemaPath}-test-output`
23 | );
24 | try {
25 | child_process.execSync(`rm -rf ${testOutputDirectory}`);
26 | child_process.execSync(
27 | `node ${nexquikMain} group --name MainGroup --init --rootName gen --schema ${path.join(
28 | prismaSchemaDirectory,
29 | schemaPath
30 | )} --output ${testOutputDirectory}`
31 | );
32 |
33 | console.log(`Schema Test: ${schemaPath}`);
34 | expect(isDirectoryNotEmpty(testOutputDirectory)).toBeTruthy();
35 |
36 | // Run npm install
37 | child_process.execSync("npm install --quiet", {
38 | cwd: testOutputDirectory,
39 | });
40 | // Run next build
41 | child_process.execSync(`npm run build`, {
42 | stdio: "inherit",
43 | cwd: testOutputDirectory,
44 | });
45 | // Run prisma generate
46 | child_process.execSync(`node ${prismaMain} generate`, {
47 | stdio: "inherit",
48 | cwd: testOutputDirectory,
49 | });
50 | // Run type check
51 | child_process.execSync("npm run typecheck", {
52 | stdio: "inherit",
53 | cwd: testOutputDirectory,
54 | });
55 | } catch (error) {
56 | console.error("TypeScript compilation error:", error.message);
57 | throw error;
58 | } finally {
59 | child_process.execSync(`rm -rf ${testOutputDirectory}`);
60 | }
61 | }
62 |
63 | // Run schema tests
64 | schemaPaths.forEach((schemaPath) => {
65 | test(`Schema Test: ${schemaPath}`, async () => {
66 | await runSchemaTest(schemaPath);
67 | });
68 | });
69 |
--------------------------------------------------------------------------------
/src/templateRoot/app/nexquikTemplateModel/create/page.tsx:
--------------------------------------------------------------------------------
1 | import { revalidatePath } from "next/cache";
2 | import Link from "next/link";
3 | import { redirect } from "next/navigation";
4 | //@nexquik prismaImport start
5 | import prisma from "@/lib/prisma";
6 | //@nexquik prismaImport stop
7 |
8 | //@nexquik prismaEnumImport start
9 | import { Enum } from "@prisma/client";
10 | //@nexquik prismaEnumImport stop
11 |
12 | export default async function CreateNexquikTemplateModel(
13 | //@nexquik props start
14 | {
15 | params,
16 | }: {
17 | params: { [key: string]: string | string[] | undefined };
18 | }
19 | //@nexquik props stop
20 | )
21 |
22 |
23 | {
24 |
25 |
26 | async function addNexquikTemplateModel(formData: FormData) {
27 | "use server";
28 | const created = await prisma.nexquikTemplateModel.create({
29 | data:
30 | //@nexquik prismaCreateDataInput start
31 | {
32 | name: formData.get("name"),
33 | lat: Number(formData.get("lat")),
34 | lng: Number(formData.get("lng")),
35 | },
36 | //@nexquik prismaCreateDataInput stop
37 | });
38 | //@nexquik revalidatePath start
39 | revalidatePath("/nexquikTemplateModel");
40 | //@nexquik revalidatePath stop
41 |
42 | //@nexquik createRedirect start
43 | redirect(`/nexquikTemplateModel/${created.id}`);
44 | //@nexquik createRedirect stop
45 | }
46 |
47 | return (
48 |
49 | {/* @nexquik createBreadcrumb start */}
50 | {/* @nexquik createBreadcrumb stop */}
51 |
52 |
59 | {/* @nexquik createForm start */}
60 |
69 | Cancel
70 | {/* @nexquik createForm stop */}
71 |
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/src/templateRoot/app/nexquikTemplateModel/[id]/edit/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { redirect } from "next/navigation";
3 | //@nexquik prismaImport start
4 | import prisma from "@/lib/prisma";
5 | //@nexquik prismaImport stop
6 |
7 | //@nexquik prismaEnumImport start
8 | import { Enum } from "@prisma/client";
9 | //@nexquik prismaEnumImport stop
10 |
11 | export default async function EditNexquikTemplateModel({
12 | params,
13 | }: {
14 | params: { [key: string]: string | string[] | undefined };
15 | }) {
16 | const nexquikTemplateModel = await prisma.nexquikTemplateModel.findUnique({
17 | where:
18 | //@nexquik prismaWhereInput start
19 | { id: params.id },
20 | //@nexquik prismaWhereInput stop
21 | });
22 |
23 | async function editNexquikTemplateModel(formData: FormData) {
24 | "use server";
25 | if (formData) {
26 | await prisma.nexquikTemplateModel.update({
27 | where:
28 | //@nexquik prismaWhereInput start
29 | { id: params.id },
30 | //@nexquik prismaWhereInput stop
31 | data:
32 | //@nexquik prismaEditDataInput start
33 | { name: formData.get("name") },
34 | //@nexquik prismaEditDataInput stop
35 | });
36 | }
37 | //@nexquik editRedirect start
38 | redirect(`/nexquikTemplateModel/${params.id}`);
39 | //@nexquik editRedirect stop
40 | }
41 |
42 | return (
43 |
44 | {/* @nexquik editBreadCrumb start */}
45 | {/* @nexquik editBreadCrumb stop */}
46 | {" "}
53 | {/* @nexquik editForm start */}
54 |
71 | {/* @nexquik editForm stop */}
72 |
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/src/templateRoot/app/nexquikTemplateModel/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import { revalidatePath } from "next/cache";
2 | import Link from "next/link";
3 | import { redirect } from "next/navigation";
4 | //@nexquik prismaImport start
5 | import prisma from "@/lib/prisma";
6 | //@nexquik prismaImport stop
7 |
8 | export default async function ShowNexquikTemplateModel({
9 | params,
10 | }: {
11 | params: { [key: string]: string | string[] | undefined };
12 | }) {
13 | const nexquikTemplateModel = await prisma.nexquikTemplateModel.findUnique({
14 | where:
15 | //@nexquik prismaWhereInput start
16 | { id: params.id },
17 | //@nexquik prismaWhereInput stop
18 | });
19 | async function deleteNexquikTemplateModel(formData: FormData) {
20 | "use server";
21 | await prisma.nexquikTemplateModel.delete({
22 | where:
23 | //@nexquik prismaDeleteClause start
24 | { id: formData.get("id") },
25 | //@nexquik prismaDeleteClause stop
26 | });
27 | //@nexquik revalidatePath start
28 | revalidatePath("/nexquikTemplateModel");
29 | //@nexquik revalidatePath stop
30 |
31 | //@nexquik listRedirect start
32 | redirect(`/nexquikTemplateModel`);
33 | //@nexquik listRedirect stop
34 | }
35 | return (
36 |
37 | {/* @nexquik breadcrumb start */}
38 | {/* @nexquik breadcrumb stop */}
39 | {" "}
46 | {/* @nexquik showForm start */}
47 |
73 | {/* @nexquik showForm stop */}
74 |
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/__tests__/core-functionality-tests/cli-arg.test.ts:
--------------------------------------------------------------------------------
1 | import * as child_process from "child_process";
2 | import path from "path";
3 | import { isDirectoryNotEmpty } from "../utils";
4 | const prismaMain = "./node_modules/prisma/build/index.js";
5 |
6 | const nexquikMain = "./dist/index.js";
7 | const schemaPath = path.join(
8 | "__tests__",
9 | "prisma-schemas",
10 | "minimal-examples",
11 | "one-to-many.prisma"
12 | );
13 | const testOutputDirectory = path.join(
14 | "__tests__",
15 | "core-functionality-tests",
16 | `testOutputDirectory`
17 | );
18 |
19 | // Create an array of schema paths
20 | const commands: {
21 | command: string;
22 | removePreviousDirectory: boolean;
23 | name: string;
24 | }[] = [
25 | // Init
26 | {
27 | command: `node ${nexquikMain} group --name MainGroup --init --rootName gen --schema ${schemaPath} --output ${testOutputDirectory}`,
28 | removePreviousDirectory: true,
29 | name: "Init",
30 | },
31 | // Deps
32 | {
33 | command: `node ${nexquikMain} deps --output ${testOutputDirectory}`,
34 | removePreviousDirectory: false,
35 | name: "Deps",
36 | },
37 | // Extend Only
38 | {
39 | command: `node ${nexquikMain} group --name MainGroup --extendOnly --rootName gen --schema ${schemaPath} --output ${testOutputDirectory}`,
40 | removePreviousDirectory: false,
41 | name: "Extend Only",
42 | },
43 | // Prisma Import
44 | {
45 | command: `node ${nexquikMain} group --name MainGroup --rootName gen --schema ${schemaPath} --output ${testOutputDirectory} --prismaImport 'import prisma from "@/lib/prisma";'`,
46 | removePreviousDirectory: false,
47 | name: "Prisma Import",
48 | },
49 | // Depth
50 | {
51 | command: `node ${nexquikMain} group --name MainGroup --extendOnly --rootName gen --schema ${schemaPath} --output ${testOutputDirectory} --depth 1`,
52 | removePreviousDirectory: false,
53 | name: "Depth",
54 | },
55 | // Disabled
56 | {
57 | command: `node ${nexquikMain} group --name MainGroup --extendOnly --rootName gen --schema ${schemaPath} --output ${testOutputDirectory} --disabled`,
58 | removePreviousDirectory: false,
59 | name: "Disabled",
60 | },
61 | ];
62 |
63 | // Define a function to run a single schema test asynchronously
64 | async function runSchemaTest({ command, removePreviousDirectory }) {
65 | try {
66 | if (removePreviousDirectory) {
67 | child_process.execSync(`rm -rf ${testOutputDirectory}`);
68 | }
69 | child_process.execSync(command);
70 | expect(isDirectoryNotEmpty(testOutputDirectory)).toBeTruthy();
71 |
72 | // Run npm install
73 | child_process.execSync("npm install --quiet", {
74 | cwd: testOutputDirectory,
75 | });
76 | // Run next build
77 | child_process.execSync(`npm run build`, {
78 | stdio: "inherit",
79 | cwd: testOutputDirectory,
80 | });
81 | // Run prisma generate
82 | child_process.execSync(`node ${prismaMain} generate`, {
83 | stdio: "inherit",
84 | cwd: testOutputDirectory,
85 | });
86 | // Run type check
87 | child_process.execSync("npm run typecheck", {
88 | stdio: "inherit",
89 | cwd: testOutputDirectory,
90 | });
91 | } catch (error) {
92 | console.error("Command Test error:", error.message);
93 | throw error;
94 | }
95 | }
96 |
97 | // Run schema tests
98 | commands.forEach((command) => {
99 | test(`Schema Test: ${command}`, async () => {
100 | await runSchemaTest(command);
101 | });
102 | });
103 |
--------------------------------------------------------------------------------
/src/templateRoot/app/nexquikTemplateModel/page.tsx:
--------------------------------------------------------------------------------
1 | import { revalidatePath } from "next/cache";
2 | import Link from "next/link";
3 | //@nexquik prismaImport start
4 | import prisma from "@/lib/prisma";
5 | //@nexquik prismaImport stop
6 | import clsx from "clsx";
7 |
8 | export default async function ListNexquikTemplateModels(
9 | //@nexquik listProps start
10 | {
11 | params,
12 | searchParams,
13 | }: {
14 | params: { [key: string]: string | string[] | undefined };
15 | searchParams?: { [key: string]: string | string[] | undefined };
16 | }
17 | //@nexquik listProps stop
18 | ) {
19 | /* @nexquik listCount start */
20 |
21 | /* @nexquik listCount stop */
22 |
23 | const nexquikTemplateModel = await prisma.nexquikTemplateModel
24 | .findMany
25 | //@nexquik prismaWhereParentClause start
26 | ();
27 | //@nexquik prismaWhereParentClause stop
28 |
29 | async function deleteNexquikTemplateModel(formData: FormData) {
30 | "use server";
31 | await prisma.nexquikTemplateModel.delete({
32 | where:
33 | //@nexquik prismaDeleteClause start
34 | { id: formData.get("id") },
35 | //@nexquik prismaDeleteClause stop
36 | });
37 | //@nexquik revalidatePath start
38 | revalidatePath("/nexquikTemplateModel");
39 | //@nexquik revalidatePath stop
40 | }
41 | return (
42 |
43 | {/* @nexquik listBreadcrumb start */}
44 | {/* @nexquik listBreadcrumb stop */}
45 |
52 |
53 | {/* @nexquik createLink start */}
54 |
55 | Create New NexquikTemplateModel
56 |
57 | {/* @nexquik createLink stop */}
58 |
59 |
60 | {/* @nexquik listForm start */}
61 |
62 | {nexquikTemplateModel?.map((nexquikTemplateModel, index) => (
63 |
64 |
87 |
88 | ))}
89 |
90 | {/* @nexquik listForm stop */}
91 |
92 | {/* @nexquik listPagination start */}
93 | {/* @nexquik listPagination stop */}
94 |
95 | );
96 | }
97 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nexquik",
3 | "version": "3.2.1",
4 | "description": "Generate Next.js components from your Prisma Schema",
5 | "main": "dist/index.js",
6 | "repository": "https://github.com/bcanfield/nexquik",
7 | "bin": {
8 | "nexquik": "./dist/index.js",
9 | "prisma-generator-nexquik": "./dist/prismaGenerator.js"
10 | },
11 | "scripts": {
12 | "build": "npm-run-all build:*",
13 | "build:1": "npm run clean",
14 | "build:2": "npx tsc",
15 | "build:3": "npm run copy-files",
16 | "build:4": "chmod -R 777 dist/",
17 | "install-locally": "npm-run-all install-locally:*",
18 | "install-locally:1": "npm run build",
19 | "install-locally:2": "rm -f ./nexquik.tgz",
20 | "install-locally:3": "npm pack --force",
21 | "install-locally:4": "mv \"$(ls *.tgz)\" \"nexquik.tgz\"",
22 | "install-locally:5": "npm i && npm i --save-dev ./nexquik.tgz",
23 | "dev": "nodemon",
24 | "dev:1": "npm run install-locally",
25 | "dev:2": "rm -rf ./devNexquikApp",
26 | "dev:3": "npm run nexquikInit",
27 | "dev:4": "cd ./devNexquikApp && npm i && npm run generate && npm run push --accept-data-loss && npm run dev",
28 | "nexquikInit": "nexquik group --name Main --init --rootName nexquik --schema ./prisma/dev.prisma --output ./devNexquikApp --appTitle Nexquik",
29 | "nexquikHelp": "nexquik -h",
30 | "test": "jest --silent=false",
31 | "clean": "rimraf dist/",
32 | "generate": "prisma generate",
33 | "copy-files": "npm-run-all copy-files:*",
34 | "copy-files:1": "copyfiles -a -u 1 \"src/templateRoot/**/*\" dist",
35 | "copy-files:2": "copyfiles template.eslintrc.json dist",
36 | "live-example-install": "cd example && npm i"
37 | },
38 | "nodemonConfig": {
39 | "watch": [
40 | "src"
41 | ],
42 | "ext": "ts,tsx,css,",
43 | "exec": "npm-run-all dev:*",
44 | "delay": 500
45 | },
46 | "author": "Brandin Canfield",
47 | "license": "MIT",
48 | "keywords": [
49 | "Prisma",
50 | "TypeScript",
51 | "Next.js",
52 | "React"
53 | ],
54 | "dependencies": {
55 | "@prisma/client": "^5.1.1",
56 | "@prisma/internals": "^4.14.1",
57 | "@prisma/sdk": "^4.0.0",
58 | "@typescript-eslint/eslint-plugin": "^6.4.1",
59 | "@typescript-eslint/eslint-plugin-tslint": "^6.4.1",
60 | "@typescript-eslint/parser": "^6.4.1",
61 | "chalk": "^4.1.2",
62 | "cli-progress": "^3.12.0",
63 | "commander": "^11.0.0",
64 | "eslint": "^8.47.0",
65 | "eslint-config-next": "^13.4.19",
66 | "eslint-plugin-import": "^2.28.1",
67 | "eslint-plugin-prettier": "^4.2.1",
68 | "eslint-plugin-react": "^7.33.2",
69 | "eslint-plugin-react-hooks": "^4.6.0",
70 | "eslint-plugin-unused-imports": "^3.0.0",
71 | "figlet": "^1.6.0",
72 | "fs-extra": "^11.1.1",
73 | "nexquik": "^3.2.0",
74 | "next": "^13.4.19",
75 | "ora": "^5.4.1",
76 | "prettier": "^2.8.8",
77 | "ts-toolbelt": "^9.6.0"
78 | },
79 | "devDependencies": {
80 | "@babel/core": "^7.22.1",
81 | "@babel/preset-env": "^7.22.4",
82 | "@babel/preset-typescript": "^7.21.5",
83 | "@semantic-release/changelog": "^6.0.3",
84 | "@semantic-release/git": "^10.0.1",
85 | "@types/cli-progress": "^3.11.0",
86 | "@types/eslint": "^8.40.0",
87 | "@types/figlet": "^1.5.6",
88 | "@types/fs-extra": "^11.0.1",
89 | "@types/jest": "^29.5.1",
90 | "@types/node": "^20.2.5",
91 | "@types/prettier": "^2.7.2",
92 | "babel-jest": "^29.5.0",
93 | "copyfiles": "^2.4.1",
94 | "jest": "^29.5.0",
95 | "nodemon": "^2.0.22",
96 | "npm-run-all": "^4.1.5",
97 | "npm-watch": "^0.11.0",
98 | "prisma": "^5.1.1",
99 | "rimraf": "^5.0.1",
100 | "typescript": "^5.0.4"
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/modelTree.ts:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env node
2 | import { DMMF } from "@prisma/generator-helper";
3 | import chalk from "chalk";
4 |
5 | export interface ModelTree {
6 | modelName: string;
7 | parent?: DMMF.Model;
8 | model: DMMF.Model;
9 | children: ModelTree[];
10 | uniqueIdentifierField: { name: string; type: string }[];
11 | }
12 |
13 | export function getCompositeIdField(model: DMMF.Model): DMMF.PrimaryKey | null {
14 | return model.primaryKey;
15 | }
16 |
17 | export function getCompositeKeyFields(
18 | model: ModelTree
19 | ): { fieldName: string; fieldType: string }[] | null {
20 | const compositeKeyFields: { fieldName: string; fieldType: string }[] = [];
21 |
22 | for (const field of model.model.fields) {
23 | if (field.kind === "object" && !field.isList && field.relationFromFields) {
24 | const relatedModel = model.children.find(
25 | (child) => child.model.name === field.type
26 | );
27 | if (relatedModel) {
28 | const relatedField = relatedModel.model.fields.find(
29 | (f) =>
30 | field.relationFromFields && f.name === field.relationFromFields[0]
31 | );
32 |
33 | if (relatedField) {
34 | compositeKeyFields.push({
35 | fieldName: field.relationFromFields[0],
36 | fieldType: relatedField.type,
37 | });
38 | }
39 | }
40 | }
41 | }
42 |
43 | if (compositeKeyFields.length >= 2) {
44 | return compositeKeyFields;
45 | }
46 |
47 | return null;
48 | }
49 |
50 | export function createModelTree(
51 | dataModel: DMMF.Datamodel,
52 | excludedModels: string[],
53 | includedModels: string[]
54 | ): ModelTree[] {
55 | const models = dataModel.models;
56 |
57 | // Create a map of models for efficient lookup
58 | const modelMap: Record = {};
59 | for (const model of models) {
60 | modelMap[model.name] = model;
61 | }
62 | // console.log({ modelMap });
63 |
64 | const visitedModels: Set = new Set();
65 | const modelTrees: ModelTree[] = [];
66 |
67 | // Function to recursively build the model tree
68 | function buildModelTree(
69 | model: DMMF.Model,
70 | parent?: DMMF.Model
71 | ): ModelTree | undefined {
72 | // If we detect a circular relationship, just stop digging down into child nodes
73 | if (visitedModels.has(model.name)) {
74 | // throw new Error(`Circular relationship detected in model: ${model.name}`);
75 | // console.log(`Circular relationship detected in model: ${model.name}`);
76 | return;
77 | }
78 |
79 | visitedModels.add(model.name);
80 |
81 | const childRelationships = model.fields.filter(
82 | (field) => field.kind === "object" && field.isList
83 | );
84 |
85 | const children: ModelTree[] = [];
86 | for (const relationship of childRelationships) {
87 | const childModel = modelMap[relationship.type];
88 | if (childModel) {
89 | const childNode = buildModelTree(childModel, model);
90 | if (childNode) {
91 | children.push(childNode);
92 | }
93 | }
94 | }
95 |
96 | visitedModels.delete(model.name);
97 | const fullUniqueIdField = model.fields.find((field) => field.isId === true);
98 | let uniqueIdFieldReturn: { name: string; type: string }[] = [];
99 | if (!fullUniqueIdField) {
100 | // Check for composite id field
101 | const compositePrimaryKey = getCompositeIdField(model);
102 | if (compositePrimaryKey) {
103 | // For each field in fields, find the actual field
104 | const actualFields = model.fields
105 | .filter((modelField) =>
106 | compositePrimaryKey.fields.includes(modelField.name)
107 | )
108 | .map((f) => ({ name: f.name, type: f.type }));
109 | uniqueIdFieldReturn = actualFields;
110 | } else {
111 | console.log(
112 | chalk.red(
113 | `Nexquik could not fund a unique ID field for Model: ${model.name}`
114 | )
115 | );
116 | return;
117 | }
118 | } else {
119 | uniqueIdFieldReturn.push({
120 | name: fullUniqueIdField.name,
121 | type: fullUniqueIdField.type,
122 | });
123 | }
124 | return {
125 | modelName: model.name,
126 | model: model,
127 | parent: parent,
128 | uniqueIdentifierField: uniqueIdFieldReturn,
129 | children,
130 | };
131 | }
132 |
133 | // Filter out included or excluded models before beginning main loop to generate Tree
134 | let topLevelModels: DMMF.Model[] = [];
135 | if (includedModels.length > 0) {
136 | topLevelModels = dataModel.models.filter((m) =>
137 | includedModels.includes(m.name)
138 | );
139 | } else {
140 | topLevelModels = dataModel.models.filter(
141 | (m) => !excludedModels.includes(m.name)
142 | );
143 | }
144 | for (const model of topLevelModels) {
145 | // Only include models that dont have required parent
146 | if (
147 | !model.fields.some(
148 | (field) => field.kind === "object" && field.isRequired && !field.isList
149 | )
150 | ) {
151 | const modelTree = buildModelTree(model);
152 | if (modelTree) {
153 | modelTrees.push(modelTree);
154 | }
155 | }
156 | }
157 |
158 | return modelTrees;
159 | }
160 |
161 | export function getParentReferenceField(
162 | modelTree: ModelTree
163 | ): string | undefined {
164 | if (!modelTree.parent) {
165 | return undefined;
166 | }
167 |
168 | const parentModel = modelTree.model;
169 | const parentField = parentModel.fields.find(
170 | (field) => field.type === modelTree.parent?.name
171 | );
172 |
173 | if (!parentField) {
174 | return undefined;
175 | }
176 |
177 | // Find the unique ID field in the current model that matches the parent reference field
178 | const uniqueIdField = modelTree.model.fields.find(
179 | (field) => field.name === parentField.name
180 | );
181 |
182 | return uniqueIdField?.name;
183 | }
184 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | /*
2 | * For a detailed explanation regarding each configuration property, visit:
3 | * https://jestjs.io/docs/configuration
4 | */
5 |
6 | module.exports = {
7 | // All imported modules in your tests should be mocked automatically
8 | // automock: false,
9 |
10 | // Stop running tests after `n` failures
11 | // bail: 0,
12 |
13 | // The directory where Jest should store its cached dependency information
14 | // cacheDirectory: "/private/var/folders/g2/5cg9h7sx517f0_jh7rjw40jc0000gq/T/jest_dz",
15 |
16 | // Automatically clear mock calls, instances, contexts and results before every test
17 | // clearMocks: false,
18 |
19 | // Indicates whether the coverage information should be collected while executing the test
20 | // collectCoverage: true,
21 |
22 | // An array of glob patterns indicating a set of files for which coverage information should be collected
23 | // collectCoverageFrom: undefined,
24 |
25 | // The directory where Jest should output its coverage files
26 | // coverageDirectory: "coverage",
27 |
28 | // An array of regexp pattern strings used to skip coverage collection
29 | // coveragePathIgnorePatterns: ["/node_modules/"],
30 |
31 | // Indicates which provider should be used to instrument code for coverage
32 | // coverageProvider: "v8",
33 |
34 | // A list of reporter names that Jest uses when writing coverage reports
35 | // coverageReporters: [
36 | // "json",
37 | // "text",
38 | // "lcov",
39 | // // "clover"
40 | // ],
41 |
42 | // An object that configures minimum threshold enforcement for coverage results
43 | // coverageThreshold: undefined,
44 |
45 | // A path to a custom dependency extractor
46 | // dependencyExtractor: undefined,
47 |
48 | // Make calling deprecated APIs throw helpful error messages
49 | // errorOnDeprecated: false,
50 |
51 | // The default configuration for fake timers
52 | // fakeTimers: {
53 | // "enableGlobally": false
54 | // },
55 |
56 | // Force coverage collection from ignored files using an array of glob patterns
57 | // forceCoverageMatch: [],
58 |
59 | // A path to a module which exports an async function that is triggered once before all test suites
60 | // globalSetup: undefined,
61 |
62 | // A path to a module which exports an async function that is triggered once after all test suites
63 | // globalTeardown: undefined,
64 |
65 | // A set of global variables that need to be available in all test environments
66 | // globals: {},
67 |
68 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
69 | // maxWorkers: "50%",
70 |
71 | // An array of directory names to be searched recursively up from the requiring module's location
72 | // moduleDirectories: [
73 | // "node_modules"
74 | // ],
75 |
76 | // An array of file extensions your modules use
77 | // moduleFileExtensions: [
78 | // "js",
79 | // "mjs",
80 | // "cjs",
81 | // "jsx",
82 | // "ts",
83 | // "tsx",
84 | // "json",
85 | // "node"
86 | // ],
87 |
88 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
89 | // moduleNameMapper: {},
90 |
91 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
92 | // modulePathIgnorePatterns: [],
93 |
94 | // Activates notifications for test results
95 | // notify: false,
96 |
97 | // An enum that specifies notification mode. Requires { notify: true }
98 | // notifyMode: "failure-change",
99 |
100 | // A preset that is used as a base for Jest's configuration
101 | // preset: undefined,
102 |
103 | // Run tests from one or more projects
104 | // projects: undefined,
105 |
106 | // Use this configuration option to add custom reporters to Jest
107 | // reporters: undefined,
108 |
109 | // Automatically reset mock state before every test
110 | // resetMocks: false,
111 |
112 | // Reset the module registry before running each individual test
113 | // resetModules: false,
114 |
115 | // A path to a custom resolver
116 | // resolver: undefined,
117 |
118 | // Automatically restore mock state and implementation before every test
119 | // restoreMocks: false,
120 |
121 | // The root directory that Jest should scan for tests and modules within
122 | // rootDir: undefined,
123 |
124 | // A list of paths to directories that Jest should use to search for files in
125 | // roots: [
126 | // ""
127 | // ],
128 |
129 | // Allows you to use a custom runner instead of Jest's default test runner
130 | // runner: "jest-runner",
131 |
132 | // The paths to modules that run some code to configure or set up the testing environment before each test
133 | // setupFiles: [],
134 |
135 | // A list of paths to modules that run some code to configure or set up the testing framework before each test
136 | // setupFilesAfterEnv: [],
137 |
138 | // The number of seconds after which a test is considered as slow and reported as such in the results.
139 | // slowTestThreshold: 5,
140 |
141 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing
142 | // snapshotSerializers: [],
143 |
144 | // The test environment that will be used for testing
145 | // testEnvironment: "jest-environment-node",
146 |
147 | // Options that will be passed to the testEnvironment
148 | // testEnvironmentOptions: {},
149 |
150 | // Adds a location field to test results
151 | // testLocationInResults: false,
152 |
153 | // The glob patterns Jest uses to detect test files
154 | // testMatch: [
155 | // "**/__tests__/**/*.[jt]s?(x)",
156 | // "**/?(*.)+(spec|test).[tj]s?(x)"
157 | // ],
158 |
159 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
160 | // testPathIgnorePatterns: [
161 | // "/node_modules/"
162 | // ],
163 |
164 | // The regexp pattern or array of patterns that Jest uses to detect test files
165 | // testRegex: [],
166 |
167 | // This option allows the use of a custom results processor
168 | // testResultsProcessor: undefined,
169 |
170 | // This option allows use of a custom test runner
171 | // testRunner: "jest-circus/runner",
172 |
173 | // A map from regular expressions to paths to transformers
174 | // transform: undefined,
175 |
176 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
177 | // transformIgnorePatterns: [
178 | // "/node_modules/",
179 | // "\\.pnp\\.[^\\/]+$"
180 | // ],
181 |
182 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
183 | // unmockedModulePathPatterns: undefined,
184 |
185 | // Indicates whether each individual test should be reported during the run
186 | // verbose: undefined,
187 |
188 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
189 | // watchPathIgnorePatterns: [],
190 |
191 | // Whether to use watchman for file crawling
192 | watchman: true,
193 | testMatch: ["**/__tests__/**/*.test.ts"],
194 | };
195 |
--------------------------------------------------------------------------------
/src/cli.ts:
--------------------------------------------------------------------------------
1 | import { GeneratorOptions } from "@prisma/generator-helper";
2 | import chalk from "chalk";
3 | import { Command } from "commander";
4 | import figlet from "figlet";
5 | import { generate } from "./generators";
6 | import { formatDirectory, installPackages } from "./helpers";
7 | import path from "path";
8 | import { spawnSync } from "child_process";
9 | import { ESLint } from "eslint";
10 | // require("eslint-plugin-unused-imports");
11 | export interface CliArgs {
12 | prismaSchemaPath: string;
13 | outputDirectory: string;
14 | }
15 | export const defaultOutputDirectory = "./";
16 | export interface Group {
17 | name: string;
18 | include: string[];
19 | exclude: string[];
20 | }
21 | const defaultPrismaSchemaPath = "schema.prisma";
22 |
23 | export async function run(options?: GeneratorOptions) {
24 | try {
25 | console.log(
26 | chalk.bgYellow.blue.bold(
27 | figlet.textSync("Nexquik", {
28 | font: "Univers",
29 | horizontalLayout: "default",
30 | verticalLayout: "default",
31 | })
32 | )
33 | );
34 | const program = new Command();
35 | let deps = false;
36 | // Create an array to collect group objects
37 | const groups: Group[] = [];
38 | let currentGroup:
39 | | { name: string; include: string[]; exclude: string[] }
40 | | undefined = undefined;
41 | program
42 | .version(require("../package.json").version)
43 | .description("Auto-generate Next.js UI components from your DB Schema")
44 | .option(
45 | "--schema ",
46 | "Path to prisma schema file",
47 | defaultPrismaSchemaPath
48 | )
49 | .option(
50 | "--output ",
51 | "Path to root directory of your project",
52 | defaultOutputDirectory
53 | )
54 |
55 | .option("--init", "Initializes a full Next.js app from scratch")
56 | .option(
57 | "--extendOnly",
58 | "Only creates the models specified in the current command, and leaves previously created ones alone."
59 | )
60 | .option(
61 | "--appTitle ",
62 | "Title to be used in the header of your app",
63 | "App"
64 | )
65 | .option(
66 | "--rootName ",
67 | "Desired name for the root app dir for your generated groups (this is the first directory nested under your 'app' directory.",
68 | "gen"
69 | )
70 | .option(
71 | "--depth ",
72 | "Maximum recursion depth for your models. (Changing this for large data models is not recommended, unless you filter down your models with the 'include' or 'exclude' flags also.)",
73 | "5"
74 | )
75 | .option(
76 | "--modelsOnly",
77 | "Only generates components for your models. Skips the boilerplate files - root page.tsx,layout.tsx, globals.css, etc...."
78 | )
79 | .option(
80 | "--prismaImport ",
81 | "Import location for your prisma client if it differs from the standard setup.",
82 | `import prisma from '@/lib/prisma';`
83 | )
84 | .option("--disabled", "Disable the generator", false);
85 |
86 | program
87 | .command("group")
88 | .description(
89 | "Create a group to organize your models into route groups. You can use this command multiple times to create many groups"
90 | )
91 | .option("--name ", "Specify a group name", (groupName) => {
92 | // Create a new group object for each group
93 | currentGroup = { name: groupName, include: [], exclude: [] };
94 | groups.push(currentGroup);
95 | })
96 | .option(
97 | "--include ",
98 | "Specify included types (comma-separated)",
99 | (modelNames) => {
100 | // Add the included types to the current group
101 | if (currentGroup) {
102 | currentGroup.include = modelNames.split(",");
103 | }
104 | }
105 | )
106 | .option(
107 | "--exclude ",
108 | "Specify excluded types (comma-separated)",
109 | (modelNames) => {
110 | // Add the excluded types to the current group
111 | if (currentGroup) {
112 | currentGroup.exclude = modelNames.split(",");
113 | }
114 | }
115 | );
116 |
117 | program
118 | .command("deps")
119 | .description(
120 | "Install nexquik dependencies and copy over required config files. (tailwind, postcss, auto-prefixer, etc)"
121 | )
122 | .action(() => {
123 | console.log("deps in here");
124 | deps = true;
125 | });
126 |
127 | // If prisma generator, parse the cli args from the generator config
128 | if (options?.generator.config) {
129 | try {
130 | const genArgs = options?.generator.config.command.split(" ") || [];
131 | program.parse(genArgs, { from: "user" });
132 | } catch {
133 | throw Error("Invalid args");
134 | }
135 | } else {
136 | // Else, parse from cli args
137 | program.parse(process.argv);
138 | }
139 |
140 | const cliArgs = program.opts();
141 | const prismaSchemaPath = options?.schemaPath || cliArgs.schema;
142 | const outputDirectory = cliArgs.output;
143 | const maxDepth = parseInt(cliArgs.Depth);
144 | const rootName = cliArgs.rootName;
145 | const prismaImportString = cliArgs.prismaImport;
146 | const init = cliArgs.init || false;
147 | const extendOnly = cliArgs.extendOnly || false;
148 | const modelsOnly = cliArgs.modelsOnly || false;
149 | const disabled =
150 | process.env.DISABLE_NEXQUIK === "true" || cliArgs.disabled === true;
151 | const appTitle = cliArgs.appTitle;
152 | if (disabled) {
153 | return console.log("Nexquik generation disabled due to env var");
154 | }
155 |
156 | await generate(
157 | prismaSchemaPath,
158 | outputDirectory,
159 | maxDepth,
160 | init,
161 | rootName,
162 | groups,
163 | extendOnly,
164 | deps,
165 | prismaImportString,
166 | appTitle,
167 | modelsOnly
168 | );
169 |
170 | if (!deps) {
171 | console.log(`${chalk.blue.bold("\nLinting Generated Files...")}`);
172 | try {
173 | const startTime = new Date().getTime();
174 | const eslint = new ESLint({
175 | fix: true,
176 | useEslintrc: false,
177 | overrideConfig: {
178 | extends: [
179 | "plugin:@typescript-eslint/eslint-recommended",
180 | "plugin:@typescript-eslint/recommended",
181 | ],
182 | parser: "@typescript-eslint/parser",
183 | plugins: ["unused-imports"],
184 | rules: {
185 | "no-unused-vars": "off",
186 | "@typescript-eslint/no-unused-vars": "error",
187 | "unused-imports/no-unused-imports": "error",
188 | "import/no-unused-modules": ["error"],
189 | },
190 | },
191 | });
192 | const results = await eslint.lintFiles([
193 | `${outputDirectory}/app/${rootName}/**/*.tsx`,
194 | ]);
195 |
196 | await ESLint.outputFixes(results);
197 | const endTime = new Date().getTime();
198 | const duration = (endTime - startTime) / 1000;
199 | console.log(chalk.gray(`(Linted in ${duration} seconds)`));
200 | } catch {
201 | console.log(
202 | chalk.gray(
203 | `Info: Something weird occured when linting. This may happen when running via 'npx', and you don't have nexquik installed in your node modules.`
204 | )
205 | );
206 | }
207 | }
208 | console.log(`${chalk.green.bold("\n✔ Success!")}`);
209 | return;
210 | } catch (error) {
211 | console.log(chalk.red.bold("Nexquik Error:\n"), error);
212 | }
213 | }
214 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Nexquik
6 |
7 |
8 |
9 |
10 | Transform your Prisma Models into stunning Next.js UI Components in seconds.
11 |
12 |
15 |
16 |
17 | Auto-generate pieces of your app, allowing you to focus on refining your more custom components.
18 |
19 |
20 | These will be Server Components fully equipped with Server Actions
21 |
22 |
23 |
24 |
25 |
26 | Usage •
27 | Options •
28 | Use Cases
29 |
30 |
31 | ## Usage
32 | ### Example - Creating a new app from scratch
33 | *Create a full Next.js app from scratch using all of your models*
34 | ```zsh
35 | npm i nexquik -g
36 | ```
37 |
38 | **Option 1: Add the generator to your Prisma Schema and run `prisma generate`**
39 |
40 | ```prisma
41 | generator nexquik {
42 | provider = "prisma-generator-nexquik"
43 | command = "--init group --name Main"
44 | }
45 | ```
46 | **Option 2: Use the command line**
47 | ```zsh
48 | npx nexquik --init group --name Main
49 | ```
50 |
51 | ### Example - Initializing Nexquik in an existing app
52 | *Install nexquik, install dependencies and required files, and generate some selected models into the app directory.*
53 | ```zsh
54 | npm i nexquik
55 | ```
56 | ```zsh
57 | nexquik deps
58 | ```
59 | Now that we installed Nexquik and initialized it in the project, you can add the generator to your schema, and your UI will be generated every time you run `prisma generate`.
60 |
61 |
62 | ```prisma
63 | generator nexquik {
64 | provider = "prisma-generator-nexquik"
65 | command = "group --name UserManagement --include User,Admin,Organization group --name TaskManagement --include Task,Product,Category"
66 | }
67 | ```
68 |
69 | Keeping the generator in your schema will ensure that any time your models change, your UI will reflect them.
70 |
71 | This also allows you to benefit from enhancements to the project from the open source community.
72 |
73 |
74 | ## Options
75 | | Options | Description |
76 | |-------------------------------------|-----------------------------------------------------------------------------------------------------------------|
77 | | -V, --version | Output the version number |
78 | | --schema | Path to prisma schema file (default: "schema.prisma") |
79 | | --output | Path to root directory of your project (default: "./") |
80 | | --init | Initializes a full Next.js app from scratch |
81 | | --extendOnly | Only creates the models specified in the current command, and leaves previously created ones alone. |
82 | | --appTitle | Title to be used in the header of your app (default: "App") |
83 | | --rootName | Desired name for the root app dir for your generated groups (this is the first directory nested under your 'app' directory. (default: "gen") |
84 | | --depth | Maximum recursion depth for your models. (Changing this for large data models is not recommended, unless you filter down your models with the 'include' or 'exclude' flags also.) (default: "5") |
85 | | --modelsOnly | Only generates components for your models. Skips the boilerplate files - root page.tsx,layout.tsx, globals.css, etc.... |
86 | | --prismaImport | Import location for your prisma client if it differs from the standard setup. (default: "import prisma from '@/lib/prisma';") |
87 | | --disabled | Disable the generator (default: false) |
88 | | -h, --help | Display help for command |
89 |
90 | | Commands | Description |
91 | |-------------------------------------|-----------------------------------------------------------------------------------------------------------------|
92 | | group [options] | Create a group to organize your models into route groups. You can use this command multiple times to create many groups |
93 | | deps | Install nexquik dependencies and copy over required config files. (tailwind, postcss, auto-prefixer, etc) |
94 | | help [command] | Display help for command |
95 |
96 | ### Disabled
97 | To disable Nexquik from generating during a Prisma generate, you can either use the `--disabled` CLI option or set the following env var.
98 | ```zsh
99 | DISABLE_NEXQUIK=true
100 | ```
101 |
102 | ## Live Demo
103 | The live demo is hosted through [CodeSandbox](https://codesandbox.io/p/sandbox/github/bcanfield/nexquik/tree/main).
104 |
105 | In CodeSandbox, the `example` directory will contain your app to poke around in.
106 |
107 | The demo is configured to re-generate your app every time you make a change to your prisma schema.
108 |
109 | ## Use Cases
110 | Portions of your app that rely on simple CRUD operations are prime candidates for auto-generation. Here are some examples.
111 |
112 | ### User Management
113 | A user management section typically involves creating, reading, updating, and deleting user accounts. This could include functionalities like user registration, profile management, password reset, and account deletion.
114 |
115 | ### Admin Screens
116 | Admin screens often require CRUD operations to manage various aspects of the application or website. This could include managing content, users, roles, permissions, settings, and more.
117 |
118 | ### Product Catalog
119 | An e-commerce website's product catalog might involve creating, reading, updating, and deleting products. Admins could add new products, update product details, and remove products that are no longer available.
120 |
121 | ### Content Management System (CMS)
122 | In a CMS, content creators might need to perform CRUD operations on articles, blog posts, images, and other types of content. They can create, edit, delete, and publish content.
123 |
124 | ### Task Management
125 | For a task management app, users may need to perform CRUD operations on tasks. This includes adding new tasks, marking tasks as completed, updating task details, and deleting tasks.
126 |
127 | ### Customer Relationship Management (CRM)
128 | CRM systems require basic CRUD operations to manage customer information. Users can add new contacts, update contact details, log interactions, and delete contacts if needed.
129 |
130 | ### Event Calendar
131 | An event calendar app may involve CRUD operations for adding, updating, and deleting events. Users can create new events, edit event details, and remove events from the calendar.
132 |
133 | ### Inventory Management
134 | For an inventory management system, CRUD operations could be used to manage stock items. Users can add new items, update quantities, adjust prices, and mark items as discontinued.
135 |
136 | ### Feedback or Comment System
137 | Websites with user-generated content might need CRUD operations for handling feedback, comments, or reviews. Users can post new comments, edit their comments, and delete them.
138 |
139 | ### Polls or Surveys
140 | Poll or survey applications may involve CRUD operations to manage questions, options, and responses. Admins can create new polls, update question wording, and analyze collected responses.
141 |
142 |
143 |
144 |
161 |
162 |
--------------------------------------------------------------------------------
/src/templateRoot/app/rootGroupRouteLayout.tsx:
--------------------------------------------------------------------------------
1 | import "./globals.css";
2 | import Link from "next/link";
3 | import { Roboto } from "next/font/google";
4 | import Image from "next/image";
5 | import background from "./images/backdrop.png";
6 | import logo from "./images/logo.png";
7 |
8 | const roboto = Roboto({
9 | weight: "400",
10 | subsets: ["latin"],
11 | display: "swap",
12 | });
13 | export default function RootLayout({
14 | children,
15 | }: {
16 | children: React.ReactNode;
17 | }) {
18 | return (
19 |
22 |
23 | {/* Start Backdrop Blur */}
24 |
35 | {/* End Backdrop Blur */}
36 | {/* Navbar */}
37 |
125 | {/* End Navbar */}
126 | {/* Page Content Start*/}
127 |
128 |
129 | {/* Side Nav */}
130 |
131 |
132 |
136 |
140 |
141 |
142 |
143 | Route Groups
144 |
145 |
146 | {/* //@nexquik routeSidebar start */}
147 | {/* //@nexquik routeSidebar stop */}
148 |
149 |
150 |
151 |
152 |
156 | {/* End Side Nav */}
157 |
158 |
162 | {/* CHILDREN START */}
163 |
164 | {children}
165 | {/* CHILDREN END */}
166 |
167 |
168 | {/*
*/}
169 |
170 |
171 | {/* Page Content End*/}
172 |
202 |
203 |
204 | );
205 | }
206 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright 2016-2022 Kong Inc.
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [3.2.1](https://github.com/bcanfield/nexquik/compare/v3.2.0...v3.2.1) (2023-10-13)
2 |
3 |
4 | ### Bug Fixes
5 |
6 | * Depdency Upgrades ([a1d6d94](https://github.com/bcanfield/nexquik/commit/a1d6d94d5bfbc0decbd2b4b949edd6c24824cb35))
7 |
8 | # [3.2.0](https://github.com/bcanfield/nexquik/compare/v3.1.5...v3.2.0) (2023-09-16)
9 |
10 |
11 | ### Features
12 |
13 | * Fix for improper parent id handling ([169de3b](https://github.com/bcanfield/nexquik/commit/169de3b1f4b3e273ecc1976d106d1dc820783a14))
14 |
15 | ## [3.1.5](https://github.com/bcanfield/nexquik/compare/v3.1.4...v3.1.5) (2023-09-05)
16 |
17 |
18 | ### Bug Fixes
19 |
20 | * Add Step attribute to Float html inputs ([4722f91](https://github.com/bcanfield/nexquik/commit/4722f91e8a5bf0ce44e578ccece746e371d2c5c5))
21 | * Fix 'step' attribute on html input for Floats ([1034c08](https://github.com/bcanfield/nexquik/commit/1034c08f6aac3e6438d24ddccc08614ef0fe3f67))
22 |
23 | ## [3.1.4](https://github.com/bcanfield/nexquik/compare/v3.1.3...v3.1.4) (2023-09-05)
24 |
25 |
26 | ### Bug Fixes
27 |
28 | * Converting optional parent reference to prisma create input ([2b91e70](https://github.com/bcanfield/nexquik/commit/2b91e70e21730657cf6ff2f38e681f1114b6bc97))
29 |
30 | ## [3.1.3](https://github.com/bcanfield/nexquik/compare/v3.1.2...v3.1.3) (2023-09-05)
31 |
32 |
33 | ### Bug Fixes
34 |
35 | * Models Only CLI Option ([601e96e](https://github.com/bcanfield/nexquik/commit/601e96e380731a4c5bce6f7fcb0190193c951936))
36 |
37 | ## [3.1.2](https://github.com/bcanfield/nexquik/compare/v3.1.1...v3.1.2) (2023-09-01)
38 |
39 |
40 | ### Bug Fixes
41 |
42 | * Update Readme and CLI Option descriptions ([1f6d26b](https://github.com/bcanfield/nexquik/commit/1f6d26b195887f959ec1732d533cd4d0bb5c1bf7))
43 |
44 | ## [3.1.1](https://github.com/bcanfield/nexquik/compare/v3.1.0...v3.1.1) (2023-09-01)
45 |
46 |
47 | ### Bug Fixes
48 |
49 | * Catch exception in linting ([#89](https://github.com/bcanfield/nexquik/issues/89)) ([aef4724](https://github.com/bcanfield/nexquik/commit/aef47245728d06ce747c0d5085c2dd24d74ac06a))
50 |
51 | # [3.1.0](https://github.com/bcanfield/nexquik/compare/v3.0.0...v3.1.0) (2023-08-31)
52 |
53 |
54 | ### Features
55 |
56 | * Installing Deps in existing Next.js project ([#88](https://github.com/bcanfield/nexquik/issues/88)) ([ce2f8de](https://github.com/bcanfield/nexquik/commit/ce2f8deaf5a9ad7d747787ab27098079f78c4749))
57 |
58 | # [3.0.0](https://github.com/bcanfield/nexquik/compare/v2.2.2...v3.0.0) (2023-08-30)
59 |
60 |
61 | ### Features
62 |
63 | * Integrate into continuous workflow of existing Next.js projects ([#84](https://github.com/bcanfield/nexquik/issues/84)) ([bc2f137](https://github.com/bcanfield/nexquik/commit/bc2f1372ca2ec0823cbcedcf6509e96a25ecc8b8))
64 |
65 |
66 | ### BREAKING CHANGES
67 |
68 | * Re-structuring the tool to better fit into the continuous workflow of existing apps
69 |
70 | ### Bug Fixes
71 |
72 | * Add tests for cli args ([a7d84fd](https://github.com/bcanfield/nexquik/commit/a7d84fd95c37b681df39b28200faa3e60e1b414a))
73 |
74 | ## [2.2.2](https://github.com/bcanfield/nexquik/compare/v2.2.1...v2.2.2) (2023-08-21)
75 |
76 |
77 | ### Bug Fixes
78 |
79 | * Models only ([fd10673](https://github.com/bcanfield/nexquik/commit/fd106731e7c3c661aeed5a449e530deef13f4bbb))
80 | * Prevent app dir generation from being called twice ([d989fa2](https://github.com/bcanfield/nexquik/commit/d989fa22d1b31627f2a06dd72656b1dd08ae5fb6))
81 |
82 | ## [2.2.1](https://github.com/bcanfield/nexquik/compare/v2.2.0...v2.2.1) (2023-08-21)
83 |
84 |
85 | ### Bug Fixes
86 |
87 | * Remove nexquik .tar dependency ([6cfbcf3](https://github.com/bcanfield/nexquik/commit/6cfbcf39955d031d6c78db87d4c73bcd151a05f8))
88 | * Testing and Organization Improvements ([#69](https://github.com/bcanfield/nexquik/issues/69)) ([2fa6e3d](https://github.com/bcanfield/nexquik/commit/2fa6e3ddf44ab9cf5462b2e280c9912784a7404f))
89 |
90 | # [2.2.0](https://github.com/bcanfield/nexquik/compare/v2.1.3...v2.2.0) (2023-08-17)
91 |
92 |
93 | ### Features
94 |
95 | * Generation Output mode ([#68](https://github.com/bcanfield/nexquik/issues/68)) ([e848086](https://github.com/bcanfield/nexquik/commit/e848086a9aba25f2173e375a1f67a01ed812a0f9))
96 |
97 | ## [2.1.3](https://github.com/bcanfield/nexquik/compare/v2.1.2...v2.1.3) (2023-08-17)
98 |
99 |
100 | ### Bug Fixes
101 |
102 | * Copy over tsconfig ([0d71a24](https://github.com/bcanfield/nexquik/commit/0d71a24b4b563c5f489236042c6ce3846bb712d9))
103 |
104 | ## [2.1.2](https://github.com/bcanfield/nexquik/compare/v2.1.1...v2.1.2) (2023-08-17)
105 |
106 |
107 | ### Bug Fixes
108 |
109 | * Create prisma directory ([41bb7fd](https://github.com/bcanfield/nexquik/commit/41bb7fd99363e6543ec61899e1a2a62ee9aede03))
110 |
111 | ## [2.1.1](https://github.com/bcanfield/nexquik/compare/v2.1.0...v2.1.1) (2023-08-17)
112 |
113 |
114 | ### Bug Fixes
115 |
116 | * Add option to disable generator via env var ([9bc9ee7](https://github.com/bcanfield/nexquik/commit/9bc9ee7d261c28188281121a97fcc50e7f176252))
117 | * Reduce bundle size ([1a38b2a](https://github.com/bcanfield/nexquik/commit/1a38b2ae997b42f30839fa6dc6371d054237a370))
118 |
119 | # [2.1.0](https://github.com/bcanfield/nexquik/compare/v2.0.6...v2.1.0) (2023-08-17)
120 |
121 |
122 | ### Features
123 |
124 | * Optimize file IO and add max-depth option ([#67](https://github.com/bcanfield/nexquik/issues/67)) ([540af5a](https://github.com/bcanfield/nexquik/commit/540af5a9795d87798e72b1aafd660dc30c42b4dd))
125 |
126 | ## [2.0.6](https://github.com/bcanfield/nexquik/compare/v2.0.5...v2.0.6) (2023-08-16)
127 |
128 |
129 | ### Bug Fixes
130 |
131 | * Remove linting step to reduce execution time ([82605c4](https://github.com/bcanfield/nexquik/commit/82605c491a3d8d6aacfdc987126e159392bcbfaf))
132 |
133 | ## [2.0.5](https://github.com/bcanfield/nexquik/compare/v2.0.4...v2.0.5) (2023-08-16)
134 |
135 |
136 | ### Bug Fixes
137 |
138 | * CLI Args ([b74f4d1](https://github.com/bcanfield/nexquik/commit/b74f4d1fe92f5912915620f53ac24b1b787ecea2))
139 |
140 | ## [2.0.4](https://github.com/bcanfield/nexquik/compare/v2.0.3...v2.0.4) (2023-08-16)
141 |
142 |
143 | ### Bug Fixes
144 |
145 | * Dependency Upgrades ([6804a36](https://github.com/bcanfield/nexquik/commit/6804a36b54e0d4618e874e78a954897d834de010))
146 |
147 | ## [2.0.3](https://github.com/bcanfield/nexquik/compare/v2.0.2...v2.0.3) (2023-08-16)
148 |
149 |
150 | ### Bug Fixes
151 |
152 | * Consistend CLI args ([ef0f1c2](https://github.com/bcanfield/nexquik/commit/ef0f1c23adae3fd91e00e4f2619602b866d0027a))
153 | * Consistent CLI options ([6846d6d](https://github.com/bcanfield/nexquik/commit/6846d6d8526995a02785955ddeadcef8eabb20d3))
154 | * Dependencies ([a4b2306](https://github.com/bcanfield/nexquik/commit/a4b23067fab60b4860e722276bba3a55da8fa255))
155 | * Tests ([56729e5](https://github.com/bcanfield/nexquik/commit/56729e5f5142be1eaaad8cafdd94255551c5078c))
156 |
157 | ## [2.0.2](https://github.com/bcanfield/nexquik/compare/v2.0.1...v2.0.2) (2023-08-16)
158 |
159 |
160 | ### Bug Fixes
161 |
162 | * CLI Arg parse ([d45732c](https://github.com/bcanfield/nexquik/commit/d45732c6c6daf606b9935d6cf27fd879407aa86f))
163 |
164 | ## [2.0.1](https://github.com/bcanfield/nexquik/compare/v2.0.0...v2.0.1) (2023-08-15)
165 |
166 |
167 | ### Bug Fixes
168 |
169 | * Binary commands and logs ([ad3304d](https://github.com/bcanfield/nexquik/commit/ad3304d595ab21f84986109764f6fdc27c8f45dd))
170 | * Tests ([8230266](https://github.com/bcanfield/nexquik/commit/82302668ba3aba34dd8ad778d5de7c0f8ee45f98))
171 |
172 | # [2.0.0](https://github.com/bcanfield/nexquik/compare/v1.2.8...v2.0.0) (2023-08-15)
173 |
174 |
175 | ### Bug Fixes
176 |
177 | * Fix binaries ([ccd60d1](https://github.com/bcanfield/nexquik/commit/ccd60d1f0be1183581197edbaa3dafec8374f436))
178 | * Remove nexquik dependency that is only used in dev env ([31ba559](https://github.com/bcanfield/nexquik/commit/31ba5596d833d21f98e1d2899083b7c928366918))
179 |
180 |
181 | ### Features
182 |
183 | * V2 ([#65](https://github.com/bcanfield/nexquik/issues/65)) ([9a34b77](https://github.com/bcanfield/nexquik/commit/9a34b77ec0c8c5d7f07c29a025b6f248a6f2c8e4))
184 |
185 |
186 | ### BREAKING CHANGES
187 |
188 | * V2 Improvements
189 |
190 | ## [1.2.8](https://github.com/bcanfield/nexquik/compare/v1.2.7...v1.2.8) (2023-07-24)
191 |
192 |
193 | ### Bug Fixes
194 |
195 | * Fix template app directory name ([#58](https://github.com/bcanfield/nexquik/issues/58)) ([54cf27f](https://github.com/bcanfield/nexquik/commit/54cf27f98cc7c0784f7e71a418a3f0eed64b487a))
196 |
197 | ## [1.2.7](https://github.com/bcanfield/nexquik/compare/v1.2.6...v1.2.7) (2023-05-30)
198 |
199 | ### Bug Fixes
200 |
201 | - Body Styling fix ([669d2d3](https://github.com/bcanfield/nexquik/commit/669d2d31d55988d812faf000ecf901b6d838a132))
202 |
203 | ## [1.2.6](https://github.com/bcanfield/nexquik/compare/v1.2.5...v1.2.6) (2023-05-30)
204 |
205 | ### Bug Fixes
206 |
207 | - Styling and bugfixes related to prisma import ([#11](https://github.com/bcanfield/nexquik/issues/11)) ([8dab68c](https://github.com/bcanfield/nexquik/commit/8dab68cc28b5af105009e25ccee5e3ab0f93a86a))
208 |
209 | ## [1.2.5](https://github.com/bcanfield/nexquik/compare/v1.2.4...v1.2.5) (2023-05-30)
210 |
211 | ### Bug Fixes
212 |
213 | - Dependency Upgrade ([d53b571](https://github.com/bcanfield/nexquik/commit/d53b571e7629b3d65e754cd34fbe73633b039440))
214 |
215 | ## [1.2.4](https://github.com/bcanfield/nexquik/compare/v1.2.3...v1.2.4) (2023-05-29)
216 |
217 | ### Bug Fixes
218 |
219 | - Dependencies ([219494b](https://github.com/bcanfield/nexquik/commit/219494badc0976fe8b227997fa741c73463962cb))
220 |
221 | ## [1.2.3](https://github.com/bcanfield/nexquik/compare/v1.2.2...v1.2.3) (2023-05-29)
222 |
223 | ### Bug Fixes
224 |
225 | - Dependencies ([3640861](https://github.com/bcanfield/nexquik/commit/364086138b40ab719d5e67aaf889af07c996406e))
226 | - Template Styling fixes and bugfixes ([#5](https://github.com/bcanfield/nexquik/issues/5)) ([1b211d1](https://github.com/bcanfield/nexquik/commit/1b211d1fe404f8d35a5df46627ea473634cfeabe))
227 |
228 | ## [1.2.2](https://github.com/bcanfield/nexquik/compare/v1.2.1...v1.2.2) (2023-05-26)
229 |
230 | ### Bug Fixes
231 |
232 | - Add jest tests ([#4](https://github.com/bcanfield/nexquik/issues/4)) ([fbf3ff2](https://github.com/bcanfield/nexquik/commit/fbf3ff29fc26a6edc0b5fc440140eb1de790004a))
233 |
234 | ## [1.2.1](https://github.com/bcanfield/nexquik/compare/v1.2.0...v1.2.1) (2023-05-26)
235 |
236 | ### Bug Fixes
237 |
238 | - Remove dist from release assets ([9a6403b](https://github.com/bcanfield/nexquik/commit/9a6403be0334a8f6213d0dd9fddc92a7022a0d7c))
239 |
240 | # [1.2.0](https://github.com/bcanfield/nexquik/compare/v1.1.0...v1.2.0) (2023-05-26)
241 |
242 | ### Features
243 |
244 | - Initial generation of app dir ([#3](https://github.com/bcanfield/nexquik/issues/3)) ([20be668](https://github.com/bcanfield/nexquik/commit/20be6689bb314d52e8589bb47c18ef173aca2316))
245 |
246 | # [1.1.0](https://github.com/bcanfield/nexquik/compare/v1.0.3...v1.1.0) (2023-05-12)
247 |
248 | ### Bug Fixes
249 |
250 | - Dependencies ([7898227](https://github.com/bcanfield/nexquik/commit/789822797645099304457d4daf44e71a8ea97bc1))
251 | - Dependencies ([b793e95](https://github.com/bcanfield/nexquik/commit/b793e954fe4554735bb587a8810c7ae348f81b98))
252 |
253 | ### Features
254 |
255 | - Prisma schema inspection and initial directory and file generation ([#2](https://github.com/bcanfield/nexquik/issues/2)) ([4218db9](https://github.com/bcanfield/nexquik/commit/4218db9a1fcae3b2f20cb9f7cccc628ab56572c0))
256 |
257 | ## [1.0.3](https://github.com/bcanfield/prisnext/compare/v1.0.2...v1.0.3) (2023-05-11)
258 |
259 | ### Bug Fixes
260 |
261 | - Version increment ([48fd986](https://github.com/bcanfield/prisnext/commit/48fd98630ddfc3a3370962ca2e3b458fbe237589))
262 |
263 | ## [1.0.2](https://github.com/bcanfield/prisnext/compare/v1.0.1...v1.0.2) (2023-05-11)
264 |
265 | ### Bug Fixes
266 |
267 | - Workflow ([236486d](https://github.com/bcanfield/prisnext/commit/236486d4d0b54b76f10050fceffb2c876ebc067d))
268 |
269 | ## [1.0.1](https://github.com/bcanfield/prisnext/compare/v1.0.0...v1.0.1) (2023-05-11)
270 |
271 | ### Bug Fixes
272 |
273 | - Workflow ([9dcd379](https://github.com/bcanfield/prisnext/commit/9dcd37945f77c5c67952f857bf69e94e337ba039))
274 |
275 | # 1.0.0 (2023-05-11)
276 |
277 | ### Bug Fixes
278 |
279 | - Remove dist ([61c163e](https://github.com/bcanfield/prisnext/commit/61c163eeae51eccfed0f60d0db4544c72656e26c))
280 | - Workflow ([986e448](https://github.com/bcanfield/prisnext/commit/986e44832e947137902ed9c40d553caffd2b07e1))
281 | - Workflow ([79156d8](https://github.com/bcanfield/prisnext/commit/79156d8182cd027391498456a3b42a4cdff215b5))
282 | - Workflow ([fbdb50e](https://github.com/bcanfield/prisnext/commit/fbdb50eb0a34ef5de33e5d43f080718e1229a718))
283 |
284 | ### Features
285 |
286 | - Initial CLI Setup ([#1](https://github.com/bcanfield/prisnext/issues/1)) ([aeb32ba](https://github.com/bcanfield/prisnext/commit/aeb32ba027944e78d756cec35fdb69cdc2e2ea17))
287 |
288 | # 1.0.0 (2023-05-11)
289 |
290 | ### Bug Fixes
291 |
292 | - Remove dist ([61c163e](https://github.com/bcanfield/prisnext/commit/61c163eeae51eccfed0f60d0db4544c72656e26c))
293 | - Workflow ([79156d8](https://github.com/bcanfield/prisnext/commit/79156d8182cd027391498456a3b42a4cdff215b5))
294 | - Workflow ([fbdb50e](https://github.com/bcanfield/prisnext/commit/fbdb50eb0a34ef5de33e5d43f080718e1229a718))
295 |
296 | ### Features
297 |
298 | - Initial CLI Setup ([#1](https://github.com/bcanfield/prisnext/issues/1)) ([aeb32ba](https://github.com/bcanfield/prisnext/commit/aeb32ba027944e78d756cec35fdb69cdc2e2ea17))
299 |
300 | # 1.1.1 (2023-05-11)
301 |
302 | ### Bug Fixes
303 |
304 | - Remove dist ([61c163e](https://github.com/bcanfield/prisnext/commit/61c163eeae51eccfed0f60d0db4544c72656e26c))
305 | - Workflow ([fbdb50e](https://github.com/bcanfield/prisnext/commit/fbdb50eb0a34ef5de33e5d43f080718e1229a718))
306 |
307 | ### Features
308 |
309 | - Initial CLI Setup ([#1](https://github.com/bcanfield/prisnext/issues/1)) ([aeb32ba](https://github.com/bcanfield/prisnext/commit/aeb32ba027944e78d756cec35fdb69cdc2e2ea17))
310 |
311 | # 1.0.0 (2023-05-11)
312 |
313 | ### Bug Fixes
314 |
315 | - Remove dist ([61c163e](https://github.com/bcanfield/prisnext/commit/61c163eeae51eccfed0f60d0db4544c72656e26c))
316 |
317 | ### Features
318 |
319 | - Initial CLI Setup ([#1](https://github.com/bcanfield/prisnext/issues/1)) ([aeb32ba](https://github.com/bcanfield/prisnext/commit/aeb32ba027944e78d756cec35fdb69cdc2e2ea17))
320 |
321 | ## [1.0.1](https://github.com/bcanfield/prisnext/compare/v1.0.0...v1.0.1) (2023-05-11)
322 |
323 | ### Bug Fixes
324 |
325 | - Troubleshoot workflow ([0ba8ccb](https://github.com/bcanfield/prisnext/commit/0ba8ccbd5e2ad5973dc555a9674af00255571400))
326 |
327 | # 1.0.0 (2023-05-11)
328 |
329 | ### Bug Fixes
330 |
331 | - Add Readme ([7bd6115](https://github.com/bcanfield/prisnext/commit/7bd6115f8521eb095faee989589d61f5a520305f))
332 | - Troubleshoot ([ac95d1c](https://github.com/bcanfield/prisnext/commit/ac95d1cb3feefeb7523b3151a4d9b0593965c92e))
333 | - Troubleshoot workflow ([637c8f0](https://github.com/bcanfield/prisnext/commit/637c8f00031dc060ea6199c6708f551ea30f3cd6))
334 | - Troubleshoot workflow ([1634868](https://github.com/bcanfield/prisnext/commit/1634868e43bec360022ed525dd7751eb2ed53ded))
335 | - Troubleshoot workflow ([8ab54cc](https://github.com/bcanfield/prisnext/commit/8ab54cc28e8734e6623335ade4ccaaf38101a63e))
336 | - Troubleshoot workflow ([e5a3df1](https://github.com/bcanfield/prisnext/commit/e5a3df162ad8c16bc9279b6ed70da22ac9942e1c))
337 | - Troubleshoot workflow ([951c61e](https://github.com/bcanfield/prisnext/commit/951c61e7d46556e9f40f48024a480ac67c72fa29))
338 |
--------------------------------------------------------------------------------
/src/helpers.ts:
--------------------------------------------------------------------------------
1 | import chalk from "chalk";
2 | import fs from "fs";
3 | import path from "path";
4 | import prettier from "prettier";
5 | import { RouteObject } from "./generators";
6 | // import ora from "ora";
7 | import { execSync } from "child_process";
8 | import { ESLint } from "eslint";
9 |
10 | interface PackageOptions {
11 | sourcePackageJson: string;
12 | destinationDirectory: string;
13 | }
14 |
15 | export function installPackages({
16 | sourcePackageJson,
17 | destinationDirectory,
18 | }: PackageOptions) {
19 | const sourcePackageData = fs.readFileSync(sourcePackageJson, "utf8");
20 | const sourcePackage = JSON.parse(sourcePackageData);
21 |
22 | const dependencies = sourcePackage.dependencies || {};
23 | const devDependencies = sourcePackage.devDependencies || {};
24 |
25 | const installDeps = (deps: Record, type: string) => {
26 | const depArray = Object.keys(deps);
27 | if (depArray.length > 0) {
28 | const cmd = `npm install --quiet ${depArray.join(
29 | " "
30 | )} --prefix ${destinationDirectory} --${type}`;
31 | try {
32 | execSync(cmd);
33 | // console.log(`${type} installed successfully.`);
34 | } catch (error: any) {
35 | console.error(`Error installing ${type}: ${error.message}`);
36 | throw error;
37 | }
38 | }
39 | };
40 |
41 | installDeps(dependencies, "save");
42 | installDeps(devDependencies, "save-dev");
43 | }
44 |
45 | export function copyAndRenameFile(
46 | sourceFilePath: string,
47 | destinationDirectory: string,
48 | newFileName: string
49 | ) {
50 | const destinationFilePath = path.join(destinationDirectory, newFileName);
51 |
52 | try {
53 | // Check if the destination file exists
54 | if (fs.existsSync(destinationFilePath)) {
55 | // Delete the existing file
56 | fs.unlinkSync(destinationFilePath);
57 | }
58 |
59 | // Copy the source file to the destination
60 | fs.copyFileSync(sourceFilePath, destinationFilePath);
61 | } catch (error) {
62 | console.error(`An error occurred in copyAndRenameFile: ${error}`);
63 | }
64 | }
65 |
66 | export async function listFilesInDirectory(
67 | directoryPath: string
68 | ): Promise {
69 | const files: string[] = [];
70 |
71 | async function traverseDirectory(currentPath: string): Promise {
72 | const entries = await fs.promises.readdir(currentPath, {
73 | withFileTypes: true,
74 | });
75 |
76 | for (const entry of entries) {
77 | const fullPath = path.join(currentPath, entry.name);
78 |
79 | if (entry.isFile()) {
80 | files.push(fullPath);
81 | } else if (entry.isDirectory()) {
82 | await traverseDirectory(fullPath);
83 | }
84 | }
85 | }
86 |
87 | await traverseDirectory(directoryPath);
88 | return files;
89 | }
90 |
91 | export const copyDirectoryContents = async (
92 | sourceDirectory: string,
93 | destinationDirectory: string
94 | ) => {
95 | if (!fs.existsSync(sourceDirectory)) {
96 | throw new Error(`Source directory "${sourceDirectory}" does not exist.`);
97 | }
98 |
99 | // Create the destination directory if it doesn't exist
100 | if (!fs.existsSync(destinationDirectory)) {
101 | fs.mkdirSync(destinationDirectory, { recursive: true });
102 | }
103 |
104 | const files = await fs.promises.readdir(sourceDirectory);
105 |
106 | for (const file of files) {
107 | const sourcePath = path.join(sourceDirectory, file);
108 | const destinationPath = path.join(destinationDirectory, file);
109 |
110 | const stat = await fs.promises.stat(sourcePath);
111 |
112 | if (stat.isDirectory()) {
113 | // If it's a sub-directory, recursively copy its contents
114 | await copyDirectoryContents(sourcePath, destinationPath);
115 | } else {
116 | // If it's a file, copy it to the destination
117 | await fs.promises.copyFile(sourcePath, destinationPath);
118 | }
119 | }
120 | };
121 | const waitForEvent = (emitter: any, event: any) =>
122 | new Promise((resolve) => emitter.once(event, resolve));
123 |
124 | export async function copyPublicDirectory(
125 | sourceDir: string,
126 | destinationDir: string,
127 | toReplace = false,
128 | skipChildDir?: string
129 | ) {
130 | // console.log(
131 | // chalk.yellowBright(`Copying directory: ${sourceDir} to ${destinationDir}`)
132 | // );
133 |
134 | try {
135 | if (toReplace && fs.existsSync(destinationDir)) {
136 | fs.rmSync(destinationDir, { recursive: true });
137 | }
138 |
139 | if (!fs.existsSync(destinationDir)) {
140 | fs.mkdirSync(destinationDir);
141 | }
142 |
143 | const files = fs.readdirSync(sourceDir, { withFileTypes: true });
144 | // for (let index = 0; index < array.length; index++) {
145 | // const element = array[index];
146 |
147 | // }
148 |
149 | for (let index = 0; index < files.length; index++) {
150 | const entry = files[index];
151 |
152 | const file = entry.name;
153 |
154 | if (file === skipChildDir) {
155 | return;
156 | }
157 |
158 | const sourceFile = path.join(sourceDir, file);
159 | const destinationFile = path.join(destinationDir, file);
160 |
161 | if (entry.isDirectory()) {
162 | copyDirectory(
163 | sourceFile,
164 | destinationFile,
165 | toReplace,
166 | skipChildDir !== undefined ? [skipChildDir] : undefined
167 | );
168 | } else {
169 | if (!fs.existsSync(destinationFile)) {
170 | // fse.copyFileSync(sourceFile, destinationFile);
171 | const srcStream = fs.createReadStream(sourceFile);
172 | await waitForEvent(srcStream, "ready");
173 | const destStream = fs.createWriteStream(destinationFile);
174 | await waitForEvent(destStream, "ready");
175 | const handleError = (err: any) => {
176 | throw new Error(err);
177 | };
178 | srcStream.on("error", handleError);
179 | destStream.on("error", handleError);
180 |
181 | srcStream.pipe(destStream);
182 | await waitForEvent(srcStream, "end");
183 | }
184 | }
185 | }
186 | } catch (error) {
187 | console.error(
188 | chalk.red("An error occurred in copyPublicDirectory:", error)
189 | );
190 | }
191 | }
192 |
193 | export async function copyImage(
194 | sourceDir: string,
195 | sourceFile: string,
196 | destinationDir: string
197 | ) {
198 | try {
199 | if (!fs.existsSync(destinationDir)) {
200 | fs.mkdirSync(destinationDir);
201 | }
202 | const destinationFile = path.join(destinationDir, sourceFile);
203 |
204 | if (!fs.existsSync(destinationFile)) {
205 | // fse.copyFileSync(sourceFile, destinationFile);
206 | const srcStream = fs.createReadStream(path.join(sourceDir, sourceFile));
207 | await waitForEvent(srcStream, "ready");
208 | const destStream = fs.createWriteStream(destinationFile);
209 | await waitForEvent(destStream, "ready");
210 | const handleError = (err: any) => {
211 | throw new Error(err);
212 | };
213 | srcStream.on("error", handleError);
214 | destStream.on("error", handleError);
215 |
216 | srcStream.pipe(destStream);
217 | await waitForEvent(srcStream, "end");
218 | }
219 | } catch (error) {
220 | console.error(chalk.red("An error occurred in copyImage:", error));
221 | }
222 | }
223 |
224 | function addStringsBetweenComments(
225 | fileContent: string,
226 | insertData: Array<{
227 | insertString: string;
228 | startComment: string;
229 | endComment: string;
230 | }>
231 | ): string {
232 | insertData.forEach(({ insertString, startComment, endComment }) => {
233 | while (
234 | fileContent.includes(startComment) &&
235 | fileContent.includes(endComment)
236 | ) {
237 | const startIndex = fileContent.indexOf(startComment);
238 | const endIndex = fileContent.indexOf(endComment) + endComment.length;
239 | const contentToRemove = fileContent.slice(startIndex, endIndex);
240 | fileContent = fileContent.replace(contentToRemove, insertString);
241 | }
242 | });
243 |
244 | return fileContent;
245 | }
246 |
247 | export async function modifyFile(
248 | sourceFilePath: string,
249 | destinationFilePath: string,
250 | insertData: Array<{
251 | insertString: string;
252 | startComment: string;
253 | endComment: string;
254 | }>,
255 | modelName?: string
256 | ) {
257 | try {
258 | const fileContent = fs.readFileSync(sourceFilePath, "utf8");
259 |
260 | // Perform string replacements
261 | let modifiedContent = addStringsBetweenComments(fileContent, insertData);
262 | modifiedContent = await prettier.format(modifiedContent, {
263 | parser: "babel-ts", // Specify the parser according to your project's configuration
264 | });
265 | if (modelName) {
266 | modifiedContent = findAndReplaceInFile(
267 | modifiedContent,
268 | "nexquikTemplateModel",
269 | modelName
270 | );
271 | }
272 | const eslint = new ESLint({ fix: true });
273 |
274 | // let lintedText = "";
275 | // // // const files = a wait getFilePaths(directoryPath);
276 | // const results = await eslint.lintText(modifiedContent);
277 | // if (
278 | // results.length > 0
279 | // // results[0].output?.includes("CreateParticipant(")
280 | // ) {
281 | // // console.log({ results: results[0].output });
282 | // // console.log({ modifiedContent });
283 | // lintedText = results[0].output || "";
284 | // }
285 | // // const formatter = await eslint.loadFormatter("stylish");
286 | // const resultText = await formatter.format(results);
287 | // Write the modified content to the destination file
288 | // await fs.promises.writeFile(
289 | // destinationFilePath,
290 | // lintedText || modifiedContent
291 | // );
292 | await fs.promises.writeFile(destinationFilePath, modifiedContent);
293 |
294 | return;
295 | } catch (error) {
296 | console.error("An error occurred in modifyFile:", error);
297 | }
298 | }
299 |
300 | export function createNestedDirectory(directory: string) {
301 | const baseParts = directory.split(path.sep).filter((item) => item !== "");
302 |
303 | let currentBasePath = "";
304 | for (const part of baseParts) {
305 | currentBasePath = path.join(currentBasePath, part);
306 | if (!fs.existsSync(currentBasePath)) {
307 | fs.mkdirSync(currentBasePath);
308 | }
309 | }
310 | return;
311 | }
312 | // copy files from one directory to another
313 | export function copyDirectory(
314 | sourceDir: string,
315 | destinationDir: string,
316 | toReplace = true,
317 | skipChildDirs: string[] = [] // Change the parameter name and set it as an array of strings
318 | ): void {
319 | try {
320 | if (!fs.existsSync(destinationDir)) {
321 | createNestedDirectory(destinationDir);
322 | }
323 |
324 | const files = fs.readdirSync(sourceDir, { withFileTypes: true });
325 | files.forEach((entry) => {
326 | const file = entry.name;
327 |
328 | if (skipChildDirs.includes(file)) {
329 | // Check if the file is in the skipChildDirs array
330 | return;
331 | }
332 |
333 | const sourceFile = path.join(sourceDir, file);
334 | const destinationFile = path.join(destinationDir, file);
335 |
336 | if (entry.isDirectory()) {
337 | copyDirectory(sourceFile, destinationFile, toReplace, skipChildDirs);
338 | } else {
339 | fs.copyFileSync(sourceFile, destinationFile);
340 | }
341 | });
342 | } catch (error) {
343 | console.error(chalk.red("An error occurred in copyDirectory:", error));
344 | }
345 | }
346 |
347 | export const formatNextJsFilesRecursively = async (directory: string) => {
348 | // Get a list of all files and directories in the current directory
349 | const entries = await fs.promises.readdir(directory);
350 |
351 | for (const entry of entries) {
352 | const entryPath = path.join(directory, entry);
353 |
354 | // Check if the entry is a file
355 | const isFile = (await fs.promises.stat(entryPath)).isFile();
356 |
357 | if (isFile) {
358 | // Filter the file to include only Next.js files (e.g., .js, .jsx, .ts, .tsx)
359 | if (/\.(jsx?|tsx?)$/.test(path.extname(entry))) {
360 | const fileContents = await fs.promises.readFile(entryPath, "utf8");
361 |
362 | // Format the file contents using Prettier
363 | const formattedContents = await prettier.format(fileContents, {
364 | parser: "babel-ts", // Specify the parser according to your project's configuration
365 | });
366 |
367 | // Write the formatted contents back to the file
368 | await fs.promises.writeFile(entryPath, formattedContents);
369 | }
370 | } else {
371 | // If the entry is a directory, recursively call the function for that directory
372 | await formatNextJsFilesRecursively(entryPath);
373 | }
374 | }
375 | };
376 | export const deleteDirectoryRecursively = async (directoryPath: string) => {
377 | if (!fs.existsSync(directoryPath)) {
378 | throw new Error(`Directory "${directoryPath}" does not exist.`);
379 | }
380 |
381 | const files = await fs.promises.readdir(directoryPath);
382 |
383 | for (const file of files) {
384 | const filePath = path.join(directoryPath, file);
385 | const stat = await fs.promises.stat(filePath);
386 |
387 | if (stat.isDirectory()) {
388 | // If it's a sub-directory, recursively delete it
389 | await deleteDirectoryRecursively(filePath);
390 | } else {
391 | // If it's a file, delete it
392 | await fs.promises.unlink(filePath);
393 | }
394 | }
395 |
396 | // Delete the empty directory
397 | await fs.promises.rmdir(directoryPath);
398 | };
399 | async function getFilePaths(directoryPath: string): Promise {
400 | const files: string[] = [];
401 |
402 | const processDirectory = async (dirPath: string) => {
403 | const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
404 |
405 | for (const entry of entries) {
406 | const fullPath = path.join(dirPath, entry.name);
407 |
408 | if (entry.isDirectory()) {
409 | await processDirectory(fullPath); // Recursively process subdirectories
410 | } else if (entry.isFile()) {
411 | files.push(fullPath); // Add file path to the array
412 | }
413 | }
414 | };
415 |
416 | await processDirectory(directoryPath);
417 |
418 | return files;
419 | }
420 |
421 | export async function formatDirectory(directoryPath: string): Promise {
422 | // const list = fs.readdirSync(directoryPath);
423 | const eslint = new ESLint({ fix: true });
424 |
425 | // const files = await getFilePaths(directoryPath);
426 |
427 | const results = await eslint.lintFiles([`${directoryPath}/**/*.tsx`]);
428 | await ESLint.outputFixes(results);
429 |
430 | // Format the files using Prettier
431 | // const prettierConfig = await prettier.resolveConfig(directoryPath);
432 | // await Promise.all(
433 | // results.map(async (result) => {
434 | // const filePath = result.filePath;
435 | // const fileContent = await fs.promises.readFile(filePath, "utf-8");
436 | // // const formattedContent = prettier.format(fileContent, {
437 | // // ...prettierConfig,
438 | // // filepath: filePath,
439 | // // });
440 | // await fs.promises.writeFile(filePath, fileContent, "utf-8");
441 | // })
442 | // );
443 | }
444 |
445 | export function findAndReplaceInFile(
446 | textContent: string,
447 | searchString: string,
448 | replacementString: string
449 | ): string {
450 | const pattern = new RegExp(searchString, "gi");
451 | return textContent.replace(pattern, (match) => {
452 | // Preserve the casing of the first character
453 | const firstChar = match.charAt(0);
454 | const replacementFirstChar = replacementString.charAt(0);
455 | const replacedFirstChar =
456 | firstChar === firstChar.toLowerCase()
457 | ? replacementFirstChar.toLowerCase()
458 | : firstChar === firstChar.toUpperCase()
459 | ? replacementFirstChar.toUpperCase()
460 | : replacementFirstChar;
461 | return replacedFirstChar + replacementString.slice(1);
462 | });
463 | }
464 |
465 | export function copyFileToDirectory(
466 | sourceFilePath: string,
467 | targetDirectoryPath: string
468 | ): void {
469 | const fileName = path.basename(sourceFilePath);
470 | const targetFilePath = path.join(targetDirectoryPath, fileName);
471 |
472 | fs.copyFileSync(sourceFilePath, targetFilePath);
473 | }
474 |
475 | export function popStringEnd(str: string, char: string): string {
476 | const lastIndex = str.lastIndexOf(char);
477 |
478 | if (lastIndex === -1) {
479 | // Character not found in the string
480 | return str;
481 | }
482 | return str.substring(0, lastIndex);
483 | }
484 |
485 | export function prettyPrintAPIRoutes(routes: RouteObject[]) {
486 | console.log("API Routes:");
487 | console.log("-----------");
488 | for (const route of routes) {
489 | console.log(
490 | `${route.segment} - ${route.operation} ${route.model}: ${route.description}`
491 | );
492 | }
493 | }
494 | export const getDynamicSlugs = (
495 | modelName: string | undefined,
496 | uniqueIdFieldNames: string[]
497 | ): string[] => {
498 | const slugs: string[] = [];
499 | uniqueIdFieldNames.forEach((idField) => {
500 | slugs.push(`${modelName}${idField}`);
501 | });
502 | return slugs;
503 | };
504 |
505 | export function convertRouteToRedirectUrl(input: string): string {
506 | const regex = /\[(.*?)\]/g;
507 | const replaced = input.replace(regex, (_, innerValue) => {
508 | return `\${params.${innerValue}}`;
509 | });
510 |
511 | return `${replaced}`;
512 | }
513 | export function convertRoutesToRedirectUrl(input: string): string {
514 | const regex = /\[(.*?)\]/g;
515 | const replaced = input.replace(regex, (_, innerValue) => {
516 | return `\${params.${innerValue}}`;
517 | });
518 |
519 | return `${replaced}`;
520 | }
521 |
--------------------------------------------------------------------------------
/src/generators.ts:
--------------------------------------------------------------------------------
1 | import { DMMF } from "@prisma/generator-helper";
2 | import { getDMMF } from "@prisma/internals";
3 | import chalk from "chalk";
4 | import fs from "fs";
5 | import * as fse from "fs-extra";
6 |
7 | import path from "path";
8 |
9 | import { promisify } from "util";
10 | import {
11 | convertRouteToRedirectUrl,
12 | copyAndRenameFile,
13 | copyDirectory,
14 | copyImage,
15 | copyPublicDirectory,
16 | createNestedDirectory,
17 | getDynamicSlugs,
18 | installPackages,
19 | modifyFile,
20 | } from "./helpers";
21 | import {
22 | ModelTree,
23 | createModelTree,
24 | getParentReferenceField,
25 | } from "./modelTree";
26 | import { Group } from "./cli";
27 |
28 | const blueButtonClass =
29 | "px-2 py-1 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-sky-700 dark:border-gray-600 dark:text-white dark:hover:text-white dark:hover:bg-sky-600 dark:focus:ring-blue-500 dark:focus:text-white";
30 | const grayButtonClass =
31 | "px-2 py-1 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-blue-500 dark:focus:text-white";
32 |
33 | const blueTextClass =
34 | "dark:text-sky-400 hover:text-sky-500 dark:hover:text-sky-600";
35 |
36 | const labelClass = "block text-slate-500 dark:text-slate-400 text-sm";
37 | const inputClass =
38 | "accent-sky-700 block text-sm leading-6 rounded-md ring-1 ring-slate-900/10 shadow-sm py-1.5 pl-2 pr-3 hover:ring-slate-300 dark:bg-slate-800 dark:highlight-white/5 dark:hover:bg-slate-700";
39 | const disabledInputClass =
40 | "block leading-6 text-slate-400 rounded-md ring-1 ring-slate-900/10 shadow-sm py-1.5 pl-2 pr-3 dark:bg-slate-800 ";
41 | const darkTextClass = "text-slate-700 dark:text-slate-400";
42 | const lightTextClass = "text-slate-900 dark:text-slate-200";
43 | const readFileAsync = promisify(fs.readFile);
44 |
45 | interface RouteSegment {
46 | segment: string;
47 | fullRoute: string;
48 | }
49 |
50 | function splitTheRoute(route: string): RouteSegment[] {
51 | const segments = route.split("/").filter((r) => r != "");
52 | let currentRoute = "";
53 |
54 | const returnSegs = segments.flatMap((segment) => {
55 | let newSegment = segment;
56 | if (segment) {
57 | if (segment.includes("[")) {
58 | currentRoute += "/$" + segment.replace(/\[(.*?)\]/g, "{params.$1}");
59 | newSegment = segment.replace(/\[(.*?)\]/g, "{params.$1}");
60 | } else {
61 | currentRoute += `/${segment}`;
62 | }
63 | return { segment: newSegment, fullRoute: currentRoute };
64 | }
65 | return [];
66 | });
67 | return returnSegs;
68 | }
69 | function generateBreadCrumb(route: string) {
70 | let routeCrumbs = "";
71 | splitTheRoute(route).forEach(({ segment, fullRoute }, index) => {
72 | routeCrumbs += `
73 |
74 |
75 |
76 | ${
77 | index === 0
78 | ? `
79 |
80 | `
81 | : `
82 |
83 | `
84 | }
85 |
86 |
${segment}
87 |
88 | `;
89 | });
90 | const breadCrumb = `
91 |
92 |
93 |
94 | ${routeCrumbs}
95 |
96 |
97 |
98 |
99 | `;
100 | return breadCrumb;
101 | }
102 |
103 | export const prismaFieldToInputType: Record = {
104 | Int: "number",
105 | Float: "number",
106 | String: "text",
107 | Boolean: "checkbox",
108 | DateTime: "datetime-local",
109 | Json: "text",
110 | };
111 |
112 | export interface RouteObject {
113 | segment: string;
114 | model: string;
115 | operation: string;
116 | description: string;
117 | }
118 |
119 | async function generateCreateForm(
120 | modelTree: ModelTree,
121 | routeUrl: string,
122 | enums: Record
123 | ): Promise {
124 | const formFields = generateFormFields(modelTree, enums);
125 | const reactComponentTemplate = `
126 |
136 | `;
137 |
138 | return reactComponentTemplate;
139 | }
140 |
141 | async function generateRedirect(redirectUrl: string): Promise {
142 | const reactComponentTemplate = `
143 | redirect(${redirectUrl});
144 | `;
145 | return reactComponentTemplate;
146 | }
147 |
148 | async function generateLink(
149 | linkUrl: string,
150 | linkText: string
151 | ): Promise {
152 | const linkString = linkUrl ? `\`${linkUrl}\`` : "'/'";
153 | const reactComponentTemplate = `
154 |
155 |
156 |
157 |
158 |
159 |
160 | ${linkText}
161 | `;
162 | return reactComponentTemplate;
163 | }
164 |
165 | async function generateRevalidatePath(
166 | revalidationUrl: string
167 | ): Promise {
168 | const reactComponentTemplate = `
169 | revalidatePath(\`${revalidationUrl}\`);
170 | `;
171 | return reactComponentTemplate;
172 | }
173 |
174 | async function generateEditForm(
175 | modelTree: ModelTree,
176 | routeUrl: string,
177 | enums: Record
178 | ): Promise {
179 | const formFields = generateFormFieldsWithDefaults(
180 | modelTree.model.fields,
181 | enums
182 | );
183 | // Define the React component template as a string
184 | const reactComponentTemplate = `
185 |
194 | `;
195 |
196 | return reactComponentTemplate;
197 | }
198 | ``;
199 | function getEnums(datamodel: DMMF.Datamodel): Record {
200 | const enums: Record = {};
201 |
202 | for (const enumDef of datamodel.enums) {
203 | const enumName = enumDef.name;
204 | const enumValues = enumDef.values.map((value) => value.name);
205 | enums[enumName] = enumValues;
206 | }
207 |
208 | return enums;
209 | }
210 | async function generateChildrenList(
211 | modelTree: ModelTree,
212 | routeUrl: string
213 | ): Promise {
214 | // Define the React component template as a string
215 | const slug = getDynamicSlugs(
216 | modelTree.model.name,
217 | modelTree.uniqueIdentifierField.map((f) => f.name)
218 | );
219 | const childrenLinks: string[] = [];
220 |
221 | const childList = `
222 |
223 |
224 |
225 |
226 | Children
227 |
228 |
229 |
230 |
231 |
232 | `;
233 | modelTree.children.forEach((c) => {
234 | let childLink = `
235 |
236 |
237 |
239 | {
244 | childLink += `\${params.${s}}/`;
245 | });
246 |
247 | childLink += `${
248 | c.modelName.charAt(0).toLowerCase() + c.modelName.slice(1)
249 | }\`} >
250 | ${c.modelName}
251 |
252 |
253 | `;
254 | childrenLinks.push(childLink);
255 | });
256 | return childList + childrenLinks.join("\n") + "
";
257 | }
258 | async function generateListForm(
259 | modelTree: ModelTree,
260 | routeUrl: string,
261 | uniqueFields: {
262 | name: string;
263 | type: string;
264 | }[],
265 | idFields: string[]
266 | ): Promise {
267 | let linkHref = routeUrl;
268 | uniqueFields.forEach((f) => {
269 | linkHref += "/" + "${" + "nexquikTemplateModel." + f.name + "}";
270 | });
271 |
272 | let uniqueFormInputs = "";
273 | uniqueFields.forEach((u) => {
274 | uniqueFormInputs += ` `;
279 | });
280 |
281 | // Define the React component template as a string
282 | const reactComponentTemplate = `
283 |
284 |
285 |
286 |
287 | ${idFields
288 | .map((field) => {
289 | return ` ${field}
`;
290 | })
291 | .join("\n")}
292 | Actions
293 |
294 |
295 |
296 |
297 | {nexquikTemplateModel?.map((nexquikTemplateModel, index) => (
298 |
299 |
300 | ${idFields
301 | .map((field) => {
302 | return ` {\`\${nexquikTemplateModel.${field}}\`} `;
304 | })
305 | .join("\n")}
306 |
307 |
308 |
346 |
347 |
348 |
349 |
350 | ))}
351 |
352 |
353 | `;
354 |
355 | return reactComponentTemplate;
356 | }
357 |
358 | export async function generate(
359 | prismaSchemaPath: string,
360 | outputDirectory: string,
361 | maxAllowedDepth: number,
362 | init: boolean,
363 | rootName: string,
364 | groups: Group[],
365 | extendOnly: boolean,
366 | deps: boolean,
367 | prismaImportString: string,
368 | appTitle: string,
369 | modelsOnly: boolean
370 | ) {
371 | // Read the Prisma schema file
372 | const prismaSchema = await readFileAsync(prismaSchemaPath, "utf-8");
373 |
374 | // Main section to build the app from the modelTree
375 | const dmmf = await getDMMF({ datamodel: prismaSchema });
376 |
377 | // i.e. devNexquikApp/app
378 | const outputAppDirectory = path.join(outputDirectory, "app");
379 | createNestedDirectory(outputAppDirectory);
380 |
381 | // i.e. devNexquikApp/app/gen
382 | const outputRouteGroup = path.join(outputAppDirectory, rootName);
383 |
384 | // Remove the directory if it exists
385 | // Check if the directory exists
386 | if (extendOnly === false) {
387 | if (fse.existsSync(outputRouteGroup)) {
388 | // Remove the directory and its contents
389 | fse.removeSync(outputRouteGroup);
390 | }
391 | } else {
392 | console.log(`Extending directory '${outputRouteGroup}'.`);
393 | }
394 | createNestedDirectory(path.join(outputAppDirectory, rootName));
395 |
396 | const enums = getEnums(dmmf.datamodel);
397 |
398 | const imagesDirectoryName = "images";
399 | // Create files in main directory if we are initializing a new app
400 | if (init === true) {
401 | copyDirectory(path.join(__dirname, "templateRoot"), outputDirectory, true, [
402 | "app",
403 | "public",
404 | ]);
405 |
406 | createNestedDirectory(path.join(outputDirectory, "prisma"));
407 |
408 | // Copy over the user's prisma schema and rename it to schema.prisma
409 | copyAndRenameFile(
410 | prismaSchemaPath,
411 | path.join(outputDirectory, "prisma"),
412 | "schema.prisma"
413 | );
414 |
415 | // Copy over tsconfig
416 | fs.copyFile(
417 | path.join(__dirname, "templateRoot", "tsconfig.json"),
418 | path.join(outputDirectory, "tsconfig.json"),
419 | (err) => {
420 | if (err) {
421 | console.error("An error occurred while copying the file:", err);
422 | }
423 | }
424 | );
425 | createNestedDirectory(path.join(outputDirectory, "app"));
426 |
427 | // Copy over root app layout
428 | fs.copyFile(
429 | path.join(__dirname, "templateRoot", "app", "layout.tsx"),
430 | path.join(outputDirectory, "app", "layout.tsx"),
431 | (err) => {
432 | if (err) {
433 | console.error("An error occurred while copying the file:", err);
434 | }
435 | }
436 | );
437 |
438 | fs.copyFile(
439 | path.join(__dirname, "templateRoot", "app", "page.tsx"),
440 | path.join(outputDirectory, "app", "page.tsx"),
441 | (err) => {
442 | if (err) {
443 | console.error("An error occurred while copying the file:", err);
444 | }
445 | }
446 | );
447 | await modifyFile(
448 | path.join(__dirname, "templateRoot", "app", "page.tsx"),
449 | path.join(outputDirectory, "app", "page.tsx"),
450 | [
451 | {
452 | startComment: "// @nexquik homeRedirect start",
453 | endComment: "// @nexquik homeRedirect stop",
454 | insertString: `redirect("/${rootName}")`,
455 | },
456 | ]
457 | );
458 | } else if (deps === true) {
459 | // Install deps in output directory
460 | try {
461 | console.log(
462 | `${chalk.blue.bold("Installing dependencies...")} ${chalk.gray(
463 | "(This may take a moment, and only needs to be run once in your peoject."
464 | )}`
465 | );
466 | installPackages({
467 | sourcePackageJson: path.join(__dirname, "templateRoot", "package.json"),
468 | destinationDirectory: outputDirectory,
469 | });
470 | // console.log("Dependencies installed successfully.");
471 | } catch (error) {
472 | console.error("Error installing dependencies:", error);
473 | }
474 | // Copy over tailwind.config.js and postcss.config
475 | fs.copyFile(
476 | path.join(__dirname, "templateRoot", "tailwind.config.js"),
477 | path.join(outputDirectory, "tailwind.config.js"),
478 | (err) => {
479 | if (err) {
480 | console.error("An error occurred while copying the file:", err);
481 | }
482 | }
483 | );
484 |
485 | fs.copyFile(
486 | path.join(__dirname, "templateRoot", "postcss.config.js"),
487 | path.join(outputDirectory, "postcss.config.js"),
488 | (err) => {
489 | if (err) {
490 | console.error("An error occurred while copying the file:", err);
491 | }
492 | }
493 | );
494 | }
495 |
496 | if (groups.length > 0) {
497 | console.log(
498 | `${chalk.blue.bold(
499 | "Generating directories for your models..."
500 | )} ${chalk.gray("(For deeply-nested schemas, this may take a moment)")}`
501 | );
502 | // Create grouped route directories
503 | groups.forEach(async ({ name, include, exclude }) => {
504 | // Create model tree and verify there is at least one valid model
505 | const modelTree = createModelTree(dmmf.datamodel, exclude, include);
506 | if (modelTree.length === 0) {
507 | console.log(chalk.red(`No valid models detected for group: ${name}`));
508 | throw new Error(`No valid models detected for group: ${name}`);
509 | }
510 | const thisGroupPath = `${rootName && rootName + "/"}${name}`;
511 | const thisOutputRouteGroupPath = path.join(
512 | outputAppDirectory,
513 | rootName,
514 | name
515 | );
516 | // Create base directory for this model under the app dir
517 | if (fse.existsSync(path.join(outputAppDirectory, rootName, name))) {
518 | // Remove the directory and its contents
519 | fse.removeSync(path.join(outputAppDirectory, rootName, name));
520 | }
521 | await generateAppDirectoryFromModelTree(
522 | modelTree,
523 | outputAppDirectory,
524 | enums,
525 | maxAllowedDepth,
526 | thisGroupPath,
527 | prismaImportString
528 | );
529 |
530 | // Nested Group Home route list
531 | const modelNames = modelTree.map((m) => m.model.name);
532 | const routeList = generateRouteList(modelNames, rootName, name);
533 |
534 | if (!modelsOnly) {
535 | createNestedDirectory(
536 | path.join(outputDirectory, "app", rootName, imagesDirectoryName)
537 | );
538 |
539 | copyDirectory(
540 | path.join(__dirname, "templateRoot", "public"),
541 | path.join(outputDirectory, "app", rootName, imagesDirectoryName),
542 | true
543 | );
544 | await modifyFile(
545 | path.join(__dirname, "templateRoot", "app", "groupRouteHome.tsx"),
546 | path.join(path.join(thisOutputRouteGroupPath, "page.tsx")),
547 | [
548 | {
549 | startComment: "{/* @nexquik routeList start */}",
550 | endComment: "{/* @nexquik routeList stop */}",
551 | insertString: routeList,
552 | },
553 | {
554 | startComment: "{/* @nexquik routeGroupName start */}",
555 | endComment: "{/* @nexquik routeGroupName start */}",
556 | insertString: name,
557 | },
558 | ]
559 | );
560 | // globals.css
561 | fs.copyFile(
562 | path.join(__dirname, "templateRoot", "app", "globals.css"),
563 | path.join(outputRouteGroup, "globals.css"),
564 | (err) => {
565 | if (err) {
566 | console.error("An error occurred while copying the file:", err);
567 | }
568 | }
569 | );
570 | }
571 | });
572 |
573 | // END GROUP LOOP
574 |
575 | const entries = fs.readdirSync(outputRouteGroup, { withFileTypes: true });
576 |
577 | // Filter only directory entries
578 | const directories = entries
579 | .filter(
580 | (entry) => entry.isDirectory() && entry.name !== imagesDirectoryName
581 | )
582 | .map((entry) => entry.name);
583 |
584 | // console.log("Directories in", directoryPath, ":", directories);
585 |
586 | // Route sidebar
587 | let routeSidebar = "";
588 | for (const dir of directories) {
589 | routeSidebar += `
590 |
591 |
595 | ${dir}
596 |
597 |
598 |
599 | `;
600 | if (!modelsOnly) {
601 | // layout.tsx
602 | await modifyFile(
603 | path.join(
604 | path.join(
605 | __dirname,
606 | "templateRoot",
607 | "app",
608 | "rootGroupRouteLayout.tsx"
609 | )
610 | ),
611 | path.join(path.join(outputRouteGroup, "layout.tsx")),
612 | [
613 | {
614 | startComment: "{/* @nexquik appTitle start */}",
615 | endComment: "{/* @nexquik appTitle stop */}",
616 | insertString: appTitle,
617 | },
618 | {
619 | startComment: "{/* //@nexquik routeSidebar start */}",
620 | endComment: "{/* //@nexquik routeSidebar stop */}",
621 | insertString: routeSidebar,
622 | },
623 | ]
624 | );
625 |
626 | const routeGroupList = generateRouteGroupList(rootName, directories);
627 |
628 | await modifyFile(
629 | path.join(__dirname, "templateRoot", "app", "rootGroupRouteHome.tsx"),
630 | path.join(outputRouteGroup, "page.tsx"),
631 | [
632 | {
633 | startComment: "{/* @nexquik routeGroupList start */}",
634 | endComment: "{/* @nexquik routeGroupList stop */}",
635 | insertString: routeGroupList,
636 | },
637 | ]
638 | );
639 | }
640 | }
641 | } else {
642 | console.log(`${chalk.blue.bold("No groups specified.")}`);
643 | }
644 | return;
645 | }
646 |
647 | export async function generateShowForm(
648 | modelTree: ModelTree,
649 | routeUrl: string,
650 | childModelLinkList: string
651 | ): Promise {
652 | const uniqueFields = modelTree.uniqueIdentifierField;
653 |
654 | let linkRoute = routeUrl;
655 | uniqueFields.forEach((f) => {
656 | linkRoute += `\${nexquikTemplateModel?.${f?.name}}/`;
657 | });
658 | const reactComponentTemplate = `
659 |
660 |
754 | ${childModelLinkList}
755 |
756 | `;
757 |
758 | return reactComponentTemplate;
759 | }
760 |
761 | function generateRouteGroupList(rootName: string, dirs: string[]) {
762 | const routeLinks = [];
763 | for (const name of dirs) {
764 | // const lowerCase = model.charAt(0).toLowerCase() + model.slice(1);
765 | routeLinks.push(`
766 |
767 |
771 |
772 |
773 | ${name}
775 |
776 |
777 |
778 |
779 | `);
780 | }
781 |
782 | return `
783 |
784 |
785 |
786 |
787 |
788 |
789 | Group
790 |
791 |
792 |
793 |
794 |
795 |
796 | ${routeLinks.join("\n")}
797 |
798 |
799 |
800 | `;
801 | }
802 |
803 | function generateRouteList(
804 | modelNames: string[],
805 | routeGroup: string,
806 | groupName: string
807 | ) {
808 | const routeLinks = [];
809 | for (const model of modelNames) {
810 | const lowerCase = model.charAt(0).toLowerCase() + model.slice(1);
811 | routeLinks.push(`
812 |
816 | ${model}
817 |
818 |
819 |
823 |
824 |
825 | Create
826 |
827 |
828 | {' '} / {' '}
829 |
830 | List
831 |
832 |
833 |
834 |
835 | `);
836 | }
837 |
838 | return `
839 |
840 |
841 |
842 |
843 |
844 |
845 | Model
846 |
847 |
848 |
849 |
850 | Operations
851 |
852 |
853 |
854 |
855 |
856 |
857 |
858 | ${routeLinks.join("\n")}
859 |
860 |
861 |
862 | `;
863 | }
864 |
865 | export async function generateAppDirectoryFromModelTree(
866 | modelTreeArray: ModelTree[],
867 | outputDirectory: string,
868 | enums: Record,
869 | maxAllowedDepth: number,
870 | rootName: string,
871 | prismaImportString: string
872 | ): Promise {
873 | const routes: RouteObject[] = [];
874 | let fileCount = 0;
875 | let directoryCount = 0;
876 | let maxDepth = 0;
877 | async function generateRoutes(
878 | modelTree: ModelTree,
879 | parentRoute: {
880 | name: string;
881 | uniqueIdentifierField: { name: string; type: string }[];
882 | },
883 | depth = 0,
884 | maxAllowedDepth: number,
885 | firstPass = false
886 | ) {
887 | if (depth > maxAllowedDepth) {
888 | maxDepth = maxAllowedDepth;
889 | process.stdout.write(
890 | chalk.yellow(`\u001b[2K\rMax depth hit for ${modelTree.modelName}`)
891 | );
892 | return;
893 | }
894 | if (depth > maxDepth) {
895 | maxDepth = depth;
896 | }
897 | process.stdout.write(`\u001b[2K\r${modelTree.model.name} - Depth ${depth}`);
898 | process.stdout.write(`\u001b[2K\r`);
899 |
900 | let childLoopPromises: Promise[] = [];
901 | directoryCount += 3;
902 | fileCount += 4;
903 | // Get the current model name
904 | const modelName = modelTree.modelName;
905 |
906 | // Get the current mode'ls array of prisma unique id fields
907 | const modelUniqueIdentifierField = modelTree.uniqueIdentifierField;
908 |
909 | let route = parentRoute.name;
910 |
911 | route += modelName.charAt(0).toLowerCase() + modelName.slice(1) + "/";
912 |
913 | routes.push({
914 | segment: `${route}create`,
915 | model: modelName,
916 | operation: "Create",
917 | description: `Create a ${modelName}`,
918 | });
919 |
920 | // List
921 | routes.push({
922 | segment: route,
923 | model: modelName,
924 | operation: "List",
925 | description: `Get a list of ${modelName}s`,
926 | });
927 | let splitRoute = "";
928 | const slugss = getDynamicSlugs(
929 | modelName,
930 | modelTree.uniqueIdentifierField.map((f) => f.name)
931 | );
932 | slugss.forEach((s) => {
933 | splitRoute += `[${s}]`;
934 | });
935 |
936 | routes.push({
937 | segment: `${route}${splitRoute}/edit`,
938 | model: modelName,
939 | operation: "Edit",
940 | description: `Edit a ${modelName} by ID`,
941 | });
942 |
943 | // Show
944 | routes.push({
945 | segment: `${route}${splitRoute}`,
946 | model: modelName,
947 | operation: "Show",
948 | description: `Get details of a ${modelName} by ID`,
949 | });
950 |
951 | const baseRoute = route;
952 | const createRedirectForm = convertRouteToRedirectUrl(baseRoute);
953 |
954 | const baseModelDirectory = path.join(outputDirectory, route);
955 | createNestedDirectory(baseModelDirectory);
956 |
957 | // Create create directory
958 | createNestedDirectory(path.join(baseModelDirectory, "create"));
959 |
960 | const templateModelDirectory = path.join(
961 | __dirname,
962 | "templateRoot",
963 | "app",
964 | "nexquikTemplateModel"
965 | );
966 | const createBreadCrumb = generateBreadCrumb(route + "/create");
967 |
968 | const listBreadCrumb = generateBreadCrumb(route);
969 |
970 | // Create dynamic directories
971 | const slugsForThisModel = getDynamicSlugs(
972 | modelTree.modelName,
973 | modelUniqueIdentifierField.map((f) => f.name)
974 | );
975 | slugsForThisModel.forEach((parentSlug) => {
976 | route += `[${parentSlug}]/`;
977 | });
978 |
979 | // Create dynamic and edit directory
980 | const dynamicOutputDirectory = path.join(outputDirectory, route);
981 | createNestedDirectory(dynamicOutputDirectory);
982 | createNestedDirectory(path.join(dynamicOutputDirectory, "edit"));
983 |
984 | const templateDynamicDirectory = path.join(
985 | __dirname,
986 | "templateRoot",
987 | "app",
988 | "nexquikTemplateModel",
989 | "[id]"
990 | );
991 |
992 | // ############### List Page
993 | const idFields = modelTree.uniqueIdentifierField;
994 | let select = "";
995 | if (idFields.length > 0) {
996 | select += "select:{";
997 | idFields.forEach(({ name }, index) => {
998 | if (index > 0) {
999 | select += ",";
1000 | }
1001 | select += `${name}: true`;
1002 | });
1003 | select += "}, ";
1004 | }
1005 | select += ` skip: (page - 1) * limit,
1006 | take: limit`;
1007 | const listFormCode = await generateListForm(
1008 | modelTree,
1009 | createRedirectForm,
1010 | modelUniqueIdentifierField,
1011 | idFields.map((f) => f.name)
1012 | );
1013 |
1014 | // Get relation fields to parent
1015 | let relationFieldToParent = "";
1016 | let fieldType = "";
1017 | if (modelTree.parent) {
1018 | // Get the field on the current model that is the id referencing the parent
1019 | modelTree.model.fields.forEach((mf) => {
1020 | if (mf.type === modelTree.parent?.name) {
1021 | if (
1022 | mf.relationFromFields?.length &&
1023 | mf.relationFromFields.length > 0
1024 | ) {
1025 | relationFieldToParent = mf.relationFromFields[0];
1026 | }
1027 | }
1028 | });
1029 |
1030 | // Get the field type on the current model that is the id referencing the parent
1031 | fieldType =
1032 | modelTree.model.fields.find((f) => f.name === relationFieldToParent)
1033 | ?.type || "";
1034 | }
1035 | const parentIdentifierFields = modelTree.model.fields.find((field) =>
1036 | field.relationFromFields?.includes(relationFieldToParent)
1037 | )?.relationToFields;
1038 | let parentIdentifierField = "";
1039 | if (parentIdentifierFields && parentIdentifierFields.length > 0) {
1040 | parentIdentifierField = parentIdentifierFields[0];
1041 | }
1042 | // Delete Where Clause
1043 | const deleteWhereClause = generateDeleteClause(modelUniqueIdentifierField);
1044 |
1045 | const listRedirect = await generateRedirect(`\`${createRedirectForm}\``);
1046 |
1047 | // In parent, loop through fields and find field of 'type' current model
1048 | let isManyToMany = false;
1049 | let referenceFieldNameToParent = "";
1050 | const parentIdField = modelTree.parent?.fields.find((f) => f.isId);
1051 | const relationNameToParent = modelTree.parent?.fields.find(
1052 | (a) => a.type === modelTree.modelName
1053 | )?.relationName;
1054 | if (relationNameToParent) {
1055 | const referenceFieldToParent = modelTree.model.fields.find(
1056 | (f) => f.relationName === relationNameToParent
1057 | );
1058 | if (referenceFieldToParent) {
1059 | referenceFieldNameToParent = referenceFieldToParent.name;
1060 | }
1061 | if (referenceFieldToParent?.isList) {
1062 | isManyToMany = true;
1063 | }
1064 | }
1065 | let manyToManyWhere = "";
1066 | let manyToManyConnect = "";
1067 | if (isManyToMany && parentIdField) {
1068 | let typecastValue = `params.${getDynamicSlugs(modelTree.parent?.name, [
1069 | parentIdField.name,
1070 | ])}`;
1071 | if (parentIdField?.type === "Int" || parentIdField?.type === "Float") {
1072 | typecastValue = `Number(${typecastValue})`;
1073 | } else if (parentIdField?.type === "Boolean") {
1074 | typecastValue = `Boolean(${typecastValue})`;
1075 | } else if (parentIdField?.type === "String") {
1076 | typecastValue = `String(${typecastValue})`;
1077 | }
1078 | manyToManyWhere = `
1079 | where: {
1080 | ${referenceFieldNameToParent}: {
1081 | some: {
1082 | ${parentIdField?.name}: {
1083 | equals: ${typecastValue}
1084 | }
1085 | }
1086 | }
1087 | }
1088 | `;
1089 |
1090 | manyToManyConnect = `
1091 | ${referenceFieldNameToParent}: {
1092 | connect: {
1093 | ${parentIdField?.name}: ${typecastValue},
1094 | },
1095 | },
1096 | `;
1097 | }
1098 |
1099 | // Unique field on parent, that points to this model
1100 | const parentReferenceField = getParentReferenceField(modelTree);
1101 | const relationsToParent = modelTree.model.fields.find(
1102 | (f) => f.name === parentReferenceField
1103 | )?.relationToFields;
1104 |
1105 | const parentReferenceFieldType = modelTree.parent?.fields.find(
1106 | (f) => relationsToParent && relationsToParent[0] === f.name
1107 | )?.type;
1108 |
1109 | const whereparentClause = modelTree.parent
1110 | ? generateWhereParentClause(
1111 | "params",
1112 | getDynamicSlugs(modelTree.parent.name, [parentIdentifierField])[0],
1113 | relationsToParent ? relationsToParent[0] : parentIdentifierField,
1114 | parentReferenceFieldType || fieldType,
1115 | getParentReferenceField(modelTree),
1116 | manyToManyWhere,
1117 | select
1118 | )
1119 | : `({${select}})`;
1120 | // Enum import for create and edit pages
1121 | const enumImport = Object.keys(enums)
1122 | .map((e) => `import { ${e} } from "@prisma/client";`)
1123 | .join("\n");
1124 |
1125 | const linkHref = createRedirectForm;
1126 |
1127 | const listPagination = `
1128 |
1129 |
1130 |
1131 |
1132 | Showing {(page - 1) * limit + 1} to {Math.min(page * limit, count)} of {count} Entries
1133 |
1134 |
1135 |
1141 | Prev
1142 |
1143 | = Math.ceil(count / limit) ? page : page + 1,
1147 | },
1148 | }} className="flex items-center justify-center px-3 h-8 text-sm font-medium text-white bg-gray-800 border-0 border-l border-gray-700 rounded-r hover:bg-gray-900 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
1149 | Next
1150 |
1151 |
1152 |
1153 |
1154 |
1155 |
1156 |
1157 |
1158 | `;
1159 |
1160 | const countWhereparentClause = modelTree.parent
1161 | ? generateWhereParentClause(
1162 | "params",
1163 | getDynamicSlugs(modelTree.parent.name, [parentIdentifierField])[0],
1164 | relationsToParent ? relationsToParent[0] : parentIdentifierField,
1165 | parentReferenceFieldType || fieldType,
1166 | getParentReferenceField(modelTree),
1167 | manyToManyWhere,
1168 | ""
1169 | )
1170 | : ``;
1171 | const props = modelTree.parent
1172 | ? `{
1173 | params,
1174 | }: {
1175 | params: { [key: string]: string | string[] | undefined };
1176 | }`
1177 | : "";
1178 |
1179 | const listProps = modelTree.parent
1180 | ? ` {
1181 | params,
1182 | searchParams,
1183 | }: {
1184 | params: { [key: string]: string | string[] | undefined };
1185 | searchParams?: { [key: string]: string | string[] | undefined };
1186 | }`
1187 | : ` {
1188 | searchParams
1189 | }: {
1190 | searchParams?: { [key: string]: string | string[] | undefined };
1191 | }`;
1192 |
1193 | const listCount = `
1194 | const page =
1195 | typeof searchParams?.page === "string" ? Number(searchParams?.page) : 1;
1196 | const limit =
1197 | typeof searchParams?.limit === "string" ? Number(searchParams?.limit) : 10;
1198 | const count = await prisma.${
1199 | modelName.charAt(0).toLowerCase() + modelName.slice(1)
1200 | }.count${countWhereparentClause ? countWhereparentClause : "()"};
1201 | `;
1202 |
1203 | const uniqueDynamicSlugs = getDynamicSlugs(
1204 | modelTree.modelName,
1205 | modelUniqueIdentifierField.map((f) => f.name)
1206 | );
1207 |
1208 | // ############### Show Page
1209 | const childModelLinkList = await generateChildrenList(
1210 | modelTree,
1211 | createRedirectForm
1212 | );
1213 | const showFormCode = await generateShowForm(
1214 | modelTree,
1215 | createRedirectForm,
1216 | childModelLinkList
1217 | );
1218 |
1219 | // If many to many, must do a connect
1220 | const createFormCode = await generateCreateForm(
1221 | modelTree,
1222 | createRedirectForm,
1223 | enums
1224 | );
1225 |
1226 | let redirectStr = "";
1227 | modelTree.uniqueIdentifierField.forEach(
1228 | (f) => (redirectStr += "/" + `\${created.${f.name}}`)
1229 | );
1230 | const createRedirect = await generateRedirect(
1231 | `\`${createRedirectForm}${redirectStr}\``
1232 | );
1233 |
1234 | const createLink = await generateLink(
1235 | `${createRedirectForm}/create`,
1236 | "Create New NexquikTemplateModel"
1237 | );
1238 |
1239 | const prismaInput = generateConvertToPrismaInputCode(modelTree);
1240 |
1241 | const parentSlugs = getDynamicSlugs(
1242 | modelTree.parent?.name,
1243 | parentRoute.uniqueIdentifierField.map((f) => f.name)
1244 | );
1245 |
1246 | const prismaCreateInput = generateConvertToPrismaCreateInputCode(
1247 | modelTree,
1248 | parentSlugs,
1249 | manyToManyConnect
1250 | );
1251 |
1252 | const whereClause = generateWhereClause(
1253 | "params",
1254 | uniqueDynamicSlugs,
1255 | modelUniqueIdentifierField
1256 | );
1257 |
1258 | // ############### Edit Page
1259 | const editFormCode = await generateEditForm(
1260 | modelTree,
1261 | createRedirectForm,
1262 | enums
1263 | );
1264 |
1265 | let redirectStr2 = "";
1266 | uniqueDynamicSlugs.forEach(
1267 | (f) => (redirectStr2 += "/" + `\${params.${f}}`)
1268 | );
1269 |
1270 | const editRedirect = await generateRedirect(
1271 | `\`${createRedirectForm}/${redirectStr2}\``
1272 | );
1273 |
1274 | // ############### Extras
1275 | const revalidatePath = await generateRevalidatePath(
1276 | `${createRedirectForm}`
1277 | );
1278 |
1279 | const baseBreadCrumb = generateBreadCrumb(route);
1280 |
1281 | const editBreadCrumb = generateBreadCrumb(route + "/edit");
1282 |
1283 | // dynamic/edit/page.tsx
1284 | childLoopPromises.push(
1285 | modifyFile(
1286 | path.join(templateDynamicDirectory, "edit", "page.tsx"),
1287 | path.join(dynamicOutputDirectory, "edit", "page.tsx"),
1288 | [
1289 | {
1290 | startComment: "//@nexquik prismaImport start",
1291 | endComment: "//@nexquik prismaImport stop",
1292 | insertString: prismaImportString,
1293 | },
1294 | {
1295 | startComment: "//@nexquik prismaEnumImport start",
1296 | endComment: "//@nexquik prismaEnumImport stop",
1297 | insertString: enumImport,
1298 | },
1299 | {
1300 | startComment: "//@nexquik prismaWhereInput start",
1301 | endComment: "//@nexquik prismaWhereInput stop",
1302 | insertString: whereClause,
1303 | },
1304 | {
1305 | startComment: "//@nexquik prismaEditDataInput start",
1306 | endComment: "//@nexquik prismaEditDataInput stop",
1307 | insertString: prismaInput,
1308 | },
1309 | {
1310 | startComment: "//@nexquik editRedirect start",
1311 | endComment: "//@nexquik editRedirect stop",
1312 | insertString: editRedirect,
1313 | },
1314 | {
1315 | startComment: "{/* @nexquik editBreadCrumb start */}",
1316 | endComment: "{/* @nexquik editBreadCrumb stop */}",
1317 | insertString: editBreadCrumb,
1318 | },
1319 | {
1320 | startComment: "{/* @nexquik editForm start */}",
1321 | endComment: "{/* @nexquik editForm stop */}",
1322 | insertString: editFormCode,
1323 | },
1324 | ],
1325 | modelTree.modelName
1326 | )
1327 | );
1328 |
1329 | // dynamic/page.tsx
1330 | childLoopPromises.push(
1331 | modifyFile(
1332 | path.join(templateDynamicDirectory, "page.tsx"),
1333 | path.join(dynamicOutputDirectory, "page.tsx"),
1334 | [
1335 | {
1336 | startComment: "//@nexquik prismaImport start",
1337 | endComment: "//@nexquik prismaImport stop",
1338 | insertString: prismaImportString,
1339 | },
1340 | {
1341 | startComment: "//@nexquik prismaWhereInput start",
1342 | endComment: "//@nexquik prismaWhereInput stop",
1343 | insertString: whereClause,
1344 | },
1345 | {
1346 | startComment: "//@nexquik prismaDeleteClause start",
1347 | endComment: "//@nexquik prismaDeleteClause stop",
1348 | insertString: deleteWhereClause,
1349 | },
1350 | {
1351 | startComment: "//@nexquik revalidatePath start",
1352 | endComment: "//@nexquik revalidatePath stop",
1353 | insertString: revalidatePath,
1354 | },
1355 | {
1356 | startComment: "//@nexquik listRedirect start",
1357 | endComment: "//@nexquik listRedirect stop",
1358 | insertString: listRedirect,
1359 | },
1360 | {
1361 | startComment: "{/* @nexquik breadcrumb start */}",
1362 | endComment: "{/* @nexquik breadcrumb stop */}",
1363 | insertString: baseBreadCrumb,
1364 | },
1365 | {
1366 | startComment: "{/* @nexquik showForm start */}",
1367 | endComment: "{/* @nexquik showForm stop */}",
1368 | insertString: showFormCode,
1369 | },
1370 | ],
1371 | modelTree.modelName
1372 | )
1373 | );
1374 |
1375 | // base/page.tsx
1376 | childLoopPromises.push(
1377 | modifyFile(
1378 | path.join(templateModelDirectory, "page.tsx"),
1379 | path.join(baseModelDirectory, "page.tsx"),
1380 | [
1381 | {
1382 | startComment: "//@nexquik prismaImport start",
1383 | endComment: "//@nexquik prismaImport stop",
1384 | insertString: prismaImportString,
1385 | },
1386 | {
1387 | startComment: "//@nexquik listProps start",
1388 | endComment: "//@nexquik listProps stop",
1389 | insertString: listProps,
1390 | },
1391 | {
1392 | startComment: "/* @nexquik listCount start */",
1393 | endComment: "/* @nexquik listCount stop */",
1394 | insertString: listCount,
1395 | },
1396 | {
1397 | startComment: "//@nexquik prismaWhereParentClause start",
1398 | endComment: "//@nexquik prismaWhereParentClause stop",
1399 | insertString: whereparentClause,
1400 | },
1401 | {
1402 | startComment: "//@nexquik prismaDeleteClause start",
1403 | endComment: "//@nexquik prismaDeleteClause stop",
1404 | insertString: deleteWhereClause,
1405 | },
1406 | {
1407 | startComment: "//@nexquik revalidatePath start",
1408 | endComment: "//@nexquik revalidatePath stop",
1409 | insertString: revalidatePath,
1410 | },
1411 | {
1412 | startComment: "{/* @nexquik listBreadcrumb start */}",
1413 | endComment: "{/* @nexquik listBreadcrumb stop */}",
1414 | insertString: listBreadCrumb,
1415 | },
1416 | {
1417 | startComment: "{/* @nexquik createLink start */}",
1418 | endComment: "{/* @nexquik createLink stop */}",
1419 | insertString: createLink,
1420 | },
1421 | {
1422 | startComment: "{/* @nexquik listForm start */}",
1423 | endComment: "{/* @nexquik listForm stop */}",
1424 | insertString: listFormCode,
1425 | },
1426 | {
1427 | startComment: "{/* @nexquik listPagination start */}",
1428 | endComment: "{/* @nexquik listPagination stop */}",
1429 | insertString: listPagination,
1430 | },
1431 | ],
1432 | modelTree.modelName
1433 | )
1434 | );
1435 |
1436 | // base/create/page.tsx
1437 | childLoopPromises.push(
1438 | modifyFile(
1439 | path.join(templateModelDirectory, "create", "page.tsx"),
1440 | path.join(baseModelDirectory, "create", "page.tsx"),
1441 | [
1442 | {
1443 | startComment: "//@nexquik prismaImport start",
1444 | endComment: "//@nexquik prismaImport stop",
1445 | insertString: prismaImportString,
1446 | },
1447 | {
1448 | startComment: "//@nexquik props start",
1449 | endComment: "//@nexquik props stop",
1450 | insertString: props,
1451 | },
1452 | {
1453 | startComment: "//@nexquik prismaEnumImport start",
1454 | endComment: "//@nexquik prismaEnumImport stop",
1455 | insertString: enumImport,
1456 | },
1457 | {
1458 | startComment: "//@nexquik prismaCreateDataInput start",
1459 | endComment: "//@nexquik prismaCreateDataInput stop",
1460 | insertString: prismaCreateInput,
1461 | },
1462 | {
1463 | startComment: "//@nexquik revalidatePath start",
1464 | endComment: "//@nexquik revalidatePath stop",
1465 | insertString: revalidatePath,
1466 | },
1467 | {
1468 | startComment: "//@nexquik createRedirect start",
1469 | endComment: "//@nexquik createRedirect stop",
1470 | insertString: createRedirect,
1471 | },
1472 |
1473 | {
1474 | startComment: "{/* @nexquik createBreadcrumb start */}",
1475 | endComment: "{/* @nexquik createBreadcrumb stop */}",
1476 | insertString: createBreadCrumb,
1477 | },
1478 | {
1479 | startComment: "{/* @nexquik createForm start */}",
1480 | endComment: "{/* @nexquik createForm stop */}",
1481 | insertString: createFormCode,
1482 | },
1483 | ],
1484 | modelTree.modelName
1485 | )
1486 | );
1487 |
1488 | childLoopPromises = childLoopPromises.concat(
1489 | modelTree.children.map(async (child) => {
1490 | try {
1491 | await generateRoutes(
1492 | child,
1493 | {
1494 | name: route,
1495 | uniqueIdentifierField: modelUniqueIdentifierField,
1496 | },
1497 | depth + 1,
1498 | maxAllowedDepth
1499 | );
1500 | } catch (error) {
1501 | console.error("An error occurred in childLoopPromises:", error);
1502 | }
1503 | })
1504 | );
1505 | await Promise.all(childLoopPromises);
1506 | }
1507 |
1508 | const startTime = new Date().getTime();
1509 |
1510 | // Start the main loop
1511 | const mainLoopPromises = modelTreeArray.map(async (modelTree) => {
1512 | try {
1513 | await generateRoutes(
1514 | modelTree,
1515 | {
1516 | name: `/${rootName}/`,
1517 | uniqueIdentifierField: [],
1518 | },
1519 | 0,
1520 | maxAllowedDepth,
1521 | true
1522 | );
1523 | } catch (error) {
1524 | console.error("An error occurred in mainLoopPromises:", error);
1525 | }
1526 | });
1527 |
1528 | // Wait for all loop promises to complete
1529 | await Promise.all(mainLoopPromises);
1530 | process.stdout.write(`\u001b[2K\r`);
1531 | const endTime = new Date().getTime();
1532 | const duration = (endTime - startTime) / 1000;
1533 | console.log(
1534 | `${chalk.blue.bold(
1535 | `\nCreated ${modelTreeArray.length} model(s) in Group: `
1536 | )}${chalk.yellow.bold(`'${rootName}'`)} ${chalk.gray(
1537 | `(Generated ${fileCount} files and ${directoryCount} directories in ${duration} seconds)`
1538 | )}`
1539 | );
1540 |
1541 | return routes;
1542 | }
1543 |
1544 | export function generateConvertToPrismaCreateInputCode(
1545 | modelTree: ModelTree,
1546 | parentSlugs: string[],
1547 | manyToManyConnect: string
1548 | ): string {
1549 | // If model has a parent, get the parent accessor
1550 | let relationFieldsToParent: string[] = [];
1551 | let fieldType = "";
1552 | if (modelTree.parent) {
1553 | // Get the field on the current model that is the id referencing the parent
1554 | modelTree.model.fields.forEach((mf) => {
1555 | if (mf.type === modelTree.parent?.name) {
1556 | if (mf.relationFromFields?.length && mf.relationFromFields.length > 0) {
1557 | relationFieldsToParent = mf.relationFromFields;
1558 | }
1559 | }
1560 | });
1561 |
1562 | // Get the field type on the current model that is the id referencing the parent
1563 | fieldType =
1564 | modelTree.model.fields.find((f) =>
1565 | relationFieldsToParent.find((a) => a === f.name)
1566 | )?.type || "";
1567 | }
1568 |
1569 | const fieldsToConvert: Partial[] = modelTree.model.fields.filter(
1570 | (field) => isFieldRenderable(field)
1571 | );
1572 |
1573 | const convertToPrismaInputLines = fieldsToConvert.map(
1574 | ({ name, type, kind, isRequired, hasDefaultValue }) => {
1575 | const nonTypeCastedValue = `formData.get('${name}')`;
1576 | let typecastValue = nonTypeCastedValue;
1577 | if (kind === "enum") {
1578 | typecastValue += ` as ${type}`;
1579 | } else {
1580 | if (type === "Int" || type === "Float") {
1581 | typecastValue = `Number(${typecastValue})`;
1582 | } else if (type === "Boolean") {
1583 | typecastValue = `Boolean(${typecastValue})`;
1584 | } else if (type === "DateTime") {
1585 | typecastValue = `new Date(String(${typecastValue}))`;
1586 | } else {
1587 | typecastValue = `String(${typecastValue})`;
1588 | }
1589 | }
1590 | if (hasDefaultValue || !isRequired) {
1591 | return ` ${name}: ${nonTypeCastedValue} ? ${typecastValue} : undefined,`;
1592 | }
1593 | return ` ${name}: ${typecastValue},`;
1594 | }
1595 | );
1596 |
1597 | // Convert the parent accessor differently
1598 | if (relationFieldsToParent && fieldType && modelTree.parent?.name) {
1599 | relationFieldsToParent.forEach((s, index) => {
1600 | let typecastValue = `params.${parentSlugs[index]}`;
1601 | if (fieldType === "Int" || fieldType === "Float") {
1602 | typecastValue = `Number(${typecastValue})`;
1603 | } else if (fieldType === "Boolean") {
1604 | typecastValue = `Boolean(${typecastValue})`;
1605 | } else if (fieldType === "DateTime") {
1606 | typecastValue = `new Date(String(${typecastValue}))`;
1607 | } else {
1608 | typecastValue = `String(${typecastValue})`;
1609 | }
1610 | convertToPrismaInputLines.push(` ${s}: ${typecastValue},`);
1611 | });
1612 | }
1613 |
1614 | // Convert fields pointing to other relations differently
1615 | modelTree.model.fields.map((field) => {
1616 | if (field.kind === "object") {
1617 | const relationFrom = field.relationFromFields && field.relationFromFields;
1618 | const referencedModelName = field.type;
1619 |
1620 | if (referencedModelName === modelTree.parent?.name) {
1621 | } else if (relationFrom) {
1622 | relationFrom.forEach((rf) => {
1623 | const fieldType2 = modelTree.model.fields.find(
1624 | (f) => f.name === rf
1625 | )?.type;
1626 |
1627 | let typecastValue = `formData.get('${relationFrom}')`;
1628 | if (fieldType2 === "Int" || fieldType2 === "Float") {
1629 | typecastValue = `Number(${typecastValue})`;
1630 | } else if (fieldType2 === "Boolean") {
1631 | typecastValue = `Boolean(${typecastValue})`;
1632 | } else if (fieldType2 === "DateTime") {
1633 | typecastValue = `new Date(String(${typecastValue}))`;
1634 | } else {
1635 | typecastValue = `String(${typecastValue})`;
1636 | }
1637 |
1638 | convertToPrismaInputLines.push(
1639 | ` ${relationFrom}: ${typecastValue},`
1640 | );
1641 | });
1642 | }
1643 | }
1644 | });
1645 | convertToPrismaInputLines.push(manyToManyConnect);
1646 | return `{
1647 | ${convertToPrismaInputLines.join("\n")}
1648 | }`;
1649 | }
1650 |
1651 | export function generateConvertToPrismaInputCode(modelTree: ModelTree): string {
1652 | const fieldsToConvert: Partial[] = modelTree.model.fields
1653 | .filter(({ isId }) => !isId)
1654 | .filter((field) => isFieldRenderable(field));
1655 |
1656 | const convertToPrismaInputLines = fieldsToConvert.map(
1657 | ({ name, type, kind }) => {
1658 | const nonTypecastValue = `formData.get('${name}')`;
1659 | let typecastValue = nonTypecastValue;
1660 | if (kind === "enum") {
1661 | typecastValue += ` as ${type}`;
1662 | } else {
1663 | if (type === "Int" || type === "Float") {
1664 | typecastValue = `Number(${typecastValue})`;
1665 | } else if (type === "Boolean") {
1666 | typecastValue = `Boolean(${typecastValue})`;
1667 | } else if (type === "DateTime") {
1668 | typecastValue = `new Date(String(${typecastValue}))`;
1669 | } else {
1670 | typecastValue = `String(${typecastValue})`;
1671 | }
1672 | }
1673 | return ` ${name}: ${nonTypecastValue} ? ${typecastValue} : undefined,`;
1674 | }
1675 | );
1676 |
1677 | // Convert fields pointing to other relations differently
1678 | modelTree.model.fields.map((field) => {
1679 | if (field.kind === "object") {
1680 | const relationFrom = field.relationFromFields && field.relationFromFields;
1681 | const referencedModelName = field.type;
1682 |
1683 | if (referencedModelName === modelTree.parent?.name) {
1684 | } else if (relationFrom) {
1685 | relationFrom.forEach((rf) => {
1686 | const fieldType2 = modelTree.model.fields.find(
1687 | (f) => f.name === rf
1688 | )?.type;
1689 | const nonTypecastValue = `formData.get('${relationFrom}')`;
1690 |
1691 | let typecastValue = nonTypecastValue;
1692 | if (fieldType2 === "Int" || fieldType2 === "Float") {
1693 | typecastValue = `Number(${typecastValue})`;
1694 | } else if (fieldType2 === "Boolean") {
1695 | typecastValue = `Boolean(${typecastValue})`;
1696 | } else if (fieldType2 === "DateTime") {
1697 | typecastValue = `new Date(String(${typecastValue}))`;
1698 | } else {
1699 | typecastValue = `String(${typecastValue})`;
1700 | }
1701 |
1702 | convertToPrismaInputLines.push(
1703 | ` ${relationFrom}: ${nonTypecastValue} ? ${typecastValue} : undefined,`
1704 | );
1705 | });
1706 | }
1707 | }
1708 | });
1709 |
1710 | return `{
1711 | ${convertToPrismaInputLines.join("\n")}
1712 | }`;
1713 | }
1714 |
1715 | export function generateWhereParentClause(
1716 | inputObject: string | undefined,
1717 | fieldAccessValue: string | undefined,
1718 | parentIdentifierFieldName: string | undefined,
1719 | parentIdentifierFieldType: string | undefined,
1720 | parentReferenceField: string | undefined,
1721 | manyToManyWhere: string,
1722 | selectClause: string
1723 | ): string {
1724 | if (manyToManyWhere == "") {
1725 | if (
1726 | inputObject &&
1727 | fieldAccessValue &&
1728 | parentIdentifierFieldName &&
1729 | parentIdentifierFieldType &&
1730 | parentReferenceField
1731 | ) {
1732 | let typecastValue = `${inputObject}.${fieldAccessValue}`;
1733 | if (
1734 | parentIdentifierFieldType === "Int" ||
1735 | parentIdentifierFieldType === "Float"
1736 | ) {
1737 | typecastValue = `Number(${typecastValue})`;
1738 | } else if (parentIdentifierFieldType === "Boolean") {
1739 | typecastValue = `Boolean(${typecastValue})`;
1740 | } else if (parentIdentifierFieldType === "String") {
1741 | typecastValue = `String(${typecastValue})`;
1742 | }
1743 | return `({ where: { ${parentReferenceField}: {${parentIdentifierFieldName}: {equals: ${typecastValue}} }, }, ${selectClause} })`;
1744 | } else {
1745 | return `{${selectClause}}`;
1746 | }
1747 | } else {
1748 | return `({ ${manyToManyWhere} , ${selectClause}}, )`;
1749 | }
1750 | }
1751 |
1752 | export function generateWhereClause(
1753 | inputObject: string | undefined,
1754 | uniqueDynamicSlugs: string[],
1755 | modelUniqueIdFields: {
1756 | name: string;
1757 | type: string;
1758 | }[]
1759 | ): string {
1760 | let returnClause = "";
1761 | if (inputObject && modelUniqueIdFields) {
1762 | if (modelUniqueIdFields.length > 1) {
1763 | returnClause +=
1764 | "{" + modelUniqueIdFields.map((f) => f.name).join("_") + ":{";
1765 | modelUniqueIdFields.forEach((f, index) => {
1766 | let typecastValue = `${inputObject}.${uniqueDynamicSlugs[index]}`;
1767 | if (f.type === "Int" || f.type === "Float") {
1768 | typecastValue = `Number(${typecastValue})`;
1769 | } else if (f.type === "Boolean") {
1770 | typecastValue = `Boolean(${typecastValue})`;
1771 | } else if (f.type === "String") {
1772 | typecastValue = `String(${typecastValue})`;
1773 | }
1774 |
1775 | returnClause += `${f.name}: ${typecastValue} ,`;
1776 | });
1777 | returnClause += "}},";
1778 | } else {
1779 | const { name, type } = modelUniqueIdFields[0];
1780 | let typecastValue = `${inputObject}.${uniqueDynamicSlugs[0]}`;
1781 | if (type === "Int" || type === "Float") {
1782 | typecastValue = `Number(${typecastValue})`;
1783 | } else if (type === "Boolean") {
1784 | typecastValue = `Boolean(${typecastValue})`;
1785 | } else if (type === "String") {
1786 | typecastValue = `String(${typecastValue})`;
1787 | }
1788 |
1789 | return `{ ${name}: ${typecastValue} },`;
1790 | }
1791 | return returnClause;
1792 | } else {
1793 | return "";
1794 | }
1795 | }
1796 |
1797 | export function generateDeleteClause(
1798 | uniqueIdentifierFields: {
1799 | name: string;
1800 | type: string;
1801 | }[]
1802 | ): string {
1803 | if (uniqueIdentifierFields.length === 1) {
1804 | const singleIdField = uniqueIdentifierFields[0];
1805 | let typecastValue = `formData.get('${singleIdField.name}')`;
1806 | if (singleIdField.type === "Int" || singleIdField.type === "Float") {
1807 | typecastValue = `Number(${typecastValue})`;
1808 | } else if (singleIdField.type === "Boolean") {
1809 | typecastValue = `Boolean(${typecastValue})`;
1810 | } else if (singleIdField.type === "String") {
1811 | typecastValue = `String(${typecastValue})`;
1812 | }
1813 | return `{ ${singleIdField.name}: ${typecastValue} },`;
1814 | } else if (uniqueIdentifierFields.length >= 2) {
1815 | let andClause = "{" + uniqueIdentifierFields.map((f) => f.name).join("_");
1816 | andClause += ":{";
1817 | uniqueIdentifierFields.forEach((f) => {
1818 | let typecastValue = `formData.get('${f.name}')`;
1819 | if (f.type === "Int" || f.type === "Float") {
1820 | typecastValue = `Number(${typecastValue})`;
1821 | } else if (f.type === "Boolean") {
1822 | typecastValue = `Boolean(${typecastValue})`;
1823 | } else if (f.type === "String") {
1824 | typecastValue = `String(${typecastValue})`;
1825 | }
1826 | andClause += `${f.name}: ${typecastValue},`;
1827 | });
1828 | andClause += "}}";
1829 | return andClause;
1830 | }
1831 | return "";
1832 | }
1833 |
1834 | // Custom export function to check if field is renderable in a form
1835 | export function isFieldRenderable(field: DMMF.Field): boolean {
1836 | return !(field.isReadOnly || field.isList || !!field.relationName);
1837 | }
1838 | export function generateFormFields(
1839 | modelTree: ModelTree,
1840 | enums: Record
1841 | ): string {
1842 | return modelTree.model.fields
1843 | .map((field) => {
1844 | const required =
1845 | field.isRequired && !field.hasDefaultValue ? "required" : "";
1846 |
1847 | const checkboxStyle =
1848 | field.type === "Boolean" ? "text-sky-700" : "text-slate-300 w-full";
1849 |
1850 | // Enum
1851 | if (field.kind === "enum") {
1852 | const enumValues = enums[field.type];
1853 | return `${field.name} ${
1854 | required && "*"
1855 | } \n
1856 |
1857 |
1860 | ${enumValues.map((v) => `${v} `)}
1861 | `;
1862 | }
1863 |
1864 | const inputType = prismaFieldToInputType[field.type] || "text";
1865 |
1866 | if (field.kind === "object") {
1867 | const relationFrom =
1868 | field.relationFromFields && field.relationFromFields[0];
1869 |
1870 | const referencedModelName = field.type;
1871 |
1872 | if (referencedModelName === modelTree.parent?.name) {
1873 | } else if (relationFrom) {
1874 | const fieldType2 = modelTree.model.fields.find(
1875 | (f) => f.name === relationFrom
1876 | )?.type;
1877 | if (fieldType2) {
1878 | const inputType2 = prismaFieldToInputType[fieldType2] || "text";
1879 |
1880 | return `${relationFrom} ${
1881 | required && "*"
1882 | } \n
1883 | `;
1886 | } else {
1887 | return "";
1888 | }
1889 | }
1890 | }
1891 |
1892 | let returnValue = "";
1893 | if (isFieldRenderable(field)) {
1894 | returnValue = `${field.name} ${
1895 | required && "*"
1896 | } \n
1897 | `;
1901 | }
1902 |
1903 | return returnValue;
1904 | })
1905 | .join("\n");
1906 | }
1907 |
1908 | export function generateFormFieldsWithDefaults(
1909 | tableFields: DMMF.Field[],
1910 | enums: Record
1911 | ): string {
1912 | return tableFields
1913 | .map((field) => {
1914 | if (!isFieldRenderable(field)) {
1915 | return "";
1916 | }
1917 | const required =
1918 | field.isRequired && !field.hasDefaultValue ? "required" : "";
1919 | const checkboxStyle =
1920 | field.type === "Boolean" ? "text-sky-700" : "text-slate-300 w-full ";
1921 | // Enum
1922 | if (field.kind === "enum") {
1923 | const enumValues = enums[field.type];
1924 | return `${field.name} \n
1925 |
1930 | ${enumValues.map((v) => `${v} `)}
1931 | `;
1932 | }
1933 | const inputType = prismaFieldToInputType[field.type] || "text";
1934 | const defaultValue = field.isId
1935 | ? `{nexquikTemplateModel?.${field.name} || 'N/A'}`
1936 | : field.type === "Number"
1937 | ? `{Number(nexquikTemplateModel?.${field.name})}`
1938 | : field.type === "DateTime"
1939 | ? `{nexquikTemplateModel?.${field.name}.toISOString().slice(0, 16)}`
1940 | : `{String(nexquikTemplateModel?.${field.name})}`;
1941 | const disabled = field.isId ? "disabled" : "";
1942 |
1943 | return `${field.name} ${
1944 | required && "*"
1945 | } \n `;
1955 | })
1956 | .join("\n");
1957 | }
1958 |
--------------------------------------------------------------------------------