├── .eslintignore ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ └── PR-CI.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── commitlint.config.cjs ├── package.json ├── pnpm-lock.yaml ├── src ├── cli │ └── index.ts ├── consts.ts ├── helpers │ ├── createProject.ts │ ├── initGit.ts │ ├── installPackages.ts │ ├── logNextSteps.ts │ ├── scaffoldProject.ts │ └── selectBoilerplate.ts ├── index.ts ├── installers │ ├── index.ts │ ├── next-auth.ts │ ├── prisma.ts │ ├── tailwind.ts │ └── trpc.ts └── utils │ ├── execAsync.ts │ ├── getT3Version.ts │ ├── getUserPkgManager.ts │ ├── logger.ts │ ├── parseNameAndPath.ts │ ├── renderTitle.ts │ ├── runPkgManagerInstall.ts │ └── validateAppName.ts ├── template ├── addons │ ├── next-auth │ │ ├── api-handler-prisma.ts │ │ ├── api-handler.ts │ │ └── restricted.ts │ ├── prisma │ │ ├── auth-schema.prisma │ │ ├── client.ts │ │ ├── sample-api.ts │ │ └── schema.prisma │ ├── tailwind │ │ ├── globals.css │ │ ├── postcss.config.js │ │ └── tailwind.config.js │ └── trpc │ │ ├── api-handler.ts │ │ ├── auth-context.ts │ │ ├── auth-index-router.ts │ │ ├── auth-prisma-context.ts │ │ ├── auth-router.ts │ │ ├── base-context.ts │ │ ├── example-prisma-router.ts │ │ ├── example-router.ts │ │ ├── index-router.ts │ │ ├── prisma-context.ts │ │ └── utils.ts ├── base │ ├── .env-example │ ├── .eslintrc.json │ ├── README.md │ ├── _gitignore │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── pages │ │ │ ├── _app.tsx │ │ │ └── index.tsx │ │ └── styles │ │ │ └── globals.css │ └── tsconfig.json └── page-studs │ ├── README.md │ ├── _app │ ├── with-auth-trpc.tsx │ ├── with-auth.tsx │ └── with-trpc.tsx │ └── index │ ├── with-auth-trpc-tw.tsx │ ├── with-auth-trpc.tsx │ ├── with-trpc-tw.tsx │ ├── with-trpc.tsx │ └── with-tw.tsx └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | dist/ 4 | template/ -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/eslintrc", 3 | "root": true, 4 | "parser": "@typescript-eslint/parser", 5 | "parserOptions": { 6 | "project": "./tsconfig.json" 7 | }, 8 | "plugins": ["@typescript-eslint", "import"], 9 | "extends": [ 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 12 | "plugin:@typescript-eslint/strict", 13 | "plugin:import/recommended", 14 | "plugin:import/typescript", 15 | "prettier" 16 | ], 17 | "env": { 18 | "node": true, 19 | "es6": true 20 | }, 21 | "rules": { 22 | "no-shadow": 1, 23 | "import/no-named-as-default": 0, 24 | "import/no-named-as-default-member": 0, 25 | "import/order": [ 26 | "error", 27 | { 28 | "groups": [ 29 | "type", 30 | "builtin", 31 | "external", 32 | "internal", 33 | ["parent", "sibling", "index"], 34 | "unknown" 35 | ], 36 | "newlines-between": "never", 37 | "alphabetize": { 38 | "order": "asc", 39 | "caseInsensitive": true 40 | }, 41 | "warnOnUnassignedImports": true 42 | } 43 | ] 44 | }, 45 | "settings": { 46 | "import/resolver": { 47 | "typescript": { 48 | "alwaysTryTypes": true, 49 | "project": "./tsconfig.json" 50 | } 51 | } 52 | }, 53 | "ignorePatterns": ["node_modules/", "build/", "dist/"] 54 | } 55 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Run this '...' 16 | 2. See error 17 | 18 | **Expected behavior** 19 | A clear and concise description of what you expected to happen. 20 | 21 | **Screenshots** 22 | If applicable, add screenshots to help explain your problem. 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # [Short title] 2 | 3 | - [ ] I reviewed linter warnings + errors, resolved formatting, types and other issues related to my work 4 | - [ ] The PR title follows the convention we established [conventional-commit](https://www.conventionalcommits.org/en/v1.0.0/) 5 | - [ ] I performed a functional test on my final commit 6 | 7 | --- 8 | 9 | _[Short summary/description of story/feature/bugfix]_ 10 | 11 | --- 12 | 13 | ## Screenshots 14 | 15 | _[Screenshots]_ 16 | 17 | 💯 18 | -------------------------------------------------------------------------------- /.github/workflows/PR-CI.yml: -------------------------------------------------------------------------------- 1 | # this workflow will run on every pr to make sure the project is following the guidelines 2 | 3 | name: PR-CI 4 | 5 | on: 6 | pull_request: 7 | branches: "*" 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | jobs: 13 | lint: 14 | name: Run ESLint 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: pnpm/action-setup@v2 19 | with: 20 | version: 7.2.1 21 | - uses: actions/setup-node@v3 22 | with: 23 | node-version: 16 24 | cache: 'pnpm' 25 | - run: pnpm install 26 | - run: pnpm lint:check 27 | 28 | lint-pr: 29 | name: Lint the PR title 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: amannn/action-semantic-pull-request@v4 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | prettier: 37 | name: Run Prettier Check 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v2 41 | - uses: pnpm/action-setup@v2 42 | with: 43 | version: 7.2.1 44 | - uses: actions/setup-node@v3 45 | with: 46 | node-version: 16 47 | cache: 'pnpm' 48 | - run: pnpm install 49 | - run: pnpm format:check 50 | 51 | tsc: 52 | name: Run Type Check 53 | runs-on: ubuntu-latest 54 | steps: 55 | - uses: actions/checkout@v2 56 | - uses: pnpm/action-setup@v2 57 | with: 58 | version: 7.2.1 59 | - uses: actions/setup-node@v3 60 | with: 61 | node-version: 16 62 | cache: 'pnpm' 63 | - run: pnpm install 64 | - run: pnpm typecheck 65 | 66 | build: 67 | name: Build the project 68 | runs-on: ubuntu-latest 69 | steps: 70 | - uses: actions/checkout@v2 71 | - uses: pnpm/action-setup@v2 72 | with: 73 | version: 7.2.1 74 | - uses: actions/setup-node@v3 75 | with: 76 | node-version: 16 77 | cache: 'pnpm' 78 | - run: pnpm install 79 | - run: pnpm build 80 | 81 | build-cardano-dapp: 82 | name: Build an outputted cardano dapp 83 | runs-on: ubuntu-latest 84 | steps: 85 | - uses: actions/checkout@v2 86 | - uses: pnpm/action-setup@v2 87 | with: 88 | version: 7.2.1 89 | - uses: actions/setup-node@v3 90 | with: 91 | node-version: 16 92 | cache: "pnpm" 93 | - run: pnpm install 94 | - run: pnpm build 95 | # has to be scaffolded outside the CLI project so that no lint/tsconfig are leaking 96 | # through. this way it ensures that it is the app's configs that are being used 97 | - run: pnpm start -y ../ci-test-app 98 | - run: cd ../ci-test-app && pnpm build 99 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### STANDARD GIT IGNORE FILE ### 2 | 3 | # DEPENDENCIES 4 | node_modules/ 5 | /.pnp 6 | .pnp.js 7 | 8 | # TESTING 9 | /coverage 10 | *.lcov 11 | .nyc_output 12 | 13 | # BUILD 14 | build/ 15 | public/build/ 16 | dist/ 17 | generated/ 18 | 19 | # ENV FILES 20 | .env 21 | .env.local 22 | .env.development.local 23 | .env.test.local 24 | .env.production.local 25 | 26 | # LOGS 27 | logs 28 | *.log 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | 33 | # MISC 34 | .idea 35 | .turbo/ 36 | .cache/ 37 | .next/ 38 | .nuxt/ 39 | tmp/ 40 | temp/ 41 | .eslintcache 42 | 43 | # MAC 44 | ._* 45 | .DS_Store 46 | Thumbs.db -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --config commitlint.config.cjs --edit "$1" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build/ 3 | dist/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "arrowParens": "always", 4 | "printWidth": 80, 5 | "singleQuote": false, 6 | "jsxSingleQuote": false, 7 | "semi": true, 8 | "trailingComma": "all", 9 | "tabWidth": 2 10 | } 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [0.1.6](https://github.com/CardanoGoat-io/create-cardano-dapp/compare/v0.1.5...v0.1.6) (2022-09-24) 6 | 7 | ### [0.1.5](https://github.com/CardanoGoat-io/create-cardano-dapp/compare/v0.1.4...v0.1.5) (2022-07-12) 8 | 9 | ### Features 10 | 11 | - adding lucid and env examples ([e6af423](https://github.com/CardanoGoat-io/create-cardano-dapp/commit/e6af4233d5d532eea934259a2ae2122e571db23b)) 12 | 13 | ### Bug Fixes 14 | 15 | - adding cardano dapp terminology and removing javascript option ([ab81e94](https://github.com/CardanoGoat-io/create-cardano-dapp/commit/ab81e949e56d47c1829e63a13b4e3c6bde966210)) 16 | 17 | ### [0.1.4](https://github.com/CardanoGoat-io/create-cardano-dapp/compare/v0.1.3...v0.1.4) (2022-07-12) 18 | 19 | ### Bug Fixes 20 | 21 | - removing javascript option ([5523abd](https://github.com/CardanoGoat-io/create-cardano-dapp/commit/5523abdcdf820817a84092f95440d4faa8806a3e)) 22 | 23 | ### [0.1.3](https://github.com/CardanoGoat-io/create-cardano-dapp/compare/v0.1.2...v0.1.3) (2022-07-11) 24 | 25 | ### Features 26 | 27 | - update icon ([ac655c0](https://github.com/CardanoGoat-io/create-cardano-dapp/commit/ac655c09bf53046484b774ee5f5c471e4dddf551)) 28 | 29 | ### [0.1.2](https://github.com/CardanoGoat-io/create-cardano-dapp/compare/v0.1.1...v0.1.2) (2022-07-11) 30 | 31 | ### Features 32 | 33 | - updating stack data and adding readme ([7463d0f](https://github.com/CardanoGoat-io/create-cardano-dapp/commit/7463d0fde68ab750fe737e6e4f1ade6186e2f669)) 34 | 35 | ### Bug Fixes 36 | 37 | - github yaml and contributing page ([6f4b4a8](https://github.com/CardanoGoat-io/create-cardano-dapp/commit/6f4b4a8e6956b575107764140fde96b1bbb6a14f)) 38 | 39 | ### 0.1.1 (2022-07-11) 40 | 41 | ### Features 42 | 43 | - src typescript file that is missing cardano and wasm ([b08eebd](https://github.com/CardanoGoat-io/create-cardano-dapp/commit/b08eebded217362f377ed20b476ad05469463ad2)) 44 | - templating files for prisma nextjs auth tailwind trpc and base react project ([9024ceb](https://github.com/CardanoGoat-io/create-cardano-dapp/commit/9024ceb12920a13f972f5f10b7e384e1e069befa)) 45 | 46 | # create-cardano-dapp 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to create-cardano-dapp 2 | 3 | ## Contribution guidelines 4 | 5 | - Be respectful, civil, and open-minded. 6 | - Before opening a new pull request, try searching through the [issue tracker](https://github.com/CardanoGoat-io/create-cardano-dapp/issues) for known issues or fixes. 7 | 8 | ## How to contribute 9 | 10 | 1. [Open an issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/creating-an-issue) and describe the problem. 11 | 2. [Fork this repository](https://docs.github.com/en/get-started/quickstart/fork-a-repo) to your own GitHub account, then [clone the repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) to your machine. 12 | 3. Follow the [README](https://github.com/cardanogoat-io/create-cardano-dapp#readme) to install the project. 13 | 4. Create a new branch and implement your changes. 14 | 15 | - Note: We use `commitlint` to autoupdate the changelog and versioning so make sure you follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) guidelines when making commits. Do not manually update the [changelog](./CHANGELOG.md) and version in the [package.json](./package.json). 16 | 17 | 4. Open a pull request! All pull requests must be made to the `main` branch. 18 | 19 | Thanks to Next.js. This contribution guide is inspired by the [Next.js contribution guide.](https://github.com/vercel/next.js/blob/canary/contributing.md) 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 CardanoGoat-io 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 |
2 | 3 | 4 | 5 | Interactive CLI to quickly set up an opinionated, full stack, Cardano NextJS project. 6 | 7 |
8 |
9 |
10 | 11 | [![PRs-Welcome][contribute-image]][contribute-url] [![NPM version][npm-image]][npm-url] 12 | [![Downloads][downloads-image]][npm-url] 13 | 14 |
15 | 16 |
17 | 18 | # Usage 19 | 20 | **npm** 21 | 22 | ```bash 23 | npx create-cardano-dapp@latest 24 | ``` 25 | 26 | **yarn** 27 | 28 | ```bash 29 | yarn create cardano-dapp 30 | ``` 31 | 32 | **pnpm** 33 | 34 | ```bash 35 | pnpm dlx create-cardano-dapp@latest 36 | ``` 37 | 38 |
39 | 40 | 41 | 42 | ## Table of contents 43 | 44 | - About 45 | - T3 Axioms 46 | - Dev/Contributor Setup 47 | - Contributors 48 | 49 |
50 | 51 | # What is this? Some kinda template? 52 | 53 | Kind of. We love all of the technologies that create-cardano-dapp includes, but we do NOT believe every project needs all of them. 54 | 55 | We made `create-cardno-dapp` to do **one thing** - simplify the complex boilerplate around the core Cardano Stack tech without compromising the modularity of the pieces. 56 | 57 | This is **NOT** an all-inclusive template. We **expect you to bring your own libraries as well**. 58 | 59 | We are selective about the packages we have included. We don't add libraries that are as simple as an `npm install zustand`. _If you cut an issue asking us to add your preferred libraries, we will make fun of you._ 60 | 61 | ## What is the Cardano Stack? 62 | 63 | The _"Cardano Stack"_ is a web development stack made by [Cardano Goat](https://twitter.com/Cardano_G_O_A_T), focused on **simplicity**, **modularity**, and **full-stack typesafety**. 64 | 65 | 75 | 76 | It consists of 77 | 78 | - [Next.js](https://nextjs.org) 79 | - [lucid-cardano](https://github.com/Berry-Pool/lucid) 80 | - [tRPC](https://trpc.io) 81 | - [Tailwind CSS](https://tailwindcss.com) 82 | - [TypeScript](https://typescriptlang.org) 83 | - [Prisma](https://prisma.io) 84 | - [NextAuth.js](https://next-auth.js.org) 85 | 86 |
87 | 88 |
89 | 90 | # T3 Axioms 91 | 92 | I'll be frank - this is an _opinionated project_. We share a handful of core beliefs around building, and we treat them as the basis for our decisions. 93 | 94 | ## 1. Solve Problems 95 | 96 | It's easy to fall in the trap of "adding everything" - we explicitly _don't_ want to do that. Everything added to `create-cardano-dapp` should solve a _specific_ problem that exists within the core technologies included. 97 | 98 | This means we **won't** add things like state libraries (zustand, redux), but we **will** add things like NextAuth.js and integrate it with Prisma and tRPC for you 99 | 100 | ## 2. Bleed Responsibly 101 | 102 | We love our bleeding edge tech. The amount of speed and, honestly, _fun_ that comes out of new shit is really cool. We think it's important to **bleed responsibly**, using riskier tech in the less risky parts. 103 | 104 | This means we **wouldn't** bet on risky new database tech (SQL is great!) - but we **happily** bet on tRPC (it's just functions, moving off it is trivial). 105 | 106 | ## 3. Typesafety Isn't Optional 107 | 108 | Two of the three T's are typesafe (Typescript, tRPC). We take typesafety seriously in these parts. Any decision that compromises the full-stack typesafe nature of `create-cardano-dapp` is a decision that should be made in a different project. 109 | 110 |
111 | 112 |
113 | 114 | # Dev/Contributor Setup 115 | 116 | Read the [Contributing guidelines](CONTRIBUTING.md) 117 | 118 | To install dependencies 119 | 120 | ```bash 121 | # Install pnpm 122 | npm install -g pnpm 123 | # Install dependencies 124 | pnpm install 125 | # Initial build 126 | pnpm run build 127 | # Start the package locally 128 | pnpm start 129 | ``` 130 | 131 |
132 | 133 | # Contributors 134 | 135 | We 💖 contributors! Feel free to contribute to this project 136 | 137 | 138 | 139 | 140 | 141 | Made with [contrib.rocks](https://contrib.rocks). 142 | 143 |

