├── .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 | [](https://www.npmjs.com/package/prisma-uml) [](https://npm-stat.com/charts.html?package=prisma-uml) [](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 | [](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 |
--------------------------------------------------------------------------------