├── .babelrc ├── .circleci └── config.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .snyk ├── README.md ├── examples ├── complex │ └── schema.prisma └── simple │ └── schema.prisma ├── package.json ├── src ├── cli │ ├── commands │ │ ├── generate.ts │ │ └── plantUMLEncode.ts │ ├── index.ts │ └── utilities │ │ ├── errors.ts │ │ └── log.ts ├── core │ ├── common.ts │ ├── entity │ │ ├── prismaModelToPlantUMLEntity.spec.ts │ │ └── prismaModelToPlantUMLEntity.ts │ ├── enum │ │ ├── prismaEnumToPlantUMLEnum.spec.ts │ │ └── prismaEnumToPlantUMLEnum.ts │ ├── graph │ │ └── getPlantUMLGraphFromPrismaDatamodel.ts │ ├── index.ts │ └── prismaToPlantUML.ts └── index.ts ├── tsconfig.json ├── tsconfig.prod.json ├── tsconfig.webpack.json ├── webpack.config.ts └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@babel/preset-env", { "targets": { "node": "current" } }], "@babel/preset-typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | - image: circleci/node:10 10 | working_directory: ~/repo 11 | steps: 12 | - checkout 13 | # Download and cache dependencies 14 | - restore_cache: 15 | keys: 16 | - v1-dependencies-{{ checksum "yarn.lock" }} 17 | - run: yarn install 18 | - save_cache: 19 | paths: 20 | - node_modules 21 | key: v1-dependencies-{{ checksum "yarn.lock" }} 22 | - run: 23 | name: Build 24 | command: yarn build 25 | - save_cache: 26 | paths: 27 | - dist 28 | key: v1-dist-{{ .Environment.CIRCLE_SHA1 }} 29 | test: 30 | docker: 31 | - image: circleci/node:10 32 | working_directory: ~/repo 33 | steps: 34 | - checkout 35 | # Download and cache dependencies 36 | - restore_cache: 37 | keys: 38 | - v1-dependencies-{{ checksum "yarn.lock" }} 39 | - run: yarn install 40 | - save_cache: 41 | paths: 42 | - node_modules 43 | key: v1-dependencies-{{ checksum "yarn.lock" }} 44 | - run: 45 | name: Test 46 | command: yarn test 47 | 48 | publish: 49 | docker: 50 | - image: circleci/node:10 51 | working_directory: ~/repo 52 | steps: 53 | - checkout 54 | - restore_cache: 55 | keys: 56 | - v1-dependencies-{{ checksum "yarn.lock" }} 57 | - v1-dependencies- 58 | - run: yarn install 59 | - save_cache: 60 | paths: 61 | - node_modules 62 | key: v1-dependencies-{{ checksum "yarn.lock" }} 63 | - restore_cache: 64 | keys: 65 | - v1-dist-{{ .Environment.CIRCLE_SHA1 }} 66 | - run: 67 | name: Publish Package 68 | command: cd dist && yarn run semantic-release 69 | 70 | workflows: 71 | version: 2 72 | build: 73 | jobs: 74 | - build 75 | - test 76 | - publish: 77 | requires: 78 | - build 79 | - test 80 | filters: 81 | branches: 82 | only: 83 | - next 84 | - master 85 | - beta 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | coverage 6 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.14.1 3 | ignore: {} 4 | # patches apply the minimum changes required to fix a vulnerability 5 | patch: 6 | SNYK-JS-LODASH-567746: 7 | - '@prisma/sdk > archiver > async > lodash': 8 | patched: '2020-04-30T21:53:33.075Z' 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Welcome to prisma-uml 👋

