├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── jest.config.js ├── package.json ├── renovate.json ├── src ├── __tests__ │ └── index.test.ts ├── index.ts └── webpack-plugin.ts ├── test-app ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── components │ ├── Layout │ │ ├── Auth.tsx │ │ ├── Body.tsx │ │ ├── Header.tsx │ │ └── index.tsx │ └── Song.tsx ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages │ ├── _app.tsx │ ├── api │ │ └── songs.ts │ ├── index.tsx │ ├── songs │ │ └── [id].tsx │ └── ssr-songs │ │ └── [id].tsx ├── prisma │ ├── dev.db │ ├── migrations │ │ ├── 20200917062459-f │ │ │ ├── README.md │ │ │ ├── schema.prisma │ │ │ └── steps.json │ │ ├── 20200917063238-dev │ │ │ ├── README.md │ │ │ ├── schema.prisma │ │ │ └── steps.json │ │ ├── 20200917063432-dev │ │ │ ├── README.md │ │ │ ├── schema.prisma │ │ │ └── steps.json │ │ ├── 20200917063536-dev │ │ │ ├── README.md │ │ │ ├── schema.prisma │ │ │ └── steps.json │ │ ├── 20200917063748-dev │ │ │ ├── README.md │ │ │ ├── schema.prisma │ │ │ └── steps.json │ │ ├── 20200917070625-dev │ │ │ ├── README.md │ │ │ ├── schema.prisma │ │ │ └── steps.json │ │ ├── 20200917070756-dev │ │ │ ├── README.md │ │ │ ├── schema.prisma │ │ │ └── steps.json │ │ ├── 20200917070851-dev │ │ │ ├── README.md │ │ │ ├── schema.prisma │ │ │ └── steps.json │ │ ├── 20200917071201-dev │ │ │ ├── README.md │ │ │ ├── schema.prisma │ │ │ └── steps.json │ │ ├── 20200917071732-dev │ │ │ ├── README.md │ │ │ ├── schema.prisma │ │ │ └── steps.json │ │ ├── 20200917071947-dev │ │ │ ├── README.md │ │ │ ├── schema.prisma │ │ │ └── steps.json │ │ ├── 20200917072129-dev │ │ │ ├── README.md │ │ │ ├── schema.prisma │ │ │ └── steps.json │ │ ├── 20200917072321-dev │ │ │ ├── README.md │ │ │ ├── schema.prisma │ │ │ └── steps.json │ │ ├── 20200917072502-dev │ │ │ ├── README.md │ │ │ ├── schema.prisma │ │ │ └── steps.json │ │ ├── 20200917072749-dev │ │ │ ├── README.md │ │ │ ├── schema.prisma │ │ │ └── steps.json │ │ ├── 20200917091642-dev │ │ │ ├── README.md │ │ │ ├── schema.prisma │ │ │ └── steps.json │ │ ├── 20200929054057-dev │ │ │ ├── README.md │ │ │ ├── schema.prisma │ │ │ └── steps.json │ │ ├── 20200929063125-dev │ │ │ ├── README.md │ │ │ ├── schema.prisma │ │ │ └── steps.json │ │ └── migrate.lock │ └── schema.prisma ├── tsconfig.json └── yarn.lock ├── tsconfig.json └── yarn.lock /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: ci-cd 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | push: 7 | branches: [master] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [10.x, 12.x] 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - run: yarn --frozen-lockfile 22 | - run: yarn build 23 | 24 | publish: 25 | needs: test 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v2 29 | - name: Get all git commits and tags 30 | run: git fetch --prune --unshallow --tags 31 | - uses: actions/setup-node@v1 32 | with: 33 | node-version: 10.x 34 | - run: yarn --frozen-lockfile 35 | - run: yarn build 36 | - run: yarn -s dripip preview-or-pr 37 | env: 38 | NPM_TOKEN: ${{secrets.NPM_TOKEN}} 39 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 William Luke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NextJS Plugin for Prisma (Very Experimental) 2 | 3 | [![Here Be Dragons](http://img.shields.io/badge/%F0%9F%90%89-Here%20be%20Dragons-green?style=flat-square)](https://en.wikipedia.org/wiki/Here_be_dragons) 4 | ![npm](https://img.shields.io/npm/v/next-prisma-plugin?style=flat-square) 5 | ![npm (tag)](https://img.shields.io/npm/v/next-prisma-plugin/next?style=flat-square) 6 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) 7 | 8 | Enables Hot Reloads for the Prisma Client 9 | 10 | ### Install 11 | 12 | ``` 13 | yarn add -D next-prisma-plugin 14 | ``` 15 | 16 | ### Configuration 17 | 18 | next.config.js 19 | 20 | ```js 21 | const withPrismaPlugin = require('next-prisma-plugin') 22 | 23 | module.exports = withPrismaPlugin() 24 | ``` 25 | 26 | # TODO 27 | 28 | - [ ] Windows Support 29 | - [ ] Tests 30 | - [ ] Automatic page reloads 31 | 32 | # Contributing 33 | 34 | ``` 35 | cd next-prisma-plugin 36 | yarn 37 | cd test-app && yarn 38 | cd .. 39 | yarn dev 40 | ``` 41 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/src'], 3 | testMatch: ['**/__tests__/**/*.+(ts|tsx|js)', '**/?(*.)+(spec|test).+(ts|tsx|js)'], 4 | transform: { 5 | '^.+\\.(ts|tsx)$': 'ts-jest', 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-prisma-plugin", 3 | "version": "0.0.0-dripip", 4 | "main": "dist/index.js", 5 | "license": "MIT", 6 | "author": "William Luke ", 7 | "description": "Next.js plugin to enable hot reloading of the Prisma Client", 8 | "files": [ 9 | "dist" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/williamluke4/next-prisma-plugin.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/williamluke4/next-prisma-plugin/issues" 17 | }, 18 | "homepage": "https://github.com/williamluke4/next-prisma-plugin#readme", 19 | "keywords": [ 20 | "next", 21 | "next.js", 22 | "plugin", 23 | "prisma", 24 | "reload", 25 | "hot", 26 | "webpack" 27 | ], 28 | "scripts": { 29 | "watch": "tsc -w", 30 | "dev:test-app": "cd test-app && yarn dev", 31 | "dev": "yarn watch & yarn dev:test-app", 32 | "build": "rimraf ./dist && tsc", 33 | "stable": "dripip stable", 34 | "test": "jest", 35 | "test:watch": "jest --watch" 36 | }, 37 | "dependencies": {}, 38 | "devDependencies": { 39 | "@prisma-labs/prettier-config": "0.1.0", 40 | "@types/jest": "^26.0.15", 41 | "@types/webpack": "4.41.22", 42 | "dripip": "0.10.0", 43 | "eslint": "7.10.0", 44 | "eslint-config-prettier": "6.12.0", 45 | "jest": "^26.6.0", 46 | "prettier": "2.1.2", 47 | "prettier-eslint": "11.0.0", 48 | "rimraf": "^3.0.2", 49 | "ts-jest": "^26.4.1", 50 | "typescript": "4.0.3", 51 | "webpack": "4.44.2" 52 | }, 53 | "prettier": "@prisma-labs/prettier-config" 54 | } 55 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import withPrismaPlugin from '../index' 2 | 3 | describe('withPrismaPlugin', () => { 4 | it('should keep nextConfig object properties in other phases', () => { 5 | const nextConfig = { foo: 'bar' } 6 | 7 | const newConfig = withPrismaPlugin(nextConfig)('phase-export', {}) 8 | expect(newConfig).toEqual(nextConfig) 9 | }) 10 | 11 | it('should call nextConfig function in other phases', () => { 12 | const nextConfig = () => ({ foo: 'bar' }) 13 | 14 | const newConfig = withPrismaPlugin(nextConfig)('phase-export', {}) 15 | expect(newConfig).toEqual(nextConfig()) 16 | }) 17 | 18 | it('should add webpack property', () => { 19 | const nextConfig = { foo: 'bar' } 20 | 21 | const newConfig = withPrismaPlugin(nextConfig)('phase-development-server', {}) 22 | expect(newConfig).toHaveProperty('webpack') 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import PrismaClientReloaderWebpackPlugin from './webpack-plugin' 2 | const PATH_DELIMITER = '[\\\\/]' // match 2 antislashes or one slash 3 | 4 | /** 5 | * On Windows, the Regex won't match as Webpack tries to resolve the 6 | * paths of the modules. So we need to check for \\ and / 7 | */ 8 | // @ts-ignore 9 | const safePath = (module) => module.split(/[\\\/]/g).join(PATH_DELIMITER) 10 | 11 | /** 12 | * Actual Next.js plugin 13 | */ 14 | 15 | const withPrismaPlugin = (nextConfig = {}) => ( 16 | phase: 'phase-export' | 'phase-production-build' | 'phase-production-server' | 'phase-development-server', 17 | thing: any 18 | ) => { 19 | if (phase === 'phase-development-server') { 20 | return Object.assign({}, nextConfig, { 21 | webpack(config: any, options: any) { 22 | const ignore = ['.prisma/client', '@prisma/client'] 23 | 24 | // const includes = ignore.map(module => (new RegExp(`${module}(?!.*node_modules)`))); 25 | const excludes = [new RegExp(`node_modules(?!/(${ignore.join('|')})(?!.*node_modules))`)] 26 | const ignored = config.watchOptions.ignored 27 | .filter((ignored: string) => ignored !== '**/node_modules/**') 28 | .concat(excludes) 29 | return Object.assign(config, { 30 | plugins: [...config.plugins, new PrismaClientReloaderWebpackPlugin()], 31 | watchOptions: { 32 | ...config.watchOptions, 33 | ignored, 34 | }, 35 | }) 36 | }, 37 | }) 38 | } 39 | let internalConfigObj = typeof nextConfig === 'function' ? nextConfig(phase, thing) : nextConfig 40 | return internalConfigObj 41 | } 42 | 43 | module.exports = withPrismaPlugin 44 | export default withPrismaPlugin 45 | -------------------------------------------------------------------------------- /src/webpack-plugin.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { Compiler } from 'webpack' 3 | 4 | const PLUGIN_NAME = 'PrismaClientReloaderWebpackPlugin' 5 | 6 | const PATHS = ['.prisma/client/index.js', '@prisma/client/index.js', '@prisma/client/runtime/index.js'] 7 | const TESTS = [ 8 | /\.prisma\/client\/index.js/, 9 | /@prisma\/client\/index.js/, 10 | /@prisma\/client\/runtime\/index.js/, 11 | ] 12 | class PrismaClientReloaderWebpackPlugin { 13 | apply(compiler: Compiler) { 14 | const clientPaths = PATHS.map(p => path.join(compiler.context, 'node_modules', p)) 15 | compiler.hooks.afterEmit.tapAsync(PLUGIN_NAME, (compilation, callback) => { 16 | const moduleIds = Object.keys(require.cache) 17 | TESTS.forEach(regex => { 18 | moduleIds.forEach(moduleId => { 19 | if (regex.test(moduleId)) { 20 | // console.log(moduleId) 21 | delete require.cache[moduleId] 22 | } 23 | }) 24 | }) 25 | callback() 26 | }) 27 | 28 | compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => { 29 | if (compilation.fileDependencies) { 30 | clientPaths.forEach(p => { 31 | compilation.fileDependencies.add(p) 32 | }) 33 | } 34 | // @ts-ignore 35 | else if (compilation.compilationDependencies) { 36 | clientPaths.forEach(p => { 37 | // @ts-ignore 38 | compilation.compilationDependencies.add(p) 39 | }) 40 | } 41 | }) 42 | } 43 | } 44 | export default PrismaClientReloaderWebpackPlugin 45 | -------------------------------------------------------------------------------- /test-app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['next-prisma'], 3 | 4 | rules: { 5 | 'next-plugin/next-plugin': 'error' 6 | // "@ts-gql/test-rule": "error", 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /test-app/.gitignore: -------------------------------------------------------------------------------- 1 | .vercel 2 | .next 3 | node_modules 4 | .DS_Store 5 | .env 6 | -------------------------------------------------------------------------------- /test-app/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'always', 3 | singleQuote: true, 4 | tabWidth: 2, 5 | trailingComma: 'none' 6 | }; 7 | -------------------------------------------------------------------------------- /test-app/components/Layout/Auth.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Flex, Text } from '@chakra-ui/core'; 2 | import React from 'react'; 3 | 4 | export default function Auth() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /test-app/components/Layout/Body.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Body: React.FC = (props) => { 4 | return ( 5 |
14 | {props.children} 15 |
16 | ); 17 | }; 18 | 19 | export default Body; 20 | -------------------------------------------------------------------------------- /test-app/components/Layout/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Auth from './Auth'; 3 | 4 | export default function Header() { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /test-app/components/Layout/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Body from './Body'; 3 | import Header from './Header'; 4 | 5 | export interface ILayoutProps {} 6 | 7 | const Layout: React.FC = (props) => { 8 | return ( 9 |
10 |
11 | {props.children} 12 |
13 | ); 14 | }; 15 | export default Layout; 16 | -------------------------------------------------------------------------------- /test-app/components/Song.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, Heading, Image, ListItem, Stack, Text } from '@chakra-ui/core'; 2 | import NextLink from 'next/link'; 3 | 4 | const Song = ({ id, name, artist, albumCoverUrl }) => ( 5 | 12 | 13 | 14 | 15 | {name} 24 | 25 | 26 | {name} 27 | 28 | {artist.name} 29 | 30 | 31 | 32 | 33 | ); 34 | 35 | export default Song; 36 | -------------------------------------------------------------------------------- /test-app/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /test-app/next.config.js: -------------------------------------------------------------------------------- 1 | const withPrismaPlugin = require('../dist'); 2 | 3 | module.exports = withPrismaPlugin(); 4 | -------------------------------------------------------------------------------- /test-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prisma-next", 3 | "description": "Static site with Next.js 9.3 and Prisma.", 4 | "version": "0.0.0-dripip", 5 | "author": { 6 | "name": "William Luke", 7 | "email": "luke@prisma.io" 8 | }, 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/williamluke4/prisma-next.git" 13 | }, 14 | "engines": { 15 | "node": ">=10.0.0" 16 | }, 17 | "scripts": { 18 | "dev": "next", 19 | "build": "next build", 20 | "start": "next start", 21 | "db": "prisma studio --experimental", 22 | "db-save": "prisma migrate save --experimental", 23 | "db-up": "prisma migrate up --experimental", 24 | "push": "prisma migrate save --experimental --name Dev && prisma migrate up --experimental && prisma generate", 25 | "remove-migrations": "sqlite3 prisma/dev.db 'delete from _Migration'", 26 | "generate": "prisma generate" 27 | }, 28 | "dependencies": { 29 | "@chakra-ui/core": "0.8.0", 30 | "@emotion/core": "10.0.35", 31 | "@emotion/styled": "10.0.27", 32 | "@prisma/client": "2.7.0", 33 | "chokidar": "^3.4.2", 34 | "download": "^8.0.0", 35 | "emotion-theming": "10.0.27", 36 | "klona": "^2.0.4", 37 | "next": "^9.5.3", 38 | "next-prisma-plugin": "file:../", 39 | "react": "16.13.1", 40 | "react-dom": "16.13.1", 41 | "swr": "^0.3.2", 42 | "tempy": "^0.7.0" 43 | }, 44 | "devDependencies": { 45 | "@prisma/cli": "2.7.0", 46 | "@types/download": "^6.2.4", 47 | "@types/next-auth": "^3.1.7", 48 | "@types/node": "^14.10.1", 49 | "typescript": "^4.0.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test-app/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { CSSReset, ThemeProvider } from '@chakra-ui/core'; 2 | import { css, Global } from '@emotion/core'; 3 | import Head from 'next/head'; 4 | import React from 'react'; 5 | import Layout from '../components/Layout'; 6 | 7 | 8 | const MyApp = ({ Component, pageProps }) => { 9 | return ( 10 | 11 | 12 | Next.js 9.5 + Prisma 13 | 17 | 18 | 19 | 20 | 21 | 22 | 29 | 30 | ); 31 | }; 32 | 33 | export default MyApp; 34 | -------------------------------------------------------------------------------- /test-app/pages/api/songs.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | const prisma = new PrismaClient(); 3 | 4 | export default async function handle(req, res) { 5 | const songs = await prisma.song.findMany({ include: { artist: true } }); 6 | return res.json(songs); 7 | } 8 | -------------------------------------------------------------------------------- /test-app/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Heading, List } from '@chakra-ui/core'; 2 | import useSWR from 'swr'; 3 | import Song from '../components/Song'; 4 | 5 | const fetcher = (url) => fetch(url).then((res) => res.json()); 6 | 7 | export default () => { 8 | const { data: songs } = useSWR('/api/songs', fetcher); 9 | return ( 10 | <> 11 | 12 | My Songs 13 | 14 | 15 | {songs && songs.map((song) => ( 16 | 17 | ))} 18 | 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /test-app/pages/songs/[id].tsx: -------------------------------------------------------------------------------- 1 | import { Box, Button, Heading, Text } from '@chakra-ui/core'; 2 | import NextLink from 'next/link'; 3 | import { PrismaClient } from '@prisma/client'; 4 | const prisma = new PrismaClient(); 5 | 6 | export async function getStaticProps({ params }) { 7 | const song = await prisma.song.findOne({ 8 | include: { artist: true }, 9 | where: { 10 | id: Number(params.id) 11 | } 12 | }); 13 | 14 | return { 15 | props: { 16 | song 17 | } 18 | }; 19 | } 20 | 21 | export async function getStaticPaths() { 22 | const songs = await prisma.song.findMany(); 23 | 24 | return { 25 | paths: songs.map((song) => ({ 26 | params: { 27 | id: song.id.toString() 28 | } 29 | })), 30 | fallback: false 31 | }; 32 | } 33 | 34 | export default ({ song }) => ( 35 | 36 | {song.name} 37 | 38 | {song.artist.name} 39 | 40 |