├── .editorconfig ├── .github └── workflows │ ├── autofix.yml │ ├── ci.yml │ └── e2e-test.yml ├── .gitignore ├── .husky ├── pre-commit └── pre-push ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── MAINTAINERS.md ├── README.md ├── eslint.config.mjs ├── package.json ├── pnpm-lock.yaml ├── src ├── bin │ └── index.ts ├── index.ts ├── templates │ ├── frameworks.ts │ └── templates.ts └── utils │ ├── create-app-task-clone-template.ts │ ├── create-app-task-initialize-git.ts │ ├── create-app-task-install-dependencies.ts │ ├── create-app-task-run-init-script.ts │ ├── create-app.ts │ ├── ensure-target-path.ts │ ├── final-note.ts │ ├── get-app-info.ts │ ├── get-args-result.ts │ ├── get-args.ts │ ├── get-init-script.ts │ ├── get-prompt-name.ts │ ├── get-prompt-template.ts │ ├── get-prompts.ts │ ├── get-version.ts │ ├── parse-version.ts │ ├── search-and-replace.ts │ ├── validate-project-name.ts │ ├── validate-version.ts │ └── vendor │ ├── child-process-utils.ts │ ├── clack-tasks.ts │ ├── git.ts │ ├── names.ts │ └── package-manager.ts ├── test ├── index.test.ts └── search-and-replace.test.ts ├── tsconfig.json └── vitest.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/workflows/autofix.yml: -------------------------------------------------------------------------------- 1 | name: autofix.ci # needed to securely identify the workflow 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: ['main', 'next'] 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | autofix: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: pnpm/action-setup@v4 17 | with: 18 | run_install: false 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: 22 22 | cache: 'pnpm' 23 | - run: pnpm install 24 | - run: pnpm lint:fix 25 | - uses: autofix-ci/action@ff86a557419858bb967097bfc916833f5647fa8c 26 | with: 27 | commit-message: 'chore: apply automated updates' 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | pull_request: 9 | branches: 10 | - main 11 | - next 12 | 13 | jobs: 14 | ci: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: pnpm/action-setup@v4 19 | with: 20 | run_install: false 21 | - uses: actions/setup-node@v4 22 | with: 23 | node-version: 22 24 | cache: 'pnpm' 25 | - run: pnpm install 26 | - run: pnpm lint 27 | - run: pnpm test:types 28 | - run: pnpm build 29 | - run: pnpm vitest --coverage 30 | -------------------------------------------------------------------------------- /.github/workflows/e2e-test.yml: -------------------------------------------------------------------------------- 1 | name: Test Templates 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.event.number || github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | pull_request: 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | node: [20, 22] 19 | pm: [npm, pnpm, yarn] 20 | template: [ 21 | legacy-next-tailwind, 22 | legacy-next-tailwind-basic, 23 | legacy-next-tailwind-counter, 24 | legacy-react-vite-tailwind, 25 | legacy-react-vite-tailwind-basic, 26 | legacy-react-vite-tailwind-counter, 27 | next-tailwind, 28 | # next-tailwind-basic, 29 | # next-tailwind-counter, 30 | node-express, 31 | node-script, 32 | node-script, 33 | react-vite-tailwind, 34 | # react-vite-tailwind-basic, 35 | # react-vite-tailwind-counter, 36 | ] 37 | 38 | steps: 39 | - name: Setup Anchor 40 | uses: metadaoproject/setup-anchor@v3.1 41 | with: 42 | anchor-version: '0.30.1' 43 | solana-cli-version: '2.0.21' 44 | node-version: ${{ matrix.node }} 45 | - name: Configure Git identity 46 | run: | 47 | git config --global user.email "ci-bot@example.com" 48 | git config --global user.name "CI Bot" 49 | - uses: actions/checkout@v4 50 | - uses: pnpm/action-setup@v4 51 | with: 52 | run_install: false 53 | - uses: actions/setup-node@v4 54 | 55 | - name: Install Node.js 56 | uses: actions/setup-node@v4 57 | with: 58 | node-version: ${{ matrix.node }} 59 | 60 | - run: pnpm install 61 | - run: pnpm build 62 | 63 | - name: Install package manager (if needed) 64 | run: | 65 | case ${{ matrix.pm }} in 66 | npm) echo "Using npm";; 67 | pnpm) npm install -g pnpm;; 68 | yarn) npm install -g yarn;; 69 | esac 70 | 71 | - name: Create and Build using create-solana-dapp 72 | run: | 73 | TEMP_DIR=$(mktemp -d) 74 | cd "$TEMP_DIR" 75 | 76 | CLI_PATH="${{ github.workspace }}/dist/bin/index.cjs" 77 | 78 | case ${{ matrix.pm }} in 79 | npm) node "$CLI_PATH" --template ${{ matrix.template }} sandbox ;; 80 | pnpm) node "$CLI_PATH" --template ${{ matrix.template }} sandbox --pnpm ;; 81 | yarn) node "$CLI_PATH" --template ${{ matrix.template }} sandbox --yarn ;; 82 | esac 83 | 84 | cd sandbox 85 | ${{ matrix.pm }} install 86 | ${{ matrix.pm }} run build 87 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | dist 5 | tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | 41 | .nx/cache 42 | .nx/workspace-data 43 | nx-cloud.env 44 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpm verify-pnpm-lock 5 | pnpm sync-readmes --check 6 | pnpm sync-schemas --check 7 | pnpm lint-staged 8 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpm nx format:check 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | tmp 6 | /.yarn 7 | pnpm-lock.yaml 8 | 9 | .nx/cache 10 | # Ignore generated schema files in packages 11 | /packages/*/src/generators/**/*-schema.d.ts 12 | 13 | /.nx/workspace-data 14 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 120, 4 | "semi": false, 5 | "trailingComma": "all", 6 | "arrowParens": "always", 7 | "endOfLine": "auto", 8 | "proseWrap": "always" 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "nrwl.angular-console", 4 | "esbenp.prettier-vscode", 5 | "dbaeumer.vscode-eslint", 6 | "firsttris.vscode-jest-runner" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": ["json"] 3 | } 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to create-solana-dapp 2 | 3 | We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: 4 | 5 | - Reporting a bug 6 | - Discussing the current state of the code 7 | - Submitting a fix 8 | - Proposing new features 9 | - Becoming a maintainer 10 | 11 | ## We Develop with GitHub 12 | 13 | We use GitHub to host code, to track issues and feature requests, as well as accept pull requests. 14 | 15 | ## We use [GitHub Flow](https://guides.github.com/introduction/flow/index.html), so all code changes happen through pull requests 16 | 17 | Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests: 18 | 19 | 1. Fork the repo and create your branch from `main`. 20 | 2. If you've added code that should be tested, add tests. 21 | 3. If you've changed APIs, update the documentation. 22 | 4. Ensure the test suite passes. 23 | 5. Make sure your code lints. 24 | 6. Issue that pull request! 25 | 26 | ## Any contributions you make will be under the MIT Software License 27 | 28 | In short, when you submit code changes, your submissions are understood to be under the same 29 | [MIT License](https://choosealicense.com/licenses/mit/) that covers the project. 30 | 31 | ## Report bugs using GitHub's [issues](https://github.com/solana-developers/create-solana-dapp/issues) 32 | 33 | We use GitHub issues to track public bugs. Report a bug by 34 | [opening a new issue](https://github.com/solana-developers/create-solana-dapp/issues/new); it's that easy! 35 | 36 | **Great Bug Reports** tend to have: 37 | 38 | - A quick summary and/or background 39 | - Steps to reproduce 40 | - Be specific! 41 | - Give sample code if you can. 42 | - What you expected would happen 43 | - What actually happens 44 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 45 | 46 | People _love_ thorough bug reports. 47 | 48 | ## Use a Consistent Coding Style 49 | 50 | - 2 spaces for indentation rather than tabs 51 | - You can try running `pnpm lint` for style unification 52 | 53 | ## Development Workflow 54 | 55 | In this section, you'll find the basic commands you need to run for building, testing, and maintaining the quality of 56 | the codebase. 57 | 58 | ### Building the Project 59 | 60 | To compile the project and generate the necessary artifacts, use the build command: 61 | 62 | ```shell 63 | pnpm build 64 | ``` 65 | 66 | You can build the project in watch mode by using the following command for faster feedback: 67 | 68 | ```shell 69 | pnpm build:watch 70 | ``` 71 | 72 | ### Running Tests 73 | 74 | To ensure your contributions do not break any existing functionality, run the test suite with the following command: 75 | 76 | ```shell 77 | pnpm test 78 | ``` 79 | 80 | You can run the tests in watch mode by running the following command for faster feedback: 81 | 82 | ```shell 83 | pnpm dev 84 | ``` 85 | 86 | ### Linting Your Code 87 | 88 | It's important to maintain the coding standards of the project. Lint your code by executing: 89 | 90 | ```shell 91 | pnpm lint 92 | ``` 93 | 94 | ### Working on the CLI 95 | 96 | If you want to quickly test your changes to the CLI, you can do the following: 97 | 98 | #### create-solana-dapp 99 | 100 | Run the build in watch mode in one terminal: 101 | 102 | ```shell 103 | pnpm build:watch 104 | ``` 105 | 106 | In another terminal, move to the directory where you want to test the `create-solana-dapp` CLI and run by invoking the 107 | `node` command with the path to the compiled CLI: 108 | 109 | ```shell 110 | cd /tmp 111 | node ~/path/to/create-solana-dapp/dist/bin/index.cjs --help 112 | ``` 113 | 114 | ### Committing Your Changes 115 | 116 | We follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification for commit: 117 | 118 | - `fix`: a commit of the type fix patches a bug in your codebase (this correlates with PATCH in semantic versioning). 119 | - `feat`: a commit of the type feat introduces a new feature to the codebase (this correlates with MINOR in semantic 120 | versioning). 121 | - `BREAKING CHANGE`: a commit that has the text BREAKING CHANGE: at the beginning of its optional body or footer section 122 | introduces a breaking API change (correlating with MAJOR in semantic versioning). A BREAKING CHANGE can be part of 123 | commits of any type. 124 | - Others: commit types other than fix: and feat: are allowed, for example @commitlint/config-conventional (based on the 125 | Angular convention) recommends build:, chore:, ci:, docs:, style:, refactor:, perf:, test:, and others. 126 | 127 | ## License 128 | 129 | By contributing, you agree that your contributions will be licensed under its MIT License. 130 | 131 | ## References 132 | 133 | This document was adapted from the open-source contribution guidelines for 134 | [Facebook's Draft](https://github.com/facebook/draft-js/blob/master/CONTRIBUTING.md) 135 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2024 Solana Foundation 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 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Maintaining create-solana-dapp 2 | 3 | The document has information for the maintainers of this package. 4 | 5 | ## Publishing new versions 6 | 7 | > Note: Your NPM cli must be logged into the NPM registry and have the correct permissions to publish a new version. 8 | 9 | ### `next` tag 10 | 11 | The `next` tag is considered the beta/testing version of the `create-solana-dapp` tool, and the specific version will 12 | normally include such a `beta` flag in it: 13 | 14 | ```shell 15 | pnpm version 16 | pnpm release:next 17 | ``` 18 | 19 | This will allow anyone to use the current beta/next version of the CLI using the following command: 20 | 21 | ```shell 22 | pnpx create-solana-dapp@next 23 | # Or use: npx create-solana-dapp@next / Yarn sadly can't do this with arbitrary tags. 24 | ``` 25 | 26 | ### `latest` tag 27 | 28 | The `latest` tag is considered the production/stable version of the `create-solana-dapp` tool. To publish to the 29 | `latest` tag: 30 | 31 | ```shell 32 | pnpm version 33 | pnpm release 34 | ``` 35 | 36 | This will allow anyone to use the current production/stable version of the CLI using the following command: 37 | 38 | ```shell 39 | pnpx create-solana-dapp@latest 40 | # Or use: npx create-solana-dapp@latest / yarn create solana-dapp 41 | ``` 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # create-solana-dapp 2 | 3 | :zap: Get up and running fast with Solana dApps :zap: 4 | 5 | Just run one simple command to generate a new project! 6 | 7 | ```shell 8 | npx create-solana-dapp@latest 9 | ``` 10 | 11 | [![npm version](https://img.shields.io/npm/v/create-solana-dapp?color=yellow)](https://npmjs.com/package/create-solana-dapp) 12 | [![npm downloads](https://img.shields.io/npm/dm/create-solana-dapp?color=yellow)](https://npmjs.com/package/create-solana-dapp) 13 | 14 | ## Templates 15 | 16 | The templates are supported within `create-solana-dapp`: 17 | 18 | - Next.js 19 | - [Next.js + Tailwind CSS (no Anchor)](https://github.com/solana-developers/template-next-tailwind) 20 | - [Next.js + Tailwind CSS + Anchor Basic Example](https://github.com/solana-developers/template-next-tailwind-basic) 21 | - [Next.js + Tailwind CSS + Anchor Counter Example](https://github.com/solana-developers/template-next-tailwind-counter) 22 | - React with Vite 23 | - [React with Vite + Tailwind CSS (no Anchor)](https://github.com/solana-developers/template-react-vite-tailwind) 24 | - [React with Vite + Tailwind CSS + Anchor Basic Example](https://github.com/solana-developers/template-react-vite-tailwind-basic) 25 | - [React with Vite + Tailwind CSS + Anchor Counter Example](https://github.com/solana-developers/template-react-vite-tailwind-counter) 26 | 27 | ## External templates 28 | 29 | You can also use `create-solana-dapp` to create projects using external templates: 30 | 31 | The `--template` (or `-t`) flag supports anything that [giget](https://github.com/unjs/giget) supports 32 | 33 | ```shell 34 | pnpx create-solana-dapp --template / 35 | ``` 36 | 37 | ## Init script 38 | 39 | Template authors can add an init script to the `package.json` file to help set up the project. 40 | 41 | Use this script to return instructions to the user, check the `anchor` and `solana` versions, and replace text and files 42 | in the project. 43 | 44 | ```jsonc 45 | { 46 | "name": "your-template", 47 | "create-solana-dapp": { 48 | // These instructions will be returned to the user after installation 49 | "instructions": [ 50 | "Run Anchor commands:", 51 | // Adding a '+' will make the line bold and '{pm}' is replaced with the package manager 52 | "+{pm} run anchor build | test | localnet | deploy", 53 | ], 54 | // Rename is a map of terms to rename 55 | "rename": { 56 | // Rename every instance of counter 57 | "counter": { 58 | // With the name of the project 59 | "to": "{{name}}", 60 | // In the following paths 61 | "paths": ["anchor", "src"], 62 | }, 63 | }, 64 | // Check versions and give a warning if it's not installed or the version is lower 65 | "versions": { 66 | "anchor": "0.30.1", 67 | "solana": "1.18.0", 68 | }, 69 | }, 70 | } 71 | ``` 72 | 73 | ### Planned frameworks to support 74 | 75 | The following UI frameworks are planned and expected to be supported in the future: 76 | 77 | - VueJS 78 | - Svelte 79 | - React Native 80 | 81 | ## Supported on-chain program frameworks 82 | 83 | The following on-chain programs (aka smart contracts) frameworks are supported within `create-solana-dapp`: 84 | 85 | - Anchor 86 | 87 | ## Contributing 88 | 89 | Contributions are welcome! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for more info. 90 | 91 | ## Local development 92 | 93 | > [!TIP] This project uses [pnpm](https://pnpm.io/) as the package manager. If you don't have it, you can install it 94 | > using `corepack`: 95 | > 96 | > ```sh 97 | > corepack enable 98 | > corepack prepare pnpm@10 --activate 99 | > ``` 100 | 101 | To install the project locally, run the following commands: 102 | 103 | ```shell 104 | git clone https://github.com/solana-developers/create-solana-dapp.git 105 | cd create-solana-dapp 106 | pnpm install 107 | pnpm build 108 | ``` 109 | 110 | Detailed instructions on the local development workflow are outlined in the 111 | [Development Workflow](./CONTRIBUTING.md#development-workflow) section of the CONTRIBUTING guidelines. 112 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import unjs from 'eslint-config-unjs' 2 | 3 | export default unjs({ 4 | ignores: [ 5 | // ignore paths 6 | 'coverage', 7 | 'dist', 8 | 'node_modules', 9 | 'tmp', 10 | ], 11 | rules: { 12 | // rule overrides 13 | '@typescript-eslint/no-require-imports': 'off', 14 | 'unicorn/no-array-reduce': 'off', 15 | 'unicorn/no-process-exit': 'off', 16 | 'unicorn/prefer-top-level-await': 'off', 17 | }, 18 | markdown: { 19 | rules: { 20 | // markdown rule overrides 21 | }, 22 | }, 23 | }) 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-solana-dapp", 3 | "version": "4.2.9", 4 | "description": "Get up and running fast with Solana dApps", 5 | "repository": { 6 | "name": "solana-developers/create-solana-dapp", 7 | "type": "git", 8 | "url": "https://github.com/solana-developers/create-solana-dapp" 9 | }, 10 | "homepage": "https://github.com/solana-developers/create-solana-dapp#readme", 11 | "bugs": { 12 | "url": "https://github.com/solana-developers/create-solana-dapp/issues" 13 | }, 14 | "license": "MIT", 15 | "sideEffects": false, 16 | "type": "module", 17 | "publishConfig": { 18 | "access": "public" 19 | }, 20 | "exports": { 21 | ".": { 22 | "types": "./dist/index.d.ts", 23 | "import": "./dist/index.mjs", 24 | "require": "./dist/index.cjs" 25 | } 26 | }, 27 | "main": "./dist/index.cjs", 28 | "module": "./dist/index.mjs", 29 | "types": "./dist/index.d.ts", 30 | "bin": { 31 | "create-solana-dapp": "./dist/bin/index.cjs" 32 | }, 33 | "files": [ 34 | "dist" 35 | ], 36 | "scripts": { 37 | "build": "unbuild", 38 | "build:watch": "unbuild --watch", 39 | "dev": "vitest dev", 40 | "lint": "eslint . && prettier -c .", 41 | "lint:fix": "automd && eslint . --fix && prettier -w .", 42 | "prepack": "pnpm build", 43 | "release": "pnpm build && pnpm test && npm publish --tag latest && git push --follow-tags", 44 | "release:next": "pnpm build && pnpm test && npm publish --tag next && git push --follow-tags", 45 | "test": "pnpm lint && pnpm test:types && vitest run --coverage", 46 | "test:types": "tsc --noEmit --skipLibCheck" 47 | }, 48 | "devDependencies": { 49 | "@types/mock-fs": "^4.13.4", 50 | "@types/node": "^22.13.1", 51 | "@types/semver": "^7.5.8", 52 | "@vitest/coverage-v8": "^3.0.5", 53 | "automd": "^0.3.12", 54 | "eslint": "^9.20.0", 55 | "eslint-config-unjs": "^0.4.2", 56 | "mock-fs": "^5.5.0", 57 | "prettier": "^3.4.2", 58 | "typescript": "^5.6.0", 59 | "unbuild": "^3.3.1", 60 | "vitest": "^3.0.5" 61 | }, 62 | "packageManager": "pnpm@10.5.2", 63 | "dependencies": { 64 | "@clack/prompts": "^0.7.0", 65 | "commander": "^13.1.0", 66 | "giget": "^1.2.4", 67 | "picocolors": "^1.1.1", 68 | "semver": "^7.7.1", 69 | "zod": "^3.24.1" 70 | }, 71 | "contributors": [ 72 | { 73 | "name": "Joe Caulfield", 74 | "url": "https://github.com/jpcaulfi" 75 | }, 76 | { 77 | "name": "Jacob Creech", 78 | "url": "https://github.com/jacobcreech" 79 | }, 80 | { 81 | "name": "Nick Frostbutter", 82 | "url": "https://github.com/nickfrosty" 83 | }, 84 | { 85 | "name": "Bram Borggreve", 86 | "url": "https://github.com/beeman" 87 | } 88 | ], 89 | "keywords": [ 90 | "solana", 91 | "next", 92 | "react", 93 | "web3", 94 | "blockchain", 95 | "nft" 96 | ] 97 | } 98 | -------------------------------------------------------------------------------- /src/bin/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { log } from '@clack/prompts' 3 | import { main } from '../index' 4 | 5 | main(process.argv) 6 | // Pretty log the error, then throw it 7 | .catch((error_) => { 8 | log.error(error_) 9 | throw error_ 10 | }) 11 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { cancel, log, note, outro } from '@clack/prompts' 2 | import * as process from 'node:process' 3 | import { createApp } from './utils/create-app' 4 | import { finalNote } from './utils/final-note' 5 | import { getAppInfo } from './utils/get-app-info' 6 | import { getArgs } from './utils/get-args' 7 | import { detectInvokedPackageManager } from './utils/vendor/package-manager' 8 | 9 | export async function main(argv: string[]) { 10 | // Get the invoked package manager 11 | const pm = detectInvokedPackageManager() 12 | 13 | // Get app info from package.json 14 | const app = getAppInfo() 15 | 16 | try { 17 | // Get the result from the command line and prompts 18 | const args = await getArgs(argv, app, pm) 19 | 20 | if (args.dryRun) { 21 | note(JSON.stringify(args, undefined, 2), 'Arguments') 22 | outro('🚀 Dry run was used, no changes were made') 23 | return 24 | } 25 | 26 | if (args.verbose) { 27 | log.warn(`Verbose output enabled`) 28 | console.warn(args) 29 | } 30 | 31 | // Create the app 32 | const instructions = await createApp(args) 33 | 34 | note( 35 | finalNote({ ...args, target: args.targetDirectory.replace(process.cwd(), '.'), instructions }), 36 | 'Installation successful', 37 | ) 38 | 39 | outro('Good luck with your project!') 40 | } catch (error) { 41 | cancel(`${error}`) 42 | process.exit(1) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/templates/frameworks.ts: -------------------------------------------------------------------------------- 1 | import { Template } from './templates' 2 | 3 | export interface Framework { 4 | id: string 5 | name: string 6 | description: string 7 | templates: Template[] 8 | } 9 | 10 | export const frameworks: Framework[] = [ 11 | { 12 | id: 'next', 13 | name: 'Next.js', 14 | description: 'A React framework by Vercel', 15 | templates: [ 16 | // { 17 | // name: 'next-tailwind-counter', 18 | // description: 'Next.js + Tailwind CSS + Anchor Counter Example', 19 | // repository: 'gh:solana-developers/solana-templates/templates/template-next-tailwind-counter', 20 | // }, 21 | // { 22 | // name: 'next-tailwind-basic', 23 | // description: 'Next.js + Tailwind CSS + Anchor Basic Example', 24 | // repository: 'gh:solana-developers/solana-templates/templates/template-next-tailwind-basic', 25 | // }, 26 | { 27 | name: 'next-tailwind', 28 | description: 'Next.js + Tailwind CSS, no Anchor', 29 | repository: 'gh:solana-developers/solana-templates/templates/template-next-tailwind', 30 | }, 31 | { 32 | name: 'legacy-next-tailwind-counter', 33 | description: 'Legacy Next.js + Tailwind CSS + Anchor Counter Example', 34 | repository: 'gh:solana-developers/solana-templates/legacy/legacy-next-tailwind-counter', 35 | }, 36 | { 37 | name: 'legacy-next-tailwind-basic', 38 | description: 'Legacy Next.js + Tailwind CSS + Anchor Basic Example', 39 | repository: 'gh:solana-developers/solana-templates/legacy/legacy-next-tailwind-basic', 40 | }, 41 | { 42 | name: 'legacy-next-tailwind', 43 | description: 'Legacy Next.js + Tailwind CSS, no Anchor', 44 | repository: 'gh:solana-developers/solana-templates/legacy/legacy-next-tailwind', 45 | }, 46 | ], 47 | }, 48 | { 49 | id: 'node', 50 | name: 'Node.js', 51 | description: "JavaScript runtime built on Chrome's V8 engine", 52 | templates: [ 53 | { 54 | name: 'node-express', 55 | description: 'Node Express server with gill', 56 | repository: 'gh:solana-developers/solana-templates/templates/template-node-express', 57 | }, 58 | { 59 | name: 'node-script', 60 | description: 'Simple Node script with gill', 61 | repository: 'gh:solana-developers/solana-templates/templates/template-node-script', 62 | }, 63 | ], 64 | }, 65 | { 66 | id: 'react-vite', 67 | name: 'React with Vite', 68 | description: 'React with Vite and React Router', 69 | templates: [ 70 | // { 71 | // name: 'react-vite-tailwind-counter', 72 | // description: 'React with Vite + Tailwind CSS + Anchor Counter Example', 73 | // repository: 'gh:solana-developers/solana-templates/templates/template-react-vite-tailwind-counter', 74 | // }, 75 | // { 76 | // name: 'react-vite-tailwind-basic', 77 | // description: 'React with Vite + Tailwind CSS + Anchor Basic Example', 78 | // repository: 'gh:solana-developers/solana-templates/templates/template-react-vite-tailwind-basic', 79 | // }, 80 | { 81 | name: 'react-vite-tailwind', 82 | description: 'React with Vite + Tailwind CSS, no Anchor', 83 | repository: 'gh:solana-developers/solana-templates/templates/template-react-vite-tailwind', 84 | }, 85 | { 86 | name: 'legacy-react-vite-tailwind-counter', 87 | description: 'React with Vite + Tailwind CSS + Anchor Counter Example', 88 | repository: 'gh:solana-developers/solana-templates/legacy/legacy-react-vite-tailwind-counter', 89 | }, 90 | { 91 | name: 'legacy-react-vite-tailwind-basic', 92 | description: 'React with Vite + Tailwind CSS + Anchor Basic Example', 93 | repository: 'gh:solana-developers/solana-templates/legacy/legacy-react-vite-tailwind-basic', 94 | }, 95 | { 96 | name: 'legacy-react-vite-tailwind', 97 | description: 'React with Vite + Tailwind CSS, no Anchor', 98 | repository: 'gh:solana-developers/solana-templates/legacy/legacy-react-vite-tailwind', 99 | }, 100 | ], 101 | }, 102 | ] 103 | -------------------------------------------------------------------------------- /src/templates/templates.ts: -------------------------------------------------------------------------------- 1 | import { log } from '@clack/prompts' 2 | import { Framework, frameworks } from './frameworks' 3 | 4 | export const templates = getTemplatesForFrameworks(frameworks) 5 | 6 | export interface Template { 7 | name: string 8 | description: string 9 | repository: string 10 | } 11 | 12 | export function findTemplate(name: string): Template { 13 | // A template name with a `/` is considered external 14 | if (name.includes('/')) { 15 | return { 16 | name, 17 | description: `${name} (external)`, 18 | repository: name.includes(':') ? name : `gh:${name}`, 19 | } 20 | } 21 | 22 | const template: Template | undefined = templates.find((template) => template.name === name) 23 | 24 | if (!template) { 25 | throw new Error(`Template ${name} not found`) 26 | } 27 | return template 28 | } 29 | 30 | function getTemplatesForFrameworks(frameworks: Framework[] = []): Template[] { 31 | return frameworks.reduce((acc, item) => { 32 | return [...acc, ...getTemplatesForFramework(item)] 33 | }, [] as Template[]) 34 | } 35 | 36 | export function getTemplatesForFramework(framework: Framework): Template[] { 37 | return framework.templates 38 | } 39 | 40 | export function listTemplates() { 41 | for (const template of templates) { 42 | log.info(`${template.name}: \n\n\t${template.description}\n\t${template.repository}`) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/create-app-task-clone-template.ts: -------------------------------------------------------------------------------- 1 | import { log } from '@clack/prompts' 2 | import { downloadTemplate } from 'giget' 3 | import { existsSync } from 'node:fs' 4 | import { readdir } from 'node:fs/promises' 5 | import { GetArgsResult } from './get-args-result' 6 | import { Task, taskFail } from './vendor/clack-tasks' 7 | 8 | export function createAppTaskCloneTemplate(args: GetArgsResult): Task { 9 | return { 10 | title: 'Cloning template', 11 | task: async (result) => { 12 | const exists = existsSync(args.targetDirectory) 13 | 14 | if (exists) { 15 | taskFail(`Target directory ${args.targetDirectory} already exists`) 16 | } 17 | if (!args.template.repository) { 18 | taskFail('No template repository specified') 19 | } 20 | if (args.verbose) { 21 | log.warn(`Cloning template ${args.template.repository} to ${args.targetDirectory}`) 22 | } 23 | try { 24 | const { dir } = await downloadTemplate(args.template.repository, { 25 | dir: args.targetDirectory, 26 | }) 27 | // make sure the dir is not empty 28 | const files = await readdir(dir) 29 | if (files.length === 0) { 30 | taskFail(`The template directory is empty. Please check the repository: ${args.template.repository}`) 31 | return 32 | } 33 | 34 | return result({ message: `Cloned template to ${dir}` }) 35 | } catch (error) { 36 | taskFail(`init: Error cloning the template: ${error}`) 37 | } 38 | }, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/utils/create-app-task-initialize-git.ts: -------------------------------------------------------------------------------- 1 | import { log } from '@clack/prompts' 2 | import { GetArgsResult } from './get-args-result' 3 | import { Task, taskFail } from './vendor/clack-tasks' 4 | import { initializeGitRepo } from './vendor/git' 5 | 6 | export function createAppTaskInitializeGit(args: GetArgsResult): Task { 7 | return { 8 | enabled: !args.skipGit, 9 | title: 'Initializing git', 10 | task: async (result) => { 11 | try { 12 | if (args.verbose) { 13 | log.warn(`Initializing git repo`) 14 | } 15 | await initializeGitRepo(args.targetDirectory, { 16 | commit: { email: '', name: '', message: 'chore: initial commit' }, 17 | }) 18 | return result({ message: 'Initialized git repo' }) 19 | } catch (error) { 20 | if (args.verbose) { 21 | log.error(`Error initializing git repo: ${error}`) 22 | console.error(error) 23 | } 24 | log.error(`${error}`) 25 | taskFail(`init: Error initializing git: ${error}`) 26 | } 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/create-app-task-install-dependencies.ts: -------------------------------------------------------------------------------- 1 | import { log } from '@clack/prompts' 2 | import { existsSync } from 'node:fs' 3 | import { join } from 'node:path' 4 | import { GetArgsResult } from './get-args-result' 5 | import { execAndWait } from './vendor/child-process-utils' 6 | import { Task, taskFail } from './vendor/clack-tasks' 7 | import { getPackageManagerCommand } from './vendor/package-manager' 8 | 9 | export function createAppTaskInstallDependencies(args: GetArgsResult): Task { 10 | const pm = args.packageManager 11 | const { install, lockFile } = getPackageManagerCommand(pm, args.verbose) 12 | return { 13 | enabled: !args.skipInstall, 14 | title: `Installing via ${pm}`, 15 | task: async (result) => { 16 | if (args.verbose) { 17 | log.warn(`Installing via ${pm}`) 18 | } 19 | const deleteLockFiles = ['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock'] 20 | // We don't want to delete the lock file for the current package manager 21 | .filter((item) => item !== lockFile) 22 | // We only want to delete the lock file if it exists 23 | .filter((item) => existsSync(join(args.targetDirectory, item))) 24 | 25 | for (const lockFile of deleteLockFiles) { 26 | if (args.verbose) { 27 | log.warn(`Deleting ${lockFile}`) 28 | } 29 | await execAndWait(`rm ${lockFile}`, args.targetDirectory) 30 | } 31 | if (args.verbose) { 32 | log.warn(`Running ${install}`) 33 | } 34 | try { 35 | await execAndWait(install, args.targetDirectory) 36 | return result({ message: `Installed via ${pm}` }) 37 | } catch (error) { 38 | taskFail(`init: Error installing dependencies: ${error}`) 39 | } 40 | }, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/create-app-task-run-init-script.ts: -------------------------------------------------------------------------------- 1 | import { log } from '@clack/prompts' 2 | import { join } from 'node:path' 3 | import { bold, yellow } from 'picocolors' 4 | import { ensureTargetPath } from './ensure-target-path' 5 | import { GetArgsResult } from './get-args-result' 6 | import { deleteInitScript, getInitScript, InitScript } from './get-init-script' 7 | import { searchAndReplace } from './search-and-replace' 8 | import { validateAnchorVersion, validateSolanaVersion } from './validate-version' 9 | import { Task, taskFail } from './vendor/clack-tasks' 10 | import { namesValues } from './vendor/names' 11 | 12 | export function createAppTaskRunInitScript(args: GetArgsResult): Task { 13 | return { 14 | enabled: !args.skipInit, 15 | title: 'Running init script', 16 | task: async (result) => { 17 | try { 18 | const init = getInitScript(args.targetDirectory) 19 | if (!init) { 20 | return result({ message: 'Repository does not have an init script' }) 21 | } 22 | if (args.verbose) { 23 | log.warn(`Running init script`) 24 | } 25 | 26 | await initCheckVersion(init) 27 | if (args.verbose) { 28 | log.warn(`initCheckVersion done`) 29 | } 30 | await initRename(args, init, args.verbose) 31 | if (args.verbose) { 32 | log.warn(`initRename done`) 33 | } 34 | 35 | const instructions: string[] = (initInstructions(init) ?? []) 36 | ?.filter(Boolean) 37 | .map((msg) => msg.replace('{pm}', args.packageManager)) 38 | 39 | if (args.verbose) { 40 | log.warn(`initInstructions done`) 41 | } 42 | deleteInitScript(args.targetDirectory) 43 | if (args.verbose) { 44 | log.warn(`deleteInitScript done`) 45 | } 46 | return result({ message: 'Executed init script', instructions }) 47 | } catch (error) { 48 | taskFail(`init: Error running init script: ${error}`) 49 | } 50 | }, 51 | } 52 | } 53 | 54 | async function initRename(args: GetArgsResult, init: InitScript, verbose: boolean) { 55 | // Rename template to project name throughout the whole project 56 | await searchAndReplace( 57 | args.targetDirectory, 58 | [`template-${args.template.name}`, args.template.name], 59 | [args.name, args.name], 60 | false, 61 | verbose, 62 | ) 63 | 64 | // Return early if there are no renames defined in the init script 65 | if (!init?.rename) { 66 | return 67 | } 68 | 69 | // Loop through each word in the rename object 70 | for (const from of Object.keys(init.rename)) { 71 | // Get the 'to' property from the rename object 72 | const to = init.rename[from].to.replace('{{name}}', args.name.replace(/-/g, '')) 73 | 74 | // Get the name matrix for the 'from' and the 'to' value 75 | const fromNames = namesValues(from) 76 | const toNames = namesValues(to) 77 | 78 | for (const path of init.rename[from].paths) { 79 | const targetPath = join(args.targetDirectory, path) 80 | if (!(await ensureTargetPath(targetPath))) { 81 | console.error(`init-script.rename: target does not exist ${targetPath}`) 82 | continue 83 | } 84 | await searchAndReplace(join(args.targetDirectory, path), fromNames, toNames, args.dryRun) 85 | } 86 | } 87 | } 88 | 89 | async function initCheckVersion(init: InitScript) { 90 | if (init?.versions?.anchor) { 91 | await initCheckVersionAnchor(init.versions.anchor) 92 | } 93 | if (init?.versions?.solana) { 94 | await initCheckVersionSolana(init.versions.solana) 95 | } 96 | } 97 | 98 | async function initCheckVersionAnchor(requiredVersion: string) { 99 | try { 100 | const { required, valid, version } = validateAnchorVersion(requiredVersion) 101 | if (!version) { 102 | log.warn( 103 | [ 104 | bold(yellow(`Could not find Anchor version. Please install Anchor.`)), 105 | 'https://www.anchor-lang.com/docs/installation', 106 | ].join(' '), 107 | ) 108 | } else if (!valid) { 109 | log.warn( 110 | [ 111 | yellow(`Found Anchor version ${version}. Expected Anchor version ${required}.`), 112 | 'https://www.anchor-lang.com/release-notes/0.30.1', 113 | ].join(' '), 114 | ) 115 | } 116 | } catch (error_) { 117 | log.warn(`Error ${error_}`) 118 | } 119 | } 120 | 121 | async function initCheckVersionSolana(requiredVersion: string) { 122 | try { 123 | const { required, valid, version } = validateSolanaVersion(requiredVersion) 124 | if (!version) { 125 | log.warn( 126 | [ 127 | bold(yellow(`Could not find Solana version. Please install Solana.`)), 128 | 'https://docs.solana.com/cli/install-solana-cli-tools', 129 | ].join(' '), 130 | ) 131 | } else if (!valid) { 132 | log.warn( 133 | [ 134 | yellow(`Found Solana version ${version}. Expected Solana version ${required}.`), 135 | 'https://docs.solana.com/cli/install-solana-cli-tools', 136 | ].join(' '), 137 | ) 138 | } 139 | } catch (error_) { 140 | log.warn(`Error ${error_}`) 141 | } 142 | } 143 | 144 | function initInstructions(init: InitScript) { 145 | return init?.instructions?.length === 0 ? [] : init?.instructions 146 | } 147 | -------------------------------------------------------------------------------- /src/utils/create-app.ts: -------------------------------------------------------------------------------- 1 | import { createAppTaskCloneTemplate } from './create-app-task-clone-template' 2 | import { createAppTaskInitializeGit } from './create-app-task-initialize-git' 3 | import { createAppTaskInstallDependencies } from './create-app-task-install-dependencies' 4 | import { createAppTaskRunInitScript } from './create-app-task-run-init-script' 5 | import { GetArgsResult } from './get-args-result' 6 | import { tasks } from './vendor/clack-tasks' 7 | 8 | export async function createApp(args: GetArgsResult) { 9 | return tasks([ 10 | // Clone the template to the target directory 11 | createAppTaskCloneTemplate(args), 12 | // Install the dependencies 13 | createAppTaskInstallDependencies(args), 14 | // Run the init script define in package.json .init property 15 | createAppTaskRunInitScript(args), 16 | // Initialize git repository 17 | createAppTaskInitializeGit(args), 18 | ]) 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/ensure-target-path.ts: -------------------------------------------------------------------------------- 1 | import { access } from 'node:fs/promises' 2 | 3 | export async function ensureTargetPath(path: string) { 4 | try { 5 | await access(path) 6 | return true 7 | } catch { 8 | return false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/final-note.ts: -------------------------------------------------------------------------------- 1 | import { bold, white } from 'picocolors' 2 | import { GetArgsResult } from './get-args-result' 3 | 4 | export function finalNote(args: GetArgsResult & { target: string; instructions: string[] }): string { 5 | const lines: string[] = [ 6 | `That's it!`, 7 | `Change to your new directory and start developing:`, 8 | msg(`cd ${args.target}`), 9 | `Start the app:`, 10 | cmd(args.packageManager, 'dev'), 11 | ...args.instructions.map((line) => (line.startsWith('+') ? msg(line.slice(1)) : line)), 12 | ] 13 | 14 | return lines.join('\n\n') 15 | } 16 | 17 | function cmd(pm: string, command: string) { 18 | return msg(`${pm} run ${command}`) 19 | } 20 | 21 | function msg(message: string) { 22 | return bold(white(message)) 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/get-app-info.ts: -------------------------------------------------------------------------------- 1 | import packageJson from '../../package.json' assert { type: 'json' } 2 | 3 | export interface AppInfo { 4 | name: string 5 | version: string 6 | } 7 | 8 | export function getAppInfo(): AppInfo { 9 | return { 10 | name: packageJson.name, 11 | version: packageJson.version, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/get-args-result.ts: -------------------------------------------------------------------------------- 1 | import { Template } from '../templates/templates' 2 | import { AppInfo } from './get-app-info' 3 | import { PackageManager } from './vendor/package-manager' 4 | 5 | export interface GetArgsResult { 6 | app: AppInfo 7 | dryRun: boolean 8 | name: string 9 | targetDirectory: string 10 | packageManager: PackageManager 11 | skipGit: boolean 12 | skipInit: boolean 13 | skipInstall: boolean 14 | template: Template 15 | verbose: boolean 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/get-args.ts: -------------------------------------------------------------------------------- 1 | import { intro, log, outro } from '@clack/prompts' 2 | import { program } from 'commander' 3 | import { findTemplate, listTemplates, Template } from '../templates/templates' 4 | import { AppInfo } from './get-app-info' 5 | import { GetArgsResult } from './get-args-result' 6 | import { getPrompts } from './get-prompts' 7 | import { listVersions } from './validate-version' 8 | import { PackageManager } from './vendor/package-manager' 9 | 10 | export async function getArgs(argv: string[], app: AppInfo, pm: PackageManager = 'npm'): Promise { 11 | // Get the result from the command line 12 | const input = program 13 | .name(app.name) 14 | .version(app.version, '-V, --version', help('Output the version number')) 15 | .argument('[name]', 'Name of the project (default: )') 16 | .option('--pm, --package-manager ', help(`Package manager to use (default: npm)`)) 17 | .option('--yarn', help(`Use yarn as the package manager`), false) 18 | .option('--pnpm', help(`Use pnpm as the package manager`), false) 19 | .option('-d, --dry-run', help('Dry run (default: false)')) 20 | .option('-t, --template ', help('Use a template')) 21 | .option('--list-templates', help('List available templates')) 22 | .option('--list-versions', help('Verify your versions of Anchor, AVM, Rust, and Solana')) 23 | .option('--skip-git', help('Skip git initialization')) 24 | .option('--skip-init', help('Skip running the init script')) 25 | .option('--skip-install', help('Skip installing dependencies')) 26 | .option('-v, --verbose', help('Verbose output (default: false)')) 27 | .helpOption('-h, --help', help('Display help for command')) 28 | .addHelpText( 29 | 'after', 30 | ` 31 | Examples: 32 | $ ${app.name} my-app 33 | $ ${app.name} my-app --package-manager pnpm # or --pm pnpm/yarn 34 | $ ${app.name} my-app --pnpm # or --yarn 35 | `, 36 | ) 37 | .parse(argv) 38 | 39 | // Get the optional name argument (positional) 40 | const name = input.args[0] 41 | 42 | // Get the options from the command line 43 | const result = input.opts() 44 | 45 | if (result.listVersions) { 46 | listVersions() 47 | process.exit(0) 48 | } 49 | if (result.listTemplates) { 50 | listTemplates() 51 | outro( 52 | `\uD83D\uDCA1 To use a template, run "${app.name}${name ? ` ${name}` : ''} --template " or "--template /" `, 53 | ) 54 | process.exit(0) 55 | } 56 | let packageManager = result.packageManager ?? pm 57 | 58 | // The 'yarn' and 'pnpm' options are mutually exclusive, and will override the 'packageManager' option 59 | if (result.pnpm && result.yarn) { 60 | log.error(`Both pnpm and yarn were specified. Please specify only one.`) 61 | throw new Error(`Both pnpm and yarn were specified. Please specify only one.`) 62 | } 63 | if (result.pnpm) { 64 | packageManager = 'pnpm' 65 | } 66 | if (result.yarn) { 67 | packageManager = 'yarn' 68 | } 69 | 70 | // Display the intro 71 | intro(`${app.name} ${app.version}`) 72 | 73 | let template: Template | undefined 74 | 75 | if (result.template) { 76 | template = findTemplate(result.template) 77 | } 78 | 79 | // Take the result from the command line and use it to populate the options 80 | const cwd = process.cwd() 81 | const options: Omit & { template?: Template } = { 82 | dryRun: result.dryRun ?? false, 83 | app, 84 | name: name ?? '', 85 | packageManager, 86 | skipGit: result.skipGit ?? false, 87 | skipInit: result.skipInit ?? false, 88 | skipInstall: result.skipInstall ?? false, 89 | targetDirectory: `${cwd}/${name}`, 90 | template, 91 | verbose: result.verbose ?? false, 92 | } 93 | 94 | // Get the prompts for any missing options 95 | const prompts = await getPrompts({ options: options as GetArgsResult }) 96 | 97 | // Populate the options with the prompts 98 | if (prompts.name) { 99 | options.name = prompts.name 100 | options.targetDirectory = `${cwd}/${options.name}` 101 | } 102 | if (prompts.template) { 103 | options.template = prompts.template 104 | } 105 | 106 | if (!options.template) { 107 | throw new Error('No template specified') 108 | } 109 | 110 | return options as GetArgsResult 111 | } 112 | 113 | // Helper function to add a newline before the text 114 | function help(text: string) { 115 | return ` 116 | 117 | ${text}` 118 | } 119 | -------------------------------------------------------------------------------- /src/utils/get-init-script.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, writeFileSync } from 'node:fs' 2 | import { join } from 'node:path' 3 | import { z } from 'zod' 4 | 5 | export const initScriptKey = 'create-solana-dapp' 6 | 7 | export function getInitScript(targetDirectory: string): InitScript | undefined { 8 | const packageJson = join(targetDirectory, 'package.json') 9 | const exists = existsSync(packageJson) 10 | if (!exists) { 11 | throw new Error('No package.json found') 12 | } 13 | const contents = require(packageJson) 14 | if (!contents) { 15 | throw new Error('Error loading package.json') 16 | } 17 | const init = contents[initScriptKey] 18 | if (!init) { 19 | return undefined 20 | } 21 | const parsed = InitScriptSchema.safeParse(init) 22 | if (!parsed.success) { 23 | throw new Error(`Invalid init script: ${parsed.error.message}`) 24 | } 25 | return parsed.data 26 | } 27 | 28 | export function deleteInitScript(targetDirectory: string) { 29 | const packageJson = join(targetDirectory, 'package.json') 30 | const contents = require(packageJson) 31 | delete contents[initScriptKey] 32 | writeFileSync(packageJson, JSON.stringify(contents, undefined, 2) + '\n') 33 | } 34 | 35 | const InitScriptSchema = z 36 | .object({ 37 | instructions: z.array(z.string()).optional(), 38 | rename: z 39 | .record( 40 | z.object({ 41 | to: z.string(), 42 | paths: z.array(z.string()), 43 | }), 44 | ) 45 | .optional(), 46 | versions: z 47 | .object({ 48 | anchor: z.string().optional(), 49 | solana: z.string().optional(), 50 | }) 51 | .optional(), 52 | }) 53 | .optional() 54 | 55 | export type InitScript = z.infer 56 | -------------------------------------------------------------------------------- /src/utils/get-prompt-name.ts: -------------------------------------------------------------------------------- 1 | import { log, text } from '@clack/prompts' 2 | import { GetArgsResult } from './get-args-result' 3 | import { validateProjectName } from './validate-project-name' 4 | 5 | export function getPromptName({ options }: { options: GetArgsResult }) { 6 | return () => { 7 | if (options.name) { 8 | log.success(`Project name: ${options.name}`) 9 | return Promise.resolve(options.name) 10 | } 11 | return text({ 12 | message: 'Enter project name', 13 | validate: validateProjectName, 14 | }) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/get-prompt-template.ts: -------------------------------------------------------------------------------- 1 | import { isCancel, log, select, SelectOptions } from '@clack/prompts' 2 | import { Framework, frameworks } from '../templates/frameworks' 3 | import { getTemplatesForFramework, Template } from '../templates/templates' 4 | import { GetArgsResult } from './get-args-result' 5 | 6 | export function getPromptTemplate({ options }: { options: GetArgsResult }) { 7 | return async () => { 8 | if (options.template) { 9 | log.success(`Template: ${options.template.description}`) 10 | return options.template 11 | } 12 | 13 | const framework: Framework = await selectFramework(frameworks) 14 | if (isCancel(framework)) { 15 | throw 'No framework selected' 16 | } 17 | 18 | return selectTemplate(getTemplatesForFramework(framework)) 19 | } 20 | } 21 | 22 | function getFrameworkSelectOptions( 23 | values: Framework[], 24 | ): SelectOptions<{ value: Framework; label: string; hint?: string | undefined }[], Framework> { 25 | return { 26 | message: 'Select a framework', 27 | options: values.map((value) => ({ 28 | label: value.name, 29 | value, 30 | hint: value.description ?? '', 31 | })), 32 | } 33 | } 34 | 35 | function selectFramework(values: Framework[]): Promise { 36 | return select(getFrameworkSelectOptions(values)) as Promise 37 | } 38 | 39 | function getTemplateSelectOptions( 40 | values: Template[], 41 | ): SelectOptions<{ value: Template; label: string; hint?: string | undefined }[], Template> { 42 | return { 43 | message: 'Select a template', 44 | options: values.map((value) => ({ 45 | label: value.name, 46 | value, 47 | hint: value.description ?? '', 48 | })), 49 | } 50 | } 51 | 52 | function selectTemplate(values: Template[]): Promise