144 | 145 | Powered by vercel 146 | 147 |

148 | 149 | ### Other Recommendations 150 | 151 | We recognize that Next.js and TypeScript don't solve every problem. While we encourage you to use the primitives they provide as often as you can, there are other tools that will help simplify and improve your developer experience. 152 | 153 | ### UI Essentials 154 | 155 | Tailwind CSS - for your styles 156 | I hated the idea of Tailwind. Then I tried it. 157 | 158 | It seems like Bootstrap. I promise it is not Bootstrap. It enables a "flow state" in UI dev I didn't know was achievable. (and yes we used it here) 159 | 160 | ### Data Management 161 | 162 | React Query - for your client 163 | This little library handles all your async React needs from data fetching to IO management. Hard to imagine doing React dev without it now. Might be able to replace your state management library too. 164 | 165 | Prisma.io - for your DB 166 | Prisma is to SQL what TypeScript is to JS. Never thought I'd love an ORM, but man, trust me on this one. 167 | 168 | tRPC - for defining and consuming APIs 169 | tRPC delivers on GraphQL's promise of seamless client development against a typesafe server without all of the boilerplate. It's a clever abuse of TypeScript that provides an incredible dev experience. 170 | 171 | ### Analytics 172 | 173 | Plausible - for user data 174 | Need analytics? Plausible is one of the quickest ways to get them. Super minimal. It even has a simple plugin for Next.js. 175 | 176 | ### Authentication 177 | 178 | NextAuth.js - for authentication 179 | Man this library makes auth easy. No ownership compromise, hooks up to your DB directly. So good that I ported two projects to Next to use it. 180 | 181 | Note: Does not support React Native directly. Yet. 😉 182 | 183 | ### Deployments, Infrastructure, Databases and CI 184 | 185 | Vercel - for hosting your website 186 | Vercel took the hell of web deployments and made it a set-and-forget GitHub integration. We've scaled to hundreds of thousands of users without issue. AWS-powered, just a way better interface :) 187 | 188 | PlanetScale - for databases without the worry 189 | PlanetScale is the best "serverless database platform" I've used by far. Insane scale, great developer experience, fantastic pricing. If you're using sql (and hopefully Prisma), this is hard to beat. 190 | 191 | Railway - for hosting your infra 192 | "Modern Heroku". Easiest way to get a real server up and running. If Vercel and PlanetScale aren't enough, Railway probably is. Point it at a GitHub repo and go. 193 | 194 | ### State Management 195 | 196 | Editor's Note: State management libraries can be great, but often aren't necessary. Start with React Query and built-in React state, reach for these when you need more 197 | 198 | Zustand - for never using Redux again 199 | The "modern, simple Redux" you didn't know you needed. Poimandres can always be trusted. I have built everything from video call apps to games to servers with this little library 200 | 201 | Jotai - for never using Context again 202 | For a more atomic approach, Jotai is hard to beat. Also by Poimandres , Jotai lets you define singletons that feel like global useState. Great option for stateful behaviors that don't need a state machine just yet 203 | 204 | [downloads-image]: https://img.shields.io/npm/dm/create-cardano-dapp?color=364fc7&logoColor=364fc7 205 | [npm-url]: https://www.npmjs.com/package/create-cardano-dapp 206 | [npm-image]: https://img.shields.io/npm/v/create-cardano-dapp?color=0b7285&logoColor=0b7285 207 | [contribute-url]: https://github.com/CardanoGoat-io/create-cardano-dapp/blob/main/CONTRIBUTING.md 208 | [contribute-image]: https://img.shields.io/badge/PRs-welcome-blue.svg 209 | -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | rules: { 4 | "type-enum": [ 5 | 2, 6 | "always", 7 | [ 8 | "feat", 9 | "fix", 10 | "docs", 11 | "chore", 12 | "style", 13 | "refactor", 14 | "ci", 15 | "test", 16 | "perf", 17 | "revert", 18 | ], 19 | ], 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-cardano-dapp", 3 | "version": "0.1.6", 4 | "description": "Create Cardano web application that works with the Cardano distributed network.", 5 | "author": "Cardano Goat ", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/CardanoGoat-io/create-cardano-dapp.git" 10 | }, 11 | "keywords": [ 12 | "create-cardano-dapp", 13 | "init.tips", 14 | "next.js", 15 | "tailwind", 16 | "tRPC", 17 | "typescript" 18 | ], 19 | "type": "module", 20 | "exports": "./dist/index.js", 21 | "bin": { 22 | "create-cardano-dapp": "./dist/index.js" 23 | }, 24 | "engines": { 25 | "node": ">=14.16" 26 | }, 27 | "scripts": { 28 | "typecheck": "tsc", 29 | "build": "tsup src/index.ts --format esm --clean --sourcemap --minify --metafile", 30 | "dev": "tsup src/index.ts --format esm --watch --clean --onSuccess \"node dist/index.js\"", 31 | "start": "node dist/index.js", 32 | "lint": "eslint src/ --fix", 33 | "lint:check": "eslint src/", 34 | "format": "prettier --write \"**/*.{ts,tsx,md,mdx,json}\"", 35 | "format:check": "prettier --check \"**/*.{ts,tsx,md,mdx,json}\"", 36 | "prepare": "husky install", 37 | "release": "standard-version", 38 | "pub:beta": "npm run build && npm publish --tag beta", 39 | "pub:release": "npm run build && npm publish" 40 | }, 41 | "dependencies": { 42 | "chalk": "5.0.1", 43 | "commander": "^9.3.0", 44 | "figlet": "^1.5.2", 45 | "fs-extra": "^10.1.0", 46 | "gradient-string": "^2.0.1", 47 | "inquirer": "^9.0.0", 48 | "ora": "6.1.1" 49 | }, 50 | "devDependencies": { 51 | "@commitlint/cli": "^17.0.3", 52 | "@commitlint/config-conventional": "^17.0.3", 53 | "@types/figlet": "^1.5.4", 54 | "@types/fs-extra": "^9.0.13", 55 | "@types/gradient-string": "^1.1.2", 56 | "@types/inquirer": "^8.2.1", 57 | "@types/node": "^18.0.0", 58 | "@typescript-eslint/eslint-plugin": "^5.30.0", 59 | "@typescript-eslint/parser": "^5.30.0", 60 | "eslint": "^8.18.0", 61 | "eslint-config-prettier": "^8.5.0", 62 | "eslint-import-resolver-typescript": "^3.1.1", 63 | "eslint-plugin-import": "^2.26.0", 64 | "husky": "^8.0.1", 65 | "lint-staged": "^13.0.3", 66 | "prettier": "^2.7.1", 67 | "standard-version": "^9.5.0", 68 | "tsup": "^6.1.2", 69 | "type-fest": "^2.14.0", 70 | "typescript": "^4.7.4" 71 | }, 72 | "lint-staged": { 73 | "src/**/*.{ts,tsx}": [ 74 | "prettier --write", 75 | "eslint --fix" 76 | ], 77 | "*.{json,md,mdx}": [ 78 | "prettier --write" 79 | ] 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/cli/index.ts: -------------------------------------------------------------------------------- 1 | import type { AvailablePackages } from "../installers/index.js"; 2 | import chalk from "chalk"; 3 | import { Command } from "commander"; 4 | import inquirer from "inquirer"; 5 | import { CREATE_CARDANO_DAPP, DEFAULT_APP_NAME } from "../consts.js"; 6 | import { availablePackages } from "../installers/index.js"; 7 | import { getVersion } from "../utils/getT3Version.js"; 8 | import { getUserPkgManager } from "../utils/getUserPkgManager.js"; 9 | import { logger } from "../utils/logger.js"; 10 | import { validateAppName } from "../utils/validateAppName.js"; 11 | 12 | interface CliFlags { 13 | noGit: boolean; 14 | noInstall: boolean; 15 | default: boolean; 16 | } 17 | 18 | interface CliResults { 19 | appName: string; 20 | packages: AvailablePackages[]; 21 | flags: CliFlags; 22 | } 23 | 24 | const defaultOptions: CliResults = { 25 | appName: DEFAULT_APP_NAME, 26 | packages: ["nextAuth", "prisma", "tailwind", "trpc"], 27 | flags: { 28 | noGit: false, 29 | noInstall: false, 30 | default: false, 31 | }, 32 | }; 33 | 34 | export const runCli = async () => { 35 | const cliResults = defaultOptions; 36 | 37 | const program = new Command().name(CREATE_CARDANO_DAPP); 38 | 39 | // TODO: This doesn't return anything typesafe. Research other options? 40 | // Emulate from: https://github.com/Schniz/soundtype-commander 41 | program 42 | .description("A CLI for creating web applications with the t3 stack") 43 | .argument( 44 | "[dir]", 45 | "The name of the application, as well as the name of the directory to create", 46 | ) 47 | .option( 48 | "--noGit", 49 | "Explicitly tell the CLI to not initialize a new git repo in the project", 50 | false, 51 | ) 52 | .option( 53 | "--noInstall", 54 | "Explicitly tell the CLI to not run the package manager's install command", 55 | false, 56 | ) 57 | .option( 58 | "-y, --default", 59 | "Bypass the CLI and use all default options to bootstrap a new cardano dapp", 60 | false, 61 | ) 62 | .version(getVersion(), "-v, --version", "Display the version number") 63 | .addHelpText( 64 | "afterAll", 65 | `\n The Cardano stack was inspired by ${chalk 66 | .hex("#E8DCFF") 67 | .bold( 68 | "@Cardano_G_O_A_T", 69 | )} and has been used to build awesome fullstack applications like\n`, 70 | ) 71 | .parse(process.argv); 72 | 73 | // FIXME: TEMPORARY WARNING WHEN USING NODE 18. SEE ISSUE #59 at https://github.com/t3-oss/create-t3-app 74 | if (process.versions.node.startsWith("18")) { 75 | logger.warn(` WARNING: You are using Node.js version 18. This is currently not compatible with Next-Auth. 76 | If you want to use Next-Auth, switch to a previous version of Node, e.g. 16 (LTS). 77 | If you have nvm installed, use 'nvm install --lts' to switch to the latest LTS version of Node. 78 | `); 79 | 80 | cliResults.packages = cliResults.packages.filter( 81 | (val) => val !== "nextAuth", 82 | ); 83 | } 84 | 85 | // Needs to be seperated outside the if statement to correctly infer the type as string | undefined 86 | const cliProvidedName = program.args[0]; 87 | if (cliProvidedName) { 88 | cliResults.appName = cliProvidedName; 89 | } 90 | 91 | cliResults.flags = program.opts(); 92 | 93 | const pkgManager = getUserPkgManager(); 94 | 95 | // Explained below why this is in a try/catch block 96 | try { 97 | if (!cliResults.flags.default) { 98 | if (!cliProvidedName) { 99 | const { appName } = await inquirer.prompt>({ 100 | name: "appName", 101 | type: "input", 102 | message: "What will your project be called?", 103 | default: defaultOptions.appName, 104 | validate: validateAppName, 105 | transformer: (input: string) => { 106 | return input.trim(); 107 | }, 108 | }); 109 | cliResults.appName = appName; 110 | } 111 | 112 | const { language } = await inquirer.prompt<{ language: string }>({ 113 | name: "language", 114 | type: "list", 115 | message: "Your only choice is TypeScript?", 116 | choices: [ 117 | { name: "TypeScript", value: "typescript", short: "TypeScript" }, 118 | // { name: "JavaScript", value: "javascript", short: "TypeScript" }, // Both options should have 'TypeScript' as the short value to improve UX and reduce confusion 119 | ], 120 | default: "typescript", 121 | }); 122 | 123 | if (language === "javascript") { 124 | logger.error("Wrong answer, using TypeScript instead..."); 125 | } else { 126 | logger.success("Good choice! Using TypeScript!"); 127 | } 128 | 129 | const { packages } = await inquirer.prompt>({ 130 | name: "packages", 131 | type: "checkbox", 132 | message: "Which packages would you like to enable?", 133 | choices: availablePackages.map((pkgName) => ({ 134 | name: pkgName, 135 | checked: false, 136 | // FIXME: TEMPORARY WARNING WHEN USING NODE 18. SEE ISSUE #59 137 | disabled: 138 | pkgName === "nextAuth" && process.versions.node.startsWith("18") 139 | ? "Node.js version 18 is currently not compatible with Next-Auth." 140 | : false, 141 | })), 142 | }); 143 | 144 | cliResults.packages = packages; 145 | 146 | // Skip if noGit flag provided 147 | if (!cliResults.flags.noGit) { 148 | const { git } = await inquirer.prompt<{ git: boolean }>({ 149 | name: "git", 150 | type: "confirm", 151 | message: "Initialize a new git repository?", 152 | default: true, 153 | }); 154 | if (git) { 155 | logger.success("Nice one! Initializing repository!"); 156 | } else { 157 | cliResults.flags.noGit = true; 158 | logger.info("Sounds good! You can come back and run git init later."); 159 | } 160 | } 161 | 162 | if (!cliResults.flags.noInstall) { 163 | const { runInstall } = await inquirer.prompt<{ runInstall: boolean }>({ 164 | name: "runInstall", 165 | type: "confirm", 166 | message: `Would you like us to run ${pkgManager} install?`, 167 | default: true, 168 | }); 169 | 170 | if (runInstall) { 171 | logger.success("Alright. We'll install the dependencies for you!"); 172 | } else { 173 | cliResults.flags.noInstall = true; 174 | logger.info( 175 | `No worries. You can run '${pkgManager} install' later to install the dependencies.`, 176 | ); 177 | } 178 | } 179 | } 180 | } catch (err) { 181 | // If the user is not calling create-cardano-dapp from an interactive terminal, inquirer will throw an error with isTTYError = true 182 | // If this happens, we catch the error, tell the user what has happened, and then continue to run the program with a default t3 app 183 | // eslint-disable-next-line -- Otherwise we have to do some fancy namespace extension logic on the Error type which feels overkill for one line 184 | if (err instanceof Error && (err as any).isTTYError) { 185 | logger.warn( 186 | `${CREATE_CARDANO_DAPP} needs an interactive terminal to provide options`, 187 | ); 188 | logger.info( 189 | `Bootstrapping a default cardano dapp in ./${cliResults.appName}`, 190 | ); 191 | } else { 192 | throw err; 193 | } 194 | } 195 | 196 | return cliResults; 197 | }; 198 | -------------------------------------------------------------------------------- /src/consts.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { fileURLToPath } from "url"; 3 | 4 | // With the move to TSUP as a build tool, this keeps path routes in other files (installers, loaders, etc) in check more easily. 5 | // Path is in relation to a single index.js file inside ./dist 6 | const __filename = fileURLToPath(import.meta.url); 7 | const distPath = path.dirname(__filename); 8 | export const PKG_ROOT = path.join(distPath, "../"); 9 | 10 | //export const PKG_ROOT = path.dirname(require.main.filename); 11 | 12 | export const TITLE_TEXT = `CREATE ₳ DAPP`; 13 | export const DEFAULT_APP_NAME = "my-cardano-dapp"; 14 | export const CREATE_CARDANO_DAPP = "create-cardano-dapp"; 15 | -------------------------------------------------------------------------------- /src/helpers/createProject.ts: -------------------------------------------------------------------------------- 1 | import type { PkgInstallerMap } from "../installers/index.js"; 2 | import path from "path"; 3 | import { getUserPkgManager } from "../utils/getUserPkgManager.js"; 4 | import { installPackages } from "./installPackages.js"; 5 | import { scaffoldProject } from "./scaffoldProject.js"; 6 | import { selectAppFile, selectIndexFile } from "./selectBoilerplate.js"; 7 | 8 | interface CreateProjectOptions { 9 | projectName: string; 10 | packages: PkgInstallerMap; 11 | noInstall: boolean; 12 | } 13 | 14 | export const createProject = async ({ 15 | projectName, 16 | packages, 17 | noInstall, 18 | }: CreateProjectOptions) => { 19 | const pkgManager = getUserPkgManager(); 20 | const projectDir = path.resolve(process.cwd(), projectName); 21 | 22 | // Bootstraps the base Next.js application 23 | await scaffoldProject({ projectName, projectDir, pkgManager, noInstall }); 24 | 25 | // Install the selected packages 26 | await installPackages({ projectDir, pkgManager, packages, noInstall }); 27 | 28 | // TODO: Look into using handlebars or other templating engine to scaffold without needing to maintain multiple copies of the same file 29 | await selectAppFile({ projectDir, packages }); 30 | await selectIndexFile({ projectDir, packages }); 31 | 32 | return projectDir; 33 | }; 34 | -------------------------------------------------------------------------------- /src/helpers/initGit.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import chalk from "chalk"; 3 | import fs from "fs-extra"; 4 | import ora from "ora"; 5 | import { execa } from "../utils/execAsync.js"; 6 | import { logger } from "../utils/logger.js"; 7 | 8 | // This initializes the Git-repository for the project 9 | export const initializeGit = async (projectDir: string) => { 10 | logger.info("Initializing Git..."); 11 | const spinner = ora("Creating a new git repo...\n").start(); 12 | try { 13 | let initCmd = "git init --initial-branch=main"; 14 | 15 | // --initial-branch flag was added in git v2.28.0 16 | const { stdout: gitVersionOutput } = await execa("git --version"); // git version 2.32.0 ... 17 | const gitVersionTag = gitVersionOutput.split(" ")[2]; 18 | const major = gitVersionTag?.split(".")[0]; 19 | const minor = gitVersionTag?.split(".")[1]; 20 | if (Number(major) < 2 || Number(minor) < 28) { 21 | initCmd = "git init && git branch -m main"; 22 | } 23 | 24 | await execa(initCmd, { cwd: projectDir }); 25 | spinner.succeed( 26 | `${chalk.green("Successfully initialized")} ${chalk.green.bold("git")}\n`, 27 | ); 28 | } catch (error) { 29 | spinner.fail( 30 | `${chalk.bold.red( 31 | "Failed:", 32 | )} could not initialize git. Update git to the latest version!\n`, 33 | ); 34 | } 35 | 36 | await fs.rename( 37 | path.join(projectDir, "_gitignore"), 38 | path.join(projectDir, ".gitignore"), 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/helpers/installPackages.ts: -------------------------------------------------------------------------------- 1 | import type { InstallerOptions, PkgInstallerMap } from "../installers/index.js"; 2 | import chalk from "chalk"; 3 | import ora from "ora"; 4 | import { logger } from "../utils/logger.js"; 5 | 6 | type InstallPackagesOptions = { 7 | packages: PkgInstallerMap; 8 | } & InstallerOptions; 9 | // This runs the installer for all the packages that the user has selected 10 | export const installPackages = async ({ 11 | projectDir, 12 | pkgManager, 13 | packages, 14 | noInstall, 15 | }: InstallPackagesOptions) => { 16 | logger.info(`${noInstall ? "Adding" : "Installing"} packages...`); 17 | 18 | for (const [name, pkgOpts] of Object.entries(packages)) { 19 | if (pkgOpts.inUse) { 20 | const spinner = ora( 21 | `${noInstall ? "Adding" : "Installing"} ${name}...`, 22 | ).start(); 23 | await pkgOpts.installer({ projectDir, pkgManager, packages, noInstall }); 24 | spinner.succeed( 25 | chalk.green( 26 | `Successfully ${noInstall ? "added" : "installed"} ${chalk.green.bold( 27 | name, 28 | )}`, 29 | ), 30 | ); 31 | } 32 | } 33 | logger.info(""); 34 | }; 35 | -------------------------------------------------------------------------------- /src/helpers/logNextSteps.ts: -------------------------------------------------------------------------------- 1 | import type { InstallerOptions } from "../installers/index.js"; 2 | import { DEFAULT_APP_NAME } from "../consts.js"; 3 | import { getUserPkgManager } from "../utils/getUserPkgManager.js"; 4 | import { logger } from "../utils/logger.js"; 5 | 6 | // This logs the next steps that the user should take in order to advance the project 7 | export const logNextSteps = ({ 8 | projectName = DEFAULT_APP_NAME, 9 | packages, 10 | noInstall, 11 | }: Pick) => { 12 | const pkgManager = getUserPkgManager(); 13 | 14 | logger.info("Next steps:"); 15 | logger.info(` cd ${projectName}`); 16 | if (noInstall) { 17 | logger.info(` ${pkgManager} install`); 18 | } 19 | 20 | if (packages?.prisma.inUse) { 21 | logger.info( 22 | ` ${pkgManager === "npm" ? "npx" : pkgManager} prisma db push`, 23 | ); 24 | } 25 | 26 | logger.info(` ${pkgManager === "npm" ? "npm run" : pkgManager} dev`); 27 | }; 28 | -------------------------------------------------------------------------------- /src/helpers/scaffoldProject.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import chalk from "chalk"; 3 | import fs from "fs-extra"; 4 | import inquirer from "inquirer"; 5 | import ora from "ora"; 6 | import { PKG_ROOT } from "../consts.js"; 7 | import { InstallerOptions } from "../installers/index.js"; 8 | import { execa } from "../utils/execAsync.js"; 9 | import { logger } from "../utils/logger.js"; 10 | 11 | // This bootstraps the base Next.js application 12 | export const scaffoldProject = async ({ 13 | projectName, 14 | projectDir, 15 | pkgManager, 16 | noInstall, 17 | }: InstallerOptions) => { 18 | const srcDir = path.join(PKG_ROOT, "template/base"); 19 | 20 | if (!noInstall) { 21 | logger.info(`\nUsing: ${chalk.cyan.bold(pkgManager)}\n`); 22 | } else { 23 | logger.info(""); 24 | } 25 | 26 | const spinner = ora(`Scaffolding in: ${projectDir}...\n`).start(); 27 | 28 | if (fs.existsSync(projectDir)) { 29 | if (fs.readdirSync(projectDir).length === 0) { 30 | spinner.info( 31 | `${chalk.cyan.bold(projectName)} exists but is empty, continuing...\n`, 32 | ); 33 | } else { 34 | spinner.stopAndPersist(); 35 | const { overwriteDir } = await inquirer.prompt<{ overwriteDir: boolean }>( 36 | { 37 | name: "overwriteDir", 38 | type: "confirm", 39 | message: `${chalk.redBright.bold("Warning:")} ${chalk.cyan.bold( 40 | projectName, 41 | )} already exists and isn't empty. Do you want to overwrite it?`, 42 | default: false, 43 | }, 44 | ); 45 | if (!overwriteDir) { 46 | spinner.fail("Aborting installation..."); 47 | process.exit(0); 48 | } else { 49 | spinner.info( 50 | `Emptying ${chalk.cyan.bold(projectName)} and creating t3 app..\n`, 51 | ); 52 | fs.emptyDirSync(projectDir); 53 | } 54 | } 55 | } 56 | 57 | spinner.start(); 58 | 59 | await fs.copy(srcDir, projectDir); 60 | 61 | if (!noInstall) { 62 | await execa(`${pkgManager} install`, { cwd: projectDir }); 63 | } 64 | spinner.succeed(`${chalk.cyan.bold(projectName)} scaffolded successfully!\n`); 65 | }; 66 | -------------------------------------------------------------------------------- /src/helpers/selectBoilerplate.ts: -------------------------------------------------------------------------------- 1 | import type { InstallerOptions } from "../installers/index.js"; 2 | import path from "path"; 3 | import fs from "fs-extra"; 4 | import { PKG_ROOT } from "../consts.js"; 5 | 6 | type SelectBoilerplateProps = Required< 7 | Pick 8 | >; 9 | // This generates the _app.tsx file that is used to render the app 10 | export const selectAppFile = async ({ 11 | projectDir, 12 | packages, 13 | }: SelectBoilerplateProps) => { 14 | const appFileDir = path.join(PKG_ROOT, "template/page-studs/_app"); 15 | 16 | const usingTrpc = packages.trpc.inUse; 17 | const usingNextAuth = packages.nextAuth.inUse; 18 | 19 | let appFile = ""; 20 | if (usingNextAuth && usingTrpc) { 21 | appFile = "with-auth-trpc.tsx"; 22 | } else if (usingNextAuth && !usingTrpc) { 23 | appFile = "with-auth.tsx"; 24 | } else if (!usingNextAuth && usingTrpc) { 25 | appFile = "with-trpc.tsx"; 26 | } 27 | 28 | if (appFile !== "") { 29 | const appSrc = path.join(appFileDir, appFile); 30 | const appDest = path.join(projectDir, "src/pages/_app.tsx"); 31 | await fs.copy(appSrc, appDest); 32 | } 33 | }; 34 | 35 | // This selects the proper index.tsx to be used that showcases the chosen tech 36 | export const selectIndexFile = async ({ 37 | projectDir, 38 | packages, 39 | }: SelectBoilerplateProps) => { 40 | const indexFileDir = path.join(PKG_ROOT, "template/page-studs/index"); 41 | 42 | const usingTrpc = packages.trpc.inUse; 43 | const usingTw = packages.tailwind.inUse; 44 | const usingAuth = packages.nextAuth.inUse; 45 | const usingPrisma = packages.prisma.inUse; 46 | 47 | let indexFile = ""; 48 | // FIXME: auth showcase doesn't work with prisma since it requires more setup 49 | if (usingTrpc && usingTw && usingAuth && !usingPrisma) { 50 | indexFile = "with-auth-trpc-tw.tsx"; 51 | } else if (usingTrpc && !usingTw && usingAuth && !usingPrisma) { 52 | indexFile = "with-auth-trpc.tsx"; 53 | } else if (usingTrpc && usingTw) { 54 | indexFile = "with-trpc-tw.tsx"; 55 | } else if (usingTrpc && !usingTw) { 56 | indexFile = "with-trpc.tsx"; 57 | } else if (!usingTrpc && usingTw) { 58 | indexFile = "with-tw.tsx"; 59 | } 60 | 61 | if (indexFile !== "") { 62 | const indexSrc = path.join(indexFileDir, indexFile); 63 | const indexDest = path.join(projectDir, "src/pages/index.tsx"); 64 | await fs.copy(indexSrc, indexDest); 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import type { PackageJson } from "type-fest"; 3 | import path from "path"; 4 | import fs from "fs-extra"; 5 | import { runCli } from "./cli/index.js"; 6 | import { createProject } from "./helpers/createProject.js"; 7 | import { initializeGit } from "./helpers/initGit.js"; 8 | import { logNextSteps } from "./helpers/logNextSteps.js"; 9 | import { buildPkgInstallerMap } from "./installers/index.js"; 10 | import { logger } from "./utils/logger.js"; 11 | import { parseNameAndPath } from "./utils/parseNameAndPath.js"; 12 | import { renderTitle } from "./utils/renderTitle.js"; 13 | 14 | const main = async () => { 15 | renderTitle(); 16 | 17 | const { 18 | appName, 19 | packages, 20 | flags: { noGit, noInstall }, 21 | } = await runCli(); 22 | 23 | const usePackages = buildPkgInstallerMap(packages); 24 | 25 | // e.g. dir/@mono/app returns ["@mono/app", "dir/app"] 26 | const [scopedAppName, appDir] = parseNameAndPath(appName); 27 | 28 | const projectDir = await createProject({ 29 | projectName: appDir, 30 | packages: usePackages, 31 | noInstall, 32 | }); 33 | 34 | if (!noGit) { 35 | await initializeGit(projectDir); 36 | } 37 | 38 | logNextSteps({ projectName: appDir, packages: usePackages, noInstall }); 39 | const pkgJson = (await fs.readJSON( 40 | path.join(projectDir, "package.json"), 41 | )) as PackageJson; 42 | pkgJson.name = scopedAppName; 43 | await fs.writeJSON(path.join(projectDir, "package.json"), pkgJson, { 44 | spaces: 2, 45 | }); 46 | 47 | process.exit(0); 48 | }; 49 | 50 | main().catch((err) => { 51 | logger.error("Aborting installation..."); 52 | if (err instanceof Error) { 53 | logger.error(err); 54 | } else { 55 | logger.error( 56 | "An unknown error has occurred. Please open an issue on github with the below:", 57 | ); 58 | console.log(err); 59 | } 60 | process.exit(1); 61 | }); 62 | -------------------------------------------------------------------------------- /src/installers/index.ts: -------------------------------------------------------------------------------- 1 | import type { PackageManager } from "../utils/getUserPkgManager.js"; 2 | import { nextAuthInstaller } from "./next-auth.js"; 3 | import { prismaInstaller } from "./prisma.js"; 4 | import { tailwindInstaller } from "./tailwind.js"; 5 | import { trpcInstaller } from "./trpc.js"; 6 | 7 | // Turning this into a const allows the list to be iterated over for programatically creating prompt options 8 | // Should increase extensability in the future 9 | export const availablePackages = [ 10 | "nextAuth", 11 | "prisma", 12 | "tailwind", 13 | "trpc", 14 | ] as const; 15 | 16 | export type AvailablePackages = typeof availablePackages[number]; 17 | 18 | export interface InstallerOptions { 19 | projectDir: string; 20 | pkgManager: PackageManager; 21 | noInstall: boolean; 22 | packages?: PkgInstallerMap; 23 | projectName?: string; 24 | } 25 | 26 | export type Installer = (opts: InstallerOptions) => Promise; 27 | 28 | export type PkgInstallerMap = { 29 | [pkg in AvailablePackages]: { 30 | inUse: boolean; 31 | installer: Installer; 32 | }; 33 | }; 34 | 35 | export const buildPkgInstallerMap = ( 36 | packages: AvailablePackages[], 37 | ): PkgInstallerMap => ({ 38 | nextAuth: { 39 | inUse: packages.includes("nextAuth"), 40 | installer: nextAuthInstaller, 41 | }, 42 | prisma: { 43 | inUse: packages.includes("prisma"), 44 | installer: prismaInstaller, 45 | }, 46 | tailwind: { 47 | inUse: packages.includes("tailwind"), 48 | installer: tailwindInstaller, 49 | }, 50 | trpc: { 51 | inUse: packages.includes("trpc"), 52 | installer: trpcInstaller, 53 | }, 54 | }); 55 | -------------------------------------------------------------------------------- /src/installers/next-auth.ts: -------------------------------------------------------------------------------- 1 | import type { Installer } from "./index.js"; 2 | import path from "path"; 3 | import fs from "fs-extra"; 4 | import { PKG_ROOT } from "../consts.js"; 5 | import { runPkgManagerInstall } from "../utils/runPkgManagerInstall.js"; 6 | 7 | export const nextAuthInstaller: Installer = async ({ 8 | pkgManager, 9 | projectDir, 10 | packages, 11 | noInstall, 12 | }) => { 13 | await runPkgManagerInstall({ 14 | pkgManager, 15 | projectDir, 16 | packages: [ 17 | "next-auth", 18 | packages?.prisma.inUse ? "@next-auth/prisma-adapter" : "", 19 | ], 20 | devMode: false, 21 | noInstallMode: noInstall, 22 | }); 23 | 24 | const nextAuthAssetDir = path.join(PKG_ROOT, "template/addons/next-auth"); 25 | 26 | const apiHandlerSrc = path.join( 27 | nextAuthAssetDir, 28 | packages?.prisma.inUse ? "api-handler-prisma.ts" : "api-handler.ts", 29 | ); 30 | const apiHandlerDest = path.join( 31 | projectDir, 32 | "src/pages/api/auth/[...nextauth].ts", 33 | ); 34 | 35 | const restrictedApiSrc = path.join(nextAuthAssetDir, "restricted.ts"); 36 | const restrictedApiDest = path.join( 37 | projectDir, 38 | "src/pages/api/restricted.ts", 39 | ); 40 | 41 | await Promise.all([ 42 | fs.copy(apiHandlerSrc, apiHandlerDest), 43 | fs.copy(restrictedApiSrc, restrictedApiDest), 44 | ]); 45 | }; 46 | -------------------------------------------------------------------------------- /src/installers/prisma.ts: -------------------------------------------------------------------------------- 1 | import type { Installer } from "./index.js"; 2 | import type { PackageJson } from "type-fest"; 3 | import path from "path"; 4 | import fs from "fs-extra"; 5 | import { PKG_ROOT } from "../consts.js"; 6 | import { execa } from "../utils/execAsync.js"; 7 | import { runPkgManagerInstall } from "../utils/runPkgManagerInstall.js"; 8 | 9 | export const prismaInstaller: Installer = async ({ 10 | projectDir, 11 | pkgManager, 12 | packages, 13 | noInstall, 14 | }) => { 15 | await runPkgManagerInstall({ 16 | pkgManager, 17 | projectDir, 18 | packages: ["prisma"], 19 | devMode: true, 20 | noInstallMode: noInstall, 21 | }); 22 | await runPkgManagerInstall({ 23 | pkgManager, 24 | projectDir, 25 | packages: ["@prisma/client"], 26 | devMode: false, 27 | noInstallMode: noInstall, 28 | }); 29 | 30 | const prismaAssetDir = path.join(PKG_ROOT, "template/addons/prisma"); 31 | 32 | const schemaSrc = path.join( 33 | prismaAssetDir, 34 | packages?.nextAuth.inUse ? "auth-schema.prisma" : "schema.prisma", 35 | ); 36 | const schemaDest = path.join(projectDir, "prisma/schema.prisma"); 37 | 38 | const clientSrc = path.join(prismaAssetDir, "client.ts"); 39 | const clientDest = path.join(projectDir, "src/server/db/client.ts"); 40 | 41 | const sampleApiRouteSrc = path.join(prismaAssetDir, "sample-api.ts"); 42 | const sampleApiRouteDest = path.join(projectDir, "src/pages/api/examples.ts"); 43 | 44 | // add postinstall script to package.json 45 | const packageJsonPath = path.join(projectDir, "package.json"); 46 | 47 | const packageJsonContent = fs.readJSONSync(packageJsonPath) as PackageJson; 48 | packageJsonContent.scripts!.postinstall = "prisma generate"; //eslint-disable-line @typescript-eslint/no-non-null-assertion 49 | 50 | await Promise.all([ 51 | fs.copy(schemaSrc, schemaDest), 52 | fs.copy(clientSrc, clientDest), 53 | fs.copy(sampleApiRouteSrc, sampleApiRouteDest), 54 | fs.writeJSON(packageJsonPath, packageJsonContent, { 55 | spaces: 2, 56 | }), 57 | ]); 58 | 59 | // only generate client if we have installed the dependencies 60 | if (!noInstall) { 61 | const generateClientCmd = 62 | pkgManager === "npm" 63 | ? "npx prisma generate" 64 | : `${pkgManager} prisma generate`; 65 | await execa(generateClientCmd, { cwd: projectDir }); 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /src/installers/tailwind.ts: -------------------------------------------------------------------------------- 1 | import type { Installer } from "./index.js"; 2 | import path from "path"; 3 | import fs from "fs-extra"; 4 | import { PKG_ROOT } from "../consts.js"; 5 | import { runPkgManagerInstall } from "../utils/runPkgManagerInstall.js"; 6 | 7 | export const tailwindInstaller: Installer = async ({ 8 | projectDir, 9 | pkgManager, 10 | noInstall, 11 | }) => { 12 | await runPkgManagerInstall({ 13 | pkgManager, 14 | projectDir, 15 | packages: ["tailwindcss", "postcss", "autoprefixer"], 16 | devMode: true, 17 | noInstallMode: noInstall, 18 | }); 19 | 20 | const twAssetDir = path.join(PKG_ROOT, "template/addons/tailwind"); 21 | 22 | const twCfgSrc = path.join(twAssetDir, "tailwind.config.js"); 23 | const twCfgDest = path.join(projectDir, "tailwind.config.js"); 24 | 25 | const postcssCfgSrc = path.join(twAssetDir, "postcss.config.js"); 26 | const postcssCfgDest = path.join(projectDir, "postcss.config.js"); 27 | 28 | const cssSrc = path.join(twAssetDir, "globals.css"); 29 | const cssDest = path.join(projectDir, "src/styles/globals.css"); 30 | 31 | await Promise.all([ 32 | fs.copy(twCfgSrc, twCfgDest), 33 | fs.copy(postcssCfgSrc, postcssCfgDest), 34 | fs.copy(cssSrc, cssDest), 35 | ]); 36 | }; 37 | -------------------------------------------------------------------------------- /src/installers/trpc.ts: -------------------------------------------------------------------------------- 1 | import type { Installer } from "./index.js"; 2 | import path from "path"; 3 | import fs from "fs-extra"; 4 | import { PKG_ROOT } from "../consts.js"; 5 | import { runPkgManagerInstall } from "../utils/runPkgManagerInstall.js"; 6 | 7 | export const trpcInstaller: Installer = async ({ 8 | projectDir, 9 | pkgManager, 10 | packages, 11 | noInstall, 12 | }) => { 13 | await runPkgManagerInstall({ 14 | pkgManager, 15 | projectDir, 16 | packages: [ 17 | "react-query", 18 | "superjson", 19 | "@trpc/server", 20 | "@trpc/client", 21 | "@trpc/next", 22 | "@trpc/react", 23 | "zod", 24 | ], 25 | devMode: false, 26 | noInstallMode: noInstall, 27 | }); 28 | const usingAuth = packages?.nextAuth.inUse; 29 | const usingPrisma = packages?.prisma.inUse; 30 | 31 | const trpcAssetDir = path.join(PKG_ROOT, "template/addons/trpc"); 32 | 33 | const apiHandlerSrc = path.join(trpcAssetDir, "api-handler.ts"); 34 | const apiHandlerDest = path.join(projectDir, "src/pages/api/trpc/[trpc].ts"); 35 | 36 | const utilsSrc = path.join(trpcAssetDir, "utils.ts"); 37 | const utilsDest = path.join(projectDir, "src/utils/trpc.ts"); 38 | 39 | const contextFile = 40 | usingAuth && usingPrisma 41 | ? "auth-prisma-context.ts" 42 | : usingAuth && !usingPrisma 43 | ? "auth-context.ts" 44 | : !usingAuth && usingPrisma 45 | ? "prisma-context.ts" 46 | : "base-context.ts"; 47 | const contextSrc = path.join(trpcAssetDir, contextFile); 48 | const contextDest = path.join(projectDir, "src/server/router/context.ts"); 49 | 50 | if (usingAuth) { 51 | const authRouterSrc = path.join(trpcAssetDir, "auth-router.ts"); 52 | const authRouterDest = path.join(projectDir, "src/server/router/auth.ts"); 53 | await fs.copy(authRouterSrc, authRouterDest); 54 | } 55 | 56 | const indexRouterFile = usingAuth 57 | ? "auth-index-router.ts" 58 | : "index-router.ts"; 59 | const indexRouterSrc = path.join(trpcAssetDir, indexRouterFile); 60 | const indexRouterDest = path.join(projectDir, "src/server/router/index.ts"); 61 | 62 | const exampleRouterFile = usingPrisma 63 | ? "example-prisma-router.ts" 64 | : "example-router.ts"; 65 | const exampleRouterSrc = path.join(trpcAssetDir, exampleRouterFile); 66 | const exampleRouterDest = path.join( 67 | projectDir, 68 | "src/server/router/example.ts", 69 | ); 70 | 71 | await Promise.all([ 72 | fs.copy(apiHandlerSrc, apiHandlerDest), 73 | fs.copy(utilsSrc, utilsDest), 74 | fs.copy(contextSrc, contextDest), 75 | fs.copy(indexRouterSrc, indexRouterDest), 76 | fs.copy(exampleRouterSrc, exampleRouterDest), 77 | ]); 78 | }; 79 | -------------------------------------------------------------------------------- /src/utils/execAsync.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "child_process"; 2 | import { promisify } from "util"; 3 | 4 | export const execa = promisify(exec); 5 | -------------------------------------------------------------------------------- /src/utils/getT3Version.ts: -------------------------------------------------------------------------------- 1 | import type { PackageJson } from "type-fest"; 2 | import path from "path"; 3 | import fs from "fs-extra"; 4 | import { PKG_ROOT } from "../consts.js"; 5 | 6 | export const getVersion = () => { 7 | const packageJsonPath = path.join(PKG_ROOT, "package.json"); 8 | 9 | const packageJsonContent = fs.readJSONSync(packageJsonPath) as PackageJson; 10 | 11 | return packageJsonContent.version ?? "1.0.0"; 12 | }; 13 | -------------------------------------------------------------------------------- /src/utils/getUserPkgManager.ts: -------------------------------------------------------------------------------- 1 | export type PackageManager = "npm" | "pnpm" | "yarn"; 2 | 3 | export const getUserPkgManager: () => PackageManager = () => { 4 | // This environment variable is set by npm and yarn but pnpm seems less consistent 5 | const userAgent = process.env.npm_config_user_agent; 6 | 7 | if (userAgent) { 8 | if (userAgent.startsWith("yarn")) { 9 | return "yarn"; 10 | } else if (userAgent.startsWith("pnpm")) { 11 | return "pnpm"; 12 | } else { 13 | return "npm"; 14 | } 15 | } else { 16 | // If no user agent is set, assume npm 17 | return "npm"; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | 3 | export const logger = { 4 | error(...args: unknown[]) { 5 | console.log(chalk.red(...args)); 6 | }, 7 | warn(...args: unknown[]) { 8 | console.log(chalk.yellow(...args)); 9 | }, 10 | info(...args: unknown[]) { 11 | console.log(chalk.cyan(...args)); 12 | }, 13 | success(...args: unknown[]) { 14 | console.log(chalk.green(...args)); 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /src/utils/parseNameAndPath.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Parses the appName and its path from the user input. 3 | * Returns an array of [appName, path] where appName is the name put in the package.json and 4 | * path is the path to the directory where the app will be created. 5 | * Handles the case where the input includes a scoped package name 6 | * in which case that is being parsed as the name, but not included as the path 7 | * e.g. dir/@mono/app => ["@mono/app", "dir/app"] 8 | * e.g. dir/app => ["app", "dir/app"] 9 | **/ 10 | export const parseNameAndPath = (input: string) => { 11 | const paths = input.split("/"); 12 | 13 | let appName = paths[paths.length - 1]; 14 | 15 | // If the first part is a @, it's a scoped package 16 | const indexOfDelimiter = paths.findIndex((p) => p.startsWith("@")); 17 | if (paths.findIndex((p) => p.startsWith("@")) !== -1) { 18 | appName = paths.slice(indexOfDelimiter).join("/"); 19 | } 20 | 21 | const path = paths.filter((p) => !p.startsWith("@")).join("/"); 22 | 23 | return [appName, path] as const; 24 | }; 25 | -------------------------------------------------------------------------------- /src/utils/renderTitle.ts: -------------------------------------------------------------------------------- 1 | import figlet from "figlet"; 2 | import gradient from "gradient-string"; 3 | import { TITLE_TEXT } from "../consts.js"; 4 | 5 | // colors brought in from vscode poimandres theme 6 | const poimandresTheme = { 7 | blue: "#add7ff", 8 | cyan: "#89ddff", 9 | green: "#5de4c7", 10 | magenta: "#fae4fc", 11 | red: "#d0679d", 12 | yellow: "#fffac2", 13 | }; 14 | 15 | export const renderTitle = () => { 16 | const text = figlet.textSync(TITLE_TEXT, { font: "Small" }); 17 | const t3Gradient = gradient(Object.values(poimandresTheme)); 18 | console.log("\n", t3Gradient.multiline(text)); 19 | }; 20 | -------------------------------------------------------------------------------- /src/utils/runPkgManagerInstall.ts: -------------------------------------------------------------------------------- 1 | import type { PackageManager } from "./getUserPkgManager.js"; 2 | import path from "path"; 3 | import fs from "fs-extra"; 4 | import { type PackageJson } from "type-fest"; 5 | import { execa } from "./execAsync.js"; 6 | import { logger } from "./logger.js"; 7 | 8 | export const runPkgManagerInstall = async (opts: { 9 | pkgManager: PackageManager; 10 | devMode: boolean; 11 | projectDir: string; 12 | packages: string[]; 13 | noInstallMode: boolean; 14 | }) => { 15 | const { pkgManager, devMode, projectDir, packages, noInstallMode } = opts; 16 | 17 | if (noInstallMode) { 18 | const pkgJson = (await fs.readJSON( 19 | path.join(projectDir, "package.json"), 20 | )) as PackageJson; 21 | 22 | for (const pkg of packages) { 23 | if (pkg === "") { 24 | // sometimes empty string is passed as a package when using ternaries so escaping that to prevent it pulling node's version 25 | continue; 26 | } 27 | const { stdout: latestVersion } = await execa(`npm show ${pkg} version`); 28 | if (!latestVersion) { 29 | logger.warn("WARN: Failed to resolve latest version of package:", pkg); 30 | continue; 31 | } 32 | 33 | // Note: We know that pkgJson.[dev]Dependencies exists in the base Next.js template so we don't need to validate it 34 | if (devMode) { 35 | pkgJson.devDependencies![pkg] = `^${latestVersion.trim()}`; //eslint-disable-line @typescript-eslint/no-non-null-assertion 36 | } else { 37 | pkgJson.dependencies![pkg] = `^${latestVersion.trim()}`; //eslint-disable-line @typescript-eslint/no-non-null-assertion 38 | } 39 | } 40 | 41 | await fs.writeJSON(path.join(projectDir, "package.json"), pkgJson, { 42 | spaces: 2, 43 | }); 44 | return; 45 | } 46 | 47 | const installCmd = 48 | pkgManager === "yarn" ? `${pkgManager} add` : `${pkgManager} install`; 49 | const flag = devMode ? "-D" : ""; 50 | const fullCmd = `${installCmd} ${flag} ${packages.join(" ")}`; 51 | await execa(fullCmd, { cwd: projectDir }); 52 | }; 53 | -------------------------------------------------------------------------------- /src/utils/validateAppName.ts: -------------------------------------------------------------------------------- 1 | const validationRegExp = 2 | /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/; 3 | 4 | //Validate a string against allowed package.json names 5 | export const validateAppName = (input: string) => { 6 | const paths = input.split("/"); 7 | 8 | // If the first part is a @, it's a scoped package 9 | const indexOfDelimiter = paths.findIndex((p) => p.startsWith("@")); 10 | 11 | let appName = paths[paths.length - 1]; 12 | if (paths.findIndex((p) => p.startsWith("@")) !== -1) { 13 | appName = paths.slice(indexOfDelimiter).join("/"); 14 | } 15 | 16 | if (validationRegExp.test(appName ?? "")) { 17 | return true; 18 | } else { 19 | return "App name must be lowercase, alphanumeric, and only use -, _, and @"; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /template/addons/next-auth/api-handler-prisma.ts: -------------------------------------------------------------------------------- 1 | import NextAuth, { type NextAuthOptions } from "next-auth"; 2 | import GithubProvider from "next-auth/providers/github"; 3 | import CredentialsProvider from "next-auth/providers/credentials"; 4 | 5 | // Prisma adapter for NextAuth, optional and can be removed 6 | import { PrismaAdapter } from "@next-auth/prisma-adapter"; 7 | import { prisma } from "../../../server/db/client"; 8 | 9 | export const authOptions: NextAuthOptions = { 10 | // Configure one or more authentication providers 11 | adapter: PrismaAdapter(prisma), 12 | providers: [ 13 | GithubProvider({ 14 | clientId: process.env.GITHUB_ID, 15 | clientSecret: process.env.GITHUB_SECRET, 16 | }), 17 | // ...add more providers here 18 | CredentialsProvider({ 19 | name: "Credentials", 20 | credentials: { 21 | name: { 22 | label: "Name", 23 | type: "text", 24 | placeholder: "Enter your name", 25 | }, 26 | }, 27 | async authorize(credentials, _req) { 28 | const user = { id: 1, name: credentials?.name ?? "J Smith" }; 29 | return user; 30 | }, 31 | }), 32 | ], 33 | }; 34 | 35 | export default NextAuth(authOptions); 36 | -------------------------------------------------------------------------------- /template/addons/next-auth/api-handler.ts: -------------------------------------------------------------------------------- 1 | import NextAuth, { type NextAuthOptions } from "next-auth"; 2 | import GithubProvider from "next-auth/providers/github"; 3 | import CredentialsProvider from "next-auth/providers/credentials"; 4 | 5 | export const authOptions: NextAuthOptions = { 6 | // Configure one or more authentication providers 7 | providers: [ 8 | GithubProvider({ 9 | clientId: process.env.GITHUB_ID, 10 | clientSecret: process.env.GITHUB_SECRET, 11 | }), 12 | // ...add more providers here 13 | CredentialsProvider({ 14 | name: "Credentials", 15 | credentials: { 16 | name: { 17 | label: "Name", 18 | type: "text", 19 | placeholder: "Enter your name", 20 | }, 21 | }, 22 | async authorize(credentials, _req) { 23 | const user = { id: 1, name: credentials?.name ?? "J Smith" }; 24 | return user; 25 | }, 26 | }), 27 | ], 28 | }; 29 | 30 | export default NextAuth(authOptions); 31 | -------------------------------------------------------------------------------- /template/addons/next-auth/restricted.ts: -------------------------------------------------------------------------------- 1 | // Example of a restricted endpoint that only authenticated users can access from https://next-auth.js.org/getting-started/example 2 | 3 | import { NextApiRequest, NextApiResponse } from "next"; 4 | import { unstable_getServerSession as getServerSession } from "next-auth"; 5 | import { authOptions as nextAuthOptions } from "./auth/[...nextauth]"; 6 | 7 | const restricted = async (req: NextApiRequest, res: NextApiResponse) => { 8 | const session = await getServerSession(req, res, nextAuthOptions); 9 | 10 | if (session) { 11 | res.send({ 12 | content: 13 | "This is protected content. You can access this content because you are signed in.", 14 | }); 15 | } else { 16 | res.send({ 17 | error: 18 | "You must be signed in to view the protected content on this page.", 19 | }); 20 | } 21 | }; 22 | 23 | export default restricted; 24 | -------------------------------------------------------------------------------- /template/addons/prisma/auth-schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "sqlite" 10 | url = "file:./db.sqlite" 11 | // url = env("DATABASE_URL") 12 | } 13 | 14 | model Example { 15 | id String @id @default(cuid()) 16 | } 17 | 18 | // Necessary for Next auth 19 | model Account { 20 | id String @id @default(cuid()) 21 | userId String 22 | type String 23 | provider String 24 | providerAccountId String 25 | refresh_token String? 26 | access_token String? 27 | expires_at Int? 28 | token_type String? 29 | scope String? 30 | id_token String? 31 | session_state String? 32 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 33 | 34 | @@unique([provider, providerAccountId]) 35 | } 36 | 37 | model Session { 38 | id String @id @default(cuid()) 39 | sessionToken String @unique 40 | userId String 41 | expires DateTime 42 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 43 | } 44 | 45 | model User { 46 | id String @id @default(cuid()) 47 | name String? 48 | email String? @unique 49 | emailVerified DateTime? 50 | image String? 51 | accounts Account[] 52 | sessions Session[] 53 | } 54 | 55 | model VerificationToken { 56 | identifier String 57 | token String @unique 58 | expires DateTime 59 | 60 | @@unique([identifier, token]) 61 | } 62 | -------------------------------------------------------------------------------- /template/addons/prisma/client.ts: -------------------------------------------------------------------------------- 1 | // src/server/db/client.ts 2 | import { PrismaClient } from "@prisma/client"; 3 | 4 | declare global { 5 | var prisma: PrismaClient | undefined; 6 | } 7 | 8 | export const prisma = 9 | global.prisma || 10 | new PrismaClient({ 11 | log: ["query"], 12 | }); 13 | 14 | if (process.env.NODE_ENV !== "production") { 15 | global.prisma = prisma; 16 | } 17 | -------------------------------------------------------------------------------- /template/addons/prisma/sample-api.ts: -------------------------------------------------------------------------------- 1 | // src/pages/api/examples.ts 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | import { prisma } from "../../server/db/client"; 4 | 5 | const examples = async (req: NextApiRequest, res: NextApiResponse) => { 6 | const examples = await prisma.example.findMany(); 7 | res.status(200).json(examples); 8 | }; 9 | 10 | export default examples; 11 | -------------------------------------------------------------------------------- /template/addons/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "sqlite" 10 | url = "file:./db.sqlite" 11 | // url = env("DATABASE_URL") 12 | } 13 | 14 | model Example { 15 | id String @id @default(cuid()) 16 | } 17 | -------------------------------------------------------------------------------- /template/addons/tailwind/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /template/addons/tailwind/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /template/addons/tailwind/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | 3 | module.exports = { 4 | content: ["./src/**/*.{js,ts,jsx,tsx}"], 5 | theme: { 6 | extend: {}, 7 | }, 8 | plugins: [], 9 | }; 10 | -------------------------------------------------------------------------------- /template/addons/trpc/api-handler.ts: -------------------------------------------------------------------------------- 1 | // src/pages/api/trpc/[trpc].ts 2 | import { createNextApiHandler } from "@trpc/server/adapters/next"; 3 | import { appRouter } from "../../../server/router"; 4 | import { createContext } from "../../../server/router/context"; 5 | 6 | // export API handler 7 | export default createNextApiHandler({ 8 | router: appRouter, 9 | createContext: createContext, 10 | }); 11 | -------------------------------------------------------------------------------- /template/addons/trpc/auth-context.ts: -------------------------------------------------------------------------------- 1 | // src/server/router/context.ts 2 | import * as trpc from "@trpc/server"; 3 | import * as trpcNext from "@trpc/server/adapters/next"; 4 | import { unstable_getServerSession as getServerSession } from "next-auth"; 5 | 6 | import { authOptions as nextAuthOptions } from "../../pages/api/auth/[...nextauth]"; 7 | 8 | export const createContext = async ( 9 | opts?: trpcNext.CreateNextContextOptions, 10 | ) => { 11 | const req = opts?.req; 12 | const res = opts?.res; 13 | 14 | const session = 15 | req && res && (await getServerSession(req, res, nextAuthOptions)); 16 | 17 | return { 18 | req, 19 | res, 20 | session, 21 | }; 22 | }; 23 | 24 | type Context = trpc.inferAsyncReturnType; 25 | 26 | export const createRouter = () => trpc.router(); 27 | -------------------------------------------------------------------------------- /template/addons/trpc/auth-index-router.ts: -------------------------------------------------------------------------------- 1 | // src/server/router/index.ts 2 | import { createRouter } from "./context"; 3 | import superjson from "superjson"; 4 | 5 | import { exampleRouter } from "./example"; 6 | import { authRouter } from "./auth"; 7 | 8 | export const appRouter = createRouter() 9 | .transformer(superjson) 10 | .merge("example.", exampleRouter) 11 | .merge("auth.", authRouter); 12 | 13 | // export type definition of API 14 | export type AppRouter = typeof appRouter; 15 | -------------------------------------------------------------------------------- /template/addons/trpc/auth-prisma-context.ts: -------------------------------------------------------------------------------- 1 | // src/server/router/context.ts 2 | import * as trpc from "@trpc/server"; 3 | import * as trpcNext from "@trpc/server/adapters/next"; 4 | import { unstable_getServerSession as getServerSession } from "next-auth"; 5 | 6 | import { authOptions as nextAuthOptions } from "../../pages/api/auth/[...nextauth]"; 7 | import { prisma } from "../db/client"; 8 | 9 | export const createContext = async ( 10 | opts?: trpcNext.CreateNextContextOptions, 11 | ) => { 12 | const req = opts?.req; 13 | const res = opts?.res; 14 | 15 | const session = 16 | req && res && (await getServerSession(req, res, nextAuthOptions)); 17 | 18 | return { 19 | req, 20 | res, 21 | session, 22 | prisma, 23 | }; 24 | }; 25 | 26 | type Context = trpc.inferAsyncReturnType; 27 | 28 | export const createRouter = () => trpc.router(); 29 | -------------------------------------------------------------------------------- /template/addons/trpc/auth-router.ts: -------------------------------------------------------------------------------- 1 | import { TRPCError } from "@trpc/server"; 2 | import { createRouter } from "./context"; 3 | 4 | export const authRouter = createRouter() 5 | .query("getSession", { 6 | resolve({ ctx }) { 7 | return ctx.session; 8 | }, 9 | }) 10 | .middleware(async ({ ctx, next }) => { 11 | // Any queries or mutations after this middleware will 12 | // raise an error unless there is a current session 13 | if (!ctx.session) { 14 | throw new TRPCError({ code: "UNAUTHORIZED" }); 15 | } 16 | return next(); 17 | }) 18 | .query("getSecretMessage", { 19 | async resolve({ ctx }) { 20 | return "You are logged in and can see this secret message!"; 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /template/addons/trpc/base-context.ts: -------------------------------------------------------------------------------- 1 | // src/server/router/context.ts 2 | import * as trpc from "@trpc/server"; 3 | import * as trpcNext from "@trpc/server/adapters/next"; 4 | 5 | export const createContext = (opts?: trpcNext.CreateNextContextOptions) => { 6 | const req = opts?.req; 7 | const res = opts?.res; 8 | 9 | return { 10 | req, 11 | res, 12 | }; 13 | }; 14 | 15 | type Context = trpc.inferAsyncReturnType; 16 | 17 | export const createRouter = () => trpc.router(); 18 | -------------------------------------------------------------------------------- /template/addons/trpc/example-prisma-router.ts: -------------------------------------------------------------------------------- 1 | import { createRouter } from "./context"; 2 | import { z } from "zod"; 3 | 4 | export const exampleRouter = createRouter() 5 | .query("hello", { 6 | input: z 7 | .object({ 8 | text: z.string().nullish(), 9 | }) 10 | .nullish(), 11 | resolve({ input }) { 12 | return { 13 | greeting: `Hello ${input?.text ?? "world"}`, 14 | }; 15 | }, 16 | }) 17 | .query("getAll", { 18 | async resolve({ ctx }) { 19 | return await ctx.prisma.example.findMany(); 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /template/addons/trpc/example-router.ts: -------------------------------------------------------------------------------- 1 | import { createRouter } from "./context"; 2 | import { z } from "zod"; 3 | 4 | export const exampleRouter = createRouter().query("hello", { 5 | input: z 6 | .object({ 7 | text: z.string().nullish(), 8 | }) 9 | .nullish(), 10 | resolve({ input }) { 11 | return { 12 | greeting: `Hello ${input?.text ?? "world"}`, 13 | }; 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /template/addons/trpc/index-router.ts: -------------------------------------------------------------------------------- 1 | // src/server/router/index.ts 2 | import { createRouter } from "./context"; 3 | import superjson from "superjson"; 4 | 5 | import { exampleRouter } from "./example"; 6 | 7 | export const appRouter = createRouter() 8 | .transformer(superjson) 9 | .merge("example.", exampleRouter); 10 | 11 | // export type definition of API 12 | export type AppRouter = typeof appRouter; 13 | -------------------------------------------------------------------------------- /template/addons/trpc/prisma-context.ts: -------------------------------------------------------------------------------- 1 | // src/server/router/context.ts 2 | import * as trpc from "@trpc/server"; 3 | import * as trpcNext from "@trpc/server/adapters/next"; 4 | import { prisma } from "../db/client"; 5 | 6 | export const createContext = (opts?: trpcNext.CreateNextContextOptions) => { 7 | const req = opts?.req; 8 | const res = opts?.res; 9 | 10 | return { 11 | req, 12 | res, 13 | prisma, 14 | }; 15 | }; 16 | 17 | type Context = trpc.inferAsyncReturnType; 18 | 19 | export const createRouter = () => trpc.router(); 20 | -------------------------------------------------------------------------------- /template/addons/trpc/utils.ts: -------------------------------------------------------------------------------- 1 | // src/utils/trpc.ts 2 | import type { AppRouter } from "../server/router"; 3 | import { createReactQueryHooks } from "@trpc/react"; 4 | 5 | export const trpc = createReactQueryHooks(); 6 | 7 | /** 8 | * Check out tRPC docs for Inference Helpers 9 | * https://trpc.io/docs/infer-types#inference-helpers 10 | */ 11 | -------------------------------------------------------------------------------- /template/base/.env-example: -------------------------------------------------------------------------------- 1 | # Note that not all variables here might be in use for your selected configuration 2 | 3 | # Blockfrost 4 | BLOCKFROST_TESTNET_URL= 5 | TESTNET_PROJECT_ID= 6 | BLOCKFROST_MAINNET_URL= 7 | MAINNET_URL= 8 | 9 | # Prisma 10 | DATABASE_URL= 11 | 12 | # Next Auth 13 | NEXTAUTH_SECRET= 14 | NEXTAUTH_URL= 15 | 16 | # Next Auth Github Provider 17 | GITHUB_ID= 18 | GITHUB_SECRET= 19 | -------------------------------------------------------------------------------- /template/base/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals"] 3 | } 4 | -------------------------------------------------------------------------------- /template/base/README.md: -------------------------------------------------------------------------------- 1 | # Create Cardano Dapp 2 | 3 | This is an app bootstrapped according to the Cardano Goat stack, also known as the Cardano-Dapp. 4 | -------------------------------------------------------------------------------- /template/base/_gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | -------------------------------------------------------------------------------- /template/base/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /template/base/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | webpack: function (config, options) { 5 | config.experiments = { 6 | asyncWebAssembly: true, 7 | topLevelAwait: true, 8 | layers: true // optional, with some bundlers/frameworks it doesn't work without 9 | }; 10 | return config; 11 | }, 12 | } 13 | 14 | module.exports = nextConfig 15 | -------------------------------------------------------------------------------- /template/base/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "template", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "next": "12.2.1", 13 | "react": "18.2.0", 14 | "react-dom": "18.2.0", 15 | "lucid-cardano": "^0.6.1" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "18.0.0", 19 | "@types/react": "18.0.14", 20 | "@types/react-dom": "18.0.5", 21 | "eslint": "8.18.0", 22 | "eslint-config-next": "12.2.1", 23 | "typescript": "4.7.4" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /template/base/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScriptAlchemist/create-cardano-dapp/727dcee5b9a9607b1ded45ec1552eccc43903f59/template/base/public/favicon.ico -------------------------------------------------------------------------------- /template/base/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import type { AppType } from "next/dist/shared/lib/utils"; 3 | 4 | const MyApp: AppType = ({ Component, pageProps }) => { 5 | return ; 6 | }; 7 | 8 | export default MyApp; 9 | -------------------------------------------------------------------------------- /template/base/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import Head from "next/head"; 3 | 4 | const Home: NextPage = () => { 5 | return ( 6 | <> 7 | 8 | Create Cardano Dapp 9 | 10 | 11 | 12 |
13 |