2 | 3 | > A CLI to transform a Prisma schema to a PlantUML Entity RelationShip Diagram 4 | 5 | [![npm](https://img.shields.io/npm/v/prisma-uml.svg?style=for-the-badge)](https://www.npmjs.com/package/prisma-uml) [![npm](https://img.shields.io/npm/dy/prisma-uml.svg?style=for-the-badge)](https://npm-stat.com/charts.html?package=prisma-uml) [![CircleCI (all branches)](https://img.shields.io/circleci/project/github/emyann/prisma-uml/master.svg?style=for-the-badge)](https://circleci.com/gh/emyann/prisma-uml) 6 | 7 | - [Installation](#installation) 8 | - [Commands](#commands) 9 | - [`prisma-uml [options]`](#prisma-uml-path-options) 10 | - [Image Rendering](#image-rendering) 11 | - [Using the official PlantUML server online](#using-the-official-plantuml-server-online) 12 | - [Using a local server with Docker](#using-a-local-server-with-docker) 13 | - [Demo](#demo) 14 | - [Incoming changes](#incoming-changes) 15 | - [Authors](#authors) 16 | - [Show your support](#show-your-support) 17 | 18 | ## Installation 19 | 20 | **Using `npx`** 21 | 22 | If you don't want to install the CLI but just execute it, you can use it through `npx` this way 23 | 24 | ```sh 25 | npx prisma-uml --help 26 | ``` 27 | 28 | **Install with `npm`** 29 | 30 | You can also install the CLI globally 31 | 32 | ```sh 33 | npm i -g prisma-uml 34 | prisma-uml --help 35 | ``` 36 | 37 | ## Commands 38 | 39 | ### `prisma-uml [options]` 40 | 41 | > **Generate a plantUML from a Prisma schema** 42 | 43 | ```sh 44 | prisma-uml [--output] [--server] [--file] 45 | ``` 46 | 47 | **Argument** 48 | 49 | | Name | Description | 50 | |----------|------------------------| 51 | | **path** | Path to Prisma schema. | 52 | 53 | **Options** 54 | 55 | | Name | Alias | Description | Type / Choices | Default | 56 | |--------------|-------|--------------------------------------|--------------------------------------|-----------------------------------| 57 | | **--output** | -o | Output of the diagram | string / [text \| svg \| png \| jpg] | text | 58 | | **--server** | -s | PlantUML Server URL | string | https://www.plantuml.com/plantuml | 59 | | **--file** | -f | Filename or File full path to output | string | | 60 | 61 |
Examples 62 |

63 | 64 | ```sh 65 | # Output a plantUML Entity Relation Diagram as text 66 | prisma-uml ./schema.prisma 67 | 68 | # Save the diagram into a .plantuml file 69 | prisma-uml ./schema.prisma > my-erd.plantuml 70 | 71 | # Output a diagram as SVG 72 | prisma-uml ./schema.prisma --output svg --file my-erd.svg 73 | 74 | # Output a diagram as PNG 75 | prisma-uml ./schema.prisma -o png -f my-erd.png 76 | 77 | # Use a plantUML custom server to render the image 78 | prisma-uml ./schema.prisma --server http://localhost:8080 79 | ``` 80 | 81 |

82 |
83 | 84 | ## Image Rendering 85 | 86 | ### Using the official PlantUML server online 87 | 88 | PlantUML usually requires to have Java installed or a server to render the images. By default the official online server (https://www.plantuml.com/plantuml) is used to render the images. The plantUML diagram is first compressed then encoded ([plantUML encoding](https://plantuml.com/fr/code-javascript-synchronous)) and finally sent to the server to execute the rendering. 89 | 90 | ### Using a local server with Docker 91 | 92 | You might want to avoid sending your diagram over the wire for some reason, `prisma-uml` allows you to specify a custom/local server. You could easily run your own local server using Docker: 93 | 94 | ```sh 95 | docker run -d -p 8080:8080 plantuml/plantuml-server:jetty 96 | ``` 97 | 98 | You server is now available (depending of you Docker installation) at `http://localhost:8080`. You can then use `prisma-uml` as follow: 99 | 100 | ```sh 101 | prisma-uml ./schema.prisma --server http://localhost:8080 102 | ``` 103 | 104 | ## Demo 105 | 106 | [![asciicast](https://asciinema.org/a/322572.svg)](https://asciinema.org/a/322572) 107 | 108 | ## Incoming changes 109 | 110 | - [ ] Feat: Split attributes by entity (scalar, enum, navigation fields / external type). 111 | - [ ] Feat: Group relations by entities. 112 | - [ ] Feat: NextJs Preview that run the CLI on server to render a prisma schema to a plantUML ERD ? 113 | - [ ] Feat: Display Version Number 114 | - [ ] Feat: Handle `-o text -f my-erd.puml|.wsd|.plantuml...` 115 | - [ ] Remove `--output` in favor of extension handling (.svg, .png, .jpg, .puml...) (?) 116 | - [ ] Fix: Multiple cardinalities when should be online one (see simple example) 117 | - [ ] Feat: Add logging to stdout to describe what the CLI is doing 118 | 119 | ## Authors 120 | 121 | 👤 **Brendan Stromberger** 122 | 123 | - Github: [@bstro](https://github.com/bstro) 124 | 125 | 👤 **Yann Renaudin** 126 | 127 | - Github: [@emyann](https://github.com/emyann) 128 | 129 | ## Show your support 130 | 131 | Give a ⭐️ if this project helped you! 132 | -------------------------------------------------------------------------------- /examples/complex/schema.prisma: -------------------------------------------------------------------------------- 1 | model Module { 2 | id String @default(cuid()) @id 3 | slug String @default(cuid()) @unique 4 | title String? 5 | description String? 6 | courses Course[] @relation(references: [id]) 7 | unlockedInTrainings Training[] @relation(references: [id]) 8 | lessons Lesson[] 9 | links Link[] 10 | creator User @relation("createdModules", fields: [creatorId], references: [id]) 11 | creatorId String 12 | sharedWith User[] @relation("sharedModules", references: [id]) 13 | isSharedWithAllTrainers Boolean 14 | isSharedWithAllUsers Boolean 15 | userTrainingSessionId String? 16 | } 17 | 18 | model User { 19 | id String @default(cuid()) @id 20 | email String @unique 21 | createdAt DateTime @default(now()) 22 | updatedAt DateTime @updatedAt 23 | role Role 24 | enabled Boolean 25 | fullName String 26 | location String? 27 | referred String? 28 | companyName String? 29 | password String 30 | createdTrainings Training[] @relation("createdTrainings") 31 | attendedTrainings UserTrainingSession[] @relation("userTrainingSession") 32 | createdCourses Course[] @relation("createdCourses") 33 | sharedCourses Course[] @relation("sharedCourses", references: [id]) 34 | createdModules Module[] @relation("createdModules") 35 | sharedModules Module[] @relation("sharedModules", references: [id]) 36 | sentCourseInvites CourseInvite[] @relation("fromUserCourse") 37 | receivedCourseInvites CourseInvite[] @relation("toUserCourses") 38 | } 39 | 40 | model Course { 41 | id String @default(cuid()) @id 42 | slug String @default(cuid()) @unique 43 | modules Module[] @relation(references: [id]) 44 | title String 45 | trainings Training[] 46 | creator User @relation(fields: [creatorId], references: [id]) 47 | creatorId String 48 | sharedWith User[] @relation("sharedCourses", references: [id]) 49 | isSharedWithAllTrainers Boolean 50 | isSharedWithAllUsers Boolean 51 | modulesIdByOrder String 52 | invites CourseInvite[] @relation("courseInvite") 53 | userId String? 54 | } 55 | 56 | model CourseInvite { 57 | id String @default(cuid()) @id 58 | fromUser User @relation("fromUserCourses", fields: [fromUserId], references: [id]) 59 | toUser User @relation("toUserCourses", fields: [toUserId], references: [id]) 60 | fromUserId String 61 | toUserId String 62 | course Course @relation("courseInvite", fields: [courseId], references: [id]) 63 | courseId String 64 | userId String? 65 | } 66 | 67 | model Lesson { 68 | id String @default(cuid()) @id 69 | slug String @default(cuid()) @unique 70 | title String 71 | markdown String 72 | embedUrl String 73 | module Module @relation(fields: [moduleId], references: [id]) 74 | moduleId String 75 | order Int? 76 | userTrainingSessionId String? 77 | } 78 | 79 | model Link { 80 | id String @default(cuid()) @id 81 | url String 82 | title String 83 | module Module @relation(fields: [moduleId], references: [id]) 84 | moduleId String 85 | } 86 | 87 | model UserTrainingSession { 88 | id String @default(cuid()) @id 89 | user User @relation("userTrainingSession", fields: [userId], references: [id]) 90 | userId String 91 | training Training @relation("trainingSession", fields: [trainingId], references: [id]) 92 | trainingId String 93 | completedModules Module[] 94 | completedLessons Lesson[] 95 | } 96 | 97 | model Training { 98 | id String @default(cuid()) @id 99 | slug String @default(cuid()) @unique 100 | dateEnd DateTime? 101 | dateStart DateTime? 102 | name String? 103 | course Course? @relation(fields: [courseId], references: [id]) 104 | courseId String? 105 | createdBy User @relation("createdTrainings", fields: [createdById], references: [id]) 106 | createdById String 107 | attendingUsers UserTrainingSession[] @relation("trainingSession") 108 | unlockedModules Module[] @relation(references: [id]) 109 | secretKey String @unique 110 | type TrainingType 111 | } 112 | 113 | enum Role { 114 | ADMIN 115 | USER 116 | CREATOR 117 | } 118 | 119 | enum TrainingType { 120 | ONLINE 121 | INPERSON 122 | COURSE 123 | } -------------------------------------------------------------------------------- /examples/simple/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "postgres" 7 | url = env("POSTGRES_URL") 8 | } 9 | 10 | model User { 11 | id String @default(cuid()) @id 12 | firstName String? 13 | lastName String? 14 | dogs Dog[] 15 | profile Profile 16 | } 17 | 18 | model Profile { 19 | id String @default(cuid()) @id 20 | email String 21 | user User @relation(fields: [userId], references: [id]) 22 | userId String 23 | } 24 | 25 | model Dog { 26 | id String @default(cuid()) @id 27 | breed Breed 28 | userId String? 29 | user User? @relation(fields: [userId], references: [id]) 30 | } 31 | 32 | enum Breed { 33 | Bulldog 34 | Poodle 35 | } 36 | 37 | enum Role { 38 | Admin 39 | User 40 | } 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prisma-uml", 3 | "version": "1.1.1", 4 | "description": "A CLI to transform a Prisma schema to a PlantUML Entity RelationShip Diagram", 5 | "main": "./dist/prisma-uml.js", 6 | "bin": { 7 | "prisma-uml": "./dist/prisma-uml.js" 8 | }, 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "start": "tsnd --respawn -r tsconfig-paths/register ./src/index.ts", 14 | "build": "run-p build:js check:types", 15 | "build:js": "TS_NODE_PROJECT=\"tsconfig.webpack.json\" webpack --mode=production", 16 | "check:types": "tsc -p tsconfig.prod.json --noEmit", 17 | "local": "yarn build && yarn link && chmod +x ./dist/prisma-uml.js", 18 | "semantic-release": "semantic-release", 19 | "test": "jest", 20 | "snyk-protect": "snyk protect", 21 | "prepare": "yarn run snyk-protect && husky install" 22 | }, 23 | "release": { 24 | "branches": [ 25 | "master", 26 | "next", 27 | { 28 | "name": "beta", 29 | "prerelease": true 30 | } 31 | ] 32 | }, 33 | "keywords": [ 34 | "UML", 35 | "prisma", 36 | "ERD", 37 | "Entity Relationship Diagram", 38 | "plantUML", 39 | "Unified Modeling Language" 40 | ], 41 | "repository": "emyann/prisma-uml", 42 | "homepage": "https://github.com/emyann/prisma-uml", 43 | "author": "Yann Renaudin", 44 | "license": "MIT", 45 | "dependencies": { 46 | "@prisma/sdk": "^3.1.1", 47 | "axios": "^0.21.1", 48 | "chalk": "^4.1.2", 49 | "typescript-generic-datastructures": "^1.3.0", 50 | "uuid": "^9.0.0", 51 | "yargs": "^16.2.0" 52 | }, 53 | "devDependencies": { 54 | "@babel/core": "^7.9.0", 55 | "@babel/preset-env": "^7.9.5", 56 | "@babel/preset-typescript": "^7.9.0", 57 | "@types/jest": "^25.2.1", 58 | "@types/node": "^13.11.1", 59 | "@types/uuid": "^7.0.2", 60 | "@types/webpack": "^4.41.12", 61 | "@types/yargs": "^15.0.4", 62 | "babel-loader": "^8.1.0", 63 | "husky": ">=6", 64 | "jest": "^25.3.0", 65 | "lint-staged": ">=10", 66 | "snyk": "^1.667.0", 67 | "npm-run-all": "^4.1.5", 68 | "prettier": "^2.2.1", 69 | "semantic-release": "^17.0.7", 70 | "source-map-loader": "^0.2.4", 71 | "ts-node-dev": "^1.0.0-pre.44", 72 | "tsconfig-paths": "^3.9.0", 73 | "tsconfig-paths-webpack-plugin": "^3.2.0", 74 | "tslib": "^2.0.0", 75 | "typescript": "^3.8.3", 76 | "webpack": "^4.43.0", 77 | "webpack-cli": "^3.3.11", 78 | "webpack-node-externals": "^1.7.2" 79 | }, 80 | "snyk": true, 81 | "lint-staged": { 82 | "*.{ts,js,css,md}": "prettier --write" 83 | }, 84 | "prettier": { 85 | "printWidth": 120, 86 | "semi": false, 87 | "singleQuote": true, 88 | "trailingComma": "all" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/cli/commands/generate.ts: -------------------------------------------------------------------------------- 1 | import { Argv } from 'yargs' 2 | import { loadPrismaSchema, prismaToPlantUML } from 'core' 3 | import chalk from 'chalk' 4 | import { plantUMLEncode } from './plantUMLEncode' 5 | import axios, { AxiosError } from 'axios' 6 | import { createWriteStream } from 'fs' 7 | import { Stream } from 'stream' 8 | import { fmtSuccess, fmtError } from 'cli/utilities/log' 9 | import { RequiredArgsError } from 'cli/utilities/errors' 10 | 11 | enum OutputType { 12 | Text = 'text', 13 | SVG = 'svg', 14 | PNG = 'png', 15 | JPG = 'jpg', 16 | } 17 | enum CommandArgument { 18 | Path = 'path', 19 | } 20 | enum CommandOptions { 21 | Output = 'output', 22 | Server = 'server', 23 | File = 'file', 24 | } 25 | const command = ['$0 '] 26 | const describe = 'Generate a plantUML from a Prisma schema' 27 | const builder = (yargs: Argv) => { 28 | return yargs 29 | .positional(CommandArgument.Path, { 30 | describe: 'Path to Prisma schema', 31 | type: 'string', 32 | }) 33 | .usage( 34 | ` 35 | Usage 36 | ${chalk.green('$0')} [options]`, 37 | ) 38 | .example(`${chalk.green('$0')} ./schema.prisma`, `Output a plantUML Entity Relation Diagram as text in the stdout`) 39 | .example(`${chalk.green('$0')} ./schema.prisma > my-erd.plantuml`, `Save the diagram into a .plantuml file`) 40 | .example(`${chalk.green('$0')} ./schema.prisma --output svg --file my-erd.svg`, `Output a diagram as SVG`) 41 | .example(`${chalk.green('$0')} ./schema.prisma -o png -f my-erd.png`, `Output a diagram as PNG`) 42 | .example( 43 | `${chalk.green('$0')} ./schema.prisma --server http://localhost:8080`, 44 | `Use a plantUML custom server to render the image`, 45 | ) 46 | .options({ 47 | [CommandOptions.Output]: { 48 | alias: 'o', 49 | describe: 'Output of the diagram', 50 | type: 'string', 51 | default: OutputType.Text, 52 | choices: [OutputType.Text, OutputType.SVG, OutputType.PNG, OutputType.JPG], 53 | }, 54 | [CommandOptions.Server]: { 55 | alias: 's', 56 | describe: 'PlantUML Server URL', 57 | type: 'string', 58 | default: 'https://www.plantuml.com/plantuml', 59 | }, 60 | [CommandOptions.File]: { 61 | alias: 'f', 62 | describe: 'Filename or File full path to output', 63 | type: 'string', 64 | }, 65 | }) 66 | .check(checkRequiredArgs) 67 | .version(false) 68 | } 69 | 70 | const handler = async (args: Arguments) => { 71 | const prismaSchemaPath = args[CommandArgument.Path]! 72 | const output = args[CommandOptions.Output]! as OutputType 73 | const server = args[CommandOptions.Server]! 74 | const file = args[CommandOptions.File]! 75 | 76 | const dmmf = await loadPrismaSchema(prismaSchemaPath) 77 | const plantUML = prismaToPlantUML(dmmf) 78 | switch (output) { 79 | case OutputType.Text: { 80 | process.stdout.write(plantUML) 81 | break 82 | } 83 | case OutputType.SVG: { 84 | try { 85 | const response = await getFileStream(plantUML, server, FileKind.SVG) 86 | await saveFile(response.data, file) 87 | process.stdout.write(`✅ File ${file} successfully created!`) 88 | } catch (error) { 89 | if (isAxiosError(error)) { 90 | process.stderr.write(error.message) 91 | } else { 92 | process.stderr.write('❌ An error has occurred while creating the file') 93 | throw error 94 | } 95 | } 96 | break 97 | } 98 | case OutputType.PNG: { 99 | try { 100 | const response = await getFileStream(plantUML, server, FileKind.PNG) 101 | await saveFile(response.data, file) 102 | process.stdout.write(fmtSuccess`✅ File ${file} successfully created!`) 103 | } catch (error) { 104 | if (isAxiosError(error)) { 105 | process.stderr.write(error.message) 106 | } else { 107 | process.stderr.write(fmtError`❌ An error has occurred while creating the file`) 108 | throw error 109 | } 110 | } 111 | break 112 | } 113 | case OutputType.JPG: { 114 | try { 115 | const response = await getFileStream(plantUML, server, FileKind.PNG) 116 | await saveFile(response.data, file) 117 | process.stdout.write(fmtSuccess`✅ File ${file} successfully created!`) 118 | } catch (error) { 119 | if (isAxiosError(error)) { 120 | process.stderr.write(error.message) 121 | } else { 122 | process.stderr.write(fmtError`❌ An error has occurred while creating the file`) 123 | throw error 124 | } 125 | } 126 | break 127 | } 128 | 129 | default: 130 | break 131 | } 132 | } 133 | 134 | type CommandBuilder = typeof builder 135 | type Arguments = ReturnType['argv'] 136 | 137 | export const generate = { 138 | command, 139 | describe, 140 | builder, 141 | handler, 142 | } 143 | 144 | enum FileKind { 145 | SVG = 'svg', 146 | PNG = 'png', 147 | JPG = 'jpg', 148 | } 149 | 150 | function getFileStream(plantUML: string, serverUrl: string, fileKind: FileKind) { 151 | const code = plantUMLEncode(plantUML) 152 | if (fileKind === FileKind.JPG) { 153 | return axios.request({ baseURL: serverUrl, url: `/img/${code}`, method: 'GET', responseType: 'stream' }) 154 | } else { 155 | return axios.request({ 156 | baseURL: serverUrl, 157 | url: `/${fileKind}/${code}`, 158 | method: 'GET', 159 | responseType: 'stream', 160 | }) 161 | } 162 | } 163 | 164 | function saveFile(fileStream: Stream, path: string) { 165 | return new Promise((resolve, reject) => { 166 | const writer = createWriteStream(path, { encoding: 'utf8' }) 167 | writer.on('finish', resolve) 168 | writer.on('error', (error) => reject(error)) 169 | fileStream.pipe(writer) 170 | }) 171 | } 172 | 173 | function isAxiosError(error: any): error is AxiosError { 174 | return !!error.isAxiosError 175 | } 176 | 177 | export function checkRequiredArgs(args: any) { 178 | const output = args[CommandOptions.Output]! as OutputType 179 | const file = args[CommandOptions.File] as string 180 | if (output !== OutputType.Text && !file) { 181 | throw new RequiredArgsError(CommandOptions.File) 182 | } 183 | 184 | return true 185 | } 186 | -------------------------------------------------------------------------------- /src/cli/commands/plantUMLEncode.ts: -------------------------------------------------------------------------------- 1 | import { deflateRawSync } from 'zlib' 2 | 3 | export function plantUMLEncode(plantUML: string) { 4 | return encode64(deflateRawSync(plantUML, { level: 9 }).toString('binary')) 5 | } 6 | 7 | function encode64(data: string) { 8 | let r = '' 9 | for (let i = 0; i < data.length; i += 3) { 10 | if (i + 2 == data.length) { 11 | r += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), 0) 12 | } else if (i + 1 == data.length) { 13 | r += append3bytes(data.charCodeAt(i), 0, 0) 14 | } else { 15 | r += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), data.charCodeAt(i + 2)) 16 | } 17 | } 18 | return r 19 | } 20 | 21 | function append3bytes(b1: number, b2: number, b3: number) { 22 | const c1 = b1 >> 2 23 | const c2 = ((b1 & 0x3) << 4) | (b2 >> 4) 24 | const c3 = ((b2 & 0xf) << 2) | (b3 >> 6) 25 | const c4 = b3 & 0x3f 26 | let r = '' 27 | r += encode6bit(c1 & 0x3f) 28 | r += encode6bit(c2 & 0x3f) 29 | r += encode6bit(c3 & 0x3f) 30 | r += encode6bit(c4 & 0x3f) 31 | return r 32 | } 33 | 34 | function encode6bit(b: number) { 35 | if (b < 10) { 36 | return String.fromCharCode(48 + b) 37 | } 38 | b -= 10 39 | if (b < 26) { 40 | return String.fromCharCode(65 + b) 41 | } 42 | b -= 26 43 | if (b < 26) { 44 | return String.fromCharCode(97 + b) 45 | } 46 | b -= 26 47 | if (b == 0) { 48 | return '-' 49 | } 50 | if (b == 1) { 51 | return '_' 52 | } 53 | return '?' 54 | } 55 | -------------------------------------------------------------------------------- /src/cli/index.ts: -------------------------------------------------------------------------------- 1 | import yargs from 'yargs' 2 | import { generate } from './commands/generate' 3 | 4 | yargs 5 | .help() 6 | .wrap(yargs.terminalWidth()) 7 | .showHelpOnFail(true) 8 | .command(generate) 9 | .demandCommand() 10 | .recommendCommands() 11 | .strict() 12 | .version(true).argv 13 | -------------------------------------------------------------------------------- /src/cli/utilities/errors.ts: -------------------------------------------------------------------------------- 1 | import { fmtError } from './log' 2 | 3 | export class GenericError extends Error { 4 | constructor(message: string) { 5 | super(fmtError`${message}`) 6 | this.name = 'GenericError' 7 | } 8 | } 9 | 10 | export class ArgsConflictError extends Error { 11 | constructor(...args: string[]) { 12 | super(fmtError`Arguments ${args.join(', ')} can't be use jointly. You might choose between one of them.`) 13 | this.name = 'ArgsConflictError' 14 | } 15 | } 16 | 17 | export class RequiredArgsError extends Error { 18 | constructor(...args: string[]) { 19 | const argsList = args.reduce((acc, cur) => { 20 | acc += `\n - ${cur}` 21 | return acc 22 | }, '') 23 | const message = ` 24 | One argument below is required: ${argsList} 25 | ` 26 | super(fmtError`${message}`) 27 | this.name = 'RequiredArgsError' 28 | } 29 | } 30 | 31 | export class NotAFileError extends Error { 32 | constructor(path: string, arg: string) { 33 | super(fmtError`Path ${path} provided to '${arg}' is not a file.`) 34 | this.name = 'NotAFileError' 35 | } 36 | } 37 | 38 | export class FileNotExistsError extends Error { 39 | constructor(path: string, arg: string) { 40 | super(fmtError`File ${path} provided to '${arg}' does not exists.`) 41 | this.name = 'FileNotExistsError' 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/cli/utilities/log.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | 3 | enum Color { 4 | Blue = '#4285f4', 5 | Green = '#34a853', 6 | Yellow = '#fbbc05', 7 | Red = '#ea4335', 8 | } 9 | 10 | export function fmtError(message: TemplateStringsArray, ...values: any) { 11 | const finalMessage = templateStringReducer(message, values) 12 | return chalk.hex(Color.Red).bold(finalMessage) 13 | } 14 | 15 | export function fmtSuccess(message: TemplateStringsArray, ...values: any) { 16 | const finalMessage = templateStringReducer(message, values) 17 | return chalk.hex(Color.Green).bold(finalMessage) 18 | } 19 | 20 | export function fmtLog(message: TemplateStringsArray, ...values: any) { 21 | const finalMessage = templateStringReducer(message, values) 22 | return chalk.hex(Color.Blue).bold(finalMessage) 23 | } 24 | 25 | export function fmtWarn(message: TemplateStringsArray, ...values: any) { 26 | const finalMessage = templateStringReducer(message, values) 27 | return chalk.hex(Color.Yellow).bold(finalMessage) 28 | } 29 | 30 | function templateStringReducer(message: TemplateStringsArray, ...values: any) { 31 | return message.reduce((acc, cur, idx) => { 32 | acc += cur 33 | acc += values[idx] !== undefined ? values[idx] : '' 34 | return acc 35 | }, '') 36 | } 37 | -------------------------------------------------------------------------------- /src/core/common.ts: -------------------------------------------------------------------------------- 1 | export enum StringBuilderArtifact { 2 | WhiteSpace = ' ', 3 | OpenBrace = '{', 4 | CloseBrace = '}', 5 | OpenBracket = '[', 6 | closeBracket = ']', 7 | Tab = '\t', 8 | Breakline = '\n', 9 | Colons = ':', 10 | QuestionMark = '?', 11 | Asterisk = '*', 12 | DoubleDots = '..', 13 | } 14 | 15 | export enum BlockType { 16 | Entity = 'entity', 17 | Enum = 'enum', 18 | } 19 | 20 | export function buildBlockHeader(type: BlockType, name: string) { 21 | return [ 22 | type, 23 | StringBuilderArtifact.WhiteSpace, 24 | name, 25 | StringBuilderArtifact.WhiteSpace, 26 | StringBuilderArtifact.OpenBrace, 27 | ].join('') 28 | } 29 | 30 | export function addTab(text: string) { 31 | return text.padStart(text.length + 2, StringBuilderArtifact.WhiteSpace) 32 | } 33 | 34 | export function addNewLine(text: string, countOfLines: number = 1) { 35 | const builder = [text] 36 | for (let i = 0; i < countOfLines; i++) { 37 | builder.push(StringBuilderArtifact.Breakline) 38 | } 39 | return builder.join('') 40 | } 41 | -------------------------------------------------------------------------------- /src/core/entity/prismaModelToPlantUMLEntity.spec.ts: -------------------------------------------------------------------------------- 1 | import { DMMF } from '@prisma/generator-helper' 2 | import { prismaModelToPlantUMLEntity } from './prismaModelToPlantUMLEntity' 3 | 4 | describe('Entity', () => { 5 | it('should transform a prisma model to plantUML entity', () => { 6 | const model: DMMF.Model = { 7 | primaryKey: { name: '', fields: [] }, 8 | dbName: '', 9 | idFields: [], 10 | isEmbedded: false, 11 | name: 'MyModel', 12 | fields: [], 13 | uniqueFields: [], 14 | uniqueIndexes: [], 15 | } 16 | 17 | const result = prismaModelToPlantUMLEntity(model) 18 | expect(result).toMatchInlineSnapshot(` 19 | "entity MyModel { 20 | }" 21 | `) 22 | }) 23 | 24 | it('should add a question mark on optional field', () => { 25 | const model: DMMF.Model = { 26 | primaryKey: { name: '', fields: [] }, 27 | dbName: '', 28 | idFields: [], 29 | isEmbedded: false, 30 | uniqueFields: [], 31 | uniqueIndexes: [], 32 | name: 'MyModel', 33 | fields: [ 34 | { 35 | name: 'Field1', 36 | type: 'String', 37 | kind: 'scalar', 38 | hasDefaultValue: false, 39 | isList: false, 40 | isRequired: false, 41 | isUnique: false, 42 | isId: true, 43 | isGenerated: false, 44 | dbNames: [], 45 | }, 46 | ], 47 | } 48 | const result = prismaModelToPlantUMLEntity(model) 49 | 50 | expect(result).toMatchInlineSnapshot(` 51 | "entity MyModel { 52 | Field1: String? 53 | }" 54 | `) 55 | }) 56 | 57 | it('should suffix a field by an asterisk(*) when required', () => { 58 | const model: DMMF.Model = { 59 | primaryKey: { name: '', fields: [] }, 60 | dbName: '', 61 | idFields: [], 62 | isEmbedded: false, 63 | name: 'MyModel', 64 | uniqueFields: [], 65 | uniqueIndexes: [], 66 | fields: [ 67 | { 68 | name: 'Field1', 69 | type: 'String', 70 | kind: 'scalar', 71 | hasDefaultValue: false, 72 | isList: false, 73 | isRequired: true, 74 | isUnique: false, 75 | isId: true, 76 | isGenerated: false, 77 | dbNames: [], 78 | }, 79 | ], 80 | } 81 | 82 | const result = prismaModelToPlantUMLEntity(model) 83 | 84 | expect(result).toMatchInlineSnapshot(` 85 | "entity MyModel { 86 | * Field1: String 87 | }" 88 | `) 89 | }) 90 | 91 | it('should suffix a field by brackets when isList', () => { 92 | const model: DMMF.Model = { 93 | primaryKey: { name: '', fields: [] }, 94 | dbName: '', 95 | idFields: [], 96 | isEmbedded: false, 97 | name: 'MyModel', 98 | uniqueFields: [], 99 | uniqueIndexes: [], 100 | fields: [ 101 | { 102 | name: 'Field1', 103 | type: 'String', 104 | kind: 'scalar', 105 | hasDefaultValue: false, 106 | isList: true, 107 | isRequired: true, 108 | isUnique: false, 109 | isId: true, 110 | isGenerated: false, 111 | dbNames: [], 112 | }, 113 | ], 114 | } 115 | 116 | const result = prismaModelToPlantUMLEntity(model) 117 | 118 | expect(result).toMatchInlineSnapshot(` 119 | "entity MyModel { 120 | * Field1: String[] 121 | }" 122 | `) 123 | }) 124 | }) 125 | -------------------------------------------------------------------------------- /src/core/entity/prismaModelToPlantUMLEntity.ts: -------------------------------------------------------------------------------- 1 | import { DMMF } from '@prisma/generator-helper' 2 | import { StringBuilderArtifact, buildBlockHeader, BlockType, addTab, addNewLine } from '../common' 3 | 4 | export function prismaModelToPlantUMLEntity(model: DMMF.Model) { 5 | const formatField = (field: DMMF.Field) => { 6 | const builder = [] 7 | builder.push( 8 | field.isRequired ? `${StringBuilderArtifact.Asterisk + StringBuilderArtifact.WhiteSpace}` : null, 9 | field.name, 10 | StringBuilderArtifact.Colons, 11 | StringBuilderArtifact.WhiteSpace, 12 | field.type, 13 | field.isList ? `${StringBuilderArtifact.OpenBracket + StringBuilderArtifact.closeBracket}` : null, 14 | field.isRequired ? null : StringBuilderArtifact.QuestionMark, 15 | ) 16 | return builder.join('') 17 | } 18 | 19 | const builder = [] 20 | builder.push(buildBlockHeader(BlockType.Entity, model.name)) 21 | builder.push( 22 | StringBuilderArtifact.Breakline, 23 | ...model.fields 24 | .map((field) => formatField(field)) 25 | .map(addTab) 26 | .map((text) => addNewLine(text)), 27 | StringBuilderArtifact.CloseBrace, 28 | ) 29 | return builder.join('') 30 | } 31 | -------------------------------------------------------------------------------- /src/core/enum/prismaEnumToPlantUMLEnum.spec.ts: -------------------------------------------------------------------------------- 1 | import { DMMF } from '@prisma/generator-helper' 2 | import { prismaEnumToPlantUMLEnum } from './prismaEnumToPlantUMLEnum' 3 | 4 | describe('Enums', () => { 5 | it('should transform a prisma enum to a plantUML enum', () => { 6 | const prismaEnum: DMMF.DatamodelEnum = { 7 | name: 'MyEnum', 8 | values: [ 9 | { name: 'Value1', dbName: '' }, 10 | { name: 'Value2', dbName: '' }, 11 | ], 12 | } 13 | 14 | const result = prismaEnumToPlantUMLEnum(prismaEnum) 15 | expect(result).toMatchInlineSnapshot(` 16 | "enum MyEnum { 17 | Value1 18 | Value2 19 | }" 20 | `) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /src/core/enum/prismaEnumToPlantUMLEnum.ts: -------------------------------------------------------------------------------- 1 | import { DMMF } from '@prisma/generator-helper' 2 | import { buildBlockHeader, StringBuilderArtifact, BlockType, addTab } from '../common' 3 | 4 | export function prismaEnumToPlantUMLEnum(prismaEnum: DMMF.DatamodelEnum) { 5 | const builder = [] 6 | builder.push(buildBlockHeader(BlockType.Enum, prismaEnum.name)) 7 | builder.push( 8 | StringBuilderArtifact.Breakline, 9 | ...prismaEnum.values.map(({ name }) => `${addTab(name)}${StringBuilderArtifact.Breakline}`), 10 | StringBuilderArtifact.CloseBrace, 11 | ) 12 | return builder.join('') 13 | } 14 | -------------------------------------------------------------------------------- /src/core/graph/getPlantUMLGraphFromPrismaDatamodel.ts: -------------------------------------------------------------------------------- 1 | import { DMMF } from '@prisma/generator-helper' 2 | import { Graph, GraphEdge, GraphVertex, VertexKeyExtractor, EdgeKeyExtractor } from 'typescript-generic-datastructures' 3 | import { v5 as uuidv5, v1 as uuidv1 } from 'uuid' 4 | 5 | export interface Cardinality { 6 | start: string 7 | end: string 8 | } 9 | interface Relation { 10 | id: string 11 | cardinality: Cardinality 12 | } 13 | 14 | type GraphEntity = DMMF.Model | DMMF.DatamodelEnum 15 | const keyExtractor: VertexKeyExtractor = (entity) => entity.name 16 | const edgeKeyExtractor: EdgeKeyExtractor> = (edge) => edge.value.id 17 | 18 | export function getPlantUMLGraphFromPrismaDatamodel(datamodel: DMMF.Datamodel) { 19 | const RELATIONS_ID_NAMESPACE = uuidv1() 20 | const graph = new Graph(false) 21 | 22 | const modelVertices = datamodel.models.map((model) => new GraphVertex(model, keyExtractor)) 23 | const enumVertices = datamodel.enums.map((pEnum) => new GraphVertex(pEnum, keyExtractor)) 24 | 25 | modelVertices.forEach((modelVertex) => { 26 | graph.addVertex(modelVertex) 27 | }) 28 | 29 | enumVertices.forEach((enumVertex) => { 30 | graph.addVertex(enumVertex) 31 | }) 32 | 33 | const modelsWithRelations = datamodel.models.filter((model) => model.fields.some((field) => field.kind !== 'scalar')) 34 | 35 | const edges = modelsWithRelations.reduce[]>((acc, model) => { 36 | const sourceVertex = graph.getVertexByKey(model.name) 37 | 38 | const edges = model.fields 39 | .filter((field) => field.kind !== 'scalar') 40 | .map((field) => { 41 | const stack = [] 42 | if (field.isList) { 43 | stack.push('{') 44 | } else { 45 | stack.push('|') 46 | } 47 | if (field.isRequired) { 48 | stack.push('|') 49 | } else { 50 | stack.push('o') 51 | } 52 | const targetVertex = graph.getVertexByKey(field.type) 53 | if (isEnum(targetVertex.value)) { 54 | stack.push('|', '|') 55 | } else if (isModel(targetVertex.value)) { 56 | const oppositeField = targetVertex.value.fields.find((field) => field.type === model.name) 57 | if (oppositeField?.isRequired) { 58 | stack.push('|') 59 | } else { 60 | stack.push('o') 61 | } 62 | if (oppositeField?.isList) { 63 | stack.push('}') 64 | } else { 65 | stack.push('|') 66 | } 67 | } 68 | const cardinality: Relation['cardinality'] = { start: '', end: '' } 69 | while (stack.length > 0) { 70 | if (stack.length > 2) { 71 | cardinality.start += stack.pop() 72 | } else { 73 | cardinality.end += stack.pop() 74 | } 75 | } 76 | return { field, cardinality } 77 | }) 78 | .map(({ field, cardinality }) => { 79 | let idFormat = `${sourceVertex.value.name}.${field.name}.${field.relationName}` 80 | const edgeId = uuidv5(idFormat, RELATIONS_ID_NAMESPACE) 81 | const targetVertex = graph.getVertexByKey(field.type) 82 | 83 | return new GraphEdge( 84 | sourceVertex, 85 | targetVertex, 86 | { id: edgeId, cardinality }, 87 | edgeKeyExtractor, 88 | ) 89 | }) 90 | 91 | acc.push(...edges) 92 | return acc 93 | }, []) 94 | 95 | edges.forEach((edge) => { 96 | graph.addEdge(edge) 97 | }) 98 | 99 | return graph 100 | } 101 | 102 | export function isEnum(object: any): object is DMMF.DatamodelEnum { 103 | return Array.isArray(object.values) 104 | } 105 | 106 | export function isModel(object: any): object is DMMF.Model { 107 | return Array.isArray(object.fields) 108 | } 109 | -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | export { prismaEnumToPlantUMLEnum } from './enum/prismaEnumToPlantUMLEnum' 2 | export { prismaModelToPlantUMLEntity } from './entity/prismaModelToPlantUMLEntity' 3 | export { prismaToPlantUML, plantUMLRelationToString, loadPrismaSchema } from './prismaToPlantUML' 4 | -------------------------------------------------------------------------------- /src/core/prismaToPlantUML.ts: -------------------------------------------------------------------------------- 1 | import { DMMF } from '@prisma/generator-helper' 2 | import { getDMMF } from '@prisma/sdk' 3 | import { prismaEnumToPlantUMLEnum } from './enum/prismaEnumToPlantUMLEnum' 4 | import { prismaModelToPlantUMLEntity } from './entity/prismaModelToPlantUMLEntity' 5 | import { addNewLine, StringBuilderArtifact } from './common' 6 | import { 7 | getPlantUMLGraphFromPrismaDatamodel, 8 | isEnum, 9 | isModel, 10 | Cardinality, 11 | } from './graph/getPlantUMLGraphFromPrismaDatamodel' 12 | 13 | export interface PlantUMLRelation { 14 | start: DMMF.Model | DMMF.DatamodelEnum 15 | cardinality: Cardinality 16 | end: DMMF.Model | DMMF.DatamodelEnum 17 | } 18 | export function prismaToPlantUML(dmmf: DMMF.Document) { 19 | const graph = getPlantUMLGraphFromPrismaDatamodel(dmmf.datamodel) 20 | const enums = graph 21 | .getAllVertices() 22 | .filter((vertex) => isEnum(vertex.value)) 23 | .map((vertexEnum) => (isEnum(vertexEnum.value) ? prismaEnumToPlantUMLEnum(vertexEnum.value) : undefined)) 24 | const entities = graph 25 | .getAllVertices() 26 | .filter((vertex) => isModel(vertex.value)) 27 | .map((vertexModel) => (isModel(vertexModel.value) ? prismaModelToPlantUMLEntity(vertexModel.value) : undefined)) 28 | 29 | const relations = graph.getAllVertices().reduce((acc, vertex) => { 30 | vertex.getEdges().forEach((edge) => { 31 | acc.push({ 32 | start: edge.startVertex.value, 33 | cardinality: edge.value.cardinality, 34 | end: edge.endVertex.value, 35 | }) 36 | }) 37 | return acc 38 | }, []) 39 | 40 | const builder = [] 41 | builder.push(addNewLine('@startuml', 2)) 42 | builder.push(addNewLine('skinparam linetype ortho', 2)) 43 | builder.push(enums.concat(entities).join(`${StringBuilderArtifact.Breakline}${StringBuilderArtifact.Breakline}`)) // Add Enums and Entities 44 | builder.push(addNewLine('', 2)) 45 | builder.push(relations.map(plantUMLRelationToString).join(StringBuilderArtifact.Breakline)) 46 | builder.push(addNewLine('', 2), addNewLine('@enduml', 1)) 47 | 48 | return builder.join('') 49 | } 50 | 51 | export function plantUMLRelationToString(relation: PlantUMLRelation) { 52 | const builder = [] 53 | builder.push( 54 | relation.start.name, 55 | StringBuilderArtifact.WhiteSpace, 56 | relation.cardinality.start, 57 | StringBuilderArtifact.DoubleDots, 58 | relation.cardinality.end, 59 | StringBuilderArtifact.WhiteSpace, 60 | relation.end.name, 61 | ) 62 | return builder.join('') 63 | } 64 | 65 | export function loadPrismaSchema(path: string) { 66 | return getDMMF({ datamodelPath: path }) 67 | } 68 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import './cli' 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "types"], 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "lib": ["dom", "esnext"], 6 | "importHelpers": true, 7 | "declaration": true, 8 | "sourceMap": true, 9 | "rootDir": "./src", 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "strictFunctionTypes": true, 14 | "strictPropertyInitialization": true, 15 | "noImplicitThis": true, 16 | "alwaysStrict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "moduleResolution": "node", 22 | "baseUrl": "./", 23 | "paths": { 24 | "*": ["src/*", "node_modules/*"] 25 | }, 26 | "jsx": "react", 27 | "esModuleInterop": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "noEmit": false, 6 | "declarationDir": "dist/types" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.webpack.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "esModuleInterop": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import path from 'path'; 3 | import nodeExternals from 'webpack-node-externals'; 4 | import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin'; 5 | 6 | const nodeEnv = process.env.NODE_ENV || 'development'; 7 | const isProd = nodeEnv === 'production'; 8 | 9 | delete process.env.TS_NODE_PROJECT; 10 | const webpackconfiguration: webpack.Configuration = { 11 | entry: path.resolve(__dirname, 'src', 'index.ts'), 12 | target: 'node', 13 | externals: [nodeExternals()], 14 | output: { 15 | filename: 'prisma-uml.js', 16 | path: path.resolve(__dirname, 'dist'), 17 | libraryTarget: 'commonjs', 18 | sourceMapFilename: 'prisma-uml.map', 19 | }, 20 | resolve: { 21 | extensions: ['.ts', '.js', '.json'], 22 | plugins: [new TsconfigPathsPlugin({ configFile: path.resolve(__dirname, 'tsconfig.prod.json') })], 23 | }, 24 | module: { 25 | rules: [{ test: /\.(ts|js)x?$/, use: ['babel-loader', 'source-map-loader'], exclude: /node_modules/ }], 26 | }, 27 | plugins: [new webpack.BannerPlugin({ banner: '#!/usr/bin/env node', raw: true })], 28 | }; 29 | 30 | export default webpackconfiguration; 31 | --------------------------------------------------------------------------------