14 | Create Cardano Dapp 15 |

16 | 17 |
18 |

This stack uses:

19 | 35 |
36 |
37 | 38 | ); 39 | }; 40 | 41 | export default Home; 42 | -------------------------------------------------------------------------------- /template/base/src/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /template/base/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "noUncheckedIndexedAccess": true 18 | }, 19 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /template/page-studs/README.md: -------------------------------------------------------------------------------- 1 | # UP FOR DEBATE 2 | 3 | The approach to this setup should probably be looked over so that there doesn't have to be this many template-files. 4 | 5 | This approach will be increasingly difficult with the new layouts RFC where the structure will be even more complex. 6 | 7 | The necessary pages should probably be generated with some codemod tooling so that there can be a minimal amount of templates. What I can think of right now is: 8 | 9 | - \_app.tsx: There will probably be 2 of this: with-trpc and without-trpc. Adding the session-provider for next-auth should be relatively trivial by using some codemod tool 10 | - index.tsx: There will probably be 2 of this: with-tw and without-tw. adding example usages for trpc, auth can probably be injected with codemod. 11 | -------------------------------------------------------------------------------- /template/page-studs/_app/with-auth-trpc.tsx: -------------------------------------------------------------------------------- 1 | // src/pages/_app.tsx 2 | import { withTRPC } from "@trpc/next"; 3 | import type { AppRouter } from "../server/router"; 4 | import type { AppType } from "next/dist/shared/lib/utils"; 5 | import superjson from "superjson"; 6 | import { SessionProvider } from "next-auth/react"; 7 | import "../styles/globals.css"; 8 | 9 | const MyApp: AppType = ({ 10 | Component, 11 | pageProps: { session, ...pageProps }, 12 | }) => { 13 | return ( 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | const getBaseUrl = () => { 21 | if (typeof window !== "undefined") { 22 | return ""; 23 | } 24 | if (process.browser) return ""; // Browser should use current path 25 | if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url 26 | 27 | return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost 28 | }; 29 | 30 | export default withTRPC({ 31 | config({ ctx }) { 32 | /** 33 | * If you want to use SSR, you need to use the server's full URL 34 | * @link https://trpc.io/docs/ssr 35 | */ 36 | const url = `${getBaseUrl()}/api/trpc`; 37 | 38 | return { 39 | url, 40 | transformer: superjson, 41 | /** 42 | * @link https://react-query.tanstack.com/reference/QueryClient 43 | */ 44 | // queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } }, 45 | }; 46 | }, 47 | /** 48 | * @link https://trpc.io/docs/ssr 49 | */ 50 | ssr: false, 51 | })(MyApp); 52 | -------------------------------------------------------------------------------- /template/page-studs/_app/with-auth.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import type { AppType } from "next/dist/shared/lib/utils"; 3 | import { SessionProvider } from "next-auth/react"; 4 | 5 | const MyApp: AppType = ({ 6 | Component, 7 | pageProps: { session, ...pageProps }, 8 | }) => { 9 | return ( 10 | 11 | ; 12 | 13 | ); 14 | }; 15 | 16 | export default MyApp; 17 | -------------------------------------------------------------------------------- /template/page-studs/_app/with-trpc.tsx: -------------------------------------------------------------------------------- 1 | // src/pages/_app.tsx 2 | import { withTRPC } from "@trpc/next"; 3 | import type { AppRouter } from "../server/router"; 4 | import type { AppType } from "next/dist/shared/lib/utils"; 5 | import superjson from "superjson"; 6 | import "../styles/globals.css"; 7 | 8 | const MyApp: AppType = ({ Component, pageProps }) => { 9 | return ; 10 | }; 11 | 12 | const getBaseUrl = () => { 13 | if (typeof window !== "undefined") { 14 | return ""; 15 | } 16 | if (process.browser) return ""; // Browser should use current path 17 | if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url 18 | 19 | return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost 20 | }; 21 | 22 | export default withTRPC({ 23 | config({ ctx }) { 24 | /** 25 | * If you want to use SSR, you need to use the server's full URL 26 | * @link https://trpc.io/docs/ssr 27 | */ 28 | const url = `${getBaseUrl()}/api/trpc`; 29 | 30 | return { 31 | url, 32 | transformer: superjson, 33 | /** 34 | * @link https://react-query.tanstack.com/reference/QueryClient 35 | */ 36 | // queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } }, 37 | }; 38 | }, 39 | /** 40 | * @link https://trpc.io/docs/ssr 41 | */ 42 | ssr: false, 43 | })(MyApp); 44 | -------------------------------------------------------------------------------- /template/page-studs/index/with-auth-trpc-tw.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import Head from "next/head"; 3 | import { trpc } from "../utils/trpc"; 4 | 5 | import { signIn, signOut, useSession } from "next-auth/react"; 6 | 7 | const AuthShowcase: React.FC = () => { 8 | const { data: secretMessage, isLoading } = trpc.useQuery([ 9 | "auth.getSecretMessage", 10 | ]); 11 | 12 | const { data: sessionData } = useSession(); 13 | 14 | return ( 15 |
16 | {sessionData &&

Logged in as {sessionData?.user?.name}

} 17 | {secretMessage &&

{secretMessage}

} 18 | 24 |
25 | ); 26 | }; 27 | 28 | const Home: NextPage = () => { 29 | return ( 30 | <> 31 | 32 | Create Cardano Dapp 33 | 34 | 35 | 36 | 37 |
38 |

39 | Create Cardano Dapp 40 |

41 |

This stack uses:

42 |
43 |
44 |

NextJS

45 |

46 | The React framework for production 47 |

48 | 54 | Documentation 55 | 56 |
57 |
58 |

TypeScript

59 |

60 | Strongly typed programming language that builds on JavaScript, 61 | giving you better tooling at any scale 62 |

63 | 69 | Documentation 70 | 71 |
72 |
73 |

TailwindCSS

74 |

75 | Rapidly build modern websites without ever leaving your HTML 76 |

77 | 83 | Documentation 84 | 85 |
86 |
87 |

tRPC

88 |

89 | End-to-end typesafe APIs made easy 90 |

91 | 97 | Documentation 98 | 99 |
100 |
101 |

Next-Auth

102 |

Authentication for Next.js

103 | 109 | Documentation 110 | 111 |
112 |
113 |

Prisma

114 |

115 | Build data-driven JavaScript & TypeScript apps in less time 116 |

117 | 123 | Documentation 124 | 125 |
126 |
127 |
128 | 129 | ); 130 | }; 131 | 132 | export default Home; 133 | -------------------------------------------------------------------------------- /template/page-studs/index/with-auth-trpc.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import Head from "next/head"; 3 | import { trpc } from "../utils/trpc"; 4 | 5 | import { signIn, signOut, useSession } from "next-auth/react"; 6 | 7 | const AuthShowcase: React.FC = () => { 8 | const { data: secretMessage, isLoading } = trpc.useQuery([ 9 | "auth.getSecretMessage", 10 | ]); 11 | 12 | const { data: sessionData } = useSession(); 13 | 14 | return ( 15 |
16 | {sessionData &&

Logged in as {sessionData?.user?.name}

} 17 | {secretMessage &&

{secretMessage}

} 18 | 21 |
22 | ); 23 | }; 24 | 25 | const Home: NextPage = () => { 26 | return ( 27 | <> 28 | 29 | Create Cardano Dapp 30 | 31 | 32 | 33 |
34 |

35 | Create Cardano Dapp 36 |

37 | 38 |
39 |

This stack uses:

40 | 65 |
66 | 67 |
68 | 69 | ); 70 | }; 71 | 72 | export default Home; 73 | -------------------------------------------------------------------------------- /template/page-studs/index/with-trpc-tw.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import Head from "next/head"; 3 | import { trpc } from "../utils/trpc"; 4 | 5 | const Home: NextPage = () => { 6 | const hello = trpc.useQuery(["example.hello", { text: "from tRPC" }]); 7 | 8 | return ( 9 | <> 10 | 11 | Create Cardano Dapp 12 | 13 | 14 | 15 | 16 |
17 |

18 | Create Cardano Dapp 19 |

20 |

This stack uses:

21 |
22 |
23 |

NextJS

24 |

25 | The React framework for production 26 |

27 | 33 | Documentation 34 | 35 |
36 |
37 |

TypeScript

38 |

39 | Strongly typed programming language that builds on JavaScript, 40 | giving you better tooling at any scale 41 |

42 | 48 | Documentation 49 | 50 |
51 |
52 |

TailwindCSS

53 |

54 | Rapidly build modern websites without ever leaving your HTML 55 |

56 | 62 | Documentation 63 | 64 |
65 |
66 |

tRPC

67 |

68 | End-to-end typesafe APIs made easy 69 |

70 | 76 | Documentation 77 | 78 |
79 |
80 |
81 | {hello.data ?

{hello.data.greeting}

:

Loading..

} 82 |
83 |
84 | 85 | ); 86 | }; 87 | 88 | export default Home; 89 | -------------------------------------------------------------------------------- /template/page-studs/index/with-trpc.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import Head from "next/head"; 3 | import { trpc } from "../utils/trpc"; 4 | 5 | const Home: NextPage = () => { 6 | const { data, isLoading } = trpc.useQuery([ 7 | "example.hello", 8 | { text: "from tRPC" }, 9 | ]); 10 | 11 | return ( 12 | <> 13 | 14 | Create Cardano Dapp 15 | 16 | 17 | 18 |
19 |

20 | Create Cardano Dapp 21 |

22 | 23 |
24 |

This stack uses:

25 | 46 | 47 |
{data ?

{data.greeting}

:

Loading..

}
48 |
49 |
50 | 51 | ); 52 | }; 53 | 54 | export default Home; 55 | -------------------------------------------------------------------------------- /template/page-studs/index/with-tw.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import Head from "next/head"; 3 | 4 | const Home: NextPage = () => { 5 | return ( 6 | <> 7 | 8 | Create Cardano Dapp 9 | 10 | 11 | 12 | 13 |
14 |

15 | Create Cardano Dapp 16 |

17 |

This stack uses:

18 |
19 |
20 |

NextJS

21 |

22 | The React framework for production 23 |

24 | 30 | Documentation 31 | 32 |
33 |
34 |

TypeScript

35 |

36 | Strongly typed programming language that builds on JavaScript, 37 | giving you better tooling at any scale 38 |

39 | 45 | Documentation 46 | 47 |
48 |
49 |

TailwindCSS

50 |

51 | Rapidly build modern websites without ever leaving your HTML 52 |

53 | 59 | Documentation 60 | 61 |
62 |
63 |
64 | 65 | ); 66 | }; 67 | 68 | export default Home; 69 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | /* LANGUAGE COMPILATION OPTIONS */ 5 | "target": "ES2020", 6 | "lib": ["DOM", "DOM.Iterable", "ES2020"], 7 | "module": "Node16", 8 | "moduleResolution": "Node16", 9 | "resolveJsonModule": true, 10 | 11 | /* EMIT RULES */ 12 | "outDir": "./dist", 13 | "noEmit": true, // TSUP takes care of emitting js for us, in a MUCH faster way 14 | "declaration": true, 15 | "declarationMap": true, 16 | "sourceMap": true, 17 | "removeComments": true, 18 | 19 | /* TYPE CHECKING RULES */ 20 | "strict": true, 21 | // "noImplicitAny": true, // Included in "Strict" 22 | // "noImplicitThis": true, // Included in "Strict" 23 | // "strictBindCallApply": true, // Included in "Strict" 24 | // "strictFunctionTypes": true, // Included in "Strict" 25 | // "strictNullChecks": true, // Included in "Strict" 26 | // "strictPropertyInitialization": true, // Included in "Strict" 27 | "noFallthroughCasesInSwitch": true, 28 | "noImplicitOverride": true, 29 | "noImplicitReturns": true, 30 | "noUnusedLocals": true, 31 | "noUnusedParameters": true, 32 | "useUnknownInCatchVariables": true, 33 | "noUncheckedIndexedAccess": true, // TLDR - Checking an indexed value (array[0]) now forces type as there is no confirmation that index exists 34 | // THE BELOW ARE EXTRA STRICT OPTIONS THAT SHOULD ONLY BY CONSIDERED IN VERY SAFE PROJECTS 35 | // "exactOptionalPropertyTypes": true, // TLDR - Setting to undefined is not the same as a property not being defined at all 36 | // "noPropertyAccessFromIndexSignature": true, // TLDR - Use dot notation for objects if youre sure it exists, use ['index'] notaion if unsure 37 | 38 | /* OTHER OPTIONS */ 39 | "allowSyntheticDefaultImports": true, 40 | "esModuleInterop": true, 41 | // "emitDecoratorMetadata": true, 42 | // "experimentalDecorators": true, 43 | "forceConsistentCasingInFileNames": true, 44 | "skipLibCheck": true, 45 | "useDefineForClassFields": true 46 | }, 47 | "include": ["src"], 48 | "exclude": ["node_modules", "template"] 49 | } 50 | --------------------------------------------------------------------------------