Repository: Melkeydev/go-blueprint Branch: main Commit: 81f56f8c2463 Files: 163 Total size: 350.9 KB Directory structure: gitextract_afvolof3/ ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug-report.yml │ │ ├── config.yml │ │ └── feature-request.yml │ ├── pull_request_template.md │ ├── semantic.yml │ └── workflows/ │ ├── ci.yml │ ├── docs.yml │ ├── generate-linter-advanced.yml │ ├── generate-linter-core.yml │ ├── npm-publish.yml │ ├── release.yml │ ├── testcontainers.yml │ └── update-htmx-version.yml ├── .gitignore ├── .goreleaser.yml ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── cmd/ │ ├── create.go │ ├── flags/ │ │ ├── advancedFeatures.go │ │ ├── database.go │ │ ├── frameworks.go │ │ └── git.go │ ├── program/ │ │ └── program.go │ ├── root.go │ ├── steps/ │ │ └── steps.go │ ├── template/ │ │ ├── advanced/ │ │ │ ├── docker.go │ │ │ ├── files/ │ │ │ │ ├── docker/ │ │ │ │ │ ├── docker_compose.yml.tmpl │ │ │ │ │ └── dockerfile.tmpl │ │ │ │ ├── htmx/ │ │ │ │ │ ├── base.templ.tmpl │ │ │ │ │ ├── efs.go.tmpl │ │ │ │ │ ├── hello.go.tmpl │ │ │ │ │ ├── hello.templ.tmpl │ │ │ │ │ ├── hello_fiber.go.tmpl │ │ │ │ │ ├── htmx.min.js.tmpl │ │ │ │ │ ├── imports/ │ │ │ │ │ │ ├── fiber.tmpl │ │ │ │ │ │ ├── gin.tmpl │ │ │ │ │ │ └── standard_library.tmpl │ │ │ │ │ ├── routes/ │ │ │ │ │ │ ├── chi.tmpl │ │ │ │ │ │ ├── echo.tmpl │ │ │ │ │ │ ├── fiber.tmpl │ │ │ │ │ │ ├── gin.tmpl │ │ │ │ │ │ ├── gorilla.tmpl │ │ │ │ │ │ ├── http_router.tmpl │ │ │ │ │ │ └── standard_library.tmpl │ │ │ │ │ └── tailwind/ │ │ │ │ │ └── tailwind.config.js.tmpl │ │ │ │ ├── react/ │ │ │ │ │ ├── app.tsx.tmpl │ │ │ │ │ └── tailwind/ │ │ │ │ │ ├── app.tsx.tmpl │ │ │ │ │ ├── index.css.tmpl │ │ │ │ │ └── vite.config.ts.tmpl │ │ │ │ ├── tailwind/ │ │ │ │ │ ├── input.css.tmpl │ │ │ │ │ └── output.css.tmpl │ │ │ │ ├── websocket/ │ │ │ │ │ └── imports/ │ │ │ │ │ ├── fiber.tmpl │ │ │ │ │ └── standard_library.tmpl │ │ │ │ └── workflow/ │ │ │ │ └── github/ │ │ │ │ ├── github_action_goreleaser.yml.tmpl │ │ │ │ ├── github_action_gotest.yml.tmpl │ │ │ │ └── github_action_releaser_config.yml.tmpl │ │ │ ├── gitHubAction.go │ │ │ └── routes.go │ │ ├── dbdriver/ │ │ │ ├── files/ │ │ │ │ ├── env/ │ │ │ │ │ ├── mongo.tmpl │ │ │ │ │ ├── mysql.tmpl │ │ │ │ │ ├── postgres.tmpl │ │ │ │ │ ├── redis.tmpl │ │ │ │ │ ├── scylla.tmpl │ │ │ │ │ └── sqlite.tmpl │ │ │ │ ├── service/ │ │ │ │ │ ├── mongo.tmpl │ │ │ │ │ ├── mysql.tmpl │ │ │ │ │ ├── postgres.tmpl │ │ │ │ │ ├── redis.tmpl │ │ │ │ │ ├── scylla.tmpl │ │ │ │ │ └── sqlite.tmpl │ │ │ │ └── tests/ │ │ │ │ ├── mongo.tmpl │ │ │ │ ├── mysql.tmpl │ │ │ │ ├── postgres.tmpl │ │ │ │ ├── redis.tmpl │ │ │ │ └── scylla.tmpl │ │ │ ├── mongo.go │ │ │ ├── mysql.go │ │ │ ├── postgres.go │ │ │ ├── redis.go │ │ │ ├── scylla.go │ │ │ └── sqlite.go │ │ ├── docker/ │ │ │ ├── files/ │ │ │ │ └── docker-compose/ │ │ │ │ ├── mongo.tmpl │ │ │ │ ├── mysql.tmpl │ │ │ │ ├── postgres.tmpl │ │ │ │ ├── redis.tmpl │ │ │ │ └── scylla.tmpl │ │ │ ├── mongo.go │ │ │ ├── mysql.go │ │ │ ├── postgres.go │ │ │ ├── redis.go │ │ │ └── scylla.go │ │ ├── framework/ │ │ │ ├── chiRoutes.go │ │ │ ├── echoRoutes.go │ │ │ ├── fiberServer.go │ │ │ ├── files/ │ │ │ │ ├── README.md.tmpl │ │ │ │ ├── air.toml.tmpl │ │ │ │ ├── gitignore.tmpl │ │ │ │ ├── globalenv.tmpl │ │ │ │ ├── main/ │ │ │ │ │ ├── fiber_main.go.tmpl │ │ │ │ │ └── main.go.tmpl │ │ │ │ ├── makefile.tmpl │ │ │ │ ├── routes/ │ │ │ │ │ ├── chi.go.tmpl │ │ │ │ │ ├── echo.go.tmpl │ │ │ │ │ ├── fiber.go.tmpl │ │ │ │ │ ├── gin.go.tmpl │ │ │ │ │ ├── gorilla.go.tmpl │ │ │ │ │ ├── http_router.go.tmpl │ │ │ │ │ └── standard_library.go.tmpl │ │ │ │ ├── server/ │ │ │ │ │ ├── fiber.go.tmpl │ │ │ │ │ └── standard_library.go.tmpl │ │ │ │ └── tests/ │ │ │ │ ├── default-test.go.tmpl │ │ │ │ ├── echo-test.go.tmpl │ │ │ │ ├── fiber-test.go.tmpl │ │ │ │ └── gin-test.go.tmpl │ │ │ ├── ginRoutes.go │ │ │ ├── gorillaRoutes.go │ │ │ ├── httpRoutes.go │ │ │ ├── main.go │ │ │ └── routerRoutes.go │ │ └── globalEnv.go │ ├── ui/ │ │ ├── multiInput/ │ │ │ └── multiInput.go │ │ ├── multiSelect/ │ │ │ └── multiSelect.go │ │ ├── spinner/ │ │ │ └── spinner.go │ │ └── textinput/ │ │ ├── textinput.go │ │ └── textinput_test.go │ ├── utils/ │ │ ├── utils.go │ │ └── utils_test.go │ └── version.go ├── contributors.yml ├── docs/ │ ├── Makefile │ ├── custom_theme/ │ │ └── main.html │ ├── docs/ │ │ ├── advanced-flag/ │ │ │ ├── advanced-flag.md │ │ │ ├── docker.md │ │ │ ├── goreleaser.md │ │ │ ├── htmx-templ.md │ │ │ ├── react-vite.md │ │ │ ├── tailwind.md │ │ │ └── websocket.md │ │ ├── blueprint-core/ │ │ │ ├── db-drivers.md │ │ │ └── frameworks.md │ │ ├── blueprint-ui.md │ │ ├── creating-project/ │ │ │ ├── air.md │ │ │ ├── makefile.md │ │ │ └── project-init.md │ │ ├── endpoints-test/ │ │ │ ├── mongo.md │ │ │ ├── redis.md │ │ │ ├── scylladb.md │ │ │ ├── server.md │ │ │ ├── sql.md │ │ │ ├── web.md │ │ │ └── websocket.md │ │ ├── index.md │ │ └── installation.md │ ├── mkdocs.yml │ └── requirements.txt ├── go.mod ├── go.sum ├── main.go └── scripts/ ├── completions.sh └── create-npm-packages.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [melkeydev] ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report.yml ================================================ name: Bug Report description: Found a bug? Please let us know! title: "[Bug]" labels: ["Bug"] body: - type: markdown attributes: value: | Please provide as much information as possible. This will help us resolve the Bug quickly and accurately. - type: markdown attributes: value: --- - type: textarea id: what-happened attributes: label: What is the problem? description: | Description of what needs to be fixed. placeholder: Tell us what you see! validations: required: true - type: input id: os attributes: label: Operating System description: What is the affected operating system? validations: required: true - type: input id: arch attributes: label: Architecture Version (x86, x64, arm, etc) description: (x86, x64, arm, etc) validations: required: true - type: textarea id: reproduce attributes: label: Steps to reproduce description: | This includes the steps for reproducing the problem, the expected result, and the actual result. placeholder: | 1. ... 2. ... 3. ... validations: required: true - type: textarea id: logs attributes: label: Relevant log output description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. render: shell ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: true contact_links: - name: Melkeydev Discord url: https://discord.gg/HHZMSCu about: Chat with the community. ================================================ FILE: .github/ISSUE_TEMPLATE/feature-request.yml ================================================ name: Feature Request description: Suggest a new idea for go-blueprint. title: "[Feature Request] " labels: ["enhancement"] body: - type: markdown attributes: value: | Please provide as much information as possible by filling out form below. - type: markdown attributes: value: --- - type: textarea id: idea attributes: label: Tell us about your feature request description: | Describe what you would like go-blueprint to be able to do. validations: required: true - type: checkboxes id: disclaimer attributes: label: Disclaimer description: I have verified that this has not been suggested before. options: - label: I agree required: true ================================================ FILE: .github/pull_request_template.md ================================================ By submitting this pull request, I confirm that my contribution is made under the terms of the MIT license. ## Problem/Feature Please include a description of the problem or feature this PR is addressing. ## Description of Changes: - Item 1 - Item 2 ## Checklist - [ ] I have self-reviewed the changes being requested - [ ] I have updated the documentation (check issue ticket #218) ================================================ FILE: .github/semantic.yml ================================================ titleOnly: true ================================================ FILE: .github/workflows/ci.yml ================================================ name: continuous integration on: push: paths: - '**.go' - go.sum - go.mod branches-ignore: - main pull_request: jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.23.x' - name: Deps cache id: cache-go-deps uses: actions/cache@v4 env: cache-name: go-deps-cache with: path: ~/godeps key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-build-${{ env.cache-name }}- - if: ${{ steps.cache-go-deps.outputs.cache-hit != 'true' }} name: List the state of go modules continue-on-error: true run: go mod graph - name: Install dependencies run: | go mod tidy go mod download go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.63.4 - name: Run golangci-lint run: golangci-lint run - name: Run tests run: | go test ./... ================================================ FILE: .github/workflows/docs.yml ================================================ name: Deploy Docs on: push: branches: - main jobs: build-deploy: name: Build and deploy docs runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 - name: Cache dependencies uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('docs/requirements.txt') }} restore-keys: | ${{ runner.os }}-pip- ${{ runner.os }}- - name: Install dependencies working-directory: docs run: make install - name: Build and deploy to GitHub Pages working-directory: docs run: make deploy # config for custom domain on gh-pages # - name: Create and push CNAME file to gh-pages # run: | # git config --local user.email "actions@github.com" # git config --local user.name "GitHub Actions" # git checkout gh-pages # echo "" > CNAME # git add CNAME # git commit -m "Add CNAME file" # git push origin gh-pages ================================================ FILE: .github/workflows/generate-linter-advanced.yml ================================================ name: Linting Generated Blueprints Advanced on: pull_request: {} workflow_dispatch: {} jobs: framework_matrix: strategy: matrix: framework: [chi, gin, fiber, gorilla/mux, httprouter, standard-library, echo] driver: [postgres] git: [commit] advanced: [htmx, githubaction, websocket, tailwind, docker, react] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version: '1.23.x' - name: Install golangci-lint run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.63.4 - name: Commit report run: | git config --global user.name 'testname' git config --global user.email 'testemail@users.noreply.github.com' - name: Set framework variable id: set-proejct-directory run: echo "PROJECT_DIRECTORY=${{ matrix.framework }}" | sed 's/\//-/g' >> $GITHUB_ENV - name: build templates run: script -q /dev/null -c "go run main.go create -n ${{ env.PROJECT_DIRECTORY }} -f ${{ matrix.framework}} -d ${{ matrix.driver }} -g ${{ matrix.git}} --advanced --feature ${{ matrix.advanced }}" - if: ${{ matrix.advanced == 'htmx' || matrix.advanced == 'tailwind' }} name: Install Templ & gen templates run: | go install github.com/a-h/templ/cmd/templ@latest /home/runner/go/bin/templ generate -path ${{ env.PROJECT_DIRECTORY }} - name: golangci-lint run: | cd ${{ env.PROJECT_DIRECTORY }} golangci-lint run - name: remove templates run: rm -rf ${{ env.PROJECT_DIRECTORY }} ================================================ FILE: .github/workflows/generate-linter-core.yml ================================================ name: Linting Generated Blueprints Core on: pull_request: {} workflow_dispatch: {} jobs: framework_matrix: strategy: matrix: framework: [chi, gin, fiber, gorilla/mux, httprouter, standard-library, echo] driver: [mysql, postgres, sqlite, mongo, redis, scylla, none] git: [commit, stage, skip] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version: '1.23.x' - name: Install golangci-lint run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.63.4 - name: Commit report run: | git config --global user.name 'testname' git config --global user.email 'testemail@users.noreply.github.com' - name: Set framework variable id: set-proejct-directory run: echo "PROJECT_DIRECTORY=${{ matrix.framework }}" | sed 's/\//-/g' >> $GITHUB_ENV - name: build templates run: script -q /dev/null -c "go run main.go create -n ${{ env.PROJECT_DIRECTORY }} -f ${{ matrix.framework}} -d ${{ matrix.driver }} -g ${{ matrix.git}}" - name: golangci-lint run: | cd ${{ env.PROJECT_DIRECTORY }} golangci-lint run - name: remove templates run: rm -rf ${{ env.PROJECT_DIRECTORY }} ================================================ FILE: .github/workflows/npm-publish.yml ================================================ name: npm-publish on: workflow_call: inputs: tag: description: "Release tag to publish (e.g., v1.0.0)" required: true type: string workflow_dispatch: inputs: tag: description: "Release tag to publish (e.g., v1.0.0)" required: true type: string jobs: npm-publish: runs-on: ubuntu-latest permissions: contents: read id-token: write env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} steps: - uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: 22 registry-url: "https://registry.npmjs.org" - name: Download release assets run: | TAG="${{ inputs.tag }}" VERSION=${TAG#v} mkdir -p dist gh release download "$TAG" --dir dist echo "VERSION=$VERSION" >> $GITHUB_ENV env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create npm packages run: | chmod +x ./scripts/create-npm-packages.sh ./scripts/create-npm-packages.sh "$VERSION" - name: Publish platform-specific packages to npm run: | for platform_dir in platform-packages/go-blueprint-*; do if [ -d "$platform_dir" ]; then cd "$platform_dir" npm publish --provenance --access public cd - > /dev/null fi done - name: Publish main package to npm run: | cd npm-package npm publish --provenance --access public ================================================ FILE: .github/workflows/release.yml ================================================ name: goreleaser on: push: tags: - "v*.*.*" permissions: contents: write jobs: goreleaser: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v4 with: go-version: "1.21.1" - name: Run GoReleaser uses: goreleaser/goreleaser-action@v5.0.0 with: distribution: goreleaser version: ${{ env.GITHUB_REF_NAME }} args: release --clean workdir: ./ env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} npm-publish: needs: goreleaser permissions: contents: read id-token: write uses: ./.github/workflows/npm-publish.yml with: tag: ${{ github.ref_name }} secrets: inherit ================================================ FILE: .github/workflows/testcontainers.yml ================================================ name: Integrations Test for the Generated Blueprints on: pull_request: {} workflow_dispatch: {} jobs: itests_matrix: strategy: matrix: driver: [mysql, postgres, mongo, redis, scylla] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version: '1.23.x' - name: Commit report run: | git config --global user.name 'testname' git config --global user.email 'testemail@users.noreply.github.com' - name: build ${{ matrix.driver }} template run: script -q /dev/null -c "go run main.go create -n ${{ matrix.driver }} -g commit -f fiber -d ${{matrix.driver}}" - name: run ${{ matrix.driver }} integration tests working-directory: ${{ matrix.driver }} run: make itest - name: remove ${{ matrix.driver }} template run: rm -rf ${{ matrix.driver }} ================================================ FILE: .github/workflows/update-htmx-version.yml ================================================ name: Check for new htmx release on: schedule: - cron: '0 0 * * Sun' jobs: check_release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Get version from file id: get_version_file run: | VERSION_FILE=$(curl -s https://raw.githubusercontent.com/Melkeydev/go-blueprint/main/cmd/template/advanced/files/htmx/htmx.min.js.tmpl | grep version | awk -F'"' '{print "v" $2}') echo "version file: $VERSION_FILE" if [[ "$VERSION_FILE" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "version_file=$VERSION_FILE" >> $GITHUB_OUTPUT else echo "Invalid VERSION_FILE format: $VERSION_FILE" >&2 exit 1 fi - name: Get version from GitHub API id: get_version_api run: | VERSION_API=$(curl -s https://api.github.com/repos/bigskysoftware/htmx/releases/latest | jq -r '.tag_name') echo "version api: $VERSION_API" if [[ "$VERSION_API" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "version_api=$VERSION_API" >> $GITHUB_OUTPUT else echo "Invalid VERSION_API format: $VERSION_API" >&2 exit 1 fi - name: Compare versions id: compare_versions run: | if [ "${{ steps.get_version_api.outputs.version_api }}" != "${{ steps.get_version_file.outputs.version_file }}" ]; then echo "release_changed=true" >> $GITHUB_OUTPUT echo "Release changed: true" else echo "release_changed=false" >> $GITHUB_OUTPUT echo "Release changed: false" fi - name: dump latest htmx version if: steps.compare_versions.outputs.release_changed == 'true' run: curl -L https://github.com/bigskysoftware/htmx/releases/latest/download/htmx.min.js -o cmd/template/advanced/files/htmx/htmx.min.js - name: Prettify code if: steps.compare_versions.outputs.release_changed == 'true' run: | npm install --save-dev --save-exact prettier npx prettier --write cmd/template/advanced/files/htmx/htmx.min.js rm -rf node_modules rm package-lock.json rm package.json - name: Create tmpl after Prettify if: steps.compare_versions.outputs.release_changed == 'true' run: mv cmd/template/advanced/files/htmx/htmx.min.js cmd/template/advanced/files/htmx/htmx.min.js.tmpl - name: Create Pull Request if: steps.compare_versions.outputs.release_changed == 'true' uses: peter-evans/create-pull-request@v6 with: commit-message: update htmx version ${{ steps.get_version_api.outputs.version_api }} title: Update htmx to version ${{ steps.get_version_api.outputs.version_api }} [Bot] body: New htmx ${{ steps.get_version_api.outputs.version_api }} version is available. This is an automatic PR to update changes. branch: htmx-version-update base: main ================================================ FILE: .gitignore ================================================ go-blueprint site ================================================ FILE: .goreleaser.yml ================================================ before: hooks: - go mod tidy - ./scripts/completions.sh builds: - binary: go-blueprint main: ./ goos: - darwin - linux - windows goarch: - amd64 - arm64 env: - CGO_ENABLED=0 ldflags: - -s -w -X github.com/melkeydev/go-blueprint/cmd.GoBlueprintVersion={{.Version}} release: prerelease: auto universal_binaries: - replace: true archives: - name_template: >- {{- .ProjectName }}_ {{- .Version }}_ {{- title .Os }}_ {{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} {{- if .Arm }}v{{ .Arm }}{{ end -}} format_overrides: - goos: windows format: zip builds_info: group: root owner: root files: - README.md - LICENSE - completions/* checksum: name_template: "checksums.txt" ================================================ FILE: .pre-commit-config.yaml ================================================ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v3.2.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-added-large-files - repo: https://github.com/golangci/golangci-lint rev: v1.55.2 hooks: - id: golangci-lint ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Thanks for helping make go-blueprint better! - [Contributing](#contributing) - [Design Principles](#design-principles) - [Report an Issue](#report-an-issue) - [Contributing Code with Pull Requests](#contributing-code-with-pull-requests) - [Requirements](#requirements) - [Licensing](#licensing) ## Design Principles Contributions to go-blueprint should align with the project’s design principles: * Maintain backwards compatibility whenever possible. ## Report an Issue If you have run into a bug or want to discuss a new feature, please [file an issue](https://github.com/Melkeydev/go-blueprint/issues). ## Contributing Code with Pull Requests go-blueprint uses [Github pull requests](https://github.com/Melkeydev/go-blueprint/pulls). Feel free to fork, hack away at your changes and submit. ### Requirements * All commands and functionality should be documented appropriately * All new functionality/features should have appropriate unit testing go-blueprint strives to have a consistent set of documentation that matches the command structure and any new functionality must have accompanying documentation in the PR. ## Licensing See the [LICENSE](https://github.com/melkeydev/go-blueprint/blob/main/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2023 Melkeydev Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ![logo](./public/logo.png)

Introducing the Ultimate Golang Blueprint Library

Go Blueprint is a CLI tool that allows users to spin up a Go project with the corresponding structure seamlessly. It also gives the option to integrate with one of the more popular Go frameworks (and the list is growing with new features)! ### Why Would I use this? - Easy to set up and install - Have the entire Go structure already established - Setting up a Go HTTP server (or Fasthttp with Fiber) - Integrate with a popular frameworks - Focus on the actual code of your application ## Table of Contents - [Install](#install) - [Frameworks Supported](#frameworks-supported) - [Database Support](#database-support) - [Advanced Features](#advanced-features) - [Blueprint UI](#blueprint-ui) - [Usage Example](#usage-example) - [GitHub Stats](#github-stats) - [License](#license)

Install

### Go Install ```bash go install github.com/melkeydev/go-blueprint@latest ``` This installs a go binary that will automatically bind to your $GOPATH > if you’re using Zsh, you’ll need to add it manually to `~/.zshrc`. ```bash GOPATH=$HOME/go PATH=$PATH:/usr/local/go/bin:$GOPATH/bin ``` don't forget to update ```bash source ~/.zshrc ``` ### NPM Install ```bash npm install -g @melkeydev/go-blueprint ``` ### Homebrew Install ```bash brew install go-blueprint ``` Then in a new terminal run: ```bash go-blueprint create ``` You can also use the provided flags to set up a project without interacting with the UI. ```bash go-blueprint create --name my-project --framework gin --driver postgres --git commit ``` See `go-blueprint create -h` for all the options and shorthands.

Frameworks Supported

- [Chi](https://github.com/go-chi/chi) - [Gin](https://github.com/gin-gonic/gin) - [Fiber](https://github.com/gofiber/fiber) - [HttpRouter](https://github.com/julienschmidt/httprouter) - [Gorilla/mux](https://github.com/gorilla/mux) - [Echo](https://github.com/labstack/echo)

Database Support

Go Blueprint now offers enhanced database support, allowing you to choose your preferred database driver during project setup. Use the `--driver` or `-d` flag to specify the database driver you want to integrate into your project. ### Supported Database Drivers Choose from a variety of supported database drivers: - [Mysql](https://github.com/go-sql-driver/mysql) - [Postgres](https://github.com/jackc/pgx/) - [Sqlite](https://github.com/mattn/go-sqlite3) - [Mongo](https://go.mongodb.org/mongo-driver) - [Redis](https://github.com/redis/go-redis) - [ScyllaDB GoCQL](https://github.com/scylladb/gocql)

Advanced Features

Blueprint is focused on being as minimalistic as possible. That being said, we wanted to offer the ability to add other features people may want without bloating the overall experience. You can now use the `--advanced` flag when running the `create` command to get access to the following features. This is a multi-option prompt; one or more features can be used at the same time: - [HTMX](https://htmx.org/) support using [Templ](https://templ.guide/) - CI/CD workflow setup using [Github Actions](https://docs.github.com/en/actions) - [Websocket](https://pkg.go.dev/github.com/coder/websocket) sets up a websocket endpoint - [Tailwind](https://tailwindcss.com/) css framework - Docker configuration for go project - [React](https://react.dev/) frontend written in TypeScript, including an example fetch request to the backend Note: Selecting Tailwind option will automatically select HTMX unless React is explicitly selected

Blueprint UI

Blueprint UI is a web application that allows you to create commands for the CLI and preview the structure of your project. You will be able to see directories and files that will be created upon command execution. Check it out at [go-blueprint.dev](https://go-blueprint.dev)

Usage Example

Here's an example of setting up a project with a specific database driver: ```bash go-blueprint create --name my-project --framework gin --driver postgres --git commit ```

Starter Image

Advanced features are accessible with the --advanced flag ```bash go-blueprint create --advanced ``` Advanced features can be enabled using the `--feature` flag along with the `--advanced` flag. HTMX: ```bash go-blueprint create --advanced --feature htmx ``` CI/CD workflow: ```bash go-blueprint create --advanced --feature githubaction ``` Websocket: ```bash go-blueprint create --advanced --feature websocket ``` Tailwind: ```bash go-blueprint create --advanced --feature tailwind ``` Docker: ```bash go-blueprint create --advanced --feature docker ``` React: ```bash go-blueprint create --advanced --feature react ``` Or all features at once: ```bash go-blueprint create --name my-project --framework chi --driver mysql --advanced --feature htmx --feature githubaction --feature websocket --feature tailwind --feature docker --git commit --feature react ```

Advanced Options

**Visit [documentation](https://docs.go-blueprint.dev) to learn more about blueprint and its features.**

GitHub Stats

Alt

License

Licensed under [MIT License](./LICENSE) ================================================ FILE: cmd/create.go ================================================ package cmd import ( "fmt" "log" "os" "strings" "sync" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/melkeydev/go-blueprint/cmd/flags" "github.com/melkeydev/go-blueprint/cmd/program" "github.com/melkeydev/go-blueprint/cmd/steps" "github.com/melkeydev/go-blueprint/cmd/ui/multiInput" "github.com/melkeydev/go-blueprint/cmd/ui/multiSelect" "github.com/melkeydev/go-blueprint/cmd/ui/spinner" "github.com/melkeydev/go-blueprint/cmd/ui/textinput" "github.com/melkeydev/go-blueprint/cmd/utils" "github.com/spf13/cobra" ) const logo = ` ____ _ _ _ | _ \| | (_) | | | |_) | |_ _ ___ _ __ _ __ _ _ __ | |_ | _ <| | | | |/ _ \ '_ \| '__| | '_ \| __| | |_) | | |_| | __/ |_) | | | | | | | |_ |____/|_|\__,_|\___| .__/|_| |_|_| |_|\__| | | |_| ` var ( logoStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#01FAC6")).Bold(true) tipMsgStyle = lipgloss.NewStyle().PaddingLeft(1).Foreground(lipgloss.Color("190")).Italic(true) endingMsgStyle = lipgloss.NewStyle().PaddingLeft(1).Foreground(lipgloss.Color("170")).Bold(true) ) func init() { var flagFramework flags.Framework var flagDBDriver flags.Database var advancedFeatures flags.AdvancedFeatures var flagGit flags.Git rootCmd.AddCommand(createCmd) createCmd.Flags().StringP("name", "n", "", "Name of project to create") createCmd.Flags().VarP(&flagFramework, "framework", "f", fmt.Sprintf("Framework to use. Allowed values: %s", strings.Join(flags.AllowedProjectTypes, ", "))) createCmd.Flags().VarP(&flagDBDriver, "driver", "d", fmt.Sprintf("Database drivers to use. Allowed values: %s", strings.Join(flags.AllowedDBDrivers, ", "))) createCmd.Flags().BoolP("advanced", "a", false, "Get prompts for advanced features") createCmd.Flags().Var(&advancedFeatures, "feature", fmt.Sprintf("Advanced feature to use. Allowed values: %s", strings.Join(flags.AllowedAdvancedFeatures, ", "))) createCmd.Flags().VarP(&flagGit, "git", "g", fmt.Sprintf("Git to use. Allowed values: %s", strings.Join(flags.AllowedGitsOptions, ", "))) utils.RegisterStaticCompletions(createCmd, "framework", flags.AllowedProjectTypes) utils.RegisterStaticCompletions(createCmd, "driver", flags.AllowedDBDrivers) utils.RegisterStaticCompletions(createCmd, "feature", flags.AllowedAdvancedFeatures) utils.RegisterStaticCompletions(createCmd, "git", flags.AllowedGitsOptions) } type Options struct { ProjectName *textinput.Output ProjectType *multiInput.Selection DBDriver *multiInput.Selection Advanced *multiSelect.Selection Workflow *multiInput.Selection Git *multiInput.Selection } // createCmd defines the "create" command for the CLI var createCmd = &cobra.Command{ Use: "create", Short: "Create a Go project and don't worry about the structure", Long: "Go Blueprint is a CLI tool that allows you to focus on the actual Go code, and not the project structure. Perfect for someone new to the Go language", Run: func(cmd *cobra.Command, args []string) { var tprogram *tea.Program var err error isInteractive := false flagName := cmd.Flag("name").Value.String() if flagName != "" && !utils.ValidateModuleName(flagName) { err = fmt.Errorf("'%s' is not a valid module name. Please choose a different name", flagName) cobra.CheckErr(textinput.CreateErrorInputModel(err).Err()) } rootDirName := utils.GetRootDir(flagName) if rootDirName != "" && doesDirectoryExistAndIsNotEmpty(rootDirName) { err = fmt.Errorf("directory '%s' already exists and is not empty. Please choose a different name", rootDirName) cobra.CheckErr(textinput.CreateErrorInputModel(err).Err()) } // VarP already validates the contents of the framework flag. // If this flag is filled, it is always valid flagFramework := flags.Framework(cmd.Flag("framework").Value.String()) flagDBDriver := flags.Database(cmd.Flag("driver").Value.String()) flagGit := flags.Git(cmd.Flag("git").Value.String()) options := Options{ ProjectName: &textinput.Output{}, ProjectType: &multiInput.Selection{}, DBDriver: &multiInput.Selection{}, Advanced: &multiSelect.Selection{ Choices: make(map[string]bool), }, Git: &multiInput.Selection{}, } project := &program.Project{ ProjectName: flagName, ProjectType: flagFramework, DBDriver: flagDBDriver, FrameworkMap: make(map[flags.Framework]program.Framework), DBDriverMap: make(map[flags.Database]program.Driver), AdvancedOptions: make(map[string]bool), GitOptions: flagGit, } steps := steps.InitSteps(flagFramework, flagDBDriver) fmt.Printf("%s\n", logoStyle.Render(logo)) // Advanced option steps: flagAdvanced, err := cmd.Flags().GetBool("advanced") if err != nil { log.Fatal("failed to retrieve advanced flag") } if flagAdvanced { fmt.Println(tipMsgStyle.Render("*** You are in advanced mode ***\n\n")) } if project.ProjectName == "" { isInteractive = true tprogram := tea.NewProgram(textinput.InitialTextInputModel(options.ProjectName, "What is the name of your project?", project)) if _, err := tprogram.Run(); err != nil { log.Printf("Name of project contains an error: %v", err) cobra.CheckErr(textinput.CreateErrorInputModel(err).Err()) } if options.ProjectName.Output != "" && !utils.ValidateModuleName(options.ProjectName.Output) { err = fmt.Errorf("'%s' is not a valid module name. Please choose a different name", options.ProjectName.Output) cobra.CheckErr(textinput.CreateErrorInputModel(err).Err()) } rootDirName = utils.GetRootDir(options.ProjectName.Output) if doesDirectoryExistAndIsNotEmpty(rootDirName) { err = fmt.Errorf("directory '%s' already exists and is not empty. Please choose a different name", rootDirName) cobra.CheckErr(textinput.CreateErrorInputModel(err).Err()) } project.ExitCLI(tprogram) project.ProjectName = options.ProjectName.Output err := cmd.Flag("name").Value.Set(project.ProjectName) if err != nil { log.Fatal("failed to set the name flag value", err) } } if project.ProjectType == "" { isInteractive = true step := steps.Steps["framework"] tprogram = tea.NewProgram(multiInput.InitialModelMulti(step.Options, options.ProjectType, step.Headers, project)) if _, err := tprogram.Run(); err != nil { cobra.CheckErr(textinput.CreateErrorInputModel(err).Err()) } project.ExitCLI(tprogram) step.Field = options.ProjectType.Choice // this type casting is always safe since the user interface can // only pass strings that can be cast to a flags.Framework instance project.ProjectType = flags.Framework(strings.ToLower(options.ProjectType.Choice)) err := cmd.Flag("framework").Value.Set(project.ProjectType.String()) if err != nil { log.Fatal("failed to set the framework flag value", err) } } if project.DBDriver == "" { isInteractive = true step := steps.Steps["driver"] tprogram = tea.NewProgram(multiInput.InitialModelMulti(step.Options, options.DBDriver, step.Headers, project)) if _, err := tprogram.Run(); err != nil { cobra.CheckErr(textinput.CreateErrorInputModel(err).Err()) } project.ExitCLI(tprogram) // this type casting is always safe since the user interface can // only pass strings that can be cast to a flags.Database instance project.DBDriver = flags.Database(strings.ToLower(options.DBDriver.Choice)) err := cmd.Flag("driver").Value.Set(project.DBDriver.String()) if err != nil { log.Fatal("failed to set the driver flag value", err) } } if flagAdvanced { featureFlags := cmd.Flag("feature").Value.String() if featureFlags != "" { featuresFlagValues := strings.Split(featureFlags, ",") for _, key := range featuresFlagValues { project.AdvancedOptions[key] = true } } else { isInteractive = true step := steps.Steps["advanced"] tprogram = tea.NewProgram((multiSelect.InitialModelMultiSelect(step.Options, options.Advanced, step.Headers, project))) if _, err := tprogram.Run(); err != nil { cobra.CheckErr(textinput.CreateErrorInputModel(err).Err()) } project.ExitCLI(tprogram) for key, opt := range options.Advanced.Choices { project.AdvancedOptions[strings.ToLower(key)] = opt err := cmd.Flag("feature").Value.Set(strings.ToLower(key)) if err != nil { log.Fatal("failed to set the feature flag value", err) } } if err != nil { log.Fatal("failed to set the htmx option", err) } } } if project.GitOptions == "" { isInteractive = true step := steps.Steps["git"] tprogram = tea.NewProgram(multiInput.InitialModelMulti(step.Options, options.Git, step.Headers, project)) if _, err := tprogram.Run(); err != nil { cobra.CheckErr(textinput.CreateErrorInputModel(err).Err()) } project.ExitCLI(tprogram) project.GitOptions = flags.Git(strings.ToLower(options.Git.Choice)) err := cmd.Flag("git").Value.Set(project.GitOptions.String()) if err != nil { log.Fatal("failed to set the git flag value", err) } } currentWorkingDir, err := os.Getwd() if err != nil { log.Printf("could not get current working directory: %v", err) cobra.CheckErr(textinput.CreateErrorInputModel(err).Err()) } project.AbsolutePath = currentWorkingDir spinner := tea.NewProgram(spinner.InitialModelNew()) // add synchronization to wait for spinner to finish wg := sync.WaitGroup{} wg.Add(1) go func() { defer wg.Done() if _, err := spinner.Run(); err != nil { cobra.CheckErr(err) } }() defer func() { if r := recover(); r != nil { fmt.Println("The program encountered an unexpected issue and had to exit. The error was:", r) fmt.Println("If you continue to experience this issue, please post a message on our GitHub page or join our Discord server for support.") if releaseErr := spinner.ReleaseTerminal(); releaseErr != nil { log.Printf("Problem releasing terminal: %v", releaseErr) } } }() // This calls the templates err = project.CreateMainFile() if err != nil { if releaseErr := spinner.ReleaseTerminal(); releaseErr != nil { log.Printf("Problem releasing terminal: %v", releaseErr) } log.Printf("Problem creating files for project.") cobra.CheckErr(textinput.CreateErrorInputModel(err).Err()) } fmt.Println(endingMsgStyle.Render("\nNext steps:")) fmt.Println(endingMsgStyle.Render(fmt.Sprintf("• cd into the newly created project with: `cd %s`\n", utils.GetRootDir(project.ProjectName)))) if options.Advanced.Choices["React"] { options.Advanced.Choices["Htmx"] = false options.Advanced.Choices["Tailwind"] = false fmt.Println(endingMsgStyle.Render("• cd into frontend\n")) fmt.Println(endingMsgStyle.Render("• npm install\n")) fmt.Println(endingMsgStyle.Render("• npm run dev\n")) } if options.Advanced.Choices["Tailwind"] { options.Advanced.Choices["Htmx"] = true fmt.Println(endingMsgStyle.Render("• Install the tailwind standalone cli if you haven't already, grab the executable for your platform from the latest release on GitHub\n")) fmt.Println(endingMsgStyle.Render("• More info about the Tailwind CLI: https://tailwindcss.com/blog/standalone-cli\n")) } if options.Advanced.Choices["Htmx"] { options.Advanced.Choices["react"] = false fmt.Println(endingMsgStyle.Render("• Install the templ cli if you haven't already by running `go install github.com/a-h/templ/cmd/templ@latest`\n")) fmt.Println(endingMsgStyle.Render("• Generate templ function files by running `templ generate`\n")) } if isInteractive { nonInteractiveCommand := utils.NonInteractiveCommand(cmd.Use, cmd.Flags()) fmt.Println(tipMsgStyle.Render("Tip: Repeat the equivalent Blueprint with the following non-interactive command:")) fmt.Println(tipMsgStyle.Italic(false).Render(fmt.Sprintf("• %s\n", nonInteractiveCommand))) } err = spinner.ReleaseTerminal() if err != nil { log.Printf("Could not release terminal: %v", err) cobra.CheckErr(err) } }, } // doesDirectoryExistAndIsNotEmpty checks if the directory exists and is not empty func doesDirectoryExistAndIsNotEmpty(name string) bool { if _, err := os.Stat(name); err == nil { dirEntries, err := os.ReadDir(name) if err != nil { log.Printf("could not read directory: %v", err) cobra.CheckErr(textinput.CreateErrorInputModel(err)) } if len(dirEntries) > 0 { return true } } return false } ================================================ FILE: cmd/flags/advancedFeatures.go ================================================ package flags import ( "fmt" "strings" ) type AdvancedFeatures []string const ( Htmx string = "htmx" GoProjectWorkflow string = "githubaction" Websocket string = "websocket" Tailwind string = "tailwind" React string = "react" Docker string = "docker" ) var AllowedAdvancedFeatures = []string{string(React), string(Htmx), string(GoProjectWorkflow), string(Websocket), string(Tailwind), string(Docker)} func (f AdvancedFeatures) String() string { return strings.Join(f, ",") } func (f *AdvancedFeatures) Type() string { return "AdvancedFeatures" } func (f *AdvancedFeatures) Set(value string) error { // Contains isn't available in 1.20 yet // if AdvancedFeatures.Contains(value) { for _, advancedFeature := range AllowedAdvancedFeatures { if advancedFeature == value { *f = append(*f, advancedFeature) return nil } } return fmt.Errorf("advanced Feature to use. Allowed values: %s", strings.Join(AllowedAdvancedFeatures, ", ")) } ================================================ FILE: cmd/flags/database.go ================================================ package flags import ( "fmt" "strings" ) type Database string // These are all the current databases supported. If you want to add one, you // can simply copy and paste a line here. Do not forget to also add it into the // AllowedDBDrivers slice too! const ( MySql Database = "mysql" Postgres Database = "postgres" Sqlite Database = "sqlite" Mongo Database = "mongo" Redis Database = "redis" Scylla Database = "scylla" None Database = "none" ) var AllowedDBDrivers = []string{string(MySql), string(Postgres), string(Sqlite), string(Mongo), string(Redis), string(Scylla), string(None)} func (f Database) String() string { return string(f) } func (f *Database) Type() string { return "Database" } func (f *Database) Set(value string) error { // Contains isn't available in 1.20 yet // if AllowedDBDrivers.Contains(value) { for _, database := range AllowedDBDrivers { if database == value { *f = Database(value) return nil } } return fmt.Errorf("Database to use. Allowed values: %s", strings.Join(AllowedDBDrivers, ", ")) } ================================================ FILE: cmd/flags/frameworks.go ================================================ package flags import ( "fmt" "strings" ) type Framework string // These are all the current frameworks supported. If you want to add one, you // can simply copy and paste a line here. Do not forget to also add it into the // AllowedProjectTypes slice too! const ( Chi Framework = "chi" Gin Framework = "gin" Fiber Framework = "fiber" GorillaMux Framework = "gorilla/mux" HttpRouter Framework = "httprouter" StandardLibrary Framework = "standard-library" Echo Framework = "echo" ) var AllowedProjectTypes = []string{string(Chi), string(Gin), string(Fiber), string(GorillaMux), string(HttpRouter), string(StandardLibrary), string(Echo)} func (f Framework) String() string { return string(f) } func (f *Framework) Type() string { return "Framework" } func (f *Framework) Set(value string) error { // Contains isn't available in 1.20 yet // if AllowedProjectTypes.Contains(value) { for _, project := range AllowedProjectTypes { if project == value { *f = Framework(value) return nil } } return fmt.Errorf("Framework to use. Allowed values: %s", strings.Join(AllowedProjectTypes, ", ")) } ================================================ FILE: cmd/flags/git.go ================================================ package flags import ( "fmt" "strings" ) type Git string const ( Commit = "commit" Stage = "stage" Skip = "skip" ) var AllowedGitsOptions = []string{string(Commit), string(Stage), string(Skip)} func (f Git) String() string { return string(f) } func (f *Git) Type() string { return "Git" } func (f *Git) Set(value string) error { for _, gitOption := range AllowedGitsOptions { if gitOption == value { *f = Git(value) return nil } } return fmt.Errorf("Git to use. Allowed values: %s", strings.Join(AllowedGitsOptions, ", ")) } ================================================ FILE: cmd/program/program.go ================================================ // Package program provides the // main functionality of Blueprint package program import ( "bytes" "fmt" "log" "os" "os/exec" "path/filepath" "runtime" "strings" "text/template" tea "github.com/charmbracelet/bubbletea" "github.com/melkeydev/go-blueprint/cmd/flags" tpl "github.com/melkeydev/go-blueprint/cmd/template" "github.com/melkeydev/go-blueprint/cmd/template/advanced" "github.com/melkeydev/go-blueprint/cmd/template/dbdriver" "github.com/melkeydev/go-blueprint/cmd/template/docker" "github.com/melkeydev/go-blueprint/cmd/template/framework" "github.com/melkeydev/go-blueprint/cmd/utils" ) // A Project contains the data for the project folder // being created, and methods that help with that process type Project struct { ProjectName string Exit bool AbsolutePath string ProjectType flags.Framework DBDriver flags.Database Docker flags.Database FrameworkMap map[flags.Framework]Framework DBDriverMap map[flags.Database]Driver DockerMap map[flags.Database]Docker AdvancedOptions map[string]bool AdvancedTemplates AdvancedTemplates GitOptions flags.Git OSCheck map[string]bool } type AdvancedTemplates struct { TemplateRoutes string TemplateImports string } // A Framework contains the name and templater for a // given Framework type Framework struct { packageName []string templater Templater } type Driver struct { packageName []string templater DBDriverTemplater } type Docker struct { packageName []string templater DockerTemplater } // A Templater has the methods that help build the files // in the Project folder, and is specific to a Framework type Templater interface { Main() []byte Server() []byte Routes() []byte TestHandler() []byte HtmxTemplRoutes() []byte HtmxTemplImports() []byte WebsocketImports() []byte } type DBDriverTemplater interface { Service() []byte Env() []byte Tests() []byte } type DockerTemplater interface { Docker() []byte } type WorkflowTemplater interface { Releaser() []byte Test() []byte ReleaserConfig() []byte } var ( chiPackage = []string{"github.com/go-chi/chi/v5"} gorillaPackage = []string{"github.com/gorilla/mux"} routerPackage = []string{"github.com/julienschmidt/httprouter"} ginPackage = []string{"github.com/gin-gonic/gin"} fiberPackage = []string{"github.com/gofiber/fiber/v2"} echoPackage = []string{"github.com/labstack/echo/v4", "github.com/labstack/echo/v4/middleware"} mysqlDriver = []string{"github.com/go-sql-driver/mysql"} postgresDriver = []string{"github.com/jackc/pgx/v5/stdlib"} sqliteDriver = []string{"github.com/mattn/go-sqlite3"} redisDriver = []string{"github.com/redis/go-redis/v9"} mongoDriver = []string{"go.mongodb.org/mongo-driver"} gocqlDriver = []string{"github.com/gocql/gocql"} scyllaDriver = "github.com/scylladb/gocql@v1.14.4" // Replacement for GoCQL godotenvPackage = []string{"github.com/joho/godotenv"} templPackage = []string{"github.com/a-h/templ"} ) const ( root = "/" cmdApiPath = "cmd/api" cmdWebPath = "cmd/web" internalServerPath = "internal/server" internalDatabasePath = "internal/database" gitHubActionPath = ".github/workflows" ) // CheckOs checks Operation system and generates MakeFile and `go build` command // Based on Project.Unixbase func (p *Project) CheckOS() { p.OSCheck = make(map[string]bool) if runtime.GOOS != "windows" { p.OSCheck["UnixBased"] = true } if runtime.GOOS == "linux" { p.OSCheck["linux"] = true } if runtime.GOOS == "darwin" { p.OSCheck["darwin"] = true } } // ExitCLI checks if the Project has been exited, and closes // out of the CLI if it has func (p *Project) ExitCLI(tprogram *tea.Program) { if p.Exit { // logo render here if err := tprogram.ReleaseTerminal(); err != nil { log.Fatal(err) } os.Exit(1) } } // createFrameWorkMap adds the current supported // Frameworks into a Project's FrameworkMap func (p *Project) createFrameworkMap() { p.FrameworkMap[flags.Chi] = Framework{ packageName: chiPackage, templater: framework.ChiTemplates{}, } p.FrameworkMap[flags.StandardLibrary] = Framework{ packageName: []string{}, templater: framework.StandardLibTemplate{}, } p.FrameworkMap[flags.Gin] = Framework{ packageName: ginPackage, templater: framework.GinTemplates{}, } p.FrameworkMap[flags.Fiber] = Framework{ packageName: fiberPackage, templater: framework.FiberTemplates{}, } p.FrameworkMap[flags.GorillaMux] = Framework{ packageName: gorillaPackage, templater: framework.GorillaTemplates{}, } p.FrameworkMap[flags.HttpRouter] = Framework{ packageName: routerPackage, templater: framework.RouterTemplates{}, } p.FrameworkMap[flags.Echo] = Framework{ packageName: echoPackage, templater: framework.EchoTemplates{}, } } func (p *Project) createDBDriverMap() { p.DBDriverMap[flags.MySql] = Driver{ packageName: mysqlDriver, templater: dbdriver.MysqlTemplate{}, } p.DBDriverMap[flags.Postgres] = Driver{ packageName: postgresDriver, templater: dbdriver.PostgresTemplate{}, } p.DBDriverMap[flags.Sqlite] = Driver{ packageName: sqliteDriver, templater: dbdriver.SqliteTemplate{}, } p.DBDriverMap[flags.Mongo] = Driver{ packageName: mongoDriver, templater: dbdriver.MongoTemplate{}, } p.DBDriverMap[flags.Redis] = Driver{ packageName: redisDriver, templater: dbdriver.RedisTemplate{}, } p.DBDriverMap[flags.Scylla] = Driver{ packageName: gocqlDriver, templater: dbdriver.ScyllaTemplate{}, } } func (p *Project) createDockerMap() { p.DockerMap = make(map[flags.Database]Docker) p.DockerMap[flags.MySql] = Docker{ packageName: []string{}, templater: docker.MysqlDockerTemplate{}, } p.DockerMap[flags.Postgres] = Docker{ packageName: []string{}, templater: docker.PostgresDockerTemplate{}, } p.DockerMap[flags.Mongo] = Docker{ packageName: []string{}, templater: docker.MongoDockerTemplate{}, } p.DockerMap[flags.Redis] = Docker{ packageName: []string{}, templater: docker.RedisDockerTemplate{}, } p.DockerMap[flags.Scylla] = Docker{ packageName: []string{}, templater: docker.ScyllaDockerTemplate{}, } } // CreateMainFile creates the project folders and files, // and writes to them depending on the selected options func (p *Project) CreateMainFile() error { // check if AbsolutePath exists if _, err := os.Stat(p.AbsolutePath); os.IsNotExist(err) { // create directory if err := os.Mkdir(p.AbsolutePath, 0o754); err != nil { log.Printf("Could not create directory: %v", err) return err } } // Check if user.email is set. if p.GitOptions.String() != flags.Skip { emailSet, err := utils.CheckGitConfig("user.email") if err != nil { return err } if !emailSet { fmt.Println("user.email is not set in git config.") fmt.Println("Please set up git config before trying again.") panic("\nGIT CONFIG ISSUE: user.email is not set in git config.\n") } } p.ProjectName = strings.TrimSpace(p.ProjectName) // Create a new directory with the project name projectPath := filepath.Join(p.AbsolutePath, utils.GetRootDir(p.ProjectName)) if _, err := os.Stat(projectPath); os.IsNotExist(err) { err := os.MkdirAll(projectPath, 0o751) if err != nil { log.Printf("Error creating root project directory %v\n", err) return err } } // Define Operating system p.CheckOS() // Create the map for our program p.createFrameworkMap() // Create go.mod err := utils.InitGoMod(p.ProjectName, projectPath) if err != nil { log.Printf("Could not initialize go.mod in new project %v\n", err) return err } // Install the correct package for the selected framework if p.ProjectType != flags.StandardLibrary { err = utils.GoGetPackage(projectPath, p.FrameworkMap[p.ProjectType].packageName) if err != nil { log.Println("Could not install go dependency for the chosen framework") return err } } // Install the correct package for the selected driver if p.DBDriver != "none" { p.createDBDriverMap() err = utils.GoGetPackage(projectPath, p.DBDriverMap[p.DBDriver].packageName) if err != nil { log.Println("Could not install go dependency for chosen driver") return err } err = p.CreatePath(internalDatabasePath, projectPath) if err != nil { log.Printf("Error creating path: %s", internalDatabasePath) return err } err = p.CreateFileWithInjection(internalDatabasePath, projectPath, "database.go", "database") if err != nil { log.Printf("Error injecting database.go file: %v", err) return err } if p.DBDriver != "sqlite" { err = p.CreateFileWithInjection(internalDatabasePath, projectPath, "database_test.go", "integration-tests") if err != nil { log.Printf("Error injecting database_test.go file: %v", err) return err } } } // Create correct docker compose for the selected driver if p.DBDriver != "none" { if p.DBDriver != "sqlite" { p.createDockerMap() p.Docker = p.DBDriver err = p.CreateFileWithInjection(root, projectPath, "docker-compose.yml", "db-docker") if err != nil { log.Printf("Error injecting docker-compose.yml file: %v", err) return err } } else { fmt.Println(" SQLite doesn't support docker-compose.yml configuration") } } // Install the godotenv package err = utils.GoGetPackage(projectPath, godotenvPackage) if err != nil { log.Println("Could not install go dependency") return err } if p.DBDriver == flags.Scylla { replace := fmt.Sprintf("%s=%s", gocqlDriver[0], scyllaDriver) err = utils.GoModReplace(projectPath, replace) if err != nil { log.Printf("Could not replace go dependency %v\n", err) return err } } err = p.CreatePath(cmdApiPath, projectPath) if err != nil { log.Printf("Error creating path: %s", projectPath) return err } err = p.CreateFileWithInjection(cmdApiPath, projectPath, "main.go", "main") if err != nil { return err } makeFile, err := os.Create(filepath.Join(projectPath, "Makefile")) if err != nil { return err } defer makeFile.Close() // inject makefile template makeFileTemplate := template.Must(template.New("makefile").Parse(string(framework.MakeTemplate()))) err = makeFileTemplate.Execute(makeFile, p) if err != nil { return err } readmeFile, err := os.Create(filepath.Join(projectPath, "README.md")) if err != nil { return err } defer readmeFile.Close() // inject readme template readmeFileTemplate := template.Must(template.New("readme").Parse(string(framework.ReadmeTemplate()))) err = readmeFileTemplate.Execute(readmeFile, p) if err != nil { return err } err = p.CreatePath(internalServerPath, projectPath) if err != nil { log.Printf("Error creating path: %s", internalServerPath) return err } if p.AdvancedOptions[string(flags.React)] { // deselect htmx option automatically since react is selected p.AdvancedOptions[string(flags.Htmx)] = false if err := p.CreateViteReactProject(projectPath); err != nil { return fmt.Errorf("failed to set up React project: %w", err) } // if everything went smoothly, remove tailwing flag option p.AdvancedOptions[string(flags.Tailwind)] = false } if p.AdvancedOptions[string(flags.Tailwind)] { // select htmx option automatically since tailwind is selected p.AdvancedOptions[string(flags.Htmx)] = true err = os.MkdirAll(fmt.Sprintf("%s/%s/assets/css", projectPath, cmdWebPath), 0o755) if err != nil { return err } err = os.MkdirAll(fmt.Sprintf("%s/%s/styles", projectPath, cmdWebPath), 0o755) if err != nil { return fmt.Errorf("failed to create styles directory: %w", err) } inputCssFile, err := os.Create(fmt.Sprintf("%s/%s/styles/input.css", projectPath, cmdWebPath)) if err != nil { return err } defer inputCssFile.Close() inputCssTemplate := advanced.InputCssTemplate() err = os.WriteFile(fmt.Sprintf("%s/%s/styles/input.css", projectPath, cmdWebPath), inputCssTemplate, 0o644) if err != nil { return err } outputCssFile, err := os.Create(fmt.Sprintf("%s/%s/assets/css/output.css", projectPath, cmdWebPath)) if err != nil { return err } defer outputCssFile.Close() outputCssTemplate := advanced.OutputCssTemplate() err = os.WriteFile(fmt.Sprintf("%s/%s/assets/css/output.css", projectPath, cmdWebPath), outputCssTemplate, 0o644) if err != nil { return err } } if p.AdvancedOptions[string(flags.Htmx)] { // create folders and hello world file err = p.CreatePath(cmdWebPath, projectPath) if err != nil { return err } helloTemplFile, err := os.Create(fmt.Sprintf("%s/%s/hello.templ", projectPath, cmdWebPath)) if err != nil { return err } defer helloTemplFile.Close() // inject hello.templ template helloTemplTemplate := template.Must(template.New("hellotempl").Parse((string(advanced.HelloTemplTemplate())))) err = helloTemplTemplate.Execute(helloTemplFile, p) if err != nil { return err } baseTemplFile, err := os.Create(fmt.Sprintf("%s/%s/base.templ", projectPath, cmdWebPath)) if err != nil { return err } defer baseTemplFile.Close() baseTemplTemplate := template.Must(template.New("basetempl").Parse((string(advanced.BaseTemplTemplate())))) err = baseTemplTemplate.Execute(baseTemplFile, p) if err != nil { return err } err = os.MkdirAll(fmt.Sprintf("%s/%s/assets/js", projectPath, cmdWebPath), 0o755) if err != nil { return err } htmxMinJsFile, err := os.Create(fmt.Sprintf("%s/%s/assets/js/htmx.min.js", projectPath, cmdWebPath)) if err != nil { return err } defer htmxMinJsFile.Close() htmxMinJsTemplate := advanced.HtmxJSTemplate() err = os.WriteFile(fmt.Sprintf("%s/%s/assets/js/htmx.min.js", projectPath, cmdWebPath), htmxMinJsTemplate, 0o644) if err != nil { return err } htmxTailwindConfigJsFile, err := os.Create(fmt.Sprintf("%s/tailwind.config.js", projectPath)) if err != nil { return err } defer htmxTailwindConfigJsFile.Close() htmxTailwindConfigJsTemplate := advanced.HtmxTailwindConfigJsTemplate() err = os.WriteFile(fmt.Sprintf("%s/tailwind.config.js", projectPath), htmxTailwindConfigJsTemplate, 0o644) if err != nil { return err } efsFile, err := os.Create(fmt.Sprintf("%s/%s/efs.go", projectPath, cmdWebPath)) if err != nil { return err } defer efsFile.Close() efsTemplate := template.Must(template.New("efs").Parse((string(advanced.EfsTemplate())))) err = efsTemplate.Execute(efsFile, p) if err != nil { return err } err = utils.GoGetPackage(projectPath, templPackage) if err != nil { log.Println("Could not install go dependency") return err } helloGoFile, err := os.Create(fmt.Sprintf("%s/%s/hello.go", projectPath, cmdWebPath)) if err != nil { return err } defer efsFile.Close() if p.ProjectType == "fiber" { helloGoTemplate := template.Must(template.New("efs").Parse((string(advanced.HelloFiberGoTemplate())))) err = helloGoTemplate.Execute(helloGoFile, p) if err != nil { return err } err = utils.GoGetPackage(projectPath, []string{"github.com/gofiber/fiber/v2/middleware/adaptor"}) if err != nil { log.Println("Could not install go dependency") return err } } else { helloGoTemplate := template.Must(template.New("efs").Parse((string(advanced.HelloGoTemplate())))) err = helloGoTemplate.Execute(helloGoFile, p) if err != nil { return err } } p.CreateHtmxTemplates() } // Create .github/workflows folder and inject release.yml and go-test.yml if p.AdvancedOptions[string(flags.GoProjectWorkflow)] { err = p.CreatePath(gitHubActionPath, projectPath) if err != nil { log.Printf("Error creating path: %s", gitHubActionPath) return err } err = p.CreateFileWithInjection(gitHubActionPath, projectPath, "release.yml", "releaser") if err != nil { log.Printf("Error injecting release.yml file: %v", err) return err } err = p.CreateFileWithInjection(gitHubActionPath, projectPath, "go-test.yml", "go-test") if err != nil { log.Printf("Error injecting go-test.yml file: %v", err) return err } err = p.CreateFileWithInjection(root, projectPath, ".goreleaser.yml", "releaser-config") if err != nil { log.Printf("Error injecting .goreleaser.yml file: %v", err) return err } } // if the websocket option is checked, a websocket dependency needs to // be added to the routes depending on the framework choosen. // Only fiber uses a different websocket library, the other frameworks // all work with the same one if p.AdvancedOptions[string(flags.Websocket)] { p.CreateWebsocketImports(projectPath) } if p.AdvancedOptions[string(flags.Docker)] { Dockerfile, err := os.Create(filepath.Join(projectPath, "Dockerfile")) if err != nil { return err } defer Dockerfile.Close() // inject Docker template dockerfileTemplate := template.Must(template.New("Dockerfile").Parse(string(advanced.Dockerfile()))) err = dockerfileTemplate.Execute(Dockerfile, p) if err != nil { return err } if p.DBDriver == "none" || p.DBDriver == "sqlite" { Dockercompose, err := os.Create(filepath.Join(projectPath, "docker-compose.yml")) if err != nil { return err } defer Dockercompose.Close() // inject DockerCompose template dockerComposeTemplate := template.Must(template.New("docker-compose.yml").Parse(string(advanced.DockerCompose()))) err = dockerComposeTemplate.Execute(Dockercompose, p) if err != nil { return err } } } err = p.CreateFileWithInjection(internalServerPath, projectPath, "routes.go", "routes") if err != nil { log.Printf("Error injecting routes.go file: %v", err) return err } err = p.CreateFileWithInjection(internalServerPath, projectPath, "routes_test.go", "tests") if err != nil { return err } err = p.CreateFileWithInjection(internalServerPath, projectPath, "server.go", "server") if err != nil { log.Printf("Error injecting server.go file: %v", err) return err } err = p.CreateFileWithInjection(root, projectPath, ".env", "env") if err != nil { log.Printf("Error injecting .env file: %v", err) return err } // Create gitignore gitignoreFile, err := os.Create(filepath.Join(projectPath, ".gitignore")) if err != nil { return err } defer gitignoreFile.Close() // inject gitignore template gitignoreTemplate := template.Must(template.New(".gitignore").Parse(string(framework.GitIgnoreTemplate()))) err = gitignoreTemplate.Execute(gitignoreFile, p) if err != nil { return err } // Create .air.toml file airTomlFile, err := os.Create(filepath.Join(projectPath, ".air.toml")) if err != nil { return err } defer airTomlFile.Close() // inject air.toml template airTomlTemplate := template.Must(template.New("airtoml").Parse(string(framework.AirTomlTemplate()))) err = airTomlTemplate.Execute(airTomlFile, p) if err != nil { return err } err = utils.GoTidy(projectPath) if err != nil { log.Printf("Could not go tidy in new project %v\n", err) return err } err = utils.GoFmt(projectPath) if err != nil { log.Printf("Could not gofmt in new project %v\n", err) return err } if p.GitOptions != flags.Skip { nameSet, err := utils.CheckGitConfig("user.name") if err != nil { return err } if !nameSet { fmt.Println("user.name is not set in git config.") fmt.Println("Please set up git config before trying again.") panic("\nGIT CONFIG ISSUE: user.name is not set in git config.\n") } // Initialize git repo err = utils.ExecuteCmd("git", []string{"init"}, projectPath) if err != nil { log.Printf("Error initializing git repo: %v", err) return err } // Git add files err = utils.ExecuteCmd("git", []string{"add", "."}, projectPath) if err != nil { log.Printf("Error adding files to git repo: %v", err) return err } if p.GitOptions == flags.Commit { // Git commit files err = utils.ExecuteCmd("git", []string{"commit", "-m", "Initial commit"}, projectPath) if err != nil { log.Printf("Error committing files to git repo: %v", err) return err } } } return nil } // CreatePath creates the given directory in the projectPath func (p *Project) CreatePath(pathToCreate string, projectPath string) error { path := filepath.Join(projectPath, pathToCreate) if _, err := os.Stat(path); os.IsNotExist(err) { err := os.MkdirAll(path, 0o751) if err != nil { log.Printf("Error creating directory %v\n", err) return err } } return nil } // CreateFileWithInjection creates the given file at the // project path, and injects the appropriate template func (p *Project) CreateFileWithInjection(pathToCreate string, projectPath string, fileName string, methodName string) error { createdFile, err := os.Create(filepath.Join(projectPath, pathToCreate, fileName)) if err != nil { return err } defer createdFile.Close() switch methodName { case "main": createdTemplate := template.Must(template.New(fileName).Parse(string(p.FrameworkMap[p.ProjectType].templater.Main()))) err = createdTemplate.Execute(createdFile, p) case "server": createdTemplate := template.Must(template.New(fileName).Parse(string(p.FrameworkMap[p.ProjectType].templater.Server()))) err = createdTemplate.Execute(createdFile, p) case "routes": routeFileBytes := p.FrameworkMap[p.ProjectType].templater.Routes() createdTemplate := template.Must(template.New(fileName).Parse(string(routeFileBytes))) err = createdTemplate.Execute(createdFile, p) case "releaser": createdTemplate := template.Must(template.New(fileName).Parse(string(advanced.Releaser()))) err = createdTemplate.Execute(createdFile, p) case "go-test": createdTemplate := template.Must(template.New(fileName).Parse(string(advanced.Test()))) err = createdTemplate.Execute(createdFile, p) case "releaser-config": createdTemplate := template.Must(template.New(fileName).Parse(string(advanced.ReleaserConfig()))) err = createdTemplate.Execute(createdFile, p) case "database": createdTemplate := template.Must(template.New(fileName).Parse(string(p.DBDriverMap[p.DBDriver].templater.Service()))) err = createdTemplate.Execute(createdFile, p) case "db-docker": createdTemplate := template.Must(template.New(fileName).Parse(string(p.DockerMap[p.Docker].templater.Docker()))) err = createdTemplate.Execute(createdFile, p) case "integration-tests": createdTemplate := template.Must(template.New(fileName).Parse(string(p.DBDriverMap[p.DBDriver].templater.Tests()))) err = createdTemplate.Execute(createdFile, p) case "tests": createdTemplate := template.Must(template.New(fileName).Parse(string(p.FrameworkMap[p.ProjectType].templater.TestHandler()))) err = createdTemplate.Execute(createdFile, p) case "env": if p.DBDriver != "none" { envBytes := [][]byte{ tpl.GlobalEnvTemplate(), p.DBDriverMap[p.DBDriver].templater.Env(), } createdTemplate := template.Must(template.New(fileName).Parse(string(bytes.Join(envBytes, []byte("\n"))))) err = createdTemplate.Execute(createdFile, p) } else { createdTemplate := template.Must(template.New(fileName).Parse(string(tpl.GlobalEnvTemplate()))) err = createdTemplate.Execute(createdFile, p) } } if err != nil { return err } return nil } func (p *Project) CreateViteReactProject(projectPath string) error { if err := checkNpmInstalled(); err != nil { return err } originalDir, err := os.Getwd() if err != nil { return fmt.Errorf("failed to get current directory: %w", err) } defer func() { if err := os.Chdir(originalDir); err != nil { fmt.Fprintf(os.Stderr, "failed to change back to original directory: %v\n", err) } }() // change into the project directory to run vite command err = os.Chdir(projectPath) if err != nil { fmt.Println("failed to change into project directory: %w", err) } // the interactive vite command will not work as we can't interact with it fmt.Println("Installing create-vite (using cache if available)...") cmd := exec.Command("npm", "create", "vite@latest", "frontend", "--", "--template", "react-ts", "--prefer-offline", "--no-fund") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("failed to use create-vite: %w", err) } frontendPath := filepath.Join(projectPath, "frontend") if err := os.MkdirAll(frontendPath, 0755); err != nil { return fmt.Errorf("failed to create frontend directory: %w", err) } if err := os.Chdir(frontendPath); err != nil { return fmt.Errorf("failed to change to frontend directory: %w", err) } srcDir := filepath.Join(frontendPath, "src") if err := os.MkdirAll(srcDir, 0755); err != nil { return fmt.Errorf("failed to create src directory: %w", err) } if err := os.WriteFile(filepath.Join(srcDir, "App.tsx"), advanced.ReactAppfile(), 0644); err != nil { return fmt.Errorf("failed to write App.tsx template: %w", err) } // Create the global `.env` file from the template err = p.CreateFileWithInjection("", projectPath, ".env", "env") if err != nil { return fmt.Errorf("failed to create global .env file: %w", err) } // Read from the global `.env` file and create the frontend-specific `.env` globalEnvPath := filepath.Join(projectPath, ".env") vitePort := "8080" // Default fallback // Read the global .env file if data, err := os.ReadFile(globalEnvPath); err == nil { lines := strings.Split(string(data), "\n") for _, line := range lines { if strings.HasPrefix(line, "PORT=") { vitePort = strings.SplitN(line, "=", 2)[1] // Get the backend port value break } } } // Use a template to generate the frontend .env file frontendEnvContent := fmt.Sprintf("VITE_PORT=%s\n", vitePort) if err := os.WriteFile(filepath.Join(frontendPath, ".env"), []byte(frontendEnvContent), 0644); err != nil { return fmt.Errorf("failed to create frontend .env file: %w", err) } // Handle Tailwind configuration if selected if p.AdvancedOptions[string(flags.Tailwind)] { fmt.Println("Installing Tailwind dependencies (using cache if available)...") cmd := exec.Command("npm", "install", "--prefer-offline", "--no-fund", "tailwindcss@^4", "@tailwindcss/vite") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("failed to install Tailwind: %w", err) } // Create the vite + react + Tailwind v4 configuration if err := os.WriteFile(filepath.Join(frontendPath, "vite.config.ts"), advanced.ViteTailwindConfigFile(), 0644); err != nil { return fmt.Errorf("failed to write vite.config.ts: %w", err) } srcDir := filepath.Join(frontendPath, "src") if err := os.MkdirAll(srcDir, 0755); err != nil { return fmt.Errorf("failed to create src directory: %w", err) } err = os.WriteFile(filepath.Join(srcDir, "index.css"), advanced.InputCssTemplateReact(), 0644) if err != nil { return fmt.Errorf("failed to update index.css: %w", err) } if err := os.WriteFile(filepath.Join(srcDir, "App.tsx"), advanced.ReactTailwindAppfile(), 0644); err != nil { return fmt.Errorf("failed to write App.tsx template: %w", err) } if err := os.Remove(filepath.Join(srcDir, "App.css")); err != nil { // Don't return error if file doesn't exist if !os.IsNotExist(err) { return fmt.Errorf("failed to remove App.css: %w", err) } } // set to false to not re-do in next step p.AdvancedOptions[string(flags.Tailwind)] = false } return nil } func (p *Project) CreateHtmxTemplates() { routesPlaceHolder := "" importsPlaceHolder := "" if p.AdvancedOptions[string(flags.Htmx)] { routesPlaceHolder += string(p.FrameworkMap[p.ProjectType].templater.HtmxTemplRoutes()) importsPlaceHolder += string(p.FrameworkMap[p.ProjectType].templater.HtmxTemplImports()) } routeTmpl, err := template.New("routes").Parse(routesPlaceHolder) if err != nil { log.Fatal(err) } importTmpl, err := template.New("imports").Parse(importsPlaceHolder) if err != nil { log.Fatal(err) } var routeBuffer bytes.Buffer var importBuffer bytes.Buffer err = routeTmpl.Execute(&routeBuffer, p) if err != nil { log.Fatal(err) } err = importTmpl.Execute(&importBuffer, p) if err != nil { log.Fatal(err) } p.AdvancedTemplates.TemplateRoutes = routeBuffer.String() p.AdvancedTemplates.TemplateImports = importBuffer.String() } func (p *Project) CreateWebsocketImports(appDir string) { websocketDependency := []string{"github.com/coder/websocket"} if p.ProjectType == flags.Fiber { websocketDependency = []string{"github.com/gofiber/contrib/websocket"} } // Websockets require a different package depending on what framework is // choosen. The application calls go mod tidy at the end so we don't // have to here err := utils.GoGetPackage(appDir, websocketDependency) if err != nil { log.Fatal(err) } importsPlaceHolder := string(p.FrameworkMap[p.ProjectType].templater.WebsocketImports()) importTmpl, err := template.New("imports").Parse(importsPlaceHolder) if err != nil { log.Fatalf("CreateWebsocketImports failed to create template: %v", err) } var importBuffer bytes.Buffer err = importTmpl.Execute(&importBuffer, p) if err != nil { log.Fatalf("CreateWebsocketImports failed write template: %v", err) } newImports := strings.Join([]string{string(p.AdvancedTemplates.TemplateImports), importBuffer.String()}, "\n") p.AdvancedTemplates.TemplateImports = newImports } func checkNpmInstalled() error { cmd := exec.Command("npm", "--version") if err := cmd.Run(); err != nil { return fmt.Errorf("npm is not installed: %w", err) } return nil } ================================================ FILE: cmd/root.go ================================================ /* Copyright © 2023 Melkey melkeydev@gmail.com */ package cmd import ( "os" "github.com/spf13/cobra" ) var rootCmd = &cobra.Command{ Use: "go-blueprint", Short: "A program to spin up a quick Go project using a popular framework", Long: `Go Blueprint is a CLI tool that allows users to spin up a Go project with the corresponding structure seamlessly. It also gives the option to integrate with one of the more popular Go frameworks!`, } func Execute() { err := rootCmd.Execute() if err != nil { os.Exit(1) } } func init() { rootCmd.AddCommand(versionCmd) rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } ================================================ FILE: cmd/steps/steps.go ================================================ // Package steps provides utility for creating // each step of the CLI package steps import "github.com/melkeydev/go-blueprint/cmd/flags" // A StepSchema contains the data that is used // for an individual step of the CLI type StepSchema struct { StepName string // The name of a given step Options []Item // The slice of each option for a given step Headers string // The title displayed at the top of a given step Field string } // Steps contains a slice of steps type Steps struct { Steps map[string]StepSchema } // An Item contains the data for each option // in a StepSchema.Options type Item struct { Flag, Title, Desc string } // InitSteps initializes and returns the *Steps to be used in the CLI program func InitSteps(projectType flags.Framework, databaseType flags.Database) *Steps { steps := &Steps{ map[string]StepSchema{ "framework": { StepName: "Go Project Framework", Options: []Item{ { Title: "Standard-library", Desc: "The built-in Go standard library HTTP package", }, { Title: "Chi", Desc: "A lightweight, idiomatic and composable router for building Go HTTP services", }, { Title: "Gin", Desc: "Features a martini-like API with performance that is up to 40 times faster thanks to httprouter", }, { Title: "Fiber", Desc: "An Express inspired web framework built on top of Fasthttp", }, { Title: "Gorilla/Mux", Desc: "Package gorilla/mux implements a request router and dispatcher for matching incoming requests to their respective handler", }, { Title: "HttpRouter", Desc: "HttpRouter is a lightweight high performance HTTP request router for Go", }, { Title: "Echo", Desc: "High performance, extensible, minimalist Go web framework", }, }, Headers: "What framework do you want to use in your Go project?", Field: projectType.String(), }, "driver": { StepName: "Go Project Database Driver", Options: []Item{ { Title: "Mysql", Desc: "MySQL-Driver for Go's database/sql package", }, { Title: "Postgres", Desc: "Go postgres driver for Go's database/sql package"}, { Title: "Sqlite", Desc: "sqlite3 driver conforming to the built-in database/sql interface"}, { Title: "Mongo", Desc: "The MongoDB supported driver for Go."}, { Title: "Redis", Desc: "Redis driver for Go."}, { Title: "Scylla", Desc: "ScyllaDB Enhanced driver from GoCQL."}, { Title: "None", Desc: "Choose this option if you don't wish to install a specific database driver."}, }, Headers: "What database driver do you want to use in your Go project?", Field: databaseType.String(), }, "advanced": { StepName: "Advanced Features", Headers: "Which advanced features do you want?", Options: []Item{ { Flag: "React", Title: "React", Desc: "Use Vite to spin up a React project in TypeScript. This disables selecting HTMX/Templ", }, { Flag: "Htmx", Title: "HTMX/Templ", Desc: "Add starter HTMX and Templ files. This disables selecting React", }, { Flag: "GitHubAction", Title: "Go Project Workflow", Desc: "Workflow templates for testing, cross-compiling and releasing Go projects", }, { Flag: "Websocket", Title: "Websocket endpoint", Desc: "Add a websocket endpoint", }, { Flag: "Tailwind", Title: "TailwindCSS", Desc: "A utility-first CSS framework (selecting this will automatically add HTMX unless React is specified)", }, { Flag: "Docker", Title: "Docker", Desc: "Dockerfile and docker-compose generic configuration for go project", }, }, }, "git": { StepName: "Git Repository", Headers: "Which git option would you like to select for your project?", Options: []Item{ { Title: "Commit", Desc: "Initialize a new git repository and commit all the changes", }, { Title: "Stage", Desc: "Initialize a new git repository but only stage the changes", }, { Title: "Skip", Desc: "Proceed without initializing a git repository", }, }, }, }, } return steps } ================================================ FILE: cmd/template/advanced/docker.go ================================================ package advanced import ( _ "embed" ) //go:embed files/docker/dockerfile.tmpl var dockerfileTemplate []byte //go:embed files/docker/docker_compose.yml.tmpl var dockerComposeTemplate []byte func Dockerfile() []byte { return dockerfileTemplate } func DockerCompose() []byte { return dockerComposeTemplate } ================================================ FILE: cmd/template/advanced/files/docker/docker_compose.yml.tmpl ================================================ services: app: build: context: . dockerfile: Dockerfile target: prod restart: unless-stopped ports: - ${PORT}:${PORT} environment: APP_ENV: ${APP_ENV} PORT: ${PORT} {{- if and (.AdvancedOptions.docker) (eq .DBDriver "sqlite") }} BLUEPRINT_DB_URL: ${BLUEPRINT_DB_URL} volumes: - sqlite_bp:/app/db {{- end }} {{- if .AdvancedOptions.react }} frontend: build: context: . dockerfile: Dockerfile target: frontend restart: unless-stopped ports: - 5173:5173 depends_on: - app {{- end }} {{- if and (.AdvancedOptions.docker) (eq .DBDriver "sqlite") }} volumes: sqlite_bp: {{- end }} ================================================ FILE: cmd/template/advanced/files/docker/dockerfile.tmpl ================================================ FROM golang:1.24.4-alpine AS build {{- if or (.AdvancedOptions.tailwind) (eq .DBDriver "sqlite") }} RUN apk add --no-cache{{- if .AdvancedOptions.tailwind }} curl libstdc++ libgcc{{ end }}{{- if (eq .DBDriver "sqlite") }} alpine-sdk{{ end }} {{- end }} WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . {{- if or .AdvancedOptions.htmx .AdvancedOptions.tailwind }} RUN go install github.com/a-h/templ/cmd/templ@latest && \ templ generate{{- if .AdvancedOptions.tailwind}} && \{{- end}} {{- end}} {{- if .AdvancedOptions.tailwind}} curl -sL https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64-musl -o tailwindcss && \ chmod +x tailwindcss && \ ./tailwindcss -i cmd/web/styles/input.css -o cmd/web/assets/css/output.css {{- end }} RUN {{ if (eq .DBDriver "sqlite") }}CGO_ENABLED=1 GOOS=linux {{ end }}go build -o main cmd/api/main.go FROM alpine:3.20.1 AS prod WORKDIR /app COPY --from=build /app/main /app/main EXPOSE ${PORT} CMD ["./main"] {{ if .AdvancedOptions.react}} FROM node:20 AS frontend_builder WORKDIR /frontend COPY frontend/package*.json ./ RUN npm install COPY frontend/. . RUN npm run build FROM node:23-slim AS frontend RUN npm install -g serve COPY --from=frontend_builder /frontend/dist /app/dist EXPOSE 5173 CMD ["serve", "-s", "/app/dist", "-l", "5173"] {{- end}} ================================================ FILE: cmd/template/advanced/files/htmx/base.templ.tmpl ================================================ package web templ Base() { Go Blueprint Hello
{ children... }
} ================================================ FILE: cmd/template/advanced/files/htmx/efs.go.tmpl ================================================ package web import "embed" //go:embed "assets" var Files embed.FS ================================================ FILE: cmd/template/advanced/files/htmx/hello.go.tmpl ================================================ package web import ( "log" "net/http" ) func HelloWebHandler(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { http.Error(w, "Bad Request", http.StatusBadRequest) } name := r.FormValue("name") component := HelloPost(name) err = component.Render(r.Context(), w) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) log.Fatalf("Error rendering in HelloWebHandler: %e", err) } } ================================================ FILE: cmd/template/advanced/files/htmx/hello.templ.tmpl ================================================ package web templ HelloForm() { @Base() {
} } templ HelloPost(name string) {

Hello, { name }

} ================================================ FILE: cmd/template/advanced/files/htmx/hello_fiber.go.tmpl ================================================ package web import ( "bytes" "fmt" "log" "github.com/gofiber/fiber/v2" ) func HelloWebHandler(c *fiber.Ctx) error { // Parse form data if err := c.BodyParser(c); err != nil { innerErr := c.Status(fiber.StatusBadRequest).SendString("Bad Request") if innerErr != nil { log.Fatalf("Could not send error in HelloWebHandler: %e", innerErr) } } // Get the name from the form data name := c.FormValue("name") // Render the component component := HelloPost(name) buf := new(bytes.Buffer) err := component.Render(c.Context(), buf) if err != nil { errorString := fmt.Sprintf("Error rendering in HelloWebHandler: %e", err) innerErr := c.Status(fiber.StatusBadRequest).SendString(errorString) if innerErr != nil { log.Fatalf("Could not send error in HelloWebHandler: %e", innerErr) } log.Fatalf("%s", errorString) } // Send the response err = c.Status(fiber.StatusOK).SendString(buf.String()) if err != nil { log.Fatalf("Could not send OK in HelloWebHandler: %e", err) } return nil } ================================================ FILE: cmd/template/advanced/files/htmx/htmx.min.js.tmpl ================================================ var htmx = (function () { "use strict"; const Q = { onLoad: null, process: null, on: null, off: null, trigger: null, ajax: null, find: null, findAll: null, closest: null, values: function (e, t) { const n = dn(e, t || "post"); return n.values; }, remove: null, addClass: null, removeClass: null, toggleClass: null, takeClass: null, swap: null, defineExtension: null, removeExtension: null, logAll: null, logNone: null, logger: null, config: { historyEnabled: true, historyCacheSize: 10, refreshOnHistoryMiss: false, defaultSwapStyle: "innerHTML", defaultSwapDelay: 0, defaultSettleDelay: 20, includeIndicatorStyles: true, indicatorClass: "htmx-indicator", requestClass: "htmx-request", addedClass: "htmx-added", settlingClass: "htmx-settling", swappingClass: "htmx-swapping", allowEval: true, allowScriptTags: true, inlineScriptNonce: "", inlineStyleNonce: "", attributesToSettle: ["class", "style", "width", "height"], withCredentials: false, timeout: 0, wsReconnectDelay: "full-jitter", wsBinaryType: "blob", disableSelector: "[hx-disable], [data-hx-disable]", scrollBehavior: "instant", defaultFocusScroll: false, getCacheBusterParam: false, globalViewTransitions: false, methodsThatUseUrlParams: ["get", "delete"], selfRequestsOnly: true, ignoreTitle: false, scrollIntoViewOnBoost: true, triggerSpecsCache: null, disableInheritance: false, responseHandling: [ { code: "204", swap: false }, { code: "[23]..", swap: true }, { code: "[45]..", swap: false, error: true }, ], allowNestedOobSwaps: true, historyRestoreAsHxRequest: true, }, parseInterval: null, location: location, _: null, version: "2.0.6", }; Q.onLoad = j; Q.process = Ft; Q.on = xe; Q.off = be; Q.trigger = ae; Q.ajax = Ln; Q.find = f; Q.findAll = x; Q.closest = g; Q.remove = z; Q.addClass = K; Q.removeClass = G; Q.toggleClass = W; Q.takeClass = Z; Q.swap = $e; Q.defineExtension = zn; Q.removeExtension = $n; Q.logAll = V; Q.logNone = _; Q.parseInterval = d; Q._ = e; const n = { addTriggerHandler: St, bodyContains: se, canAccessLocalStorage: B, findThisElement: Se, filterValues: yn, swap: $e, hasAttribute: s, getAttributeValue: a, getClosestAttributeValue: ne, getClosestMatch: q, getExpressionVars: Tn, getHeaders: mn, getInputValues: dn, getInternalData: oe, getSwapSpecification: bn, getTriggerSpecs: st, getTarget: Ee, makeFragment: P, mergeObjects: le, makeSettleInfo: Sn, oobSwap: He, querySelectorExt: ue, settleImmediately: Yt, shouldCancel: ht, triggerEvent: ae, triggerErrorEvent: fe, withExtensions: jt, }; const de = ["get", "post", "put", "delete", "patch"]; const T = de .map(function (e) { return "[hx-" + e + "], [data-hx-" + e + "]"; }) .join(", "); function d(e) { if (e == undefined) { return undefined; } let t = NaN; if (e.slice(-2) == "ms") { t = parseFloat(e.slice(0, -2)); } else if (e.slice(-1) == "s") { t = parseFloat(e.slice(0, -1)) * 1e3; } else if (e.slice(-1) == "m") { t = parseFloat(e.slice(0, -1)) * 1e3 * 60; } else { t = parseFloat(e); } return isNaN(t) ? undefined : t; } function ee(e, t) { return e instanceof Element && e.getAttribute(t); } function s(e, t) { return ( !!e.hasAttribute && (e.hasAttribute(t) || e.hasAttribute("data-" + t)) ); } function a(e, t) { return ee(e, t) || ee(e, "data-" + t); } function u(e) { const t = e.parentElement; if (!t && e.parentNode instanceof ShadowRoot) return e.parentNode; return t; } function te() { return document; } function y(e, t) { return e.getRootNode ? e.getRootNode({ composed: t }) : te(); } function q(e, t) { while (e && !t(e)) { e = u(e); } return e || null; } function o(e, t, n) { const r = a(t, n); const o = a(t, "hx-disinherit"); var i = a(t, "hx-inherit"); if (e !== t) { if (Q.config.disableInheritance) { if (i && (i === "*" || i.split(" ").indexOf(n) >= 0)) { return r; } else { return null; } } if (o && (o === "*" || o.split(" ").indexOf(n) >= 0)) { return "unset"; } } return r; } function ne(t, n) { let r = null; q(t, function (e) { return !!(r = o(t, ce(e), n)); }); if (r !== "unset") { return r; } } function h(e, t) { return e instanceof Element && e.matches(t); } function A(e) { const t = /<([a-z][^\/\0>\x20\t\r\n\f]*)/i; const n = t.exec(e); if (n) { return n[1].toLowerCase(); } else { return ""; } } function L(e) { const t = new DOMParser(); return t.parseFromString(e, "text/html"); } function N(e, t) { while (t.childNodes.length > 0) { e.append(t.childNodes[0]); } } function r(e) { const t = te().createElement("script"); ie(e.attributes, function (e) { t.setAttribute(e.name, e.value); }); t.textContent = e.textContent; t.async = false; if (Q.config.inlineScriptNonce) { t.nonce = Q.config.inlineScriptNonce; } return t; } function i(e) { return ( e.matches("script") && (e.type === "text/javascript" || e.type === "module" || e.type === "") ); } function I(e) { Array.from(e.querySelectorAll("script")).forEach((e) => { if (i(e)) { const t = r(e); const n = e.parentNode; try { n.insertBefore(t, e); } catch (e) { R(e); } finally { e.remove(); } } }); } function P(e) { const t = e.replace(/]*)?>[\s\S]*?<\/head>/i, ""); const n = A(t); let r; if (n === "html") { r = new DocumentFragment(); const i = L(e); N(r, i.body); r.title = i.title; } else if (n === "body") { r = new DocumentFragment(); const i = L(t); N(r, i.body); r.title = i.title; } else { const i = L( '", ); r = i.querySelector("template").content; r.title = i.title; var o = r.querySelector("title"); if (o && o.parentNode === r) { o.remove(); r.title = o.innerText; } } if (r) { if (Q.config.allowScriptTags) { I(r); } else { r.querySelectorAll("script").forEach((e) => e.remove()); } } return r; } function re(e) { if (e) { e(); } } function t(e, t) { return Object.prototype.toString.call(e) === "[object " + t + "]"; } function D(e) { return typeof e === "function"; } function k(e) { return t(e, "Object"); } function oe(e) { const t = "htmx-internal-data"; let n = e[t]; if (!n) { n = e[t] = {}; } return n; } function M(t) { const n = []; if (t) { for (let e = 0; e < t.length; e++) { n.push(t[e]); } } return n; } function ie(t, n) { if (t) { for (let e = 0; e < t.length; e++) { n(t[e]); } } } function F(e) { const t = e.getBoundingClientRect(); const n = t.top; const r = t.bottom; return n < window.innerHeight && r >= 0; } function se(e) { return e.getRootNode({ composed: true }) === document; } function X(e) { return e.trim().split(/\s+/); } function le(e, t) { for (const n in t) { if (t.hasOwnProperty(n)) { e[n] = t[n]; } } return e; } function v(e) { try { return JSON.parse(e); } catch (e) { R(e); return null; } } function B() { const e = "htmx:sessionStorageTest"; try { sessionStorage.setItem(e, e); sessionStorage.removeItem(e); return true; } catch (e) { return false; } } function U(e) { const t = new URL(e, "http://x"); if (t) { e = t.pathname + t.search; } if (e != "/") { e = e.replace(/\/+$/, ""); } return e; } function e(e) { return On(te().body, function () { return eval(e); }); } function j(t) { const e = Q.on("htmx:load", function (e) { t(e.detail.elt); }); return e; } function V() { Q.logger = function (e, t, n) { if (console) { console.log(t, e, n); } }; } function _() { Q.logger = null; } function f(e, t) { if (typeof e !== "string") { return e.querySelector(t); } else { return f(te(), e); } } function x(e, t) { if (typeof e !== "string") { return e.querySelectorAll(t); } else { return x(te(), e); } } function b() { return window; } function z(e, t) { e = w(e); if (t) { b().setTimeout(function () { z(e); e = null; }, t); } else { u(e).removeChild(e); } } function ce(e) { return e instanceof Element ? e : null; } function $(e) { return e instanceof HTMLElement ? e : null; } function J(e) { return typeof e === "string" ? e : null; } function p(e) { return e instanceof Element || e instanceof Document || e instanceof DocumentFragment ? e : null; } function K(e, t, n) { e = ce(w(e)); if (!e) { return; } if (n) { b().setTimeout(function () { K(e, t); e = null; }, n); } else { e.classList && e.classList.add(t); } } function G(e, t, n) { let r = ce(w(e)); if (!r) { return; } if (n) { b().setTimeout(function () { G(r, t); r = null; }, n); } else { if (r.classList) { r.classList.remove(t); if (r.classList.length === 0) { r.removeAttribute("class"); } } } } function W(e, t) { e = w(e); e.classList.toggle(t); } function Z(e, t) { e = w(e); ie(e.parentElement.children, function (e) { G(e, t); }); K(ce(e), t); } function g(e, t) { e = ce(w(e)); if (e) { return e.closest(t); } return null; } function l(e, t) { return e.substring(0, t.length) === t; } function Y(e, t) { return e.substring(e.length - t.length) === t; } function pe(e) { const t = e.trim(); if (l(t, "<") && Y(t, "/>")) { return t.substring(1, t.length - 2); } else { return t; } } function m(t, r, n) { if (r.indexOf("global ") === 0) { return m(t, r.slice(7), true); } t = w(t); const o = []; { let t = 0; let n = 0; for (let e = 0; e < r.length; e++) { const l = r[e]; if (l === "," && t === 0) { o.push(r.substring(n, e)); n = e + 1; continue; } if (l === "<") { t++; } else if (l === "/" && e < r.length - 1 && r[e + 1] === ">") { t--; } } if (n < r.length) { o.push(r.substring(n)); } } const i = []; const s = []; while (o.length > 0) { const r = pe(o.shift()); let e; if (r.indexOf("closest ") === 0) { e = g(ce(t), pe(r.slice(8))); } else if (r.indexOf("find ") === 0) { e = f(p(t), pe(r.slice(5))); } else if (r === "next" || r === "nextElementSibling") { e = ce(t).nextElementSibling; } else if (r.indexOf("next ") === 0) { e = ge(t, pe(r.slice(5)), !!n); } else if (r === "previous" || r === "previousElementSibling") { e = ce(t).previousElementSibling; } else if (r.indexOf("previous ") === 0) { e = me(t, pe(r.slice(9)), !!n); } else if (r === "document") { e = document; } else if (r === "window") { e = window; } else if (r === "body") { e = document.body; } else if (r === "root") { e = y(t, !!n); } else if (r === "host") { e = t.getRootNode().host; } else { s.push(r); } if (e) { i.push(e); } } if (s.length > 0) { const e = s.join(","); const c = p(y(t, !!n)); i.push(...M(c.querySelectorAll(e))); } return i; } var ge = function (t, e, n) { const r = p(y(t, n)).querySelectorAll(e); for (let e = 0; e < r.length; e++) { const o = r[e]; if (o.compareDocumentPosition(t) === Node.DOCUMENT_POSITION_PRECEDING) { return o; } } }; var me = function (t, e, n) { const r = p(y(t, n)).querySelectorAll(e); for (let e = r.length - 1; e >= 0; e--) { const o = r[e]; if (o.compareDocumentPosition(t) === Node.DOCUMENT_POSITION_FOLLOWING) { return o; } } }; function ue(e, t) { if (typeof e !== "string") { return m(e, t)[0]; } else { return m(te().body, e)[0]; } } function w(e, t) { if (typeof e === "string") { return f(p(t) || document, e); } else { return e; } } function ye(e, t, n, r) { if (D(t)) { return { target: te().body, event: J(e), listener: t, options: n }; } else { return { target: w(e), event: J(t), listener: n, options: r }; } } function xe(t, n, r, o) { Gn(function () { const e = ye(t, n, r, o); e.target.addEventListener(e.event, e.listener, e.options); }); const e = D(n); return e ? n : r; } function be(t, n, r) { Gn(function () { const e = ye(t, n, r); e.target.removeEventListener(e.event, e.listener); }); return D(n) ? n : r; } const ve = te().createElement("output"); function we(t, n) { const e = ne(t, n); if (e) { if (e === "this") { return [Se(t, n)]; } else { const r = m(t, e); const o = /(^|,)(\s*)inherit(\s*)($|,)/.test(e); if (o) { const i = ce( q(t, function (e) { return e !== t && s(ce(e), n); }), ); if (i) { r.push(...we(i, n)); } } if (r.length === 0) { R('The selector "' + e + '" on ' + n + " returned no matches!"); return [ve]; } else { return r; } } } } function Se(e, t) { return ce( q(e, function (e) { return a(ce(e), t) != null; }), ); } function Ee(e) { const t = ne(e, "hx-target"); if (t) { if (t === "this") { return Se(e, "hx-target"); } else { return ue(e, t); } } else { const n = oe(e); if (n.boosted) { return te().body; } else { return e; } } } function Ce(e) { return Q.config.attributesToSettle.includes(e); } function Oe(t, n) { ie(t.attributes, function (e) { if (!n.hasAttribute(e.name) && Ce(e.name)) { t.removeAttribute(e.name); } }); ie(n.attributes, function (e) { if (Ce(e.name)) { t.setAttribute(e.name, e.value); } }); } function Re(t, e) { const n = Jn(e); for (let e = 0; e < n.length; e++) { const r = n[e]; try { if (r.isInlineSwap(t)) { return true; } } catch (e) { R(e); } } return t === "outerHTML"; } function He(e, o, i, t) { t = t || te(); let n = "#" + CSS.escape(ee(o, "id")); let s = "outerHTML"; if (e === "true") { } else if (e.indexOf(":") > 0) { s = e.substring(0, e.indexOf(":")); n = e.substring(e.indexOf(":") + 1); } else { s = e; } o.removeAttribute("hx-swap-oob"); o.removeAttribute("data-hx-swap-oob"); const r = m(t, n, false); if (r.length) { ie(r, function (e) { let t; const n = o.cloneNode(true); t = te().createDocumentFragment(); t.appendChild(n); if (!Re(s, e)) { t = p(n); } const r = { shouldSwap: true, target: e, fragment: t }; if (!ae(e, "htmx:oobBeforeSwap", r)) return; e = r.target; if (r.shouldSwap) { qe(t); _e(s, e, e, t, i); Te(); } ie(i.elts, function (e) { ae(e, "htmx:oobAfterSwap", r); }); }); o.parentNode.removeChild(o); } else { o.parentNode.removeChild(o); fe(te().body, "htmx:oobErrorNoTarget", { content: o }); } return e; } function Te() { const e = f("#--htmx-preserve-pantry--"); if (e) { for (const t of [...e.children]) { const n = f("#" + t.id); n.parentNode.moveBefore(t, n); n.remove(); } e.remove(); } } function qe(e) { ie(x(e, "[hx-preserve], [data-hx-preserve]"), function (e) { const t = a(e, "id"); const n = te().getElementById(t); if (n != null) { if (e.moveBefore) { let e = f("#--htmx-preserve-pantry--"); if (e == null) { te().body.insertAdjacentHTML( "afterend", "
", ); e = f("#--htmx-preserve-pantry--"); } e.moveBefore(n, null); } else { e.parentNode.replaceChild(n, e); } } }); } function Ae(l, e, c) { ie(e.querySelectorAll("[id]"), function (t) { const n = ee(t, "id"); if (n && n.length > 0) { const r = n.replace("'", "\\'"); const o = t.tagName.replace(":", "\\:"); const e = p(l); const i = e && e.querySelector(o + "[id='" + r + "']"); if (i && i !== e) { const s = t.cloneNode(); Oe(t, i); c.tasks.push(function () { Oe(t, s); }); } } }); } function Le(e) { return function () { G(e, Q.config.addedClass); Ft(ce(e)); Ne(p(e)); ae(e, "htmx:load"); }; } function Ne(e) { const t = "[autofocus]"; const n = $(h(e, t) ? e : e.querySelector(t)); if (n != null) { n.focus(); } } function c(e, t, n, r) { Ae(e, n, r); while (n.childNodes.length > 0) { const o = n.firstChild; K(ce(o), Q.config.addedClass); e.insertBefore(o, t); if (o.nodeType !== Node.TEXT_NODE && o.nodeType !== Node.COMMENT_NODE) { r.tasks.push(Le(o)); } } } function Ie(e, t) { let n = 0; while (n < e.length) { t = ((t << 5) - t + e.charCodeAt(n++)) | 0; } return t; } function Pe(t) { let n = 0; for (let e = 0; e < t.attributes.length; e++) { const r = t.attributes[e]; if (r.value) { n = Ie(r.name, n); n = Ie(r.value, n); } } return n; } function De(t) { const n = oe(t); if (n.onHandlers) { for (let e = 0; e < n.onHandlers.length; e++) { const r = n.onHandlers[e]; be(t, r.event, r.listener); } delete n.onHandlers; } } function ke(e) { const t = oe(e); if (t.timeout) { clearTimeout(t.timeout); } if (t.listenerInfos) { ie(t.listenerInfos, function (e) { if (e.on) { be(e.on, e.trigger, e.listener); } }); } De(e); ie(Object.keys(t), function (e) { if (e !== "firstInitCompleted") delete t[e]; }); } function S(e) { ae(e, "htmx:beforeCleanupElement"); ke(e); ie(e.children, function (e) { S(e); }); } function Me(t, e, n) { if (t.tagName === "BODY") { return Ve(t, e, n); } let r; const o = t.previousSibling; const i = u(t); if (!i) { return; } c(i, t, e, n); if (o == null) { r = i.firstChild; } else { r = o.nextSibling; } n.elts = n.elts.filter(function (e) { return e !== t; }); while (r && r !== t) { if (r instanceof Element) { n.elts.push(r); } r = r.nextSibling; } S(t); t.remove(); } function Fe(e, t, n) { return c(e, e.firstChild, t, n); } function Xe(e, t, n) { return c(u(e), e, t, n); } function Be(e, t, n) { return c(e, null, t, n); } function Ue(e, t, n) { return c(u(e), e.nextSibling, t, n); } function je(e) { S(e); const t = u(e); if (t) { return t.removeChild(e); } } function Ve(e, t, n) { const r = e.firstChild; c(e, r, t, n); if (r) { while (r.nextSibling) { S(r.nextSibling); e.removeChild(r.nextSibling); } S(r); e.removeChild(r); } } function _e(t, e, n, r, o) { switch (t) { case "none": return; case "outerHTML": Me(n, r, o); return; case "afterbegin": Fe(n, r, o); return; case "beforebegin": Xe(n, r, o); return; case "beforeend": Be(n, r, o); return; case "afterend": Ue(n, r, o); return; case "delete": je(n); return; default: var i = Jn(e); for (let e = 0; e < i.length; e++) { const s = i[e]; try { const l = s.handleSwap(t, n, r, o); if (l) { if (Array.isArray(l)) { for (let e = 0; e < l.length; e++) { const c = l[e]; if ( c.nodeType !== Node.TEXT_NODE && c.nodeType !== Node.COMMENT_NODE ) { o.tasks.push(Le(c)); } } } return; } } catch (e) { R(e); } } if (t === "innerHTML") { Ve(n, r, o); } else { _e(Q.config.defaultSwapStyle, e, n, r, o); } } } function ze(e, n, r) { var t = x(e, "[hx-swap-oob], [data-hx-swap-oob]"); ie(t, function (e) { if (Q.config.allowNestedOobSwaps || e.parentElement === null) { const t = a(e, "hx-swap-oob"); if (t != null) { He(t, e, n, r); } } else { e.removeAttribute("hx-swap-oob"); e.removeAttribute("data-hx-swap-oob"); } }); return t.length > 0; } function $e(h, d, p, g) { if (!g) { g = {}; } let m = null; let n = null; let e = function () { re(g.beforeSwapCallback); h = w(h); const r = g.contextElement ? y(g.contextElement, false) : te(); const e = document.activeElement; let t = {}; t = { elt: e, start: e ? e.selectionStart : null, end: e ? e.selectionEnd : null, }; const o = Sn(h); if (p.swapStyle === "textContent") { h.textContent = d; } else { let n = P(d); o.title = g.title || n.title; if (g.historyRequest) { n = n.querySelector("[hx-history-elt],[data-hx-history-elt]") || n; } if (g.selectOOB) { const i = g.selectOOB.split(","); for (let t = 0; t < i.length; t++) { const s = i[t].split(":", 2); let e = s[0].trim(); if (e.indexOf("#") === 0) { e = e.substring(1); } const l = s[1] || "true"; const c = n.querySelector("#" + e); if (c) { He(l, c, o, r); } } } ze(n, o, r); ie(x(n, "template"), function (e) { if (e.content && ze(e.content, o, r)) { e.remove(); } }); if (g.select) { const u = te().createDocumentFragment(); ie(n.querySelectorAll(g.select), function (e) { u.appendChild(e); }); n = u; } qe(n); _e(p.swapStyle, g.contextElement, h, n, o); Te(); } if (t.elt && !se(t.elt) && ee(t.elt, "id")) { const f = document.getElementById(ee(t.elt, "id")); const a = { preventScroll: p.focusScroll !== undefined ? !p.focusScroll : !Q.config.defaultFocusScroll, }; if (f) { if (t.start && f.setSelectionRange) { try { f.setSelectionRange(t.start, t.end); } catch (e) {} } f.focus(a); } } h.classList.remove(Q.config.swappingClass); ie(o.elts, function (e) { if (e.classList) { e.classList.add(Q.config.settlingClass); } ae(e, "htmx:afterSwap", g.eventInfo); }); re(g.afterSwapCallback); if (!p.ignoreTitle) { Bn(o.title); } const n = function () { ie(o.tasks, function (e) { e.call(); }); ie(o.elts, function (e) { if (e.classList) { e.classList.remove(Q.config.settlingClass); } ae(e, "htmx:afterSettle", g.eventInfo); }); if (g.anchor) { const e = ce(w("#" + g.anchor)); if (e) { e.scrollIntoView({ block: "start", behavior: "auto" }); } } En(o.elts, p); re(g.afterSettleCallback); re(m); }; if (p.settleDelay > 0) { b().setTimeout(n, p.settleDelay); } else { n(); } }; let t = Q.config.globalViewTransitions; if (p.hasOwnProperty("transition")) { t = p.transition; } const r = g.contextElement || te(); if ( t && ae(r, "htmx:beforeTransition", g.eventInfo) && typeof Promise !== "undefined" && document.startViewTransition ) { const o = new Promise(function (e, t) { m = e; n = t; }); const i = e; e = function () { document.startViewTransition(function () { i(); return o; }); }; } try { if (p?.swapDelay && p.swapDelay > 0) { b().setTimeout(e, p.swapDelay); } else { e(); } } catch (e) { fe(r, "htmx:swapError", g.eventInfo); re(n); throw e; } } function Je(e, t, n) { const r = e.getResponseHeader(t); if (r.indexOf("{") === 0) { const o = v(r); for (const i in o) { if (o.hasOwnProperty(i)) { let e = o[i]; if (k(e)) { n = e.target !== undefined ? e.target : n; } else { e = { value: e }; } ae(n, i, e); } } } else { const s = r.split(","); for (let e = 0; e < s.length; e++) { ae(n, s[e].trim(), []); } } } const Ke = /\s/; const E = /[\s,]/; const Ge = /[_$a-zA-Z]/; const We = /[_$a-zA-Z0-9]/; const Ze = ['"', "'", "/"]; const C = /[^\s]/; const Ye = /[{(]/; const Qe = /[})]/; function et(e) { const t = []; let n = 0; while (n < e.length) { if (Ge.exec(e.charAt(n))) { var r = n; while (We.exec(e.charAt(n + 1))) { n++; } t.push(e.substring(r, n + 1)); } else if (Ze.indexOf(e.charAt(n)) !== -1) { const o = e.charAt(n); var r = n; n++; while (n < e.length && e.charAt(n) !== o) { if (e.charAt(n) === "\\") { n++; } n++; } t.push(e.substring(r, n + 1)); } else { const i = e.charAt(n); t.push(i); } n++; } return t; } function tt(e, t, n) { return ( Ge.exec(e.charAt(0)) && e !== "true" && e !== "false" && e !== "this" && e !== n && t !== "." ); } function nt(r, o, i) { if (o[0] === "[") { o.shift(); let e = 1; let t = " return (function(" + i + "){ return ("; let n = null; while (o.length > 0) { const s = o[0]; if (s === "]") { e--; if (e === 0) { if (n === null) { t = t + "true"; } o.shift(); t += ")})"; try { const l = On( r, function () { return Function(t)(); }, function () { return true; }, ); l.source = t; return l; } catch (e) { fe(te().body, "htmx:syntax:error", { error: e, source: t }); return null; } } } else if (s === "[") { e++; } if (tt(s, n, i)) { t += "((" + i + "." + s + ") ? (" + i + "." + s + ") : (window." + s + "))"; } else { t = t + s; } n = o.shift(); } } } function O(e, t) { let n = ""; while (e.length > 0 && !t.test(e[0])) { n += e.shift(); } return n; } function rt(e) { let t; if (e.length > 0 && Ye.test(e[0])) { e.shift(); t = O(e, Qe).trim(); e.shift(); } else { t = O(e, E); } return t; } const ot = "input, textarea, select"; function it(e, t, n) { const r = []; const o = et(t); do { O(o, C); const l = o.length; const c = O(o, /[,\[\s]/); if (c !== "") { if (c === "every") { const u = { trigger: "every" }; O(o, C); u.pollInterval = d(O(o, /[,\[\s]/)); O(o, C); var i = nt(e, o, "event"); if (i) { u.eventFilter = i; } r.push(u); } else { const f = { trigger: c }; var i = nt(e, o, "event"); if (i) { f.eventFilter = i; } O(o, C); while (o.length > 0 && o[0] !== ",") { const a = o.shift(); if (a === "changed") { f.changed = true; } else if (a === "once") { f.once = true; } else if (a === "consume") { f.consume = true; } else if (a === "delay" && o[0] === ":") { o.shift(); f.delay = d(O(o, E)); } else if (a === "from" && o[0] === ":") { o.shift(); if (Ye.test(o[0])) { var s = rt(o); } else { var s = O(o, E); if ( s === "closest" || s === "find" || s === "next" || s === "previous" ) { o.shift(); const h = rt(o); if (h.length > 0) { s += " " + h; } } } f.from = s; } else if (a === "target" && o[0] === ":") { o.shift(); f.target = rt(o); } else if (a === "throttle" && o[0] === ":") { o.shift(); f.throttle = d(O(o, E)); } else if (a === "queue" && o[0] === ":") { o.shift(); f.queue = O(o, E); } else if (a === "root" && o[0] === ":") { o.shift(); f[a] = rt(o); } else if (a === "threshold" && o[0] === ":") { o.shift(); f[a] = O(o, E); } else { fe(e, "htmx:syntax:error", { token: o.shift() }); } O(o, C); } r.push(f); } } if (o.length === l) { fe(e, "htmx:syntax:error", { token: o.shift() }); } O(o, C); } while (o[0] === "," && o.shift()); if (n) { n[t] = r; } return r; } function st(e) { const t = a(e, "hx-trigger"); let n = []; if (t) { const r = Q.config.triggerSpecsCache; n = (r && r[t]) || it(e, t, r); } if (n.length > 0) { return n; } else if (h(e, "form")) { return [{ trigger: "submit" }]; } else if (h(e, 'input[type="button"], input[type="submit"]')) { return [{ trigger: "click" }]; } else if (h(e, ot)) { return [{ trigger: "change" }]; } else { return [{ trigger: "click" }]; } } function lt(e) { oe(e).cancelled = true; } function ct(e, t, n) { const r = oe(e); r.timeout = b().setTimeout(function () { if (se(e) && r.cancelled !== true) { if (!pt(n, e, Bt("hx:poll:trigger", { triggerSpec: n, target: e }))) { t(e); } ct(e, t, n); } }, n.pollInterval); } function ut(e) { return ( location.hostname === e.hostname && ee(e, "href") && ee(e, "href").indexOf("#") !== 0 ); } function ft(e) { return g(e, Q.config.disableSelector); } function at(t, n, e) { if ( (t instanceof HTMLAnchorElement && ut(t) && (t.target === "" || t.target === "_self")) || (t.tagName === "FORM" && String(ee(t, "method")).toLowerCase() !== "dialog") ) { n.boosted = true; let r, o; if (t.tagName === "A") { r = "get"; o = ee(t, "href"); } else { const i = ee(t, "method"); r = i ? i.toLowerCase() : "get"; o = ee(t, "action"); if (o == null || o === "") { o = location.href; } if (r === "get" && o.includes("?")) { o = o.replace(/\?[^#]+/, ""); } } e.forEach(function (e) { gt( t, function (e, t) { const n = ce(e); if (ft(n)) { S(n); return; } he(r, o, n, t); }, n, e, true, ); }); } } function ht(e, t) { if (e.type === "submit" || e.type === "click") { t = ce(e.target) || t; if (t.tagName === "FORM") { return true; } if (t.form && t.type === "submit") { return true; } t = t.closest("a"); if ( t && t.href && (t.getAttribute("href") === "#" || t.getAttribute("href").indexOf("#") !== 0) ) { return true; } } return false; } function dt(e, t) { return ( oe(e).boosted && e instanceof HTMLAnchorElement && t.type === "click" && (t.ctrlKey || t.metaKey) ); } function pt(e, t, n) { const r = e.eventFilter; if (r) { try { return r.call(t, n) !== true; } catch (e) { const o = r.source; fe(te().body, "htmx:eventFilter:error", { error: e, source: o }); return true; } } return false; } function gt(l, c, e, u, f) { const a = oe(l); let t; if (u.from) { t = m(l, u.from); } else { t = [l]; } if (u.changed) { if (!("lastValue" in a)) { a.lastValue = new WeakMap(); } t.forEach(function (e) { if (!a.lastValue.has(u)) { a.lastValue.set(u, new WeakMap()); } a.lastValue.get(u).set(e, e.value); }); } ie(t, function (i) { const s = function (e) { if (!se(l)) { i.removeEventListener(u.trigger, s); return; } if (dt(l, e)) { return; } if (f || ht(e, l)) { e.preventDefault(); } if (pt(u, l, e)) { return; } const t = oe(e); t.triggerSpec = u; if (t.handledFor == null) { t.handledFor = []; } if (t.handledFor.indexOf(l) < 0) { t.handledFor.push(l); if (u.consume) { e.stopPropagation(); } if (u.target && e.target) { if (!h(ce(e.target), u.target)) { return; } } if (u.once) { if (a.triggeredOnce) { return; } else { a.triggeredOnce = true; } } if (u.changed) { const n = e.target; const r = n.value; const o = a.lastValue.get(u); if (o.has(n) && o.get(n) === r) { return; } o.set(n, r); } if (a.delayed) { clearTimeout(a.delayed); } if (a.throttle) { return; } if (u.throttle > 0) { if (!a.throttle) { ae(l, "htmx:trigger"); c(l, e); a.throttle = b().setTimeout(function () { a.throttle = null; }, u.throttle); } } else if (u.delay > 0) { a.delayed = b().setTimeout(function () { ae(l, "htmx:trigger"); c(l, e); }, u.delay); } else { ae(l, "htmx:trigger"); c(l, e); } } }; if (e.listenerInfos == null) { e.listenerInfos = []; } e.listenerInfos.push({ trigger: u.trigger, listener: s, on: i }); i.addEventListener(u.trigger, s); }); } let mt = false; let yt = null; function xt() { if (!yt) { yt = function () { mt = true; }; window.addEventListener("scroll", yt); window.addEventListener("resize", yt); setInterval(function () { if (mt) { mt = false; ie( te().querySelectorAll( "[hx-trigger*='revealed'],[data-hx-trigger*='revealed']", ), function (e) { bt(e); }, ); } }, 200); } } function bt(e) { if (!s(e, "data-hx-revealed") && F(e)) { e.setAttribute("data-hx-revealed", "true"); const t = oe(e); if (t.initHash) { ae(e, "revealed"); } else { e.addEventListener( "htmx:afterProcessNode", function () { ae(e, "revealed"); }, { once: true }, ); } } } function vt(e, t, n, r) { const o = function () { if (!n.loaded) { n.loaded = true; ae(e, "htmx:trigger"); t(e); } }; if (r > 0) { b().setTimeout(o, r); } else { o(); } } function wt(t, n, e) { let i = false; ie(de, function (r) { if (s(t, "hx-" + r)) { const o = a(t, "hx-" + r); i = true; n.path = o; n.verb = r; e.forEach(function (e) { St(t, e, n, function (e, t) { const n = ce(e); if (ft(n)) { S(n); return; } he(r, o, n, t); }); }); } }); return i; } function St(r, e, t, n) { if (e.trigger === "revealed") { xt(); gt(r, n, t, e); bt(ce(r)); } else if (e.trigger === "intersect") { const o = {}; if (e.root) { o.root = ue(r, e.root); } if (e.threshold) { o.threshold = parseFloat(e.threshold); } const i = new IntersectionObserver(function (t) { for (let e = 0; e < t.length; e++) { const n = t[e]; if (n.isIntersecting) { ae(r, "intersect"); break; } } }, o); i.observe(ce(r)); gt(ce(r), n, t, e); } else if (!t.firstInitCompleted && e.trigger === "load") { if (!pt(e, r, Bt("load", { elt: r }))) { vt(ce(r), n, t, e.delay); } } else if (e.pollInterval > 0) { t.polling = true; ct(ce(r), n, e); } else { gt(r, n, t, e); } } function Et(e) { const t = ce(e); if (!t) { return false; } const n = t.attributes; for (let e = 0; e < n.length; e++) { const r = n[e].name; if ( l(r, "hx-on:") || l(r, "data-hx-on:") || l(r, "hx-on-") || l(r, "data-hx-on-") ) { return true; } } return false; } const Ct = new XPathEvaluator().createExpression( './/*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or' + ' starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]', ); function Ot(e, t) { if (Et(e)) { t.push(ce(e)); } const n = Ct.evaluate(e); let r = null; while ((r = n.iterateNext())) t.push(ce(r)); } function Rt(e) { const t = []; if (e instanceof DocumentFragment) { for (const n of e.childNodes) { Ot(n, t); } } else { Ot(e, t); } return t; } function Ht(e) { if (e.querySelectorAll) { const n = ", [hx-boost] a, [data-hx-boost] a, a[hx-boost], a[data-hx-boost]"; const r = []; for (const i in Vn) { const s = Vn[i]; if (s.getSelectors) { var t = s.getSelectors(); if (t) { r.push(t); } } } const o = e.querySelectorAll( T + n + ", form, [type='submit']," + " [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger]" + r .flat() .map((e) => ", " + e) .join(""), ); return o; } else { return []; } } function Tt(e) { const t = At(e.target); const n = Nt(e); if (n) { n.lastButtonClicked = t; } } function qt(e) { const t = Nt(e); if (t) { t.lastButtonClicked = null; } } function At(e) { return g(ce(e), "button, input[type='submit']"); } function Lt(e) { return e.form || g(e, "form"); } function Nt(e) { const t = At(e.target); if (!t) { return; } const n = Lt(t); return oe(n); } function It(e) { e.addEventListener("click", Tt); e.addEventListener("focusin", Tt); e.addEventListener("focusout", qt); } function Pt(t, e, n) { const r = oe(t); if (!Array.isArray(r.onHandlers)) { r.onHandlers = []; } let o; const i = function (e) { On(t, function () { if (ft(t)) { return; } if (!o) { o = new Function("event", n); } o.call(t, e); }); }; t.addEventListener(e, i); r.onHandlers.push({ event: e, listener: i }); } function Dt(t) { De(t); for (let e = 0; e < t.attributes.length; e++) { const n = t.attributes[e].name; const r = t.attributes[e].value; if (l(n, "hx-on") || l(n, "data-hx-on")) { const o = n.indexOf("-on") + 3; const i = n.slice(o, o + 1); if (i === "-" || i === ":") { let e = n.slice(o + 1); if (l(e, ":")) { e = "htmx" + e; } else if (l(e, "-")) { e = "htmx:" + e.slice(1); } else if (l(e, "htmx-")) { e = "htmx:" + e.slice(5); } Pt(t, e, r); } } } } function kt(t) { ae(t, "htmx:beforeProcessNode"); const n = oe(t); const e = st(t); const r = wt(t, n, e); if (!r) { if (ne(t, "hx-boost") === "true") { at(t, n, e); } else if (s(t, "hx-trigger")) { e.forEach(function (e) { St(t, e, n, function () {}); }); } } if (t.tagName === "FORM" || (ee(t, "type") === "submit" && s(t, "form"))) { It(t); } n.firstInitCompleted = true; ae(t, "htmx:afterProcessNode"); } function Mt(e) { if (!(e instanceof Element)) { return false; } const t = oe(e); const n = Pe(e); if (t.initHash !== n) { ke(e); t.initHash = n; return true; } return false; } function Ft(e) { e = w(e); if (ft(e)) { S(e); return; } const t = []; if (Mt(e)) { t.push(e); } ie(Ht(e), function (e) { if (ft(e)) { S(e); return; } if (Mt(e)) { t.push(e); } }); ie(Rt(e), Dt); ie(t, kt); } function Xt(e) { return e.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(); } function Bt(e, t) { return new CustomEvent(e, { bubbles: true, cancelable: true, composed: true, detail: t, }); } function fe(e, t, n) { ae(e, t, le({ error: t }, n)); } function Ut(e) { return e === "htmx:afterProcessNode"; } function jt(e, t, n) { ie(Jn(e, [], n), function (e) { try { t(e); } catch (e) { R(e); } }); } function R(e) { console.error(e); } function ae(e, t, n) { e = w(e); if (n == null) { n = {}; } n.elt = e; const r = Bt(t, n); if (Q.logger && !Ut(t)) { Q.logger(e, t, n); } if (n.error) { R(n.error); ae(e, "htmx:error", { errorInfo: n }); } let o = e.dispatchEvent(r); const i = Xt(t); if (o && i !== t) { const s = Bt(i, r.detail); o = o && e.dispatchEvent(s); } jt(ce(e), function (e) { o = o && e.onEvent(t, r) !== false && !r.defaultPrevented; }); return o; } let Vt = location.pathname + location.search; function _t(e) { Vt = e; if (B()) { sessionStorage.setItem("htmx-current-path-for-history", e); } } function zt() { const e = te().querySelector("[hx-history-elt],[data-hx-history-elt]"); return e || te().body; } function $t(t, e) { if (!B()) { return; } const n = Kt(e); const r = te().title; const o = window.scrollY; if (Q.config.historyCacheSize <= 0) { sessionStorage.removeItem("htmx-history-cache"); return; } t = U(t); const i = v(sessionStorage.getItem("htmx-history-cache")) || []; for (let e = 0; e < i.length; e++) { if (i[e].url === t) { i.splice(e, 1); break; } } const s = { url: t, content: n, title: r, scroll: o }; ae(te().body, "htmx:historyItemCreated", { item: s, cache: i }); i.push(s); while (i.length > Q.config.historyCacheSize) { i.shift(); } while (i.length > 0) { try { sessionStorage.setItem("htmx-history-cache", JSON.stringify(i)); break; } catch (e) { fe(te().body, "htmx:historyCacheError", { cause: e, cache: i }); i.shift(); } } } function Jt(t) { if (!B()) { return null; } t = U(t); const n = v(sessionStorage.getItem("htmx-history-cache")) || []; for (let e = 0; e < n.length; e++) { if (n[e].url === t) { return n[e]; } } return null; } function Kt(e) { const t = Q.config.requestClass; const n = e.cloneNode(true); ie(x(n, "." + t), function (e) { G(e, t); }); ie(x(n, "[data-disabled-by-htmx]"), function (e) { e.removeAttribute("disabled"); }); return n.innerHTML; } function Gt() { const e = zt(); let t = Vt; if (B()) { t = sessionStorage.getItem("htmx-current-path-for-history"); } t = t || location.pathname + location.search; const n = te().querySelector( '[hx-history="false" i],[data-hx-history="false" i]', ); if (!n) { ae(te().body, "htmx:beforeHistorySave", { path: t, historyElt: e }); $t(t, e); } if (Q.config.historyEnabled) history.replaceState({ htmx: true }, te().title, location.href); } function Wt(e) { if (Q.config.getCacheBusterParam) { e = e.replace(/org\.htmx\.cache-buster=[^&]*&?/, ""); if (Y(e, "&") || Y(e, "?")) { e = e.slice(0, -1); } } if (Q.config.historyEnabled) { history.pushState({ htmx: true }, "", e); } _t(e); } function Zt(e) { if (Q.config.historyEnabled) history.replaceState({ htmx: true }, "", e); _t(e); } function Yt(e) { ie(e, function (e) { e.call(undefined); }); } function Qt(e) { const t = new XMLHttpRequest(); const n = { swapStyle: "innerHTML", swapDelay: 0, settleDelay: 0 }; const r = { path: e, xhr: t, historyElt: zt(), swapSpec: n }; t.open("GET", e, true); if (Q.config.historyRestoreAsHxRequest) { t.setRequestHeader("HX-Request", "true"); } t.setRequestHeader("HX-History-Restore-Request", "true"); t.setRequestHeader("HX-Current-URL", location.href); t.onload = function () { if (this.status >= 200 && this.status < 400) { r.response = this.response; ae(te().body, "htmx:historyCacheMissLoad", r); $e(r.historyElt, r.response, n, { contextElement: r.historyElt, historyRequest: true, }); _t(r.path); ae(te().body, "htmx:historyRestore", { path: e, cacheMiss: true, serverResponse: r.response, }); } else { fe(te().body, "htmx:historyCacheMissLoadError", r); } }; if (ae(te().body, "htmx:historyCacheMiss", r)) { t.send(); } } function en(e) { Gt(); e = e || location.pathname + location.search; const t = Jt(e); if (t) { const n = { swapStyle: "innerHTML", swapDelay: 0, settleDelay: 0, scroll: t.scroll, }; const r = { path: e, item: t, historyElt: zt(), swapSpec: n }; if (ae(te().body, "htmx:historyCacheHit", r)) { $e(r.historyElt, t.content, n, { contextElement: r.historyElt, title: t.title, }); _t(r.path); ae(te().body, "htmx:historyRestore", r); } } else { if (Q.config.refreshOnHistoryMiss) { Q.location.reload(true); } else { Qt(e); } } } function tn(e) { let t = we(e, "hx-indicator"); if (t == null) { t = [e]; } ie(t, function (e) { const t = oe(e); t.requestCount = (t.requestCount || 0) + 1; e.classList.add.call(e.classList, Q.config.requestClass); }); return t; } function nn(e) { let t = we(e, "hx-disabled-elt"); if (t == null) { t = []; } ie(t, function (e) { const t = oe(e); t.requestCount = (t.requestCount || 0) + 1; e.setAttribute("disabled", ""); e.setAttribute("data-disabled-by-htmx", ""); }); return t; } function rn(e, t) { ie(e.concat(t), function (e) { const t = oe(e); t.requestCount = (t.requestCount || 1) - 1; }); ie(e, function (e) { const t = oe(e); if (t.requestCount === 0) { e.classList.remove.call(e.classList, Q.config.requestClass); } }); ie(t, function (e) { const t = oe(e); if (t.requestCount === 0) { e.removeAttribute("disabled"); e.removeAttribute("data-disabled-by-htmx"); } }); } function on(t, n) { for (let e = 0; e < t.length; e++) { const r = t[e]; if (r.isSameNode(n)) { return true; } } return false; } function sn(e) { const t = e; if ( t.name === "" || t.name == null || t.disabled || g(t, "fieldset[disabled]") ) { return false; } if ( t.type === "button" || t.type === "submit" || t.tagName === "image" || t.tagName === "reset" || t.tagName === "file" ) { return false; } if (t.type === "checkbox" || t.type === "radio") { return t.checked; } return true; } function ln(t, e, n) { if (t != null && e != null) { if (Array.isArray(e)) { e.forEach(function (e) { n.append(t, e); }); } else { n.append(t, e); } } } function cn(t, n, r) { if (t != null && n != null) { let e = r.getAll(t); if (Array.isArray(n)) { e = e.filter((e) => n.indexOf(e) < 0); } else { e = e.filter((e) => e !== n); } r.delete(t); ie(e, (e) => r.append(t, e)); } } function un(e) { if (e instanceof HTMLSelectElement && e.multiple) { return M(e.querySelectorAll("option:checked")).map(function (e) { return e.value; }); } if (e instanceof HTMLInputElement && e.files) { return M(e.files); } return e.value; } function fn(t, n, r, e, o) { if (e == null || on(t, e)) { return; } else { t.push(e); } if (sn(e)) { const i = ee(e, "name"); ln(i, un(e), n); if (o) { an(e, r); } } if (e instanceof HTMLFormElement) { ie(e.elements, function (e) { if (t.indexOf(e) >= 0) { cn(e.name, un(e), n); } else { t.push(e); } if (o) { an(e, r); } }); new FormData(e).forEach(function (e, t) { if (e instanceof File && e.name === "") { return; } ln(t, e, n); }); } } function an(e, t) { const n = e; if (n.willValidate) { ae(n, "htmx:validation:validate"); if (!n.checkValidity()) { t.push({ elt: n, message: n.validationMessage, validity: n.validity }); ae(n, "htmx:validation:failed", { message: n.validationMessage, validity: n.validity, }); } } } function hn(n, e) { for (const t of e.keys()) { n.delete(t); } e.forEach(function (e, t) { n.append(t, e); }); return n; } function dn(e, t) { const n = []; const r = new FormData(); const o = new FormData(); const i = []; const s = oe(e); if (s.lastButtonClicked && !se(s.lastButtonClicked)) { s.lastButtonClicked = null; } let l = (e instanceof HTMLFormElement && e.noValidate !== true) || a(e, "hx-validate") === "true"; if (s.lastButtonClicked) { l = l && s.lastButtonClicked.formNoValidate !== true; } if (t !== "get") { fn(n, o, i, Lt(e), l); } fn(n, r, i, e, l); if ( s.lastButtonClicked || e.tagName === "BUTTON" || (e.tagName === "INPUT" && ee(e, "type") === "submit") ) { const u = s.lastButtonClicked || e; const f = ee(u, "name"); ln(f, u.value, o); } const c = we(e, "hx-include"); ie(c, function (e) { fn(n, r, i, ce(e), l); if (!h(e, "form")) { ie(p(e).querySelectorAll(ot), function (e) { fn(n, r, i, e, l); }); } }); hn(r, o); return { errors: i, formData: r, values: kn(r) }; } function pn(e, t, n) { if (e !== "") { e += "&"; } if (String(n) === "[object Object]") { n = JSON.stringify(n); } const r = encodeURIComponent(n); e += encodeURIComponent(t) + "=" + r; return e; } function gn(e) { e = Pn(e); let n = ""; e.forEach(function (e, t) { n = pn(n, t, e); }); return n; } function mn(e, t, n) { const r = { "HX-Request": "true", "HX-Trigger": ee(e, "id"), "HX-Trigger-Name": ee(e, "name"), "HX-Target": a(t, "id"), "HX-Current-URL": location.href, }; Cn(e, "hx-headers", false, r); if (n !== undefined) { r["HX-Prompt"] = n; } if (oe(e).boosted) { r["HX-Boosted"] = "true"; } return r; } function yn(n, e) { const t = ne(e, "hx-params"); if (t) { if (t === "none") { return new FormData(); } else if (t === "*") { return n; } else if (t.indexOf("not ") === 0) { ie(t.slice(4).split(","), function (e) { e = e.trim(); n.delete(e); }); return n; } else { const r = new FormData(); ie(t.split(","), function (t) { t = t.trim(); if (n.has(t)) { n.getAll(t).forEach(function (e) { r.append(t, e); }); } }); return r; } } else { return n; } } function xn(e) { return !!ee(e, "href") && ee(e, "href").indexOf("#") >= 0; } function bn(e, t) { const n = t || ne(e, "hx-swap"); const r = { swapStyle: oe(e).boosted ? "innerHTML" : Q.config.defaultSwapStyle, swapDelay: Q.config.defaultSwapDelay, settleDelay: Q.config.defaultSettleDelay, }; if (Q.config.scrollIntoViewOnBoost && oe(e).boosted && !xn(e)) { r.show = "top"; } if (n) { const s = X(n); if (s.length > 0) { for (let e = 0; e < s.length; e++) { const l = s[e]; if (l.indexOf("swap:") === 0) { r.swapDelay = d(l.slice(5)); } else if (l.indexOf("settle:") === 0) { r.settleDelay = d(l.slice(7)); } else if (l.indexOf("transition:") === 0) { r.transition = l.slice(11) === "true"; } else if (l.indexOf("ignoreTitle:") === 0) { r.ignoreTitle = l.slice(12) === "true"; } else if (l.indexOf("scroll:") === 0) { const c = l.slice(7); var o = c.split(":"); const u = o.pop(); var i = o.length > 0 ? o.join(":") : null; r.scroll = u; r.scrollTarget = i; } else if (l.indexOf("show:") === 0) { const f = l.slice(5); var o = f.split(":"); const a = o.pop(); var i = o.length > 0 ? o.join(":") : null; r.show = a; r.showTarget = i; } else if (l.indexOf("focus-scroll:") === 0) { const h = l.slice("focus-scroll:".length); r.focusScroll = h == "true"; } else if (e == 0) { r.swapStyle = l; } else { R("Unknown modifier in hx-swap: " + l); } } } } return r; } function vn(e) { return ( ne(e, "hx-encoding") === "multipart/form-data" || (h(e, "form") && ee(e, "enctype") === "multipart/form-data") ); } function wn(t, n, r) { let o = null; jt(n, function (e) { if (o == null) { o = e.encodeParameters(t, r, n); } }); if (o != null) { return o; } else { if (vn(n)) { return hn(new FormData(), Pn(r)); } else { return gn(r); } } } function Sn(e) { return { tasks: [], elts: [e] }; } function En(e, t) { const n = e[0]; const r = e[e.length - 1]; if (t.scroll) { var o = null; if (t.scrollTarget) { o = ce(ue(n, t.scrollTarget)); } if (t.scroll === "top" && (n || o)) { o = o || n; o.scrollTop = 0; } if (t.scroll === "bottom" && (r || o)) { o = o || r; o.scrollTop = o.scrollHeight; } if (typeof t.scroll === "number") { b().setTimeout(function () { window.scrollTo(0, t.scroll); }, 0); } } if (t.show) { var o = null; if (t.showTarget) { let e = t.showTarget; if (t.showTarget === "window") { e = "body"; } o = ce(ue(n, e)); } if (t.show === "top" && (n || o)) { o = o || n; o.scrollIntoView({ block: "start", behavior: Q.config.scrollBehavior }); } if (t.show === "bottom" && (r || o)) { o = o || r; o.scrollIntoView({ block: "end", behavior: Q.config.scrollBehavior }); } } } function Cn(r, e, o, i, s) { if (i == null) { i = {}; } if (r == null) { return i; } const l = a(r, e); if (l) { let e = l.trim(); let t = o; if (e === "unset") { return null; } if (e.indexOf("javascript:") === 0) { e = e.slice(11); t = true; } else if (e.indexOf("js:") === 0) { e = e.slice(3); t = true; } if (e.indexOf("{") !== 0) { e = "{" + e + "}"; } let n; if (t) { n = On( r, function () { if (s) { return Function("event", "return (" + e + ")").call(r, s); } else { return Function("return (" + e + ")").call(r); } }, {}, ); } else { n = v(e); } for (const c in n) { if (n.hasOwnProperty(c)) { if (i[c] == null) { i[c] = n[c]; } } } } return Cn(ce(u(r)), e, o, i, s); } function On(e, t, n) { if (Q.config.allowEval) { return t(); } else { fe(e, "htmx:evalDisallowedError"); return n; } } function Rn(e, t, n) { return Cn(e, "hx-vars", true, n, t); } function Hn(e, t, n) { return Cn(e, "hx-vals", false, n, t); } function Tn(e, t) { return le(Rn(e, t), Hn(e, t)); } function qn(t, n, r) { if (r !== null) { try { t.setRequestHeader(n, r); } catch (e) { t.setRequestHeader(n, encodeURIComponent(r)); t.setRequestHeader(n + "-URI-AutoEncoded", "true"); } } } function An(t) { if (t.responseURL) { try { const e = new URL(t.responseURL); return e.pathname + e.search; } catch (e) { fe(te().body, "htmx:badResponseUrl", { url: t.responseURL }); } } } function H(e, t) { return t.test(e.getAllResponseHeaders()); } function Ln(t, n, r) { t = t.toLowerCase(); if (r) { if (r instanceof Element || typeof r === "string") { return he(t, n, null, null, { targetOverride: w(r) || ve, returnPromise: true, }); } else { let e = w(r.target); if ((r.target && !e) || (r.source && !e && !w(r.source))) { e = ve; } return he(t, n, w(r.source), r.event, { handler: r.handler, headers: r.headers, values: r.values, targetOverride: e, swapOverride: r.swap, select: r.select, returnPromise: true, }); } } else { return he(t, n, null, null, { returnPromise: true }); } } function Nn(e) { const t = []; while (e) { t.push(e); e = e.parentElement; } return t; } function In(e, t, n) { const r = new URL( t, location.protocol !== "about:" ? location.href : window.origin, ); const o = location.protocol !== "about:" ? location.origin : window.origin; const i = o === r.origin; if (Q.config.selfRequestsOnly) { if (!i) { return false; } } return ae(e, "htmx:validateUrl", le({ url: r, sameHost: i }, n)); } function Pn(e) { if (e instanceof FormData) return e; const t = new FormData(); for (const n in e) { if (e.hasOwnProperty(n)) { if (e[n] && typeof e[n].forEach === "function") { e[n].forEach(function (e) { t.append(n, e); }); } else if (typeof e[n] === "object" && !(e[n] instanceof Blob)) { t.append(n, JSON.stringify(e[n])); } else { t.append(n, e[n]); } } } return t; } function Dn(r, o, e) { return new Proxy(e, { get: function (t, e) { if (typeof e === "number") return t[e]; if (e === "length") return t.length; if (e === "push") { return function (e) { t.push(e); r.append(o, e); }; } if (typeof t[e] === "function") { return function () { t[e].apply(t, arguments); r.delete(o); t.forEach(function (e) { r.append(o, e); }); }; } if (t[e] && t[e].length === 1) { return t[e][0]; } else { return t[e]; } }, set: function (e, t, n) { e[t] = n; r.delete(o); e.forEach(function (e) { r.append(o, e); }); return true; }, }); } function kn(o) { return new Proxy(o, { get: function (e, t) { if (typeof t === "symbol") { const r = Reflect.get(e, t); if (typeof r === "function") { return function () { return r.apply(o, arguments); }; } else { return r; } } if (t === "toJSON") { return () => Object.fromEntries(o); } if (t in e) { if (typeof e[t] === "function") { return function () { return o[t].apply(o, arguments); }; } } const n = o.getAll(t); if (n.length === 0) { return undefined; } else if (n.length === 1) { return n[0]; } else { return Dn(e, t, n); } }, set: function (t, n, e) { if (typeof n !== "string") { return false; } t.delete(n); if (e && typeof e.forEach === "function") { e.forEach(function (e) { t.append(n, e); }); } else if (typeof e === "object" && !(e instanceof Blob)) { t.append(n, JSON.stringify(e)); } else { t.append(n, e); } return true; }, deleteProperty: function (e, t) { if (typeof t === "string") { e.delete(t); } return true; }, ownKeys: function (e) { return Reflect.ownKeys(Object.fromEntries(e)); }, getOwnPropertyDescriptor: function (e, t) { return Reflect.getOwnPropertyDescriptor(Object.fromEntries(e), t); }, }); } function he(t, n, r, o, i, k) { let s = null; let l = null; i = i != null ? i : {}; if (i.returnPromise && typeof Promise !== "undefined") { var e = new Promise(function (e, t) { s = e; l = t; }); } if (r == null) { r = te().body; } const M = i.handler || jn; const F = i.select || null; if (!se(r)) { re(s); return e; } const c = i.targetOverride || ce(Ee(r)); if (c == null || c == ve) { fe(r, "htmx:targetError", { target: ne(r, "hx-target") }); re(l); return e; } let u = oe(r); const f = u.lastButtonClicked; if (f) { const A = ee(f, "formaction"); if (A != null) { n = A; } const L = ee(f, "formmethod"); if (L != null) { if (de.includes(L.toLowerCase())) { t = L; } else { re(s); return e; } } } const a = ne(r, "hx-confirm"); if (k === undefined) { const K = function (e) { return he(t, n, r, o, i, !!e); }; const G = { target: c, elt: r, path: n, verb: t, triggeringEvent: o, etc: i, issueRequest: K, question: a, }; if (ae(r, "htmx:confirm", G) === false) { re(s); return e; } } let h = r; let d = ne(r, "hx-sync"); let p = null; let X = false; if (d) { const N = d.split(":"); const I = N[0].trim(); if (I === "this") { h = Se(r, "hx-sync"); } else { h = ce(ue(r, I)); } d = (N[1] || "drop").trim(); u = oe(h); if (d === "drop" && u.xhr && u.abortable !== true) { re(s); return e; } else if (d === "abort") { if (u.xhr) { re(s); return e; } else { X = true; } } else if (d === "replace") { ae(h, "htmx:abort"); } else if (d.indexOf("queue") === 0) { const W = d.split(" "); p = (W[1] || "last").trim(); } } if (u.xhr) { if (u.abortable) { ae(h, "htmx:abort"); } else { if (p == null) { if (o) { const P = oe(o); if (P && P.triggerSpec && P.triggerSpec.queue) { p = P.triggerSpec.queue; } } if (p == null) { p = "last"; } } if (u.queuedRequests == null) { u.queuedRequests = []; } if (p === "first" && u.queuedRequests.length === 0) { u.queuedRequests.push(function () { he(t, n, r, o, i); }); } else if (p === "all") { u.queuedRequests.push(function () { he(t, n, r, o, i); }); } else if (p === "last") { u.queuedRequests = []; u.queuedRequests.push(function () { he(t, n, r, o, i); }); } re(s); return e; } } const g = new XMLHttpRequest(); u.xhr = g; u.abortable = X; const m = function () { u.xhr = null; u.abortable = false; if (u.queuedRequests != null && u.queuedRequests.length > 0) { const e = u.queuedRequests.shift(); e(); } }; const B = ne(r, "hx-prompt"); if (B) { var y = prompt(B); if (y === null || !ae(r, "htmx:prompt", { prompt: y, target: c })) { re(s); m(); return e; } } if (a && !k) { if (!confirm(a)) { re(s); m(); return e; } } let x = mn(r, c, y); if (t !== "get" && !vn(r)) { x["Content-Type"] = "application/x-www-form-urlencoded"; } if (i.headers) { x = le(x, i.headers); } const U = dn(r, t); let b = U.errors; const j = U.formData; if (i.values) { hn(j, Pn(i.values)); } const V = Pn(Tn(r, o)); const v = hn(j, V); let w = yn(v, r); if (Q.config.getCacheBusterParam && t === "get") { w.set("org.htmx.cache-buster", ee(c, "id") || "true"); } if (n == null || n === "") { n = location.href; } const S = Cn(r, "hx-request"); const _ = oe(r).boosted; let E = Q.config.methodsThatUseUrlParams.indexOf(t) >= 0; const C = { boosted: _, useUrlParams: E, formData: w, parameters: kn(w), unfilteredFormData: v, unfilteredParameters: kn(v), headers: x, elt: r, target: c, verb: t, errors: b, withCredentials: i.credentials || S.credentials || Q.config.withCredentials, timeout: i.timeout || S.timeout || Q.config.timeout, path: n, triggeringEvent: o, }; if (!ae(r, "htmx:configRequest", C)) { re(s); m(); return e; } n = C.path; t = C.verb; x = C.headers; w = Pn(C.parameters); b = C.errors; E = C.useUrlParams; if (b && b.length > 0) { ae(r, "htmx:validation:halted", C); re(s); m(); return e; } const z = n.split("#"); const $ = z[0]; const O = z[1]; let R = n; if (E) { R = $; const Z = !w.keys().next().done; if (Z) { if (R.indexOf("?") < 0) { R += "?"; } else { R += "&"; } R += gn(w); if (O) { R += "#" + O; } } } if (!In(r, R, C)) { fe(r, "htmx:invalidPath", C); re(l); m(); return e; } g.open(t.toUpperCase(), R, true); g.overrideMimeType("text/html"); g.withCredentials = C.withCredentials; g.timeout = C.timeout; if (S.noHeaders) { } else { for (const D in x) { if (x.hasOwnProperty(D)) { const Y = x[D]; qn(g, D, Y); } } } const H = { xhr: g, target: c, requestConfig: C, etc: i, boosted: _, select: F, pathInfo: { requestPath: n, finalRequestPath: R, responsePath: null, anchor: O, }, }; g.onload = function () { try { const t = Nn(r); H.pathInfo.responsePath = An(g); M(r, H); if (H.keepIndicators !== true) { rn(T, q); } ae(r, "htmx:afterRequest", H); ae(r, "htmx:afterOnLoad", H); if (!se(r)) { let e = null; while (t.length > 0 && e == null) { const n = t.shift(); if (se(n)) { e = n; } } if (e) { ae(e, "htmx:afterRequest", H); ae(e, "htmx:afterOnLoad", H); } } re(s); } catch (e) { fe(r, "htmx:onLoadError", le({ error: e }, H)); throw e; } finally { m(); } }; g.onerror = function () { rn(T, q); fe(r, "htmx:afterRequest", H); fe(r, "htmx:sendError", H); re(l); m(); }; g.onabort = function () { rn(T, q); fe(r, "htmx:afterRequest", H); fe(r, "htmx:sendAbort", H); re(l); m(); }; g.ontimeout = function () { rn(T, q); fe(r, "htmx:afterRequest", H); fe(r, "htmx:timeout", H); re(l); m(); }; if (!ae(r, "htmx:beforeRequest", H)) { re(s); m(); return e; } var T = tn(r); var q = nn(r); ie(["loadstart", "loadend", "progress", "abort"], function (t) { ie([g, g.upload], function (e) { e.addEventListener(t, function (e) { ae(r, "htmx:xhr:" + t, { lengthComputable: e.lengthComputable, loaded: e.loaded, total: e.total, }); }); }); }); ae(r, "htmx:beforeSend", H); const J = E ? null : wn(g, r, w); g.send(J); return e; } function Mn(e, t) { const n = t.xhr; let r = null; let o = null; if (H(n, /HX-Push:/i)) { r = n.getResponseHeader("HX-Push"); o = "push"; } else if (H(n, /HX-Push-Url:/i)) { r = n.getResponseHeader("HX-Push-Url"); o = "push"; } else if (H(n, /HX-Replace-Url:/i)) { r = n.getResponseHeader("HX-Replace-Url"); o = "replace"; } if (r) { if (r === "false") { return {}; } else { return { type: o, path: r }; } } const i = t.pathInfo.finalRequestPath; const s = t.pathInfo.responsePath; const l = ne(e, "hx-push-url"); const c = ne(e, "hx-replace-url"); const u = oe(e).boosted; let f = null; let a = null; if (l) { f = "push"; a = l; } else if (c) { f = "replace"; a = c; } else if (u) { f = "push"; a = s || i; } if (a) { if (a === "false") { return {}; } if (a === "true") { a = s || i; } if (t.pathInfo.anchor && a.indexOf("#") === -1) { a = a + "#" + t.pathInfo.anchor; } return { type: f, path: a }; } else { return {}; } } function Fn(e, t) { var n = new RegExp(e.code); return n.test(t.toString(10)); } function Xn(e) { for (var t = 0; t < Q.config.responseHandling.length; t++) { var n = Q.config.responseHandling[t]; if (Fn(n, e.status)) { return n; } } return { swap: false }; } function Bn(e) { if (e) { const t = f("title"); if (t) { t.textContent = e; } else { window.document.title = e; } } } function Un(e, t) { if (t === "this") { return e; } const n = ce(ue(e, t)); if (n == null) { fe(e, "htmx:targetError", { target: t }); throw new Error(`Invalid re-target ${t}`); } return n; } function jn(t, e) { const n = e.xhr; let r = e.target; const o = e.etc; const i = e.select; if (!ae(t, "htmx:beforeOnLoad", e)) return; if (H(n, /HX-Trigger:/i)) { Je(n, "HX-Trigger", t); } if (H(n, /HX-Location:/i)) { Gt(); let e = n.getResponseHeader("HX-Location"); var s; if (e.indexOf("{") === 0) { s = v(e); e = s.path; delete s.path; } Ln("get", e, s).then(function () { Wt(e); }); return; } const l = H(n, /HX-Refresh:/i) && n.getResponseHeader("HX-Refresh") === "true"; if (H(n, /HX-Redirect:/i)) { e.keepIndicators = true; Q.location.href = n.getResponseHeader("HX-Redirect"); l && Q.location.reload(); return; } if (l) { e.keepIndicators = true; Q.location.reload(); return; } const c = Mn(t, e); const u = Xn(n); const f = u.swap; let a = !!u.error; let h = Q.config.ignoreTitle || u.ignoreTitle; let d = u.select; if (u.target) { e.target = Un(t, u.target); } var p = o.swapOverride; if (p == null && u.swapOverride) { p = u.swapOverride; } if (H(n, /HX-Retarget:/i)) { e.target = Un(t, n.getResponseHeader("HX-Retarget")); } if (H(n, /HX-Reswap:/i)) { p = n.getResponseHeader("HX-Reswap"); } var g = n.response; var m = le( { shouldSwap: f, serverResponse: g, isError: a, ignoreTitle: h, selectOverride: d, swapOverride: p, }, e, ); if (u.event && !ae(r, u.event, m)) return; if (!ae(r, "htmx:beforeSwap", m)) return; r = m.target; g = m.serverResponse; a = m.isError; h = m.ignoreTitle; d = m.selectOverride; p = m.swapOverride; e.target = r; e.failed = a; e.successful = !a; if (m.shouldSwap) { if (n.status === 286) { lt(t); } jt(t, function (e) { g = e.transformResponse(g, n, t); }); if (c.type) { Gt(); } var y = bn(t, p); if (!y.hasOwnProperty("ignoreTitle")) { y.ignoreTitle = h; } r.classList.add(Q.config.swappingClass); if (i) { d = i; } if (H(n, /HX-Reselect:/i)) { d = n.getResponseHeader("HX-Reselect"); } const x = ne(t, "hx-select-oob"); const b = ne(t, "hx-select"); $e(r, g, y, { select: d === "unset" ? null : d || b, selectOOB: x, eventInfo: e, anchor: e.pathInfo.anchor, contextElement: t, afterSwapCallback: function () { if (H(n, /HX-Trigger-After-Swap:/i)) { let e = t; if (!se(t)) { e = te().body; } Je(n, "HX-Trigger-After-Swap", e); } }, afterSettleCallback: function () { if (H(n, /HX-Trigger-After-Settle:/i)) { let e = t; if (!se(t)) { e = te().body; } Je(n, "HX-Trigger-After-Settle", e); } }, beforeSwapCallback: function () { if (c.type) { ae(te().body, "htmx:beforeHistoryUpdate", le({ history: c }, e)); if (c.type === "push") { Wt(c.path); ae(te().body, "htmx:pushedIntoHistory", { path: c.path }); } else { Zt(c.path); ae(te().body, "htmx:replacedInHistory", { path: c.path }); } } }, }); } if (a) { fe( t, "htmx:responseError", le( { error: "Response Status Error Code " + n.status + " from " + e.pathInfo.requestPath, }, e, ), ); } } const Vn = {}; function _n() { return { init: function (e) { return null; }, getSelectors: function () { return null; }, onEvent: function (e, t) { return true; }, transformResponse: function (e, t, n) { return e; }, isInlineSwap: function (e) { return false; }, handleSwap: function (e, t, n, r) { return false; }, encodeParameters: function (e, t, n) { return null; }, }; } function zn(e, t) { if (t.init) { t.init(n); } Vn[e] = le(_n(), t); } function $n(e) { delete Vn[e]; } function Jn(e, n, r) { if (n == undefined) { n = []; } if (e == undefined) { return n; } if (r == undefined) { r = []; } const t = a(e, "hx-ext"); if (t) { ie(t.split(","), function (e) { e = e.replace(/ /g, ""); if (e.slice(0, 7) == "ignore:") { r.push(e.slice(7)); return; } if (r.indexOf(e) < 0) { const t = Vn[e]; if (t && n.indexOf(t) < 0) { n.push(t); } } }); } return Jn(ce(u(e)), n, r); } var Kn = false; te().addEventListener("DOMContentLoaded", function () { Kn = true; }); function Gn(e) { if (Kn || te().readyState === "complete") { e(); } else { te().addEventListener("DOMContentLoaded", e); } } function Wn() { if (Q.config.includeIndicatorStyles !== false) { const e = Q.config.inlineStyleNonce ? ` nonce="${Q.config.inlineStyleNonce}"` : ""; te().head.insertAdjacentHTML( "beforeend", " ." + Q.config.indicatorClass + "{opacity:0} ." + Q.config.requestClass + " ." + Q.config.indicatorClass + "{opacity:1; transition: opacity 200ms ease-in;} ." + Q.config.requestClass + "." + Q.config.indicatorClass + "{opacity:1; transition: opacity 200ms ease-in;} ", ); } } function Zn() { const e = te().querySelector('meta[name="htmx-config"]'); if (e) { return v(e.content); } else { return null; } } function Yn() { const e = Zn(); if (e) { Q.config = le(Q.config, e); } } Gn(function () { Yn(); Wn(); let e = te().body; Ft(e); const t = te().querySelectorAll( "[hx-trigger='restored'],[data-hx-trigger='restored']", ); e.addEventListener("htmx:abort", function (e) { const t = e.target; const n = oe(t); if (n && n.xhr) { n.xhr.abort(); } }); const n = window.onpopstate ? window.onpopstate.bind(window) : null; window.onpopstate = function (e) { if (e.state && e.state.htmx) { en(); ie(t, function (e) { ae(e, "htmx:restored", { document: te(), triggerEvent: ae }); }); } else { if (n) { n(e); } } }; b().setTimeout(function () { ae(e, "htmx:load", {}); e = null; }, 0); }); return Q; })(); ================================================ FILE: cmd/template/advanced/files/htmx/imports/fiber.tmpl ================================================ "github.com/a-h/templ" "{{.ProjectName}}/cmd/web" "github.com/gofiber/fiber/v2/middleware/adaptor" "github.com/gofiber/fiber/v2/middleware/filesystem" "net/http" ================================================ FILE: cmd/template/advanced/files/htmx/imports/gin.tmpl ================================================ "github.com/a-h/templ" "{{.ProjectName}}/cmd/web" "io/fs" ================================================ FILE: cmd/template/advanced/files/htmx/imports/standard_library.tmpl ================================================ "github.com/a-h/templ" "{{.ProjectName}}/cmd/web" ================================================ FILE: cmd/template/advanced/files/htmx/routes/chi.tmpl ================================================ fileServer := http.FileServer(http.FS(web.Files)) r.Handle("/assets/*", fileServer) r.Get("/web", templ.Handler(web.HelloForm()).ServeHTTP) r.Post("/hello", web.HelloWebHandler) ================================================ FILE: cmd/template/advanced/files/htmx/routes/echo.tmpl ================================================ fileServer := http.FileServer(http.FS(web.Files)) e.GET("/assets/*", echo.WrapHandler(fileServer)) e.GET("/web", echo.WrapHandler(templ.Handler(web.HelloForm()))) e.POST("/hello", echo.WrapHandler(http.HandlerFunc(web.HelloWebHandler))) ================================================ FILE: cmd/template/advanced/files/htmx/routes/fiber.tmpl ================================================ s.App.Use("/assets", filesystem.New(filesystem.Config{ Root: http.FS(web.Files), PathPrefix: "assets", Browse: false, })) s.App.Get("/web", adaptor.HTTPHandler(templ.Handler(web.HelloForm()))) s.App.Post("/hello", func(c *fiber.Ctx) error { return web.HelloWebHandler(c) }) ================================================ FILE: cmd/template/advanced/files/htmx/routes/gin.tmpl ================================================ staticFiles, _ := fs.Sub(web.Files, "assets") r.StaticFS("/assets", http.FS(staticFiles)) r.GET("/web", func(c *gin.Context) { templ.Handler(web.HelloForm()).ServeHTTP(c.Writer, c.Request) }) r.POST("/hello", func(c *gin.Context) { web.HelloWebHandler(c.Writer, c.Request) }) ================================================ FILE: cmd/template/advanced/files/htmx/routes/gorilla.tmpl ================================================ fileServer := http.FileServer(http.FS(web.Files)) r.PathPrefix("/assets/").Handler(fileServer) r.HandleFunc("/web", func(w http.ResponseWriter, r *http.Request) { templ.Handler(web.HelloForm()).ServeHTTP(w, r) }) r.HandleFunc("/hello", web.HelloWebHandler) ================================================ FILE: cmd/template/advanced/files/htmx/routes/http_router.tmpl ================================================ fileServer := http.FileServer(http.FS(web.Files)) r.Handler(http.MethodGet, "/assets/*filepath", fileServer) r.Handler(http.MethodGet, "/web", templ.Handler(web.HelloForm())) r.HandlerFunc(http.MethodPost, "/hello", web.HelloWebHandler) ================================================ FILE: cmd/template/advanced/files/htmx/routes/standard_library.tmpl ================================================ fileServer := http.FileServer(http.FS(web.Files)) mux.Handle("/assets/", fileServer) mux.Handle("/web", templ.Handler(web.HelloForm())) mux.HandleFunc("/hello", web.HelloWebHandler) ================================================ FILE: cmd/template/advanced/files/htmx/tailwind/tailwind.config.js.tmpl ================================================ module.exports = { content: ["./**/*.html", "./**/*.templ", "./**/*.go",], theme: { extend: {}, }, plugins: [], } ================================================ FILE: cmd/template/advanced/files/react/app.tsx.tmpl ================================================ import { useState } from 'react' import reactLogo from './assets/react.svg' import viteLogo from '/vite.svg' import './App.css' function App() { const [count, setCount] = useState(0) const fetchData = () => { fetch(`http://localhost:${import.meta.env.VITE_PORT}/`) .then(response => response.text()) .then(data => setMessage(data)) .catch(error => console.error('Error fetching data:', error)) } const [message, setMessage] = useState('') return ( <>
Vite logo React logo

Vite + React

Edit src/App.tsx and save to test HMR

Click on the Vite and React logos to learn more

{message && (

Server Response:

{message}

)} ) } export default App ================================================ FILE: cmd/template/advanced/files/react/tailwind/app.tsx.tmpl ================================================ import { useState } from 'react' function App() { const [count, setCount] = useState(0) const [message, setMessage] = useState('') const fetchData = () => { fetch(`http://localhost:${import.meta.env.VITE_PORT}/`) .then(response => response.text()) .then(data => setMessage(data)) .catch(error => console.error('Error fetching data:', error)) } return (

Welcome to Vite + React

Get started by editing src/App.tsx

{message && (

Server Response:

{message}

)}
Built with Vite, React, and Tailwind CSS
) } export default App ================================================ FILE: cmd/template/advanced/files/react/tailwind/index.css.tmpl ================================================ @import "tailwindcss"; ================================================ FILE: cmd/template/advanced/files/react/tailwind/vite.config.ts.tmpl ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' // https://vite.dev/config/ export default defineConfig({ plugins: [react(),tailwindcss()], }) ================================================ FILE: cmd/template/advanced/files/tailwind/input.css.tmpl ================================================ @import "tailwindcss" ================================================ FILE: cmd/template/advanced/files/tailwind/output.css.tmpl ================================================ ================================================ FILE: cmd/template/advanced/files/websocket/imports/fiber.tmpl ================================================ "github.com/gofiber/contrib/websocket" ================================================ FILE: cmd/template/advanced/files/websocket/imports/standard_library.tmpl ================================================ "github.com/coder/websocket" ================================================ FILE: cmd/template/advanced/files/workflow/github/github_action_goreleaser.yml.tmpl ================================================ name: goreleaser on: push: tags: - "v*.*.*" permissions: contents: write jobs: goreleaser: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v4 with: go-version: '1.22.x' {{if or ( .AdvancedOptions.htmx ) ( .AdvancedOptions.tailwind )}} - name: Install templ shell: bash run: go install github.com/a-h/templ/cmd/templ@latest - name: Run templ generate shell: bash run: templ generate -path . {{end}} - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser version: ${{"{{"}} env.GITHUB_REF_NAME {{"}}"}} args: release --clean workdir: ./ env: GITHUB_TOKEN: ${{"{{"}} secrets.GITHUB_TOKEN {{"}}"}} ================================================ FILE: cmd/template/advanced/files/workflow/github/github_action_gotest.yml.tmpl ================================================ name: Go-test on: [push, pull_request] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v4 with: go-version: '1.22.x' {{if or ( .AdvancedOptions.htmx ) ( .AdvancedOptions.tailwind )}} - name: Install templ shell: bash run: go install github.com/a-h/templ/cmd/templ@latest - name: Run templ generate shell: bash run: templ generate -path . {{end}} - name: Build run: go build -v ./... - name: Test with the Go CLI run: go test ./... ================================================ FILE: cmd/template/advanced/files/workflow/github/github_action_releaser_config.yml.tmpl ================================================ version: 2 before: hooks: - go mod tidy env: - PACKAGE_PATH=github.com///cmd builds: - binary: "{{"{{"}} .ProjectName {{"}}"}}" main: ./cmd/api goos: - darwin - linux - windows goarch: - amd64 - arm64 env: - CGO_ENABLED=0 ldflags: - -s -w -X {{"{{"}}.Env.PACKAGE_PATH{{"}}"}}={{"{{"}}.Version{{"}}"}} release: prerelease: auto universal_binaries: - replace: true archives: - name_template: > {{"{{"}}- .ProjectName {{"}}"}}_{{"{{"}}- .Version {{"}}"}}_{{"{{"}}- title .Os {{"}}"}}_{{"{{"}}- if eq .Arch "amd64" {{"}}"}}x86_64{{"{{"}}- else if eq .Arch "386" {{"}}"}}i386{{"{{"}}- else {{"}}"}}{{"{{"}} .Arch {{"}}"}}{{"{{"}} end {{"}}"}}{{"{{"}}- if .Arm {{"}}"}}v{{"{{"}} .Arm {{"}}"}}{{"{{"}} end -{{"}}"}} format_overrides: - goos: windows format: zip builds_info: group: root owner: root files: - README.md checksum: name_template: 'checksums.txt' ================================================ FILE: cmd/template/advanced/gitHubAction.go ================================================ package advanced import ( _ "embed" ) //go:embed files/workflow/github/github_action_goreleaser.yml.tmpl var gitHubActionBuildTemplate []byte //go:embed files/workflow/github/github_action_gotest.yml.tmpl var gitHubActionTestTemplate []byte //go:embed files/workflow/github/github_action_releaser_config.yml.tmpl var gitHubActionConfigTemplate []byte func Releaser() []byte { return gitHubActionBuildTemplate } func Test() []byte { return gitHubActionTestTemplate } func ReleaserConfig() []byte { return gitHubActionConfigTemplate } ================================================ FILE: cmd/template/advanced/routes.go ================================================ package advanced import ( _ "embed" ) //go:embed files/htmx/hello.templ.tmpl var helloTemplTemplate []byte //go:embed files/htmx/base.templ.tmpl var baseTemplTemplate []byte //go:embed files/react/tailwind/index.css.tmpl var inputCssTemplateReact []byte //go:embed files/react/tailwind/vite.config.ts.tmpl var viteTailwindConfigFile []byte //go:embed files/react/tailwind/app.tsx.tmpl var reactTailwindAppFile []byte //go:embed files/react/app.tsx.tmpl var reactAppFile []byte //go:embed files/tailwind/input.css.tmpl var inputCssTemplate []byte //go:embed files/tailwind/output.css.tmpl var outputCssTemplate []byte //go:embed files/htmx/tailwind/tailwind.config.js.tmpl var htmxTailwindConfigJsTemplate []byte //go:embed files/htmx/htmx.min.js.tmpl var htmxMinJsTemplate []byte //go:embed files/htmx/efs.go.tmpl var efsTemplate []byte //go:embed files/htmx/hello.go.tmpl var helloGoTemplate []byte //go:embed files/htmx/hello_fiber.go.tmpl var helloFiberGoTemplate []byte //go:embed files/htmx/routes/http_router.tmpl var httpRouterHtmxTemplRoutes []byte //go:embed files/htmx/routes/standard_library.tmpl var stdLibHtmxTemplRoutes []byte //go:embed files/htmx/imports/standard_library.tmpl var stdLibHtmxTemplImports []byte //go:embed files/websocket/imports/standard_library.tmpl var stdLibWebsocketImports []byte //go:embed files/htmx/routes/chi.tmpl var chiHtmxTemplRoutes []byte //go:embed files/htmx/routes/gin.tmpl var ginHtmxTemplRoutes []byte //go:embed files/htmx/imports/gin.tmpl var ginHtmxTemplImports []byte //go:embed files/htmx/routes/gorilla.tmpl var gorillaHtmxTemplRoutes []byte //go:embed files/htmx/routes/echo.tmpl var echoHtmxTemplRoutes []byte //go:embed files/htmx/routes/fiber.tmpl var fiberHtmxTemplRoutes []byte //go:embed files/htmx/imports/fiber.tmpl var fiberHtmxTemplImports []byte //go:embed files/websocket/imports/fiber.tmpl var fiberWebsocketTemplImports []byte func EchoHtmxTemplRoutesTemplate() []byte { return echoHtmxTemplRoutes } func GorillaHtmxTemplRoutesTemplate() []byte { return gorillaHtmxTemplRoutes } func ChiHtmxTemplRoutesTemplate() []byte { return chiHtmxTemplRoutes } func GinHtmxTemplRoutesTemplate() []byte { return ginHtmxTemplRoutes } func HttpRouterHtmxTemplRoutesTemplate() []byte { return httpRouterHtmxTemplRoutes } func StdLibHtmxTemplRoutesTemplate() []byte { return stdLibHtmxTemplRoutes } func StdLibHtmxTemplImportsTemplate() []byte { return stdLibHtmxTemplImports } func StdLibWebsocketTemplImportsTemplate() []byte { return stdLibWebsocketImports } func HelloTemplTemplate() []byte { return helloTemplTemplate } func BaseTemplTemplate() []byte { return baseTemplTemplate } func ReactTailwindAppfile() []byte { return reactTailwindAppFile } func ReactAppfile() []byte { return reactAppFile } func InputCssTemplateReact() []byte { return inputCssTemplateReact } func ViteTailwindConfigFile() []byte { return viteTailwindConfigFile } func InputCssTemplate() []byte { return inputCssTemplate } func OutputCssTemplate() []byte { return outputCssTemplate } func HtmxTailwindConfigJsTemplate() []byte { return htmxTailwindConfigJsTemplate } func HtmxJSTemplate() []byte { return htmxMinJsTemplate } func EfsTemplate() []byte { return efsTemplate } func HelloGoTemplate() []byte { return helloGoTemplate } func HelloFiberGoTemplate() []byte { return helloFiberGoTemplate } func FiberHtmxTemplRoutesTemplate() []byte { return fiberHtmxTemplRoutes } func FiberHtmxTemplImportsTemplate() []byte { return fiberHtmxTemplImports } func FiberWebsocketTemplImportsTemplate() []byte { return fiberWebsocketTemplImports } func GinHtmxTemplImportsTemplate() []byte { return ginHtmxTemplImports } ================================================ FILE: cmd/template/dbdriver/files/env/mongo.tmpl ================================================ {{ if .AdvancedOptions.docker }} BLUEPRINT_DB_HOST=mongo_bp {{- else }} BLUEPRINT_DB_HOST=localhost {{- end }} BLUEPRINT_DB_PORT=27017 BLUEPRINT_DB_USERNAME=melkey BLUEPRINT_DB_ROOT_PASSWORD=password1234 ================================================ FILE: cmd/template/dbdriver/files/env/mysql.tmpl ================================================ {{- if .AdvancedOptions.docker }} BLUEPRINT_DB_HOST=mysql_bp {{- else }} BLUEPRINT_DB_HOST=localhost {{- end }} BLUEPRINT_DB_PORT=3306 BLUEPRINT_DB_DATABASE=blueprint BLUEPRINT_DB_USERNAME=melkey BLUEPRINT_DB_PASSWORD=password1234 BLUEPRINT_DB_ROOT_PASSWORD=password4321 ================================================ FILE: cmd/template/dbdriver/files/env/postgres.tmpl ================================================ {{- if .AdvancedOptions.docker }} BLUEPRINT_DB_HOST=psql_bp {{- else }} BLUEPRINT_DB_HOST=localhost {{- end }} BLUEPRINT_DB_PORT=5432 BLUEPRINT_DB_DATABASE=blueprint BLUEPRINT_DB_USERNAME=melkey BLUEPRINT_DB_PASSWORD=password1234 BLUEPRINT_DB_SCHEMA=public ================================================ FILE: cmd/template/dbdriver/files/env/redis.tmpl ================================================ {{- if .AdvancedOptions.docker }} BLUEPRINT_DB_ADDRESS=redis_bp {{- else }} BLUEPRINT_DB_ADDRESS=localhost {{- end }} BLUEPRINT_DB_PORT=6379 BLUEPRINT_DB_PASSWORD= BLUEPRINT_DB_DATABASE=0 ================================================ FILE: cmd/template/dbdriver/files/env/scylla.tmpl ================================================ {{- if .AdvancedOptions.docker }} # BLUEPRINT_DB_HOSTS=scylla_bp:9042 # ScyllaDB default port BLUEPRINT_DB_HOSTS=scylla_bp:19042 # ScyllaDB Shard-Aware port {{- else }} # BLUEPRINT_DB_HOSTS=localhost:9042 # ScyllaDB default port BLUEPRINT_DB_HOSTS=localhost:19042 # ScyllaDB Shard-Aware port {{- end }} BLUEPRINT_DB_CONSISTENCY="LOCAL_QUORUM" # BLUEPRINT_DB_USERNAME= # BLUEPRINT_DB_PASSWORD= ================================================ FILE: cmd/template/dbdriver/files/env/sqlite.tmpl ================================================ {{- if .AdvancedOptions.docker }} BLUEPRINT_DB_URL=./db/test.db {{- else }} BLUEPRINT_DB_URL=./test.db {{- end }} ================================================ FILE: cmd/template/dbdriver/files/service/mongo.tmpl ================================================ package database import ( "context" "fmt" "log" "os" "time" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" _ "github.com/joho/godotenv/autoload" ) type Service interface { Health() map[string]string } type service struct { db *mongo.Client } var ( host = os.Getenv("BLUEPRINT_DB_HOST") port = os.Getenv("BLUEPRINT_DB_PORT") //database = os.Getenv("BLUEPRINT_DB_DATABASE") ) func New() Service { client, err := mongo.Connect(context.Background(), options.Client().ApplyURI(fmt.Sprintf("mongodb://%s:%s", host, port))) if err != nil { log.Fatal(err) } return &service{ db: client, } } func (s *service) Health() map[string]string { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() err := s.db.Ping(ctx, nil) if err != nil { log.Fatalf("db down: %v", err) } return map[string]string{ "message": "It's healthy", } } ================================================ FILE: cmd/template/dbdriver/files/service/mysql.tmpl ================================================ package database import ( "context" "database/sql" "fmt" "log" "os" "strconv" "time" _ "github.com/go-sql-driver/mysql" _ "github.com/joho/godotenv/autoload" ) // Service represents a service that interacts with a database. type Service interface { // Health returns a map of health status information. // The keys and values in the map are service-specific. Health() map[string]string // Close terminates the database connection. // It returns an error if the connection cannot be closed. Close() error } type service struct { db *sql.DB } var ( dbname = os.Getenv("BLUEPRINT_DB_DATABASE") password = os.Getenv("BLUEPRINT_DB_PASSWORD") username = os.Getenv("BLUEPRINT_DB_USERNAME") port = os.Getenv("BLUEPRINT_DB_PORT") host = os.Getenv("BLUEPRINT_DB_HOST") dbInstance *service ) func New() Service { // Reuse Connection if dbInstance != nil { return dbInstance } // Opening a driver typically will not attempt to connect to the database. db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", username, password, host, port, dbname)) if err != nil { // This will not be a connection error, but a DSN parse error or // another initialization error. log.Fatal(err) } db.SetConnMaxLifetime(0) db.SetMaxIdleConns(50) db.SetMaxOpenConns(50) dbInstance = &service{ db: db, } return dbInstance } // Health checks the health of the database connection by pinging the database. // It returns a map with keys indicating various health statistics. func (s *service) Health() map[string]string { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() stats := make(map[string]string) // Ping the database err := s.db.PingContext(ctx) if err != nil { stats["status"] = "down" stats["error"] = fmt.Sprintf("db down: %v", err) log.Fatalf("db down: %v", err) // Log the error and terminate the program return stats } // Database is up, add more statistics stats["status"] = "up" stats["message"] = "It's healthy" // Get database stats (like open connections, in use, idle, etc.) dbStats := s.db.Stats() stats["open_connections"] = strconv.Itoa(dbStats.OpenConnections) stats["in_use"] = strconv.Itoa(dbStats.InUse) stats["idle"] = strconv.Itoa(dbStats.Idle) stats["wait_count"] = strconv.FormatInt(dbStats.WaitCount, 10) stats["wait_duration"] = dbStats.WaitDuration.String() stats["max_idle_closed"] = strconv.FormatInt(dbStats.MaxIdleClosed, 10) stats["max_lifetime_closed"] = strconv.FormatInt(dbStats.MaxLifetimeClosed, 10) // Evaluate stats to provide a health message if dbStats.OpenConnections > 40 { // Assuming 50 is the max for this example stats["message"] = "The database is experiencing heavy load." } if dbStats.WaitCount > 1000 { stats["message"] = "The database has a high number of wait events, indicating potential bottlenecks." } if dbStats.MaxIdleClosed > int64(dbStats.OpenConnections)/2 { stats["message"] = "Many idle connections are being closed, consider revising the connection pool settings." } if dbStats.MaxLifetimeClosed > int64(dbStats.OpenConnections)/2 { stats["message"] = "Many connections are being closed due to max lifetime, consider increasing max lifetime or revising the connection usage pattern." } return stats } // Close closes the database connection. // It logs a message indicating the disconnection from the specific database. // If the connection is successfully closed, it returns nil. // If an error occurs while closing the connection, it returns the error. func (s *service) Close() error { log.Printf("Disconnected from database: %s", dbname) return s.db.Close() } ================================================ FILE: cmd/template/dbdriver/files/service/postgres.tmpl ================================================ package database import ( "context" "database/sql" "fmt" "log" "os" "strconv" "time" _ "github.com/jackc/pgx/v5/stdlib" _ "github.com/joho/godotenv/autoload" ) // Service represents a service that interacts with a database. type Service interface { // Health returns a map of health status information. // The keys and values in the map are service-specific. Health() map[string]string // Close terminates the database connection. // It returns an error if the connection cannot be closed. Close() error } type service struct { db *sql.DB } var ( database = os.Getenv("BLUEPRINT_DB_DATABASE") password = os.Getenv("BLUEPRINT_DB_PASSWORD") username = os.Getenv("BLUEPRINT_DB_USERNAME") port = os.Getenv("BLUEPRINT_DB_PORT") host = os.Getenv("BLUEPRINT_DB_HOST") schema = os.Getenv("BLUEPRINT_DB_SCHEMA") dbInstance *service ) func New() Service { // Reuse Connection if dbInstance != nil { return dbInstance } connStr := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable&search_path=%s", username, password, host, port, database, schema) db, err := sql.Open("pgx", connStr) if err != nil { log.Fatal(err) } dbInstance = &service{ db: db, } return dbInstance } // Health checks the health of the database connection by pinging the database. // It returns a map with keys indicating various health statistics. func (s *service) Health() map[string]string { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() stats := make(map[string]string) // Ping the database err := s.db.PingContext(ctx) if err != nil { stats["status"] = "down" stats["error"] = fmt.Sprintf("db down: %v", err) log.Fatalf("db down: %v", err) // Log the error and terminate the program return stats } // Database is up, add more statistics stats["status"] = "up" stats["message"] = "It's healthy" // Get database stats (like open connections, in use, idle, etc.) dbStats := s.db.Stats() stats["open_connections"] = strconv.Itoa(dbStats.OpenConnections) stats["in_use"] = strconv.Itoa(dbStats.InUse) stats["idle"] = strconv.Itoa(dbStats.Idle) stats["wait_count"] = strconv.FormatInt(dbStats.WaitCount, 10) stats["wait_duration"] = dbStats.WaitDuration.String() stats["max_idle_closed"] = strconv.FormatInt(dbStats.MaxIdleClosed, 10) stats["max_lifetime_closed"] = strconv.FormatInt(dbStats.MaxLifetimeClosed, 10) // Evaluate stats to provide a health message if dbStats.OpenConnections > 40 { // Assuming 50 is the max for this example stats["message"] = "The database is experiencing heavy load." } if dbStats.WaitCount > 1000 { stats["message"] = "The database has a high number of wait events, indicating potential bottlenecks." } if dbStats.MaxIdleClosed > int64(dbStats.OpenConnections)/2 { stats["message"] = "Many idle connections are being closed, consider revising the connection pool settings." } if dbStats.MaxLifetimeClosed > int64(dbStats.OpenConnections)/2 { stats["message"] = "Many connections are being closed due to max lifetime, consider increasing max lifetime or revising the connection usage pattern." } return stats } // Close closes the database connection. // It logs a message indicating the disconnection from the specific database. // If the connection is successfully closed, it returns nil. // If an error occurs while closing the connection, it returns the error. func (s *service) Close() error { log.Printf("Disconnected from database: %s", database) return s.db.Close() } ================================================ FILE: cmd/template/dbdriver/files/service/redis.tmpl ================================================ package database import ( "context" "fmt" "log" "math" "os" "strconv" "strings" "time" _ "github.com/joho/godotenv/autoload" "github.com/redis/go-redis/v9" ) type Service interface { Health() map[string]string } type service struct { db *redis.Client } var ( address = os.Getenv("BLUEPRINT_DB_ADDRESS") port = os.Getenv("BLUEPRINT_DB_PORT") password = os.Getenv("BLUEPRINT_DB_PASSWORD") database = os.Getenv("BLUEPRINT_DB_DATABASE") ) func New() Service { num, err := strconv.Atoi(database) if err != nil { log.Fatalf("database incorrect %v", err) } fullAddress := fmt.Sprintf("%s:%s", address, port) rdb := redis.NewClient(&redis.Options{ Addr: fullAddress, Password: password, DB: num, // Note: It's important to add this for a secure connection. Most cloud services that offer Redis should already have this configured in their services. // For manual setup, please refer to the Redis documentation: https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/ // TLSConfig: &tls.Config{ // MinVersion: tls.VersionTLS12, // }, }) s := &service{db: rdb} return s } // Health returns the health status and statistics of the Redis server. func (s *service) Health() map[string]string { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) // Default is now 5s defer cancel() stats := make(map[string]string) // Check Redis health and populate the stats map stats = s.checkRedisHealth(ctx, stats) return stats } // checkRedisHealth checks the health of the Redis server and adds the relevant statistics to the stats map. func (s *service) checkRedisHealth(ctx context.Context, stats map[string]string) map[string]string { // Ping the Redis server to check its availability. pong, err := s.db.Ping(ctx).Result() // Note: By extracting and simplifying like this, `log.Fatalf("db down: %v", err)` // can be changed into a standard error instead of a fatal error. if err != nil { log.Fatalf("db down: %v", err) } // Redis is up stats["redis_status"] = "up" stats["redis_message"] = "It's healthy" stats["redis_ping_response"] = pong // Retrieve Redis server information. info, err := s.db.Info(ctx).Result() if err != nil { stats["redis_message"] = fmt.Sprintf("Failed to retrieve Redis info: %v", err) return stats } // Parse the Redis info response. redisInfo := parseRedisInfo(info) // Get the pool stats of the Redis client. poolStats := s.db.PoolStats() // Prepare the stats map with Redis server information and pool statistics. // Note: The "stats" map in the code uses string keys and values, // which is suitable for structuring and serializing the data for the frontend (e.g., JSON, XML, HTMX). // Using string types allows for easy conversion and compatibility with various data formats, // making it convenient to create health stats for monitoring or other purposes. // Also note that any raw "memory" (e.g., used_memory) value here is in bytes and can be converted to megabytes or gigabytes as a float64. stats["redis_version"] = redisInfo["redis_version"] stats["redis_mode"] = redisInfo["redis_mode"] stats["redis_connected_clients"] = redisInfo["connected_clients"] stats["redis_used_memory"] = redisInfo["used_memory"] stats["redis_used_memory_peak"] = redisInfo["used_memory_peak"] stats["redis_uptime_in_seconds"] = redisInfo["uptime_in_seconds"] stats["redis_hits_connections"] = strconv.FormatUint(uint64(poolStats.Hits), 10) stats["redis_misses_connections"] = strconv.FormatUint(uint64(poolStats.Misses), 10) stats["redis_timeouts_connections"] = strconv.FormatUint(uint64(poolStats.Timeouts), 10) stats["redis_total_connections"] = strconv.FormatUint(uint64(poolStats.TotalConns), 10) stats["redis_idle_connections"] = strconv.FormatUint(uint64(poolStats.IdleConns), 10) stats["redis_stale_connections"] = strconv.FormatUint(uint64(poolStats.StaleConns), 10) stats["redis_max_memory"] = redisInfo["maxmemory"] // Calculate the number of active connections. // Note: We use math.Max to ensure that activeConns is always non-negative, // avoiding the need for an explicit check for negative values. // This prevents a potential underflow situation. activeConns := uint64(math.Max(float64(poolStats.TotalConns-poolStats.IdleConns), 0)) stats["redis_active_connections"] = strconv.FormatUint(activeConns, 10) // Calculate the pool size percentage. poolSize := s.db.Options().PoolSize connectedClients, _ := strconv.Atoi(redisInfo["connected_clients"]) poolSizePercentage := float64(connectedClients) / float64(poolSize) * 100 stats["redis_pool_size_percentage"] = fmt.Sprintf("%.2f%%", poolSizePercentage) // Evaluate Redis stats and update the stats map with relevant messages. return s.evaluateRedisStats(redisInfo, stats) } // evaluateRedisStats evaluates the Redis server statistics and updates the stats map with relevant messages. func (s *service) evaluateRedisStats(redisInfo, stats map[string]string) map[string]string { poolSize := s.db.Options().PoolSize poolStats := s.db.PoolStats() connectedClients, _ := strconv.Atoi(redisInfo["connected_clients"]) highConnectionThreshold := int(float64(poolSize) * 0.8) // Check if the number of connected clients is high. if connectedClients > highConnectionThreshold { stats["redis_message"] = "Redis has a high number of connected clients" } // Check if the number of stale connections exceeds a threshold. minStaleConnectionsThreshold := 500 if int(poolStats.StaleConns) > minStaleConnectionsThreshold { stats["redis_message"] = fmt.Sprintf("Redis has %d stale connections.", poolStats.StaleConns) } // Check if Redis is using a significant amount of memory. usedMemory, _ := strconv.ParseInt(redisInfo["used_memory"], 10, 64) maxMemory, _ := strconv.ParseInt(redisInfo["maxmemory"], 10, 64) if maxMemory > 0 { usedMemoryPercentage := float64(usedMemory) / float64(maxMemory) * 100 if usedMemoryPercentage >= 90 { stats["redis_message"] = "Redis is using a significant amount of memory" } } // Check if Redis has been recently restarted. uptimeInSeconds, _ := strconv.ParseInt(redisInfo["uptime_in_seconds"], 10, 64) if uptimeInSeconds < 3600 { stats["redis_message"] = "Redis has been recently restarted" } // Check if the number of idle connections is high. idleConns := int(poolStats.IdleConns) highIdleConnectionThreshold := int(float64(poolSize) * 0.7) if idleConns > highIdleConnectionThreshold { stats["redis_message"] = "Redis has a high number of idle connections" } // Check if the connection pool utilization is high. poolUtilization := float64(poolStats.TotalConns-poolStats.IdleConns) / float64(poolSize) * 100 highPoolUtilizationThreshold := 90.0 if poolUtilization > highPoolUtilizationThreshold { stats["redis_message"] = "Redis connection pool utilization is high" } return stats } // parseRedisInfo parses the Redis info response and returns a map of key-value pairs. func parseRedisInfo(info string) map[string]string { result := make(map[string]string) lines := strings.Split(info, "\r\n") for _, line := range lines { if strings.Contains(line, ":") { parts := strings.Split(line, ":") key := strings.TrimSpace(parts[0]) value := strings.TrimSpace(parts[1]) result[key] = value } } return result } ================================================ FILE: cmd/template/dbdriver/files/service/scylla.tmpl ================================================ package database import ( "context" "fmt" "log" "os" "strconv" "strings" "time" "github.com/gocql/gocql" _ "github.com/joho/godotenv/autoload" ) // Service defines the interface for health checks. type Service interface { Health() map[string]string Close() error } // service implements the Service interface. type service struct { Session *gocql.Session } // Environment variables for ScyllaDB connection. var ( hosts = os.Getenv("BLUEPRINT_DB_HOSTS") // Comma-separated list of hosts:port username = os.Getenv("BLUEPRINT_DB_USERNAME") // Username for authentication password = os.Getenv("BLUEPRINT_DB_PASSWORD") // Password for authentication consistencyLevel = os.Getenv("BLUEPRINT_DB_CONSISTENCY") // Consistency level ) // New initializes a new Service with a ScyllaDB Session. func New() Service { cluster := gocql.NewCluster(strings.Split(hosts, ",")...) cluster.PoolConfig.HostSelectionPolicy = gocql.TokenAwareHostPolicy(gocql.RoundRobinHostPolicy()) // Set authentication if provided if username != "" && password != "" { cluster.Authenticator = gocql.PasswordAuthenticator{ Username: username, Password: password, } } // Set consistency level if provided if consistencyLevel != "" { if cl, err := parseConsistency(consistencyLevel); err == nil { cluster.Consistency = cl } else { log.Printf("Invalid SCYLLA_DB_CONSISTENCY '%s', using default: %v", consistencyLevel, err) } } // Create Session session, err := cluster.CreateSession() if err != nil { log.Fatalf("Failed to connect to ScyllaDB cluster: %v", err) } s := &service{Session: session} return s } // parseConsistency converts a string to a gocql.Consistency value. func parseConsistency(cons string) (gocql.Consistency, error) { consistencyMap := map[string]gocql.Consistency{ "ANY": gocql.Any, "ONE": gocql.One, "TWO": gocql.Two, "THREE": gocql.Three, "QUORUM": gocql.Quorum, "ALL": gocql.All, "LOCAL_ONE": gocql.LocalOne, "LOCAL_QUORUM": gocql.LocalQuorum, "EACH_QUORUM": gocql.EachQuorum, } if consistency, ok := consistencyMap[strings.ToUpper(cons)]; ok { return consistency, nil } return gocql.LocalQuorum, fmt.Errorf("unknown consistency level: %s", cons) } // Health returns the health status and statistics of the ScyllaDB cluster. func (s *service) Health() map[string]string { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() stats := make(map[string]string) // Check ScyllaDB health and populate the stats map startedAt := time.Now() // Execute a simple query to check connectivity query := "SELECT now() FROM system.local" iter := s.Session.Query(query).WithContext(ctx).Iter() var currentTime time.Time if !iter.Scan(¤tTime) { if err := iter.Close(); err != nil { stats["status"] = "down" stats["message"] = fmt.Sprintf("Failed to execute query: %v", err) return stats } } if err := iter.Close(); err != nil { stats["status"] = "down" stats["message"] = fmt.Sprintf("Error during query execution: %v", err) return stats } // ScyllaDB is up stats["status"] = "up" stats["message"] = "It's healthy" stats["scylla_current_time"] = currentTime.String() // Retrieve cluster information // Get keyspace information getKeyspacesQuery := "SELECT keyspace_name FROM system_schema.keyspaces" keyspacesIterator := s.Session.Query(getKeyspacesQuery).Iter() stats["scylla_keyspaces"] = strconv.Itoa(keyspacesIterator.NumRows()) if err := keyspacesIterator.Close(); err != nil { log.Fatalf("Failed to close keyspaces iterator: %v", err) } // Get cluster information var currentDatacenter string var currentHostStatus bool var clusterNodesUp uint var clusterNodesDown uint var clusterSize uint clusterNodesIterator := s.Session.Query("SELECT dc, up FROM system.cluster_status").Iter() for clusterNodesIterator.Scan(¤tDatacenter, ¤tHostStatus) { clusterSize++ if currentHostStatus { clusterNodesUp++ } else { clusterNodesDown++ } } if err := clusterNodesIterator.Close(); err != nil { log.Fatalf("Failed to close cluster nodes iterator: %v", err) } stats["scylla_cluster_size"] = strconv.Itoa(int(clusterSize)) stats["scylla_cluster_nodes_up"] = strconv.Itoa(int(clusterNodesUp)) stats["scylla_cluster_nodes_down"] = strconv.Itoa(int(clusterNodesDown)) stats["scylla_current_datacenter"] = currentDatacenter // Calculate the time taken to perform the health check stats["scylla_health_check_duration"] = time.Since(startedAt).String() return stats } // Close gracefully closes the ScyllaDB Session. func (s *service) Close() error { s.Session.Close() return nil } ================================================ FILE: cmd/template/dbdriver/files/service/sqlite.tmpl ================================================ package database import ( "context" "database/sql" "fmt" "log" "os" "strconv" "time" _ "github.com/mattn/go-sqlite3" _ "github.com/joho/godotenv/autoload" ) // Service represents a service that interacts with a database. type Service interface { // Health returns a map of health status information. // The keys and values in the map are service-specific. Health() map[string]string // Close terminates the database connection. // It returns an error if the connection cannot be closed. Close() error } type service struct { db *sql.DB } var ( dburl = os.Getenv("BLUEPRINT_DB_URL") dbInstance *service ) func New() Service { // Reuse Connection if dbInstance != nil { return dbInstance } db, err := sql.Open("sqlite3", dburl) if err != nil { // This will not be a connection error, but a DSN parse error or // another initialization error. log.Fatal(err) } dbInstance = &service{ db: db, } return dbInstance } // Health checks the health of the database connection by pinging the database. // It returns a map with keys indicating various health statistics. func (s *service) Health() map[string]string { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() stats := make(map[string]string) // Ping the database err := s.db.PingContext(ctx) if err != nil { stats["status"] = "down" stats["error"] = fmt.Sprintf("db down: %v", err) log.Fatalf("db down: %v", err) // Log the error and terminate the program return stats } // Database is up, add more statistics stats["status"] = "up" stats["message"] = "It's healthy" // Get database stats (like open connections, in use, idle, etc.) dbStats := s.db.Stats() stats["open_connections"] = strconv.Itoa(dbStats.OpenConnections) stats["in_use"] = strconv.Itoa(dbStats.InUse) stats["idle"] = strconv.Itoa(dbStats.Idle) stats["wait_count"] = strconv.FormatInt(dbStats.WaitCount, 10) stats["wait_duration"] = dbStats.WaitDuration.String() stats["max_idle_closed"] = strconv.FormatInt(dbStats.MaxIdleClosed, 10) stats["max_lifetime_closed"] = strconv.FormatInt(dbStats.MaxLifetimeClosed, 10) // Evaluate stats to provide a health message if dbStats.OpenConnections > 40 { // Assuming 50 is the max for this example stats["message"] = "The database is experiencing heavy load." } if dbStats.WaitCount > 1000 { stats["message"] = "The database has a high number of wait events, indicating potential bottlenecks." } if dbStats.MaxIdleClosed > int64(dbStats.OpenConnections)/2 { stats["message"] = "Many idle connections are being closed, consider revising the connection pool settings." } if dbStats.MaxLifetimeClosed > int64(dbStats.OpenConnections)/2 { stats["message"] = "Many connections are being closed due to max lifetime, consider increasing max lifetime or revising the connection usage pattern." } return stats } // Close closes the database connection. // It logs a message indicating the disconnection from the specific database. // If the connection is successfully closed, it returns nil. // If an error occurs while closing the connection, it returns the error. func (s *service) Close() error { log.Printf("Disconnected from database: %s", dburl) return s.db.Close() } ================================================ FILE: cmd/template/dbdriver/files/tests/mongo.tmpl ================================================ package database import ( "context" "log" "testing" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/mongodb" ) func mustStartMongoContainer() (func(context.Context, ...testcontainers.TerminateOption) error, error) { dbContainer, err := mongodb.Run(context.Background(), "mongo:latest") if err != nil { return nil, err } dbHost, err := dbContainer.Host(context.Background()) if err != nil { return dbContainer.Terminate, err } dbPort, err := dbContainer.MappedPort(context.Background(), "27017/tcp") if err != nil { return dbContainer.Terminate, err } host = dbHost port = dbPort.Port() return dbContainer.Terminate, err } func TestMain(m *testing.M) { teardown, err := mustStartMongoContainer() if err != nil { log.Fatalf("could not start mongodb container: %v", err) } m.Run() if teardown != nil && teardown(context.Background()) != nil { log.Fatalf("could not teardown mongodb container: %v", err) } } func TestNew(t *testing.T) { srv := New() if srv == nil { t.Fatal("New() returned nil") } } func TestHealth(t *testing.T) { srv := New() stats := srv.Health() if stats["message"] != "It's healthy" { t.Fatalf("expected message to be 'It's healthy', got %s", stats["message"]) } } ================================================ FILE: cmd/template/dbdriver/files/tests/mysql.tmpl ================================================ package database import ( "context" "log" "testing" "time" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/mysql" "github.com/testcontainers/testcontainers-go/wait" ) func mustStartMySQLContainer() (func(context.Context, ...testcontainers.TerminateOption) error, error) { var ( dbName = "database" dbPwd = "password" dbUser = "user" ) dbContainer, err := mysql.Run(context.Background(), "mysql:8.0.36", mysql.WithDatabase(dbName), mysql.WithUsername(dbUser), mysql.WithPassword(dbPwd), testcontainers.WithWaitStrategy(wait.ForLog("port: 3306 MySQL Community Server - GPL").WithStartupTimeout(30*time.Second)), ) if err != nil { return nil, err } dbname = dbName password = dbPwd username = dbUser dbHost, err := dbContainer.Host(context.Background()) if err != nil { return dbContainer.Terminate, err } dbPort, err := dbContainer.MappedPort(context.Background(), "3306/tcp") if err != nil { return dbContainer.Terminate, err } host = dbHost port = dbPort.Port() return dbContainer.Terminate, err } func TestMain(m *testing.M) { teardown, err := mustStartMySQLContainer() if err != nil { log.Fatalf("could not start mysql container: %v", err) } m.Run() if teardown != nil && teardown(context.Background()) != nil { log.Fatalf("could not teardown mysql container: %v", err) } } func TestNew(t *testing.T) { srv := New() if srv == nil { t.Fatal("New() returned nil") } } func TestHealth(t *testing.T) { srv := New() stats := srv.Health() if stats["status"] != "up" { t.Fatalf("expected status to be up, got %s", stats["status"]) } if _, ok := stats["error"]; ok { t.Fatalf("expected error not to be present") } if stats["message"] != "It's healthy" { t.Fatalf("expected message to be 'It's healthy', got %s", stats["message"]) } } func TestClose(t *testing.T) { srv := New() if srv.Close() != nil { t.Fatalf("expected Close() to return nil") } } ================================================ FILE: cmd/template/dbdriver/files/tests/postgres.tmpl ================================================ package database import ( "context" "log" "testing" "time" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/postgres" "github.com/testcontainers/testcontainers-go/wait" ) func mustStartPostgresContainer() (func(context.Context, ...testcontainers.TerminateOption) error, error) { var ( dbName = "database" dbPwd = "password" dbUser = "user" ) dbContainer, err := postgres.Run( context.Background(), "postgres:latest", postgres.WithDatabase(dbName), postgres.WithUsername(dbUser), postgres.WithPassword(dbPwd), testcontainers.WithWaitStrategy( wait.ForLog("database system is ready to accept connections"). WithOccurrence(2). WithStartupTimeout(5*time.Second)), ) if err != nil { return nil, err } database = dbName password = dbPwd username = dbUser dbHost, err := dbContainer.Host(context.Background()) if err != nil { return dbContainer.Terminate, err } dbPort, err := dbContainer.MappedPort(context.Background(), "5432/tcp") if err != nil { return dbContainer.Terminate, err } host = dbHost port = dbPort.Port() return dbContainer.Terminate, err } func TestMain(m *testing.M) { teardown, err := mustStartPostgresContainer() if err != nil { log.Fatalf("could not start postgres container: %v", err) } m.Run() if teardown != nil && teardown(context.Background()) != nil { log.Fatalf("could not teardown postgres container: %v", err) } } func TestNew(t *testing.T) { srv := New() if srv == nil { t.Fatal("New() returned nil") } } func TestHealth(t *testing.T) { srv := New() stats := srv.Health() if stats["status"] != "up" { t.Fatalf("expected status to be up, got %s", stats["status"]) } if _, ok := stats["error"]; ok { t.Fatalf("expected error not to be present") } if stats["message"] != "It's healthy" { t.Fatalf("expected message to be 'It's healthy', got %s", stats["message"]) } } func TestClose(t *testing.T) { srv := New() if srv.Close() != nil { t.Fatalf("expected Close() to return nil") } } ================================================ FILE: cmd/template/dbdriver/files/tests/redis.tmpl ================================================ package database import ( "context" "log" "testing" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/redis" ) func mustStartRedisContainer() (func(context.Context, ...testcontainers.TerminateOption) error, error) { dbContainer, err := redis.Run( context.Background(), "docker.io/redis:7.2.4", redis.WithSnapshotting(10, 1), redis.WithLogLevel(redis.LogLevelVerbose), ) if err != nil { return nil, err } dbHost, err := dbContainer.Host(context.Background()) if err != nil { return dbContainer.Terminate, err } dbPort, err := dbContainer.MappedPort(context.Background(), "6379/tcp") if err != nil { return dbContainer.Terminate, err } address = dbHost port = dbPort.Port() database = "0" return dbContainer.Terminate, err } func TestMain(m *testing.M) { teardown, err := mustStartRedisContainer() if err != nil { log.Fatalf("could not start redis container: %v", err) } m.Run() if teardown != nil && teardown(context.Background()) != nil { log.Fatalf("could not teardown redis container: %v", err) } } func TestNew(t *testing.T) { srv := New() if srv == nil { t.Fatal("New() returned nil") } } func TestHealth(t *testing.T) { srv := New() stats := srv.Health() if stats["redis_status"] != "up" { t.Fatalf("expected status to be up, got %s", stats["redis_status"]) } if _, ok := stats["redis_version"]; !ok { t.Fatalf("expected redis_version to be present, got %v", stats["redis_version"]) } } ================================================ FILE: cmd/template/dbdriver/files/tests/scylla.tmpl ================================================ package database import ( "context" "fmt" "github.com/docker/go-connections/nat" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" "io" "log" "strings" "testing" ) const ( port = nat.Port("19042/tcp") ) func mustStartScyllaDBContainer() (testcontainers.Container, error) { // Define the container image := "scylladb/scylla:6.2" exposedPorts := []string{"9042/tcp", "19042/tcp"} commands := []string{ "--smp=2", "--memory=1G", "--developer-mode=1", "--overprovisioned=1", } req := testcontainers.ContainerRequest{ FromDockerfile: testcontainers.FromDockerfile{}, Image: image, ExposedPorts: exposedPorts, Cmd: commands, WaitingFor: wait.ForAll( wait.ForLog(".*initialization completed.").AsRegexp(), wait.ForListeningPort(port), wait.ForExec([]string{"cqlsh", "-e", "SELECT bootstrapped FROM system.local"}).WithResponseMatcher(func(body io.Reader) bool { data, _ := io.ReadAll(body) return strings.Contains(string(data), "COMPLETED") }), ), } // Start the container scyllaDBContainer, err := testcontainers.GenericContainer( context.Background(), testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, }) if err != nil { return nil, err } mappedPort, err := scyllaDBContainer.MappedPort(context.Background(), "19042/tcp") if err != nil { return nil, err } hosts = fmt.Sprintf("localhost:%v", mappedPort.Port()) return scyllaDBContainer, nil } func TestMain(m *testing.M) { container, err := mustStartScyllaDBContainer() if err != nil { log.Fatalf("could not start scylla container: %v", err) } m.Run() err = container.Terminate(context.Background()) if err != nil { return } } func TestNew(t *testing.T) { srv := New() if srv == nil { t.Fatal("New() returned nil") } err := srv.Close() if err != nil { t.Fatalf("expected Close() to return nil") } } func TestHealth(t *testing.T) { srv := New() stats := srv.Health() if stats["status"] != "up" { t.Fatalf("expected status to be up, got %s", stats["status"]) } if _, ok := stats["error"]; ok { t.Fatalf("expected error not to be present") } if stats["message"] != "It's healthy" { t.Fatalf("expected message to be 'It's healthy', got %s", stats["message"]) } if stats["scylla_cluster_nodes_up"] != "1" { t.Fatalf("expected nodes up '1', got %s", stats["scylla_cluster_nodes_up"]) } if stats["scylla_cluster_nodes_down"] != "0" { t.Fatalf("expected nodes down '0', got %s", stats["scylla_cluster_nodes_down"]) } if stats["scylla_current_datacenter"] != "datacenter1" { t.Fatalf("expected connected dc 'datacenter', got %s", stats["scylla_current_datacenter"]) } err := srv.Close() if err != nil { t.Fatalf("expected Close() to return nil") } } func TestClose(t *testing.T) { srv := New() if srv.Close() != nil { t.Fatalf("expected Close() to return nil") } } ================================================ FILE: cmd/template/dbdriver/mongo.go ================================================ package dbdriver import ( _ "embed" ) type MongoTemplate struct{} //go:embed files/service/mongo.tmpl var mongoServiceTemplate []byte //go:embed files/env/mongo.tmpl var mongoEnvTemplate []byte //go:embed files/tests/mongo.tmpl var mongoTestcontainersTemplate []byte func (m MongoTemplate) Service() []byte { return mongoServiceTemplate } func (m MongoTemplate) Env() []byte { return mongoEnvTemplate } func (m MongoTemplate) Tests() []byte { return mongoTestcontainersTemplate } ================================================ FILE: cmd/template/dbdriver/mysql.go ================================================ package dbdriver import ( _ "embed" ) type MysqlTemplate struct{} //go:embed files/service/mysql.tmpl var mysqlServiceTemplate []byte //go:embed files/env/mysql.tmpl var mysqlEnvTemplate []byte //go:embed files/tests/mysql.tmpl var mysqlTestcontainersTemplate []byte func (m MysqlTemplate) Service() []byte { return mysqlServiceTemplate } func (m MysqlTemplate) Env() []byte { return mysqlEnvTemplate } func (m MysqlTemplate) Tests() []byte { return mysqlTestcontainersTemplate } ================================================ FILE: cmd/template/dbdriver/postgres.go ================================================ package dbdriver import ( _ "embed" ) type PostgresTemplate struct{} //go:embed files/service/postgres.tmpl var postgresServiceTemplate []byte //go:embed files/env/postgres.tmpl var postgresEnvTemplate []byte //go:embed files/tests/postgres.tmpl var postgresTestcontainersTemplate []byte func (m PostgresTemplate) Service() []byte { return postgresServiceTemplate } func (m PostgresTemplate) Env() []byte { return postgresEnvTemplate } func (m PostgresTemplate) Tests() []byte { return postgresTestcontainersTemplate } ================================================ FILE: cmd/template/dbdriver/redis.go ================================================ package dbdriver import ( _ "embed" ) type RedisTemplate struct{} //go:embed files/service/redis.tmpl var redisServiceTemplate []byte //go:embed files/env/redis.tmpl var redisEnvTemplate []byte //go:embed files/tests/redis.tmpl var redisTestcontainersTemplate []byte func (r RedisTemplate) Service() []byte { return redisServiceTemplate } func (r RedisTemplate) Env() []byte { return redisEnvTemplate } func (r RedisTemplate) Tests() []byte { return redisTestcontainersTemplate } ================================================ FILE: cmd/template/dbdriver/scylla.go ================================================ package dbdriver import ( _ "embed" ) type ScyllaTemplate struct{} //go:embed files/service/scylla.tmpl var scyllaServiceTemplate []byte //go:embed files/env/scylla.tmpl var scyllaEnvTemplate []byte //go:embed files/tests/scylla.tmpl var scyllaTestcontainersTemplate []byte func (r ScyllaTemplate) Service() []byte { return scyllaServiceTemplate } func (r ScyllaTemplate) Env() []byte { return scyllaEnvTemplate } func (r ScyllaTemplate) Tests() []byte { return scyllaTestcontainersTemplate } ================================================ FILE: cmd/template/dbdriver/sqlite.go ================================================ package dbdriver import ( _ "embed" ) type SqliteTemplate struct{} //go:embed files/service/sqlite.tmpl var sqliteServiceTemplate []byte //go:embed files/env/sqlite.tmpl var sqliteEnvTemplate []byte func (m SqliteTemplate) Service() []byte { return sqliteServiceTemplate } func (m SqliteTemplate) Env() []byte { return sqliteEnvTemplate } func (m SqliteTemplate) Tests() []byte { return []byte{} } ================================================ FILE: cmd/template/docker/files/docker-compose/mongo.tmpl ================================================ services: {{- if .AdvancedOptions.docker }} app: build: context: . dockerfile: Dockerfile target: prod restart: unless-stopped ports: - ${PORT}:${PORT} environment: APP_ENV: ${APP_ENV} PORT: ${PORT} BLUEPRINT_DB_HOST: ${BLUEPRINT_DB_HOST} BLUEPRINT_DB_PORT: ${BLUEPRINT_DB_PORT} BLUEPRINT_DB_USERNAME: ${BLUEPRINT_DB_USERNAME} BLUEPRINT_DB_ROOT_PASSWORD: ${BLUEPRINT_DB_ROOT_PASSWORD} depends_on: mongo_bp: condition: service_healthy networks: - blueprint {{- end }} {{- if and .AdvancedOptions.react .AdvancedOptions.docker }} frontend: build: context: . dockerfile: Dockerfile target: frontend restart: unless-stopped depends_on: - app ports: - 5173:5173 networks: - blueprint {{- end }} mongo_bp: image: mongo:latest restart: unless-stopped environment: MONGO_INITDB_ROOT_USERNAME: ${BLUEPRINT_DB_USERNAME} MONGO_INITDB_ROOT_PASSWORD: ${BLUEPRINT_DB_ROOT_PASSWORD} ports: - "${BLUEPRINT_DB_PORT}:27017" volumes: - mongo_volume_bp:/data/db {{- if .AdvancedOptions.docker }} healthcheck: test: ["CMD","mongosh", "--eval", "db.adminCommand('ping')"] interval: 5s timeout: 5s retries: 3 start_period: 15s networks: - blueprint {{- end }} volumes: mongo_volume_bp: {{- if .AdvancedOptions.docker }} networks: blueprint: {{- end }} ================================================ FILE: cmd/template/docker/files/docker-compose/mysql.tmpl ================================================ services: {{- if .AdvancedOptions.docker }} app: build: context: . dockerfile: Dockerfile target: prod restart: unless-stopped ports: - ${PORT}:${PORT} environment: APP_ENV: ${APP_ENV} PORT: ${PORT} BLUEPRINT_DB_HOST: ${BLUEPRINT_DB_HOST} BLUEPRINT_DB_PORT: ${BLUEPRINT_DB_PORT} BLUEPRINT_DB_DATABASE: ${BLUEPRINT_DB_DATABASE} BLUEPRINT_DB_USERNAME: ${BLUEPRINT_DB_USERNAME} BLUEPRINT_DB_PASSWORD: ${BLUEPRINT_DB_PASSWORD} depends_on: mysql_bp: condition: service_healthy networks: - blueprint {{- end }} {{- if and .AdvancedOptions.react .AdvancedOptions.docker }} frontend: build: context: . dockerfile: Dockerfile target: frontend restart: unless-stopped depends_on: - app ports: - 5173:5173 networks: - blueprint {{- end }} mysql_bp: image: mysql:latest restart: unless-stopped environment: MYSQL_DATABASE: ${BLUEPRINT_DB_DATABASE} MYSQL_USER: ${BLUEPRINT_DB_USERNAME} MYSQL_PASSWORD: ${BLUEPRINT_DB_PASSWORD} MYSQL_ROOT_PASSWORD: ${BLUEPRINT_DB_ROOT_PASSWORD} ports: - "${BLUEPRINT_DB_PORT}:3306" volumes: - mysql_volume_bp:/var/lib/mysql {{- if .AdvancedOptions.docker }} healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "${BLUEPRINT_DB_HOST}", "-u", "${BLUEPRINT_DB_USERNAME}", "--password=${BLUEPRINT_DB_PASSWORD}"] interval: 5s timeout: 5s retries: 3 start_period: 15s networks: - blueprint {{- end }} volumes: mysql_volume_bp: {{- if .AdvancedOptions.docker }} networks: blueprint: {{- end }} ================================================ FILE: cmd/template/docker/files/docker-compose/postgres.tmpl ================================================ services: {{- if .AdvancedOptions.docker }} app: build: context: . dockerfile: Dockerfile target: prod restart: unless-stopped ports: - ${PORT}:${PORT} environment: APP_ENV: ${APP_ENV} PORT: ${PORT} BLUEPRINT_DB_HOST: ${BLUEPRINT_DB_HOST} BLUEPRINT_DB_PORT: ${BLUEPRINT_DB_PORT} BLUEPRINT_DB_DATABASE: ${BLUEPRINT_DB_DATABASE} BLUEPRINT_DB_USERNAME: ${BLUEPRINT_DB_USERNAME} BLUEPRINT_DB_PASSWORD: ${BLUEPRINT_DB_PASSWORD} BLUEPRINT_DB_SCHEMA: ${BLUEPRINT_DB_SCHEMA} depends_on: psql_bp: condition: service_healthy networks: - blueprint {{- end }} {{- if and .AdvancedOptions.react .AdvancedOptions.docker }} frontend: build: context: . dockerfile: Dockerfile target: frontend restart: unless-stopped depends_on: - app ports: - 5173:5173 networks: - blueprint {{- end }} psql_bp: image: postgres:latest restart: unless-stopped environment: POSTGRES_DB: ${BLUEPRINT_DB_DATABASE} POSTGRES_USER: ${BLUEPRINT_DB_USERNAME} POSTGRES_PASSWORD: ${BLUEPRINT_DB_PASSWORD} ports: - "${BLUEPRINT_DB_PORT}:5432" volumes: - psql_volume_bp:/var/lib/postgresql/data {{- if .AdvancedOptions.docker }} healthcheck: test: ["CMD-SHELL", "sh -c 'pg_isready -U ${BLUEPRINT_DB_USERNAME} -d ${BLUEPRINT_DB_DATABASE}'"] interval: 5s timeout: 5s retries: 3 start_period: 15s networks: - blueprint {{- end }} volumes: psql_volume_bp: {{- if .AdvancedOptions.docker }} networks: blueprint: {{- end }} ================================================ FILE: cmd/template/docker/files/docker-compose/redis.tmpl ================================================ services: {{- if .AdvancedOptions.docker }} app: build: context: . dockerfile: Dockerfile target: prod restart: unless-stopped ports: - ${PORT}:${PORT} environment: APP_ENV: ${APP_ENV} PORT: ${PORT} BLUEPRINT_DB_PORT: ${BLUEPRINT_DB_PORT} BLUEPRINT_DB_ADDRESS: ${BLUEPRINT_DB_ADDRESS} BLUEPRINT_DB_PASSWORD: ${BLUEPRINT_DB_PASSWORD} BLUEPRINT_DB_DATABASE: ${BLUEPRINT_DB_DATABASE} depends_on: redis_bp: condition: service_healthy networks: - blueprint {{- end }} {{- if and .AdvancedOptions.react .AdvancedOptions.docker }} frontend: build: context: . dockerfile: Dockerfile target: frontend restart: unless-stopped depends_on: - app ports: - 5173:5173 networks: - blueprint {{- end }} redis_bp: image: redis:7.2.4 restart: unless-stopped ports: - "${BLUEPRINT_DB_PORT}:6379" {{- if .AdvancedOptions.docker }} healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 5s retries: 3 start_period: 15s networks: - blueprint networks: blueprint: {{- end }} ================================================ FILE: cmd/template/docker/files/docker-compose/scylla.tmpl ================================================ services: {{- if .AdvancedOptions.docker }} app: build: context: . dockerfile: Dockerfile target: prod restart: unless-stopped ports: - ${PORT}:${PORT} environment: APP_ENV: ${APP_ENV} PORT: ${PORT} BLUEPRINT_DB_HOSTS: ${BLUEPRINT_DB_HOSTS} BLUEPRINT_DB_PORT: ${BLUEPRINT_DB_PORT} BLUEPRINT_DB_CONSISTENCY: ${BLUEPRINT_DB_CONSISTENCY} BLUEPRINT_DB_KEYSPACE: ${BLUEPRINT_DB_KEYSPACE} BLUEPRINT_DB_USERNAME: ${BLUEPRINT_DB_USERNAME} BLUEPRINT_DB_PASSWORD: ${BLUEPRINT_DB_PASSWORD} BLUEPRINT_DB_CONNECTIONS: ${BLUEPRINT_DB_CONNECTIONS} depends_on: scylla_bp: condition: service_healthy networks: - blueprint {{- end }} {{- if and .AdvancedOptions.react .AdvancedOptions.docker }} frontend: build: context: . dockerfile: Dockerfile target: frontend restart: unless-stopped depends_on: - app ports: - 5173:5173 networks: - blueprint {{- end }} scylla_bp: image: scylladb/scylla:6.2 restart: unless-stopped command: - --smp=2 - --memory=1GB - --overprovisioned=1 - --developer-mode=1 # Disable for production - --seeds=scylla_bp ports: - "9042:9042" - "19042:19042" volumes: - scylla_bp:/var/lib/scylla {{- if .AdvancedOptions.docker }} healthcheck: test: ["CMD-SHELL", 'cqlsh -e "SHOW VERSION" || exit 1'] interval: 15s timeout: 30s retries: 15 start_period: 30s networks: - blueprint {{- end }} volumes: scylla_bp: {{- if .AdvancedOptions.docker }} networks: blueprint: {{- end }} ================================================ FILE: cmd/template/docker/mongo.go ================================================ package docker import ( _ "embed" ) type MongoDockerTemplate struct{} //go:embed files/docker-compose/mongo.tmpl var mongoDockerTemplate []byte func (m MongoDockerTemplate) Docker() []byte { return mongoDockerTemplate } ================================================ FILE: cmd/template/docker/mysql.go ================================================ package docker import ( _ "embed" ) type MysqlDockerTemplate struct{} //go:embed files/docker-compose/mysql.tmpl var mysqlDockerTemplate []byte func (m MysqlDockerTemplate) Docker() []byte { return mysqlDockerTemplate } ================================================ FILE: cmd/template/docker/postgres.go ================================================ package docker import ( _ "embed" ) type PostgresDockerTemplate struct{} //go:embed files/docker-compose/postgres.tmpl var postgresDockerTemplate []byte func (m PostgresDockerTemplate) Docker() []byte { return postgresDockerTemplate } ================================================ FILE: cmd/template/docker/redis.go ================================================ package docker import ( _ "embed" ) type RedisDockerTemplate struct{} //go:embed files/docker-compose/redis.tmpl var redisDockerTemplate []byte func (r RedisDockerTemplate) Docker() []byte { return redisDockerTemplate } ================================================ FILE: cmd/template/docker/scylla.go ================================================ package docker import ( _ "embed" ) type ScyllaDockerTemplate struct{} //go:embed files/docker-compose/scylla.tmpl var scyllaDockerTemplate []byte func (r ScyllaDockerTemplate) Docker() []byte { return scyllaDockerTemplate } ================================================ FILE: cmd/template/framework/chiRoutes.go ================================================ package framework import ( _ "embed" "github.com/melkeydev/go-blueprint/cmd/template/advanced" ) //go:embed files/routes/chi.go.tmpl var chiRoutesTemplate []byte //go:embed files/tests/default-test.go.tmpl var chiTestHandlerTemplate []byte // ChiTemplates contains the methods used for building // an app that uses [github.com/go-chi/chi] type ChiTemplates struct{} func (c ChiTemplates) Main() []byte { return mainTemplate } func (c ChiTemplates) Server() []byte { return standardServerTemplate } func (c ChiTemplates) Routes() []byte { return chiRoutesTemplate } func (c ChiTemplates) TestHandler() []byte { return chiTestHandlerTemplate } func (c ChiTemplates) HtmxTemplImports() []byte { return advanced.StdLibHtmxTemplImportsTemplate() } func (c ChiTemplates) HtmxTemplRoutes() []byte { return advanced.ChiHtmxTemplRoutesTemplate() } func (c ChiTemplates) WebsocketImports() []byte { return advanced.StdLibWebsocketTemplImportsTemplate() } ================================================ FILE: cmd/template/framework/echoRoutes.go ================================================ package framework import ( _ "embed" "github.com/melkeydev/go-blueprint/cmd/template/advanced" ) //go:embed files/routes/echo.go.tmpl var echoRoutesTemplate []byte //go:embed files/tests/echo-test.go.tmpl var echoTestHandlerTemplate []byte // EchoTemplates contains the methods used for building // an app that uses [github.com/labstack/echo] type EchoTemplates struct{} func (e EchoTemplates) Main() []byte { return mainTemplate } func (e EchoTemplates) Server() []byte { return standardServerTemplate } func (e EchoTemplates) Routes() []byte { return echoRoutesTemplate } func (e EchoTemplates) TestHandler() []byte { return echoTestHandlerTemplate } func (e EchoTemplates) HtmxTemplImports() []byte { return advanced.StdLibHtmxTemplImportsTemplate() } func (e EchoTemplates) HtmxTemplRoutes() []byte { return advanced.EchoHtmxTemplRoutesTemplate() } func (e EchoTemplates) WebsocketImports() []byte { return advanced.StdLibWebsocketTemplImportsTemplate() } ================================================ FILE: cmd/template/framework/fiberServer.go ================================================ package framework import ( _ "embed" "github.com/melkeydev/go-blueprint/cmd/template/advanced" ) //go:embed files/routes/fiber.go.tmpl var fiberRoutesTemplate []byte //go:embed files/server/fiber.go.tmpl var fiberServerTemplate []byte //go:embed files/main/fiber_main.go.tmpl var fiberMainTemplate []byte //go:embed files/tests/fiber-test.go.tmpl var fiberTestHandlerTemplate []byte // FiberTemplates contains the methods used for building // an app that uses [github.com/gofiber/fiber] type FiberTemplates struct{} func (f FiberTemplates) Main() []byte { return fiberMainTemplate } func (f FiberTemplates) Server() []byte { return fiberServerTemplate } func (f FiberTemplates) Routes() []byte { return fiberRoutesTemplate } func (f FiberTemplates) TestHandler() []byte { return fiberTestHandlerTemplate } func (f FiberTemplates) HtmxTemplImports() []byte { return advanced.FiberHtmxTemplImportsTemplate() } func (f FiberTemplates) HtmxTemplRoutes() []byte { return advanced.FiberHtmxTemplRoutesTemplate() } func (f FiberTemplates) WebsocketImports() []byte { return advanced.FiberWebsocketTemplImportsTemplate() } ================================================ FILE: cmd/template/framework/files/README.md.tmpl ================================================ # Project {{.ProjectName}} One Paragraph of project description goes here ## Getting Started These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See deployment for notes on how to deploy the project on a live system. ## MakeFile Run build make command with tests ```bash make all ``` Build the application ```bash make build ``` Run the application ```bash make run ``` {{- if or .AdvancedOptions.docker (and (ne .DBDriver "none") (ne .DBDriver "sqlite")) }} Create DB container ```bash make docker-run ``` Shutdown DB Container ```bash make docker-down ``` DB Integrations Test: ```bash make itest ``` {{- end }} Live reload the application: ```bash make watch ``` Run the test suite: ```bash make test ``` Clean up binary from the last build: ```bash make clean ``` ================================================ FILE: cmd/template/framework/files/air.toml.tmpl ================================================ root = "." testdata_dir = "testdata" tmp_dir = "tmp" [build] args_bin = [] bin = {{if .OSCheck.UnixBased }}"./main"{{ else }}".\\main.exe"{{ end }} cmd = "make build" delay = 1000 exclude_dir = ["assets", "tmp", "vendor", "testdata", "node_modules"] exclude_file = [] exclude_regex = ["_test.go"{{if .AdvancedOptions.htmx}}, ".*_templ.go"{{end}}] exclude_unchanged = false follow_symlink = false full_bin = "" include_dir = [] include_ext = ["go", "tpl", "tmpl", "html"{{if .AdvancedOptions.htmx}}, "templ"{{end}}] include_file = [] kill_delay = "0s" log = "build-errors.log" poll = false poll_interval = 0 post_cmd = [] pre_cmd = [] rerun = false rerun_delay = 500 send_interrupt = false stop_on_error = false [color] app = "" build = "yellow" main = "magenta" runner = "green" watcher = "cyan" [log] main_only = false time = false [misc] clean_on_exit = false [screen] clear_on_rebuild = false keep_scroll = true ================================================ FILE: cmd/template/framework/files/gitignore.tmpl ================================================ # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with "go test -c" *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ # Go workspace file go.work tmp/ # IDE specific files .vscode .idea # .env file .env # Project build main *templ.go # OS X generated file .DS_Store {{if ( .AdvancedOptions.tailwind )}} # Tailwind CSS cmd/web/assets/css/output.css tailwindcss {{end}} ================================================ FILE: cmd/template/framework/files/globalenv.tmpl ================================================ PORT=8080 APP_ENV=local ================================================ FILE: cmd/template/framework/files/main/fiber_main.go.tmpl ================================================ package main import ( "context" "fmt" "log" "os" "os/signal" "strconv" "syscall" "time" "{{.ProjectName}}/internal/server" _ "github.com/joho/godotenv/autoload" ) func gracefulShutdown(fiberServer *server.FiberServer, done chan bool) { // Create context that listens for the interrupt signal from the OS. ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() // Listen for the interrupt signal. <-ctx.Done() log.Println("shutting down gracefully, press Ctrl+C again to force") stop() // Allow Ctrl+C to force shutdown // The context is used to inform the server it has 5 seconds to finish // the request it is currently handling ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := fiberServer.ShutdownWithContext(ctx); err != nil { log.Printf("Server forced to shutdown with error: %v", err) } log.Println("Server exiting") // Notify the main goroutine that the shutdown is complete done <- true } func main() { server := server.New() server.RegisterFiberRoutes() // Create a done channel to signal when the shutdown is complete done := make(chan bool, 1) go func() { port, _ := strconv.Atoi(os.Getenv("PORT")) err := server.Listen(fmt.Sprintf(":%d", port)) if err != nil { panic(fmt.Sprintf("http server error: %s", err)) } }() // Run graceful shutdown in a separate goroutine go gracefulShutdown(server, done) // Wait for the graceful shutdown to complete <-done log.Println("Graceful shutdown complete.") } ================================================ FILE: cmd/template/framework/files/main/main.go.tmpl ================================================ package main import ( "context" "fmt" "log" "net/http" "os/signal" "syscall" "time" "{{.ProjectName}}/internal/server" ) func gracefulShutdown(apiServer *http.Server, done chan bool) { // Create context that listens for the interrupt signal from the OS. ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() // Listen for the interrupt signal. <-ctx.Done() log.Println("shutting down gracefully, press Ctrl+C again to force") stop() // Allow Ctrl+C to force shutdown // The context is used to inform the server it has 5 seconds to finish // the request it is currently handling ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := apiServer.Shutdown(ctx); err != nil { log.Printf("Server forced to shutdown with error: %v", err) } log.Println("Server exiting") // Notify the main goroutine that the shutdown is complete done <- true } func main() { server := server.NewServer() // Create a done channel to signal when the shutdown is complete done := make(chan bool, 1) // Run graceful shutdown in a separate goroutine go gracefulShutdown(server, done) err := server.ListenAndServe() if err != nil && err != http.ErrServerClosed { panic(fmt.Sprintf("http server error: %s", err)) } // Wait for the graceful shutdown to complete <-done log.Println("Graceful shutdown complete.") } ================================================ FILE: cmd/template/framework/files/makefile.tmpl ================================================ # Simple Makefile for a Go project # Build the application all: build test {{- if and (or .AdvancedOptions.htmx .AdvancedOptions.tailwind) (not .AdvancedOptions.react) }} {{- if .OSCheck.UnixBased }} templ-install: @if ! command -v templ > /dev/null; then \ read -p "Go's 'templ' is not installed on your machine. Do you want to install it? [Y/n] " choice; \ if [ "$$choice" != "n" ] && [ "$$choice" != "N" ]; then \ go install github.com/a-h/templ/cmd/templ@latest; \ if [ ! -x "$$(command -v templ)" ]; then \ echo "templ installation failed. Exiting..."; \ exit 1; \ fi; \ else \ echo "You chose not to install templ. Exiting..."; \ exit 1; \ fi; \ fi {{- else }} templ-install: @powershell -ExecutionPolicy Bypass -Command "if (Get-Command templ -ErrorAction SilentlyContinue) { \ ; \ } else { \ Write-Output 'Installing templ...'; \ go install github.com/a-h/templ/cmd/templ@latest; \ if (-not (Get-Command templ -ErrorAction SilentlyContinue)) { \ Write-Output 'templ installation failed. Exiting...'; \ exit 1; \ } else { \ Write-Output 'templ installed successfully.'; \ } \ }" {{- end }} {{- end }} {{- if and .AdvancedOptions.tailwind (not .AdvancedOptions.react) }} {{- if .OSCheck.UnixBased}} tailwind-install: {{ if .OSCheck.linux }}@if [ ! -f tailwindcss ]; then curl -sL https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64 -o tailwindcss; fi{{- end }} {{ if .OSCheck.darwin }}@if [ ! -f tailwindcss ]; then curl -sL https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-macos-x64 -o tailwindcss; fi{{- end }} @chmod +x tailwindcss {{- else }} tailwind-install: @if not exist tailwindcss.exe powershell -ExecutionPolicy Bypass -Command "Invoke-WebRequest -Uri 'https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-windows-x64.exe' -OutFile 'tailwindcss.exe'"{{- end }} {{- end }} build:{{- if and .AdvancedOptions.tailwind (not .AdvancedOptions.react) }} tailwind-install{{- end }}{{- if and (or .AdvancedOptions.htmx .AdvancedOptions.tailwind) (not .AdvancedOptions.react) }} templ-install{{- end }} @echo "Building..." {{ if and (or .AdvancedOptions.htmx .AdvancedOptions.tailwind) (not .AdvancedOptions.react) }}@templ generate{{- end }} {{ if and .AdvancedOptions.tailwind (not .AdvancedOptions.react) }}@{{ if .OSCheck.UnixBased }}./tailwindcss{{ else }}.\tailwindcss.exe{{ end }} -i cmd/web/styles/input.css -o cmd/web/assets/css/output.css{{ end }} {{ if .OSCheck.UnixBased }}@{{- if and (.AdvancedOptions.docker) (eq .DBDriver "sqlite") }}CGO_ENABLED=1 GOOS=linux {{ end }}go build -o main cmd/api/main.go{{- else }}@go build -o main.exe cmd/api/main.go{{- end }} # Run the application run: @go run cmd/api/main.go{{- if .AdvancedOptions.react }} & @npm install --prefer-offline --no-fund --prefix ./frontend @npm run dev --prefix ./frontend {{- end }} {{- if or .AdvancedOptions.docker (and (ne .DBDriver "none") (ne .DBDriver "sqlite")) }} {{- if .OSCheck.UnixBased }} # Create DB container docker-run: @if docker compose up --build 2>/dev/null; then \ : ; \ else \ echo "Falling back to Docker Compose V1"; \ docker-compose up --build; \ fi # Shutdown DB container docker-down: @if docker compose down 2>/dev/null; then \ : ; \ else \ echo "Falling back to Docker Compose V1"; \ docker-compose down; \ fi {{- else }} # Create DB container docker-run: @docker compose up --build # Shutdown DB container docker-down: @docker compose down {{- end }} {{- end }} # Test the application test: @echo "Testing..." @go test ./... -v {{- if and (ne .DBDriver "none") (ne .DBDriver "sqlite") }} # Integrations Tests for the application itest: @echo "Running integration tests..." @go test ./internal/database -v {{- end }} # Clean the binary clean: @echo "Cleaning..." @rm -f main # Live Reload {{- if .OSCheck.UnixBased }} watch: @if command -v air > /dev/null; then \ air; \ echo "Watching...";\ else \ read -p "Go's 'air' is not installed on your machine. Do you want to install it? [Y/n] " choice; \ if [ "$$choice" != "n" ] && [ "$$choice" != "N" ]; then \ go install github.com/air-verse/air@latest; \ air; \ echo "Watching...";\ else \ echo "You chose not to install air. Exiting..."; \ exit 1; \ fi; \ fi {{- else }} watch: @powershell -ExecutionPolicy Bypass -Command "if (Get-Command air -ErrorAction SilentlyContinue) { \ air; \ Write-Output 'Watching...'; \ } else { \ Write-Output 'Installing air...'; \ go install github.com/air-verse/air@latest; \ air; \ Write-Output 'Watching...'; \ }" {{- end }} .PHONY: all build run test clean watch{{- if and (not .AdvancedOptions.react) .AdvancedOptions.tailwind }} tailwind-install{{- end }}{{- if and (ne .DBDriver "none") (ne .DBDriver "sqlite") }} docker-run docker-down itest{{- end }}{{- if and (or .AdvancedOptions.htmx .AdvancedOptions.tailwind) (not .AdvancedOptions.react) }} templ-install{{- end }} ================================================ FILE: cmd/template/framework/files/routes/chi.go.tmpl ================================================ package server import ( "encoding/json" "log" "net/http" {{if .AdvancedOptions.websocket}} "fmt" "time" {{end}} "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" {{.AdvancedTemplates.TemplateImports}} ) func (s *Server) RegisterRoutes() http.Handler { r := chi.NewRouter() r.Use(middleware.Logger) r.Use(cors.Handler(cors.Options{ AllowedOrigins: []string{"https://*", "http://*"}, AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}, AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"}, AllowCredentials: true, MaxAge: 300, })) r.Get("/", s.HelloWorldHandler) {{if ne .DBDriver "none"}} r.Get("/health", s.healthHandler) {{end}} {{if .AdvancedOptions.websocket}} r.Get("/websocket", s.websocketHandler) {{end}} {{.AdvancedTemplates.TemplateRoutes}} return r } func (s *Server) HelloWorldHandler(w http.ResponseWriter, r *http.Request) { resp := make(map[string]string) resp["message"] = "Hello World" jsonResp, err := json.Marshal(resp) if err != nil { log.Fatalf("error handling JSON marshal. Err: %v", err) } _, _ = w.Write(jsonResp) } {{if ne .DBDriver "none"}} func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) { jsonResp, _ := json.Marshal(s.db.Health()) _, _ = w.Write(jsonResp) } {{end}} {{if .AdvancedOptions.websocket}} func (s *Server) websocketHandler(w http.ResponseWriter, r *http.Request) { socket, err := websocket.Accept(w, r, nil) if err != nil { log.Printf("could not open websocket: %v", err) _, _ = w.Write([]byte("could not open websocket")) w.WriteHeader(http.StatusInternalServerError) return } defer socket.Close(websocket.StatusGoingAway, "server closing websocket") ctx := r.Context() socketCtx := socket.CloseRead(ctx) for { payload := fmt.Sprintf("server timestamp: %d", time.Now().UnixNano()) err := socket.Write(socketCtx, websocket.MessageText, []byte(payload)) if err != nil { break } time.Sleep(time.Second * 2) } } {{end}} ================================================ FILE: cmd/template/framework/files/routes/echo.go.tmpl ================================================ package server import ( "net/http" {{if .AdvancedOptions.websocket}} "log" "fmt" "time" {{end}} "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" {{.AdvancedTemplates.TemplateImports}} ) func (s *Server) RegisterRoutes() http.Handler { e := echo.New() e.Use(middleware.Logger()) e.Use(middleware.Recover()) e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ AllowOrigins: []string{"https://*", "http://*"}, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}, AllowHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, AllowCredentials: true, MaxAge: 300, })) {{.AdvancedTemplates.TemplateRoutes}} e.GET("/", s.HelloWorldHandler) {{if ne .DBDriver "none"}} e.GET("/health", s.healthHandler) {{end}} {{if .AdvancedOptions.websocket}} e.GET("/websocket", s.websocketHandler) {{end}} return e } func (s *Server) HelloWorldHandler(c echo.Context) error { resp := map[string]string{ "message": "Hello World", } return c.JSON(http.StatusOK, resp) } {{if ne .DBDriver "none"}} func (s *Server) healthHandler(c echo.Context) error { return c.JSON(http.StatusOK, s.db.Health()) } {{end}} {{if .AdvancedOptions.websocket}} func (s *Server) websocketHandler(c echo.Context) error { w := c.Response().Writer r := c.Request() socket, err := websocket.Accept(w, r, nil) if err != nil { log.Printf("could not open websocket: %v", err) _, _ = w.Write([]byte("could not open websocket")) w.WriteHeader(http.StatusInternalServerError) return nil } defer socket.Close(websocket.StatusGoingAway, "server closing websocket") ctx := r.Context() socketCtx := socket.CloseRead(ctx) for { payload := fmt.Sprintf("server timestamp: %d", time.Now().UnixNano()) err := socket.Write(socketCtx, websocket.MessageText, []byte(payload)) if err != nil { break } time.Sleep(time.Second * 2) } return nil } {{end}} ================================================ FILE: cmd/template/framework/files/routes/fiber.go.tmpl ================================================ package server import ( {{if .AdvancedOptions.websocket}} "context" "log" "fmt" "time" {{end}} "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" {{.AdvancedTemplates.TemplateImports}} ) func (s *FiberServer) RegisterFiberRoutes() { // Apply CORS middleware s.App.Use(cors.New(cors.Config{ AllowOrigins: "*", AllowMethods: "GET,POST,PUT,DELETE,OPTIONS,PATCH", AllowHeaders: "Accept,Authorization,Content-Type", AllowCredentials: false, // credentials require explicit origins MaxAge: 300, })) s.App.Get("/", s.HelloWorldHandler) {{if ne .DBDriver "none"}} s.App.Get("/health", s.healthHandler) {{end}} {{if .AdvancedOptions.websocket}} s.App.Get("/websocket", websocket.New(s.websocketHandler)) {{end}} {{.AdvancedTemplates.TemplateRoutes}} } func (s *FiberServer) HelloWorldHandler(c *fiber.Ctx) error { resp := fiber.Map{ "message": "Hello World", } return c.JSON(resp) } {{if ne .DBDriver "none"}} func (s *FiberServer) healthHandler(c *fiber.Ctx) error { return c.JSON(s.db.Health()) } {{end}} {{if .AdvancedOptions.websocket}} func (s *FiberServer) websocketHandler(con *websocket.Conn) { ctx, cancel := context.WithCancel(context.Background()) go func() { for { _, _, err := con.ReadMessage() if err != nil { cancel() log.Println("Receiver Closing", err) break } } }() for { select { case <-ctx.Done(): return default: payload := fmt.Sprintf("server timestamp: %d", time.Now().UnixNano()) if err := con.WriteMessage(websocket.TextMessage, []byte(payload)); err != nil { log.Printf("could not write to socket: %v", err) return } time.Sleep(time.Second * 2) } } } {{end}} ================================================ FILE: cmd/template/framework/files/routes/gin.go.tmpl ================================================ package server import ( "net/http" {{if .AdvancedOptions.websocket}} "log" "fmt" "time" {{end}} "github.com/gin-gonic/gin" "github.com/gin-contrib/cors" {{.AdvancedTemplates.TemplateImports}} ) func (s *Server) RegisterRoutes() http.Handler { r := gin.Default() r.Use(cors.New(cors.Config{ AllowOrigins: []string{"http://localhost:5173"}, // Add your frontend URL AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}, AllowHeaders: []string{"Accept", "Authorization", "Content-Type"}, AllowCredentials: true, // Enable cookies/auth })) r.GET("/", s.HelloWorldHandler) {{if ne .DBDriver "none"}} r.GET("/health", s.healthHandler) {{end}} {{if .AdvancedOptions.websocket}} r.GET("/websocket", s.websocketHandler) {{end}} {{.AdvancedTemplates.TemplateRoutes}} return r } func (s *Server) HelloWorldHandler(c *gin.Context) { resp := make(map[string]string) resp["message"] = "Hello World" c.JSON(http.StatusOK, resp) } {{if ne .DBDriver "none"}} func (s *Server) healthHandler(c *gin.Context) { c.JSON(http.StatusOK, s.db.Health()) } {{end}} {{if .AdvancedOptions.websocket}} func (s *Server) websocketHandler(c *gin.Context) { w := c.Writer r := c.Request socket, err := websocket.Accept(w, r, nil) if err != nil { log.Printf("could not open websocket: %v", err) _, _ = w.Write([]byte("could not open websocket")) w.WriteHeader(http.StatusInternalServerError) return } defer socket.Close(websocket.StatusGoingAway, "server closing websocket") ctx := r.Context() socketCtx := socket.CloseRead(ctx) for { payload := fmt.Sprintf("server timestamp: %d", time.Now().UnixNano()) err := socket.Write(socketCtx, websocket.MessageText, []byte(payload)) if err != nil { break } time.Sleep(time.Second * 2) } } {{end}} ================================================ FILE: cmd/template/framework/files/routes/gorilla.go.tmpl ================================================ package server import ( "encoding/json" "log" "net/http" {{if .AdvancedOptions.websocket}} "fmt" "time" {{end}} "github.com/gorilla/mux" {{.AdvancedTemplates.TemplateImports}} ) func (s *Server) RegisterRoutes() http.Handler { r := mux.NewRouter() // Apply CORS middleware r.Use(s.corsMiddleware) r.HandleFunc("/", s.HelloWorldHandler) {{if ne .DBDriver "none"}} r.HandleFunc("/health", s.healthHandler) {{end}} {{if .AdvancedOptions.websocket}} r.HandleFunc("/websocket", s.websocketHandler) {{end}} {{.AdvancedTemplates.TemplateRoutes}} return r } // CORS middleware func (s *Server) corsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // CORS Headers w.Header().Set("Access-Control-Allow-Origin", "*") // Wildcard allows all origins w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH") w.Header().Set("Access-Control-Allow-Headers", "Accept, Authorization, Content-Type") w.Header().Set("Access-Control-Allow-Credentials", "false") // Credentials not allowed with wildcard origins // Handle preflight OPTIONS requests if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) } func (s *Server) HelloWorldHandler(w http.ResponseWriter, r *http.Request) { resp := make(map[string]string) resp["message"] = "Hello World" jsonResp, err := json.Marshal(resp) if err != nil { log.Fatalf("error handling JSON marshal. Err: %v", err) } _, _ = w.Write(jsonResp) } {{if ne .DBDriver "none"}} func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) { jsonResp, err := json.Marshal(s.db.Health()) if err != nil { log.Fatalf("error handling JSON marshal. Err: %v", err) } _, _ = w.Write(jsonResp) } {{end}} {{if .AdvancedOptions.websocket}} func (s *Server) websocketHandler(w http.ResponseWriter, r *http.Request) { socket, err := websocket.Accept(w, r, nil) if err != nil { log.Printf("could not open websocket: %v", err) _, _ = w.Write([]byte("could not open websocket")) w.WriteHeader(http.StatusInternalServerError) return } defer socket.Close(websocket.StatusGoingAway, "server closing websocket") ctx := r.Context() socketCtx := socket.CloseRead(ctx) for { payload := fmt.Sprintf("server timestamp: %d", time.Now().UnixNano()) err := socket.Write(socketCtx, websocket.MessageText, []byte(payload)) if err != nil { break } time.Sleep(time.Second * 2) } } {{end}} ================================================ FILE: cmd/template/framework/files/routes/http_router.go.tmpl ================================================ package server import ( "encoding/json" "log" "net/http" {{if .AdvancedOptions.websocket}} "fmt" "time" {{end}} "github.com/julienschmidt/httprouter" {{.AdvancedTemplates.TemplateImports}} ) func (s *Server) RegisterRoutes() http.Handler { r := httprouter.New() // Wrap all routes with CORS middleware corsWrapper := s.corsMiddleware(r) r.HandlerFunc(http.MethodGet, "/", s.HelloWorldHandler) {{if ne .DBDriver "none"}} r.HandlerFunc(http.MethodGet, "/health", s.healthHandler) {{end}} {{if .AdvancedOptions.websocket}} r.HandlerFunc(http.MethodGet, "/websocket", s.websocketHandler) {{end}} {{.AdvancedTemplates.TemplateRoutes}} return corsWrapper } // CORS middleware func (s *Server) corsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // CORS headers w.Header().Set("Access-Control-Allow-Origin", "*") // Use "*" for all origins, or replace with specific origins w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH") w.Header().Set("Access-Control-Allow-Headers", "Accept, Authorization, Content-Type, X-CSRF-Token") w.Header().Set("Access-Control-Allow-Credentials", "false") // Set to "true" if credentials are needed // Handle preflight OPTIONS requests if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) } func (s *Server) HelloWorldHandler(w http.ResponseWriter, r *http.Request) { resp := make(map[string]string) resp["message"] = "Hello World" jsonResp, err := json.Marshal(resp) if err != nil { log.Fatalf("error handling JSON marshal. Err: %v", err) } _, _ = w.Write(jsonResp) } {{if ne .DBDriver "none"}} func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) { jsonResp, err := json.Marshal(s.db.Health()) if err != nil { log.Fatalf("error handling JSON marshal. Err: %v", err) } _, _ = w.Write(jsonResp) } {{end}} {{if .AdvancedOptions.websocket}} func (s *Server) websocketHandler(w http.ResponseWriter, r *http.Request) { socket, err := websocket.Accept(w, r, nil) if err != nil { log.Printf("could not open websocket: %v", err) _, _ = w.Write([]byte("could not open websocket")) w.WriteHeader(http.StatusInternalServerError) return } defer socket.Close(websocket.StatusGoingAway, "server closing websocket") ctx := r.Context() socketCtx := socket.CloseRead(ctx) for { payload := fmt.Sprintf("server timestamp: %d", time.Now().UnixNano()) err := socket.Write(socketCtx, websocket.MessageText, []byte(payload)) if err != nil { break } time.Sleep(time.Second * 2) } } {{end}} ================================================ FILE: cmd/template/framework/files/routes/standard_library.go.tmpl ================================================ package server import ( "encoding/json" "log" "net/http" {{if .AdvancedOptions.websocket}} "fmt" "time" {{end}} {{.AdvancedTemplates.TemplateImports}} ) func (s *Server) RegisterRoutes() http.Handler { mux := http.NewServeMux() // Register routes mux.HandleFunc("/", s.HelloWorldHandler) {{if ne .DBDriver "none"}} mux.HandleFunc("/health", s.healthHandler) {{end}} {{if .AdvancedOptions.websocket}} mux.HandleFunc("/websocket", s.websocketHandler) {{end}} {{.AdvancedTemplates.TemplateRoutes}} // Wrap the mux with CORS middleware return s.corsMiddleware(mux) } func (s *Server) corsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Set CORS headers w.Header().Set("Access-Control-Allow-Origin", "*") // Replace "*" with specific origins if needed w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH") w.Header().Set("Access-Control-Allow-Headers", "Accept, Authorization, Content-Type, X-CSRF-Token") w.Header().Set("Access-Control-Allow-Credentials", "false") // Set to "true" if credentials are required // Handle preflight OPTIONS requests if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } // Proceed with the next handler next.ServeHTTP(w, r) }) } func (s *Server) HelloWorldHandler(w http.ResponseWriter, r *http.Request) { resp := map[string]string{"message": "Hello World"} jsonResp, err := json.Marshal(resp) if err != nil { http.Error(w, "Failed to marshal response", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") if _, err := w.Write(jsonResp); err != nil { log.Printf("Failed to write response: %v", err) } } {{if ne .DBDriver "none"}} func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) { resp, err := json.Marshal(s.db.Health()) if err != nil { http.Error(w, "Failed to marshal health check response", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") if _, err := w.Write(resp); err != nil { log.Printf("Failed to write response: %v", err) } } {{end}} {{if .AdvancedOptions.websocket}} func (s *Server) websocketHandler(w http.ResponseWriter, r *http.Request) { socket, err := websocket.Accept(w, r, nil) if err != nil { http.Error(w, "Failed to open websocket", http.StatusInternalServerError) return } defer socket.Close(websocket.StatusGoingAway, "Server closing websocket") ctx := r.Context() socketCtx := socket.CloseRead(ctx) for { payload := fmt.Sprintf("server timestamp: %d", time.Now().UnixNano()) if err := socket.Write(socketCtx, websocket.MessageText, []byte(payload)); err != nil { log.Printf("Failed to write to socket: %v", err) break } time.Sleep(2 * time.Second) } } {{end}} ================================================ FILE: cmd/template/framework/files/server/fiber.go.tmpl ================================================ package server import ( "github.com/gofiber/fiber/v2" {{if ne .DBDriver "none"}} "{{.ProjectName}}/internal/database" {{end}} ) type FiberServer struct { *fiber.App {{if ne .DBDriver "none"}} db database.Service {{end}} } func New() *FiberServer { server := &FiberServer{ App: fiber.New(fiber.Config{ ServerHeader: "{{.ProjectName}}", AppName: "{{.ProjectName}}", }), {{if ne .DBDriver "none"}} db: database.New(), {{end}} } return server } ================================================ FILE: cmd/template/framework/files/server/standard_library.go.tmpl ================================================ package server import ( "fmt" "net/http" "os" "strconv" "time" _ "github.com/joho/godotenv/autoload" {{if ne .DBDriver "none"}} "{{.ProjectName}}/internal/database" {{end}} ) type Server struct { port int {{if ne .DBDriver "none"}} db database.Service {{end}} } func NewServer() *http.Server { port, _ := strconv.Atoi(os.Getenv("PORT")) NewServer := &Server{ port: port, {{if ne .DBDriver "none"}} db: database.New(), {{end}} } // Declare Server config server := &http.Server{ Addr: fmt.Sprintf(":%d", NewServer.port), Handler: NewServer.RegisterRoutes(), IdleTimeout: time.Minute, ReadTimeout: 10 * time.Second, WriteTimeout: 30 * time.Second, } return server } ================================================ FILE: cmd/template/framework/files/tests/default-test.go.tmpl ================================================ package server import ( "io" "net/http" "net/http/httptest" "testing" ) func TestHandler(t *testing.T) { s := &Server{} server := httptest.NewServer(http.HandlerFunc(s.HelloWorldHandler)) defer server.Close() resp, err := http.Get(server.URL) if err != nil { t.Fatalf("error making request to server. Err: %v", err) } defer resp.Body.Close() // Assertions if resp.StatusCode != http.StatusOK { t.Errorf("expected status OK; got %v", resp.Status) } expected := "{\"message\":\"Hello World\"}" body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("error reading response body. Err: %v", err) } if expected != string(body) { t.Errorf("expected response body to be %v; got %v", expected, string(body)) } } ================================================ FILE: cmd/template/framework/files/tests/echo-test.go.tmpl ================================================ package server import ( "encoding/json" "net/http" "net/http/httptest" "reflect" "testing" "github.com/labstack/echo/v4" ) func TestHandler(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) resp := httptest.NewRecorder() c := e.NewContext(req, resp) s := &Server{} // Assertions if err := s.HelloWorldHandler(c); err != nil { t.Errorf("handler() error = %v", err) return } if resp.Code != http.StatusOK { t.Errorf("handler() wrong status code = %v", resp.Code) return } expected := map[string]string{"message": "Hello World"} var actual map[string]string // Decode the response body into the actual map if err := json.NewDecoder(resp.Body).Decode(&actual); err != nil { t.Errorf("handler() error decoding response body: %v", err) return } // Compare the decoded response with the expected value if !reflect.DeepEqual(expected, actual) { t.Errorf("handler() wrong response body. expected = %v, actual = %v", expected, actual) return } } ================================================ FILE: cmd/template/framework/files/tests/fiber-test.go.tmpl ================================================ package server import ( "io" "net/http" "testing" "github.com/gofiber/fiber/v2" ) func TestHandler(t *testing.T) { // Create a Fiber app for testing app := fiber.New() // Inject the Fiber app into the server s := &FiberServer{App: app} // Define a route in the Fiber app app.Get("/", s.HelloWorldHandler) // Create a test HTTP request req,err := http.NewRequest("GET", "/", nil) if err != nil { t.Fatalf("error creating request. Err: %v", err) } // Perform the request resp, err := app.Test(req) if err != nil { t.Fatalf("error making request to server. Err: %v", err) } // Your test assertions... if resp.StatusCode != http.StatusOK { t.Errorf("expected status OK; got %v", resp.Status) } expected := "{\"message\":\"Hello World\"}" body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("error reading response body. Err: %v", err) } if expected != string(body) { t.Errorf("expected response body to be %v; got %v", expected, string(body)) } } ================================================ FILE: cmd/template/framework/files/tests/gin-test.go.tmpl ================================================ package server import ( "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" ) func TestHelloWorldHandler(t *testing.T) { s := &Server{} r := gin.New() r.GET("/", s.HelloWorldHandler) // Create a test HTTP request req, err := http.NewRequest("GET", "/", nil) if err != nil { t.Fatal(err) } // Create a ResponseRecorder to record the response rr := httptest.NewRecorder() // Serve the HTTP request r.ServeHTTP(rr, req) // Check the status code if status := rr.Code; status != http.StatusOK { t.Errorf("Handler returned wrong status code: got %v want %v", status, http.StatusOK) } // Check the response body expected := "{\"message\":\"Hello World\"}" if rr.Body.String() != expected { t.Errorf("Handler returned unexpected body: got %v want %v", rr.Body.String(), expected) } } ================================================ FILE: cmd/template/framework/ginRoutes.go ================================================ package framework import ( _ "embed" "github.com/melkeydev/go-blueprint/cmd/template/advanced" ) //go:embed files/routes/gin.go.tmpl var ginRoutesTemplate []byte //go:embed files/tests/gin-test.go.tmpl var ginTestHandlerTemplate []byte // GinTemplates contains the methods used for building // an app that uses [github.com/gin-gonic/gin] type GinTemplates struct{} func (g GinTemplates) Main() []byte { return mainTemplate } func (g GinTemplates) Server() []byte { return standardServerTemplate } func (g GinTemplates) Routes() []byte { return ginRoutesTemplate } func (g GinTemplates) TestHandler() []byte { return ginTestHandlerTemplate } func (g GinTemplates) HtmxTemplImports() []byte { return advanced.GinHtmxTemplImportsTemplate() } func (g GinTemplates) HtmxTemplRoutes() []byte { return advanced.GinHtmxTemplRoutesTemplate() } func (g GinTemplates) WebsocketImports() []byte { return advanced.StdLibWebsocketTemplImportsTemplate() } ================================================ FILE: cmd/template/framework/gorillaRoutes.go ================================================ package framework import ( _ "embed" "github.com/melkeydev/go-blueprint/cmd/template/advanced" ) //go:embed files/routes/gorilla.go.tmpl var gorillaRoutesTemplate []byte //go:embed files/tests/default-test.go.tmpl var gorillaTestHandlerTemplate []byte // GorillaTemplates contains the methods used for building // an app that uses [github.com/gorilla/mux] type GorillaTemplates struct{} func (g GorillaTemplates) Main() []byte { return mainTemplate } func (g GorillaTemplates) Server() []byte { return standardServerTemplate } func (g GorillaTemplates) Routes() []byte { return gorillaRoutesTemplate } func (g GorillaTemplates) TestHandler() []byte { return gorillaTestHandlerTemplate } func (g GorillaTemplates) HtmxTemplImports() []byte { return advanced.StdLibHtmxTemplImportsTemplate() } func (g GorillaTemplates) HtmxTemplRoutes() []byte { return advanced.GorillaHtmxTemplRoutesTemplate() } func (g GorillaTemplates) WebsocketImports() []byte { return advanced.StdLibWebsocketTemplImportsTemplate() } ================================================ FILE: cmd/template/framework/httpRoutes.go ================================================ package framework import ( _ "embed" "github.com/melkeydev/go-blueprint/cmd/template/advanced" ) //go:embed files/routes/standard_library.go.tmpl var standardRoutesTemplate []byte //go:embed files/server/standard_library.go.tmpl var standardServerTemplate []byte //go:embed files/tests/default-test.go.tmpl var standardTestHandlerTemplate []byte // StandardLibTemplate contains the methods used for building // an app that uses [net/http] type StandardLibTemplate struct{} func (s StandardLibTemplate) Main() []byte { return mainTemplate } func (s StandardLibTemplate) Server() []byte { return standardServerTemplate } func (s StandardLibTemplate) Routes() []byte { return standardRoutesTemplate } func (s StandardLibTemplate) TestHandler() []byte { return standardTestHandlerTemplate } func (s StandardLibTemplate) HtmxTemplImports() []byte { return advanced.StdLibHtmxTemplImportsTemplate() } func (s StandardLibTemplate) HtmxTemplRoutes() []byte { return advanced.StdLibHtmxTemplRoutesTemplate() } func (s StandardLibTemplate) WebsocketImports() []byte { return advanced.StdLibWebsocketTemplImportsTemplate() } ================================================ FILE: cmd/template/framework/main.go ================================================ // Package template provides utility functions that // help with the templating of created files. package framework import ( _ "embed" ) //go:embed files/main/main.go.tmpl var mainTemplate []byte //go:embed files/air.toml.tmpl var airTomlTemplate []byte //go:embed files/README.md.tmpl var readmeTemplate []byte //go:embed files/makefile.tmpl var makeTemplate []byte //go:embed files/gitignore.tmpl var gitIgnoreTemplate []byte // MakeTemplate returns a byte slice that represents // the default Makefile template. func MakeTemplate() []byte { return makeTemplate } func GitIgnoreTemplate() []byte { return gitIgnoreTemplate } func AirTomlTemplate() []byte { return airTomlTemplate } // ReadmeTemplate returns a byte slice that represents // the default README.md file template. func ReadmeTemplate() []byte { return readmeTemplate } ================================================ FILE: cmd/template/framework/routerRoutes.go ================================================ package framework import ( _ "embed" "github.com/melkeydev/go-blueprint/cmd/template/advanced" ) //go:embed files/routes/http_router.go.tmpl var httpRouterRoutesTemplate []byte //go:embed files/tests/default-test.go.tmpl var httpRouterTestHandlerTemplate []byte // RouterTemplates contains the methods used for building // an app that uses [github.com/julienschmidt/httprouter] type RouterTemplates struct{} func (r RouterTemplates) Main() []byte { return mainTemplate } func (r RouterTemplates) Server() []byte { return standardServerTemplate } func (r RouterTemplates) Routes() []byte { return httpRouterRoutesTemplate } func (r RouterTemplates) TestHandler() []byte { return httpRouterTestHandlerTemplate } func (r RouterTemplates) HtmxTemplImports() []byte { return advanced.StdLibHtmxTemplImportsTemplate() } func (r RouterTemplates) HtmxTemplRoutes() []byte { return advanced.HttpRouterHtmxTemplRoutesTemplate() } func (r RouterTemplates) WebsocketImports() []byte { return advanced.StdLibWebsocketTemplImportsTemplate() } ================================================ FILE: cmd/template/globalEnv.go ================================================ package template import ( _ "embed" ) //go:embed framework/files/globalenv.tmpl var globalEnvTemplate []byte func GlobalEnvTemplate() []byte { return globalEnvTemplate } ================================================ FILE: cmd/ui/multiInput/multiInput.go ================================================ // Package multiInput provides functions that // help define and draw a multi-input step package multiInput import ( "fmt" "github.com/melkeydev/go-blueprint/cmd/program" "github.com/melkeydev/go-blueprint/cmd/steps" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) // Change this var ( focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#01FAC6")).Bold(true) titleStyle = lipgloss.NewStyle().Background(lipgloss.Color("#01FAC6")).Foreground(lipgloss.Color("#030303")).Bold(true).Padding(0, 1, 0) selectedItemStyle = lipgloss.NewStyle().PaddingLeft(1).Foreground(lipgloss.Color("170")).Bold(true) selectedItemDescStyle = lipgloss.NewStyle().PaddingLeft(1).Foreground(lipgloss.Color("170")) descriptionStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#40BDA3")) ) // A Selection represents a choice made in a multiInput step type Selection struct { Choice string } // Update changes the value of a Selection's Choice func (s *Selection) Update(value string) { s.Choice = value } // A multiInput.model contains the data for the multiInput step. // // It has the required methods that make it a bubbletea.Model type model struct { cursor int choices []steps.Item selected map[int]struct{} choice *Selection header string exit *bool } func (m model) Init() tea.Cmd { return nil } // InitialModelMulti initializes a multiInput step with // the given data func InitialModelMulti(choices []steps.Item, selection *Selection, header string, program *program.Project) model { return model{ choices: choices, selected: make(map[int]struct{}), choice: selection, header: titleStyle.Render(header), exit: &program.Exit, } } // Update is called when "things happen", it checks for // important keystrokes to signal when to quit, change selection, // and confirm the selection. func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "ctrl+c", "q": *m.exit = true return m, tea.Quit case "up", "k": if m.cursor > 0 { m.cursor-- } case "down", "j": if m.cursor < len(m.choices)-1 { m.cursor++ } case "enter", " ": if len(m.selected) == 1 { m.selected = make(map[int]struct{}) } _, ok := m.selected[m.cursor] if ok { delete(m.selected, m.cursor) } else { m.selected[m.cursor] = struct{}{} } case "y": if len(m.selected) == 1 { for selectedKey := range m.selected { m.choice.Update(m.choices[selectedKey].Title) m.cursor = selectedKey } return m, tea.Quit } } } return m, nil } // View is called to draw the multiInput step func (m model) View() string { s := m.header + "\n\n" for i, choice := range m.choices { cursor := " " if m.cursor == i { cursor = focusedStyle.Render(">") choice.Title = selectedItemStyle.Render(choice.Title) choice.Desc = selectedItemDescStyle.Render(choice.Desc) } checked := " " if _, ok := m.selected[i]; ok { checked = focusedStyle.Render("x") } title := focusedStyle.Render(choice.Title) description := descriptionStyle.Render(choice.Desc) s += fmt.Sprintf("%s [%s] %s\n%s\n\n", cursor, checked, title, description) } s += fmt.Sprintf("Press %s to confirm choice.\n\n", focusedStyle.Render("y")) return s } ================================================ FILE: cmd/ui/multiSelect/multiSelect.go ================================================ // Package multiSelect provides functions that // help define and draw a multi-select step package multiSelect import ( "fmt" "github.com/melkeydev/go-blueprint/cmd/program" "github.com/melkeydev/go-blueprint/cmd/steps" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) // Change this var ( focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#01FAC6")).Bold(true) titleStyle = lipgloss.NewStyle().Background(lipgloss.Color("#01FAC6")).Foreground(lipgloss.Color("#030303")).Bold(true).Padding(0, 1, 0) selectedItemStyle = lipgloss.NewStyle().PaddingLeft(1).Foreground(lipgloss.Color("170")).Bold(true) selectedItemDescStyle = lipgloss.NewStyle().PaddingLeft(1).Foreground(lipgloss.Color("170")) descriptionStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#40BDA3")) ) // A Selection represents a choice made in a multiSelect step type Selection struct { Choices map[string]bool } // Update changes the value of a Selection's Choice func (s *Selection) Update(optionName string, value bool) { s.Choices[optionName] = value } // A multiSelect.model contains the data for the multiSelect step. // // It has the required methods that make it a bubbletea.Model type model struct { cursor int options []steps.Item selected map[int]struct{} choices *Selection header string exit *bool } func (m model) Init() tea.Cmd { return nil } // InitialModelMulti initializes a multiSelect step with // the given data func InitialModelMultiSelect(options []steps.Item, selection *Selection, header string, program *program.Project) model { return model{ options: options, selected: make(map[int]struct{}), choices: selection, header: titleStyle.Render(header), exit: &program.Exit, } } // Update is called when "things happen", it checks for // important keystrokes to signal when to quit, change selection, // and confirm the selection. func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "ctrl+c", "q": *m.exit = true return m, tea.Quit case "up", "k": if m.cursor > 0 { m.cursor-- } case "down", "j": if m.cursor < len(m.options)-1 { m.cursor++ } case "enter", " ": _, ok := m.selected[m.cursor] if ok { delete(m.selected, m.cursor) } else { m.selected[m.cursor] = struct{}{} } case "y": for selectedKey := range m.selected { m.choices.Update(m.options[selectedKey].Flag, true) m.cursor = selectedKey } return m, tea.Quit } } return m, nil } // View is called to draw the multiSelect step func (m model) View() string { s := m.header + "\n\n" for i, option := range m.options { cursor := " " if m.cursor == i { cursor = focusedStyle.Render(">") option.Title = selectedItemStyle.Render(option.Title) option.Desc = selectedItemDescStyle.Render(option.Desc) } checked := " " if _, ok := m.selected[i]; ok { checked = focusedStyle.Render("*") } title := focusedStyle.Render(option.Title) description := descriptionStyle.Render(option.Desc) s += fmt.Sprintf("%s [%s] %s\n%s\n\n", cursor, checked, title, description) } s += fmt.Sprintf("Press %s to confirm choice.\n", focusedStyle.Render("y")) return s } ================================================ FILE: cmd/ui/spinner/spinner.go ================================================ package spinner import ( "fmt" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) type errMsg error type model struct { spinner spinner.Model quitting bool err error } func InitialModelNew() model { s := spinner.New() s.Spinner = spinner.Line s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) return model{spinner: s} } func (m model) Init() tea.Cmd { return m.spinner.Tick } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "q", "esc", "ctrl+c": m.quitting = true return m, tea.Quit default: return m, nil } case errMsg: m.err = msg return m, nil default: var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) return m, cmd } } func (m model) View() string { if m.err != nil { return m.err.Error() } str := fmt.Sprintf("%s Preparing...", m.spinner.View()) if m.quitting { return str + "\n" } return str } ================================================ FILE: cmd/ui/textinput/textinput.go ================================================ // Package textinput provides functions that // help define and draw a text-input step package textinput import ( "errors" "fmt" "regexp" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/melkeydev/go-blueprint/cmd/program" ) var ( titleStyle = lipgloss.NewStyle().Background(lipgloss.Color("#01FAC6")).Foreground(lipgloss.Color("#030303")).Bold(true).Padding(0, 1, 0) errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF8700")).Bold(true).Padding(0, 0, 0) ) type ( errMsg error ) // Output represents the text provided in a textinput step type Output struct { Output string } // Output.update updates the value of the Output func (o *Output) update(val string) { o.Output = val } // A textnput.model contains the data for the textinput step. // // It has the required methods that make it a bubbletea.Model type model struct { textInput textinput.Model err error output *Output header string exit *bool } // sanitizeInput verifies that an input text string gets validated func sanitizeInput(input string) error { matched, err := regexp.MatchString("^[a-zA-Z0-9_\\/.-]+$", input) if !matched { return fmt.Errorf("string violates the input regex pattern, err: %v", err) } return nil } // InitialTextInputModel initializes a textinput step // with the given data func InitialTextInputModel(output *Output, header string, program *program.Project) model { ti := textinput.New() ti.Focus() ti.CharLimit = 156 ti.Width = 20 ti.Validate = sanitizeInput return model{ textInput: ti, err: nil, output: output, header: titleStyle.Render(header), exit: &program.Exit, } } // CreateErrorInputModel creates a textinput step // with the given error func CreateErrorInputModel(err error) model { ti := textinput.New() ti.Focus() ti.CharLimit = 156 ti.Width = 20 exit := true return model{ textInput: ti, err: errors.New(errorStyle.Render(err.Error())), output: nil, header: "", exit: &exit, } } // Init is called at the beginning of a textinput step // and sets the cursor to blink func (m model) Init() tea.Cmd { return textinput.Blink } // Update is called when "things happen", it checks for the users text input, // and for Ctrl+C or Esc to close the program. func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { case tea.KeyEnter: if len(m.textInput.Value()) > 1 { m.output.update(m.textInput.Value()) return m, tea.Quit } case tea.KeyCtrlC, tea.KeyEsc: *m.exit = true return m, tea.Quit } // We handle errors just like any other message case errMsg: m.err = msg *m.exit = true return m, nil } m.textInput, cmd = m.textInput.Update(msg) return m, cmd } // View is called to draw the textinput step func (m model) View() string { return fmt.Sprintf("%s\n\n%s\n\n", m.header, m.textInput.View(), ) } func (m model) Err() string { return m.err.Error() } ================================================ FILE: cmd/ui/textinput/textinput_test.go ================================================ package textinput import "testing" func TestInputSanitization(t *testing.T) { passTestCases := []string{ "chi", "new_project", "NEW_PROJECT", "new-project", "new/project", "new.project", } for _, testCase := range passTestCases { if err := sanitizeInput(testCase); err != nil { t.Errorf("expected no error, got: %v", err) } } failTestCases := []string{ "new project", "NEW\\PROJECT", "new%project", " ", " ", "#", "@", } for _, testCase := range failTestCases { if err := sanitizeInput(testCase); err == nil { t.Errorf("expected error for input %v, got nil", testCase) } } } ================================================ FILE: cmd/utils/utils.go ================================================ // Package utils provides extra utility // for the program package utils import ( "bytes" "fmt" "log" "os/exec" "regexp" "strings" "github.com/spf13/cobra" "github.com/spf13/pflag" ) const ProgramName = "go-blueprint" // NonInteractiveCommand creates the command string from a flagSet // to be used for getting the equivalent non-interactive shell command func NonInteractiveCommand(use string, flagSet *pflag.FlagSet) string { nonInteractiveCommand := fmt.Sprintf("%s %s", ProgramName, use) visitFn := func(flag *pflag.Flag) { if flag.Name != "help" { if flag.Name == "feature" { featureFlagsString := "" // Creates string representation for the feature flags to be // concatenated with the nonInteractiveCommand for _, k := range strings.Split(flag.Value.String(), ",") { if k != "" { featureFlagsString += fmt.Sprintf(" --feature %s", k) } } nonInteractiveCommand += featureFlagsString } else if flag.Value.Type() == "bool" { if flag.Value.String() == "true" { nonInteractiveCommand = fmt.Sprintf("%s --%s", nonInteractiveCommand, flag.Name) } } else { nonInteractiveCommand = fmt.Sprintf("%s --%s %s", nonInteractiveCommand, flag.Name, flag.Value.String()) } } } flagSet.SortFlags = false flagSet.VisitAll(visitFn) return nonInteractiveCommand } func RegisterStaticCompletions(cmd *cobra.Command, flag string, options []string) { err := cmd.RegisterFlagCompletionFunc(flag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return options, cobra.ShellCompDirectiveNoFileComp }) if err != nil { log.Printf("warning: could not register completion for --%s: %v", flag, err) } } // ExecuteCmd provides a shorthand way to run a shell command func ExecuteCmd(name string, args []string, dir string) error { command := exec.Command(name, args...) command.Dir = dir var out bytes.Buffer var stdErr bytes.Buffer command.Stdout = &out command.Stderr = &stdErr if err := command.Run(); err != nil { return fmt.Errorf("%v\n%v", err, stdErr.String()) } return nil } // InitGoMod initializes go.mod with the given project name // in the selected directory func InitGoMod(projectName string, appDir string) error { if err := ExecuteCmd("go", []string{"mod", "init", projectName}, appDir); err != nil { return err } return nil } // GoGetPackage runs "go get" for a given package in the // selected directory func GoGetPackage(appDir string, packages []string) error { for _, packageName := range packages { if err := ExecuteCmd("go", []string{"get", "-u", packageName}, appDir); err != nil { return err } } return nil } // GoFmt runs "gofmt" in a selected directory using the // simplify and overwrite flags func GoFmt(appDir string) error { if err := ExecuteCmd("gofmt", []string{"-s", "-w", "."}, appDir); err != nil { return err } return nil } // GoModReplace runs "go mod edit -replace" in the selected // replace_payload e.g: github.com/gocql/gocql=github.com/scylladb/gocql@v1.14.4 func GoModReplace(appDir string, replace string) error { if err := ExecuteCmd("go", []string{"mod", "edit", "-replace", replace}, appDir, ); err != nil { return err } return nil } func GoTidy(appDir string) error { err := ExecuteCmd("go", []string{"mod", "tidy"}, appDir) if err != nil { return err } return nil } func CheckGitConfig(key string) (bool, error) { cmd := exec.Command("git", "config", "--get", key) if err := cmd.Run(); err != nil { if exitError, ok := err.(*exec.ExitError); ok { // The command failed to run. if exitError.ExitCode() == 1 { // The 'git config --get' command returns 1 if the key was not found. return false, nil } } // Some other error occurred. return false, err } // The command ran successfully, so the key is set. return true, nil } // ValidateModuleName returns true if it's a valid module name. // It allows any number of / and . characters in between. func ValidateModuleName(moduleName string) bool { matched, _ := regexp.MatchString("^[a-zA-Z0-9_-]+(?:[\\/.][a-zA-Z0-9_-]+)*$", moduleName) return matched } // GetRootDir returns the project directory name from the module path. // Returns the last token by splitting the moduleName with / func GetRootDir(moduleName string) string { tokens := strings.Split(moduleName, "/") return tokens[len(tokens)-1] } ================================================ FILE: cmd/utils/utils_test.go ================================================ package utils import "testing" func TestValidateModuleName(t *testing.T) { passTestCases := []string{ "github.com/user/project", "github.com/user/projec1-hyphen", "github.com/user/projecT_under_Score", "github.com/user/project.hyphen3", "project", "ProJEct", "PRo_45-.4Jc", "PRo_/4J/c", } for _, testCase := range passTestCases { ok := ValidateModuleName(testCase) if !ok { t.Errorf("testing:%s expected:true got:%v", testCase, ok) } } failTestCases := []string{ "", "/", ".", "//", "/project", "ProJEct/", "PRo_$4Jc", "PRo_@J/c", } for _, testCase := range failTestCases { ok := ValidateModuleName(testCase) if ok { t.Errorf("testing:%s expected:false got:%v", testCase, ok) } } } func TestGeRootDir(t *testing.T) { testCases := map[string]string{ "github.com/user/pro-ject": "pro-ject", "pro-ject": "pro-ject", "/": "", "": "", "//": "", "@": "@", } for intput, output := range testCases { rootDir := GetRootDir(intput) if rootDir != output { t.Errorf("testing:%s expected:%s got:%s", intput, output, rootDir) } } } ================================================ FILE: cmd/version.go ================================================ /* Go blueprint version */ package cmd import ( "fmt" "runtime/debug" "time" "github.com/spf13/cobra" ) // GoBlueprintVersion is the version of the cli to be overwritten by goreleaser in the CI run with the version of the release in github var GoBlueprintVersion string // Go Blueprint needs to be built in a specific way to provide useful version information. // First we try to get the version from ldflags embedded into GoBlueprintVersion. // Then we try to get the version from from the go.mod build info. // If Go Blueprint is installed with a specific version tag or using @latest then that version will be included in bi.Main.Version. // This won't give any version info when running 'go install' with the source code locally. // Finally we try to get the version from other embedded VCS info. func getGoBlueprintVersion() string { noVersionAvailable := "No version info available for this build, run 'go-blueprint help version' for additional info" if len(GoBlueprintVersion) != 0 { return GoBlueprintVersion } bi, ok := debug.ReadBuildInfo() if !ok { return noVersionAvailable } // If no main version is available, Go defaults it to (devel) if bi.Main.Version != "(devel)" { return bi.Main.Version } var vcsRevision string var vcsTime time.Time for _, setting := range bi.Settings { switch setting.Key { case "vcs.revision": vcsRevision = setting.Value case "vcs.time": vcsTime, _ = time.Parse(time.RFC3339, setting.Value) } } if vcsRevision != "" { return fmt.Sprintf("%s, (%s)", vcsRevision, vcsTime) } return noVersionAvailable } // versionCmd represents the version command var versionCmd = &cobra.Command{ Use: "version", Short: "Display application version information.", Long: ` The version command provides information about the application's version. Go Blueprint requires version information to be embedded at compile time. For detailed version information, Go Blueprint needs to be built as specified in the README installation instructions. If Go Blueprint is built within a version control repository and other version info isn't available, the revision hash will be used instead. `, Run: func(cmd *cobra.Command, args []string) { version := getGoBlueprintVersion() fmt.Printf("Go Blueprint CLI version: %v\n", version) }, } ================================================ FILE: contributors.yml ================================================ - Melkeydev - Ujstor - tylermeekel - MitchellBerend - H0llyW00dzZ - SudoSurya - mimatache - SputNikPlop - limesten - itschip - CamPen21 - xsadia - arnevm123 - muandane - joshjms - brijesh-amin - briancbarrow - arafays - LrsK - juleszs - vadhe - Patel-Raj - PrajvalBadiger - pradytpk - pellizzetti - Owbird - Jamlie - alexandear - NimishKashyap - narasaka - mubashiroliyantakath - abhishekmj303 - Sakelig - reavessm - young-steveo - sibteali786 - tomasohCHOM - vinitparekh17 - vsnaichuk - Waldeedle - jpx40 - nhlmg93 - rustafariandev - s0up4200 - alexanderilyin - Yoquec - jexroid - andrerocco - ashupednekar - basokant - schoolboybru - brendonotto - danielhe4rt - spankie - Echo5678 - EinarLogi - silaselisha - eric-jacobson - KennyMwendwaX - KibuuleNoah - LarsArtmann - mdelapenya - Marcellofabrizio - kobamkode - MatthewAraujo - mikelerch - MohammadAlhallaq ================================================ FILE: docs/Makefile ================================================ .PHONY: docs default: install all: install build h help: @grep '^[a-z]' Makefile install: pip install pip --upgrade pip install -r requirements.txt upgrade: pip install pip --upgrade pip install -r requirements.txt --upgrade s serve: mkdocs serve --strict b build: mkdocs build --strict d deploy: mkdocs gh-deploy --strict --force ================================================ FILE: docs/custom_theme/main.html ================================================ {% extends "base.html" %} {% block libs %} {{ super() }} {% endblock %} ================================================ FILE: docs/docs/advanced-flag/advanced-flag.md ================================================ # Advanced Flag in Blueprint The `--advanced` flag in Blueprint serves as a switch to enable additional features during project creation. It is applied with the `create` command and unlocks the following features: - **HTMX Support using Templ:** Enables the integration of HTMX support for dynamic web pages using Templ. - **CI/CD Workflow Setup using GitHub Actions:** Automates the setup of a CI/CD workflow using GitHub Actions. - **Websocket Support:** WebSocket endpoint that sends continuous data streams through the WS protocol. - **Tailwind:** Adds Tailwind CSS support to the project. - **Docker:** Docker configuration for go project. - **React:** Frontend written in TypeScript, including an example fetch request to the backend. To utilize the `--advanced` flag, use the following command: ```bash go-blueprint create --name --framework --driver --advanced ``` By including the `--advanced` flag, users can choose one or all of the advanced features. The flag enhances the simplicity of Blueprint while offering flexibility for users who require additional functionality. To recreate the project using the same configuration semi-interactively, use the following command: ```bash go-blueprint create --name my-project --framework chi --driver mysql --advanced ``` Non-Interactive Setup is also possible: ```bash go-blueprint create --name my-project --framework chi --driver mysql --advanced --feature htmx --feature githubaction --feature websocket --feature tailwind ``` ================================================ FILE: docs/docs/advanced-flag/docker.md ================================================ The Docker advanced flag provides the app's Dockerfile configuration and creates or updates the docker-compose.yml file, which is generated if a DB driver is used. The Dockerfile includes a two-stage build, and the final config depends on the use of advanced features. In the end, you will have a smaller image without unnecessary build dependencies. ## Dockerfile ```dockerfile FROM golang:1.24.4-alpine AS build RUN apk add --no-cache curl libstdc++ libgcc WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN go install github.com/a-h/templ/cmd/templ@latest && \ templ generate && \ curl -sL https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64-musl -o tailwindcss && \ chmod +x tailwindcss && \ ./tailwindcss -i cmd/web/styles/input.css -o cmd/web/assets/css/output.css RUN go build -o main cmd/api/main.go FROM alpine:3.20.1 AS prod WORKDIR /app COPY --from=build /app/main /app/main EXPOSE ${PORT} CMD ["./main"] ``` Docker config if React flag is used: ```dockerfile FROM golang:1.24.4-alpine AS build WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN go build -o main cmd/api/main.go FROM alpine:3.20.1 AS prod WORKDIR /app COPY --from=build /app/main /app/main EXPOSE ${PORT} CMD ["./main"] FROM node:20 AS frontend_builder WORKDIR /frontend COPY frontend/package*.json ./ RUN npm install COPY frontend/. . RUN npm run build FROM node:20-slim AS frontend RUN npm install -g serve COPY --from=frontend_builder /frontend/dist /app/dist EXPOSE 5173 CMD ["serve", "-s", "/app/dist", "-l", "5173"] ``` ## Docker compose Docker and docker-compose.yml pull environment variables from the .env file. Example if the Docker flag is used with the MySQL DB driver: ```yaml services: app: build: context: . dockerfile: Dockerfile target: prod restart: unless-stopped ports: - ${PORT}:${PORT} environment: APP_ENV: ${APP_ENV} PORT: ${PORT} BLUEPRINT_DB_HOST: ${BLUEPRINT_DB_HOST} BLUEPRINT_DB_PORT: ${BLUEPRINT_DB_PORT} BLUEPRINT_DB_DATABASE: ${BLUEPRINT_DB_DATABASE} BLUEPRINT_DB_USERNAME: ${BLUEPRINT_DB_USERNAME} BLUEPRINT_DB_PASSWORD: ${BLUEPRINT_DB_PASSWORD} depends_on: mysql_bp: condition: service_healthy networks: - blueprint mysql_bp: image: mysql:latest restart: unless-stopped environment: MYSQL_DATABASE: ${BLUEPRINT_DB_DATABASE} MYSQL_USER: ${BLUEPRINT_DB_USERNAME} MYSQL_PASSWORD: ${BLUEPRINT_DB_PASSWORD} MYSQL_ROOT_PASSWORD: ${BLUEPRINT_DB_ROOT_PASSWORD} ports: - "${BLUEPRINT_DB_PORT}:3306" volumes: - mysql_volume_bp:/var/lib/mysql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "${BLUEPRINT_DB_HOST}", "-u", "${BLUEPRINT_DB_USERNAME}", "--password=${BLUEPRINT_DB_PASSWORD}"] interval: 5s timeout: 5s retries: 3 start_period: 15s networks: - blueprint volumes: mysql_volume_bp: networks: blueprint: ``` ## Note If you are testing more than one framework locally, be aware of Docker leftovers such as volumes. For proper cleaning and building, use `docker compose down --volumes` and `docker compose up --build`. or ```bash docker compose build --no-cache && docker compose up ``` ================================================ FILE: docs/docs/advanced-flag/goreleaser.md ================================================ Release process for Go projects, providing extensive customization options through its configuration file, `.goreleaser.yml`. By default, it ensures dependency cleanliness, builds binaries for various platforms and architectures, facilitates pre-release creation, and organizes binary packaging into archives with naming schemes. For comprehensive insights into customization possibilities, refer to the [GoReleaser documentation](https://goreleaser.com/customization/). ## Usage with Tags To initiate release builds with GoReleaser, you need to follow these steps: - **Tag Creation:** When your project is ready for a release, create a new tag in your Git repository. For example: ```bash git tag v1.0.0 ``` - **Tag Pushing:** Push the tag to the repository to trigger GoReleaser: ```bash git push origin v1.0.0 ``` Following these steps ensures proper tagging of your project, prompting GoReleaser to execute configured releases. This approach simplifies release management and automates artifact distribution. ## Go Test - Continuous Integration for Go Projects The `go-test.yml` file defines a GitHub Actions workflow for continuous integration (CI) of Go projects within a GitHub repository. ## Workflow Steps The job outlined in this workflow includes the following steps: 1. **Checkout:** Fetches the project's codebase from the repository. 2. **Go Setup:** Configures the Go environment with version 1.21.x. 3. **Build and Test:** Builds the project using `go build` and runs tests across all packages (`./...`) using `go test`. This workflow serves to automate the testing process of a Go project within a GitHub repository, ensuring code quality and reliability with each commit and pull request. ================================================ FILE: docs/docs/advanced-flag/htmx-templ.md ================================================ The WEB directory contains the web-related components and assets for the project. It leverages [htmx](https://github.com/bigskysoftware/htmx) and [templ](https://github.com/a-h/templ) in Go for dynamic web content generation. ## Structure ``` web/ │ │ ├── assets/ │ └── js/ │ └── htmx.min.js # htmx library for dynamic HTML content │ ├── base.templ # Base template for HTML structure ├── base_templ.go # Generated Go code for base template ├── efs.go # Embeds static files into the Go binary │ ├── hello.go # Handler for the Hello Web functionality ├── hello.templ # Template for rendering the Hello form and post data └── hello_templ.go # Generated Go code for hello template ``` ## Usage - **Navigate to Project Directory:** ```bash cd my-project ``` - **Install Templ CLI:** ```bash go install github.com/a-h/templ/cmd/templ@latest ``` - **Generate Templ Function Files:** ```bash templ generate ``` - **Start Server:** ```bash make run ``` ## Makefile Automates templ with Makefile entries, which are automatically created if the htmx advanced flag is used. It detects if templ is installed or not and generates templates with the make build command. Both Windows and Unix-like OS are supported. ```bash all: build templ-install: @if ! command -v templ > /dev/null; then \ read -p "Go's 'templ' is not installed on your machine. Do you want to install it? [Y/n] " choice; \ if [ "$$choice" != "n" ] && [ "$$choice" != "N" ]; then \ go install github.com/a-h/templ/cmd/templ@latest; \ if [ ! -x "$$(command -v templ)" ]; then \ echo "templ installation failed. Exiting..."; \ exit 1; \ fi; \ else \ echo "You chose not to install templ. Exiting..."; \ exit 1; \ fi; \ fi build: templ-install @echo "Building..." @templ generate @go build -o main cmd/api/main.go ``` ## Templating Templates are generated using the `templ generate` command after project creation. These templates are then compiled into Go code for efficient execution. You can test HTMX functionality on `localhost:PORT/web` endpoint. ================================================ FILE: docs/docs/advanced-flag/react-vite.md ================================================ This template provides a minimal setup for getting React working with Vite for the frontend and go on the backend. It allows you to easily integrate React with Tailwind CSS and Vite for fast development. The React advanced flag can be combined with the Tailwind flag for enhanced styling capabilities. ## Project Structure ```bash / (Root) ├── frontend/ # React advanced flag. Excludes HTMX. │ ├── .env # Frontend environment configuration. │ ├── node_modules/ # Node dependencies. │ ├── public/ │ │ ├── index.html │ │ └── favicon.ico │ ├── src/ # React source files. │ │ ├── App.tsx # Main React component. │ │ ├── assets/ # React assets directory. │ │ │ └── logo.svg │ │ ├── components/ # React components directory. │ │ │ ├── Header.tsx │ │ │ └── Footer.tsx │ │ ├── styles/ # CSS/SCSS styles directory. │ │ │ └── global.css │ │ └── index.tsx # Main entry point for React │ ├── eslint.config.js # ESLint configuration file. │ ├── index.html # Base HTML template. │ ├── package.json # Node.js package configuration. │ ├── package-lock.json # Lock file for Node.js dependencies. │ ├── README.md # README file for the React project. │ ├── tsconfig.app.json # TypeScript configuration for the app. │ ├── tsconfig.json # Root TypeScript configuration. │ ├── tsconfig.node.json # TypeScript configuration for Node.js. │ └── vite.config.ts # Vite configuration file. ``` ## Usage - **Navigate to the `frontend` directory**: First, navigate to the `frontend` directory where the React project resides. ```bash cd frontend ``` - **Install Dependencies**: Use npm to install all necessary dependencies. ```bash npm install ``` - **Run the Development Server**: Start the Vite development server for local development. This will launch a live-reloading server on a default port. ```bash npm run dev ``` You should now be able to access the React application by opening a browser and navigating to `http://localhost:5173`. You can extend the `vite.config.ts` to include additional configurations as needed, such as adding plugins for optimizing the build process, enabling TypeScript support, or configuring Tailwind CSS. ## Makefile The make run target will start the Go server in the backend, install frontend dependencies, and run the Vite development server for the frontend. ```bash run: @go run cmd/api/main.go & @npm install --prefix ./frontend @npm run dev --prefix ./frontend ``` After running this command, you can verify the connection between the frontend and backend by checking the console. You can also fetch data from the backend to test the integration. ![React](../public/react.png) ## Dockerfile Combine React advanced flag with Docker flag to get Docker and docker-compose configuration and run them with: ```bash make docker-run ``` ### Dockerfile ```dockerfile FROM golang:1.24.4-alpine AS build WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN go build -o main cmd/api/main.go FROM alpine:3.20.1 AS prod WORKDIR /app COPY --from=build /app/main /app/main EXPOSE ${PORT} CMD ["./main"] FROM node:20 AS frontend_builder WORKDIR /frontend COPY frontend/package*.json ./ RUN npm install COPY frontend/. . RUN npm run build FROM node:20-slim AS frontend RUN npm install -g serve COPY --from=frontend_builder /frontend/dist /app/dist EXPOSE 5173 CMD ["serve", "-s", "/app/dist", "-l", "5173"] ``` ### Docker compose without db ```yaml services: app: build: context: . dockerfile: Dockerfile target: prod restart: unless-stopped ports: - ${PORT}:${PORT} environment: APP_ENV: ${APP_ENV} PORT: ${PORT} frontend: build: context: . dockerfile: Dockerfile target: frontend restart: unless-stopped ports: - 5173:5173 depends_on: - app ``` ### Docker compose with db ```yaml services: app: build: context: . dockerfile: Dockerfile target: prod restart: unless-stopped ports: - ${PORT}:${PORT} environment: APP_ENV: ${APP_ENV} PORT: ${PORT} BLUEPRINT_DB_HOST: ${BLUEPRINT_DB_HOST} BLUEPRINT_DB_PORT: ${BLUEPRINT_DB_PORT} BLUEPRINT_DB_DATABASE: ${BLUEPRINT_DB_DATABASE} BLUEPRINT_DB_USERNAME: ${BLUEPRINT_DB_USERNAME} BLUEPRINT_DB_PASSWORD: ${BLUEPRINT_DB_PASSWORD} BLUEPRINT_DB_SCHEMA: ${BLUEPRINT_DB_SCHEMA} depends_on: psql_bp: condition: service_healthy networks: - blueprint frontend: build: context: . dockerfile: Dockerfile target: frontend restart: unless-stopped depends_on: - app ports: - 5173:5173 networks: - blueprint psql_bp: image: postgres:latest restart: unless-stopped environment: POSTGRES_DB: ${BLUEPRINT_DB_DATABASE} POSTGRES_USER: ${BLUEPRINT_DB_USERNAME} POSTGRES_PASSWORD: ${BLUEPRINT_DB_PASSWORD} ports: - "${BLUEPRINT_DB_PORT}:5432" volumes: - psql_volume_bp:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "sh -c 'pg_isready -U ${BLUEPRINT_DB_USERNAME} -d ${BLUEPRINT_DB_DATABASE}'"] interval: 5s timeout: 5s retries: 3 start_period: 15s networks: - blueprint volumes: psql_volume_bp: networks: blueprint: ``` ## Environment Variables The `VITE_PORT` in .env refers `PORT` from .env in project root ( for backend ). If value of `PORT` is changed than `VITE_PORT` must also be changed so that requests to backend work fine and have no conflicts. ## Notes - First time running the project creation with Tailwind can take longer (~10 mins) as npm needs to download and cache all packages - Subsequent runs will be faster as they utilize npm's cache, which we enforce during project creation. - If encountering issues with package installation, try these npm commands: ```bash # Check cache status npm cache verify # View cache contents npm cache ls # Clean cache if needed npm cache clean --force ``` ================================================ FILE: docs/docs/advanced-flag/tailwind.md ================================================ Tailwind is closely coupled with the advanced HTMX flag, and HTMX will be automatically used if you select Tailwind in your project. We do not introduce outside dependencies automatically, and you need compile output.css (file is empty by default) with the Tailwind CLI tool. The project tree would look like this: ```bash / (Root) ├── cmd/ │ ├── api/ │ │ └── main.go │ └── web/ │ ├── styles/ │ │ └── input.css │ ├── assets/ │ │ ├── css/ │ │ │ └── output.css │ │ └── js/ │ │ └── htmx.min.js │ ├── base.templ │ ├── base_templ.go │ ├── efs.go │ ├── hello.go │ ├── hello.templ │ └── hello_templ.go ├── internal/ │ └── server/ │ ├── routes.go │ ├── routes_test.go │ └── server.go ├── go.mod ├── go.sum ├── Makefile └── README.md ``` ## Standalone Tailwind CLI The idea is to avoid using Node.js and npm to build output.css. The Makefile will have entries for downloading and compiling CSS. It will automatically detect the OS and download the latest release from the [official repository](https://github.com/tailwindlabs/tailwindcss/releases). ## Linux Makefile Example ```bash all: build templ-install: @if ! command -v templ > /dev/null; then \ read -p "Go's 'templ' is not installed on your machine. Do you want to install it? [Y/n] " choice; \ if [ "$$choice" != "n" ] && [ "$$choice" != "N" ]; then \ go install github.com/a-h/templ/cmd/templ@latest; \ if [ ! -x "$$(command -v templ)" ]; then \ echo "templ installation failed. Exiting..."; \ exit 1; \ fi; \ else \ echo "You chose not to install templ. Exiting..."; \ exit 1; \ fi; \ fi tailwind-install: @if [ ! -f tailwindcss ]; then curl -sL https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64 -o tailwindcss; fi @chmod +x tailwindcss build: tailwind-install templ-install @echo "Building..." @templ generate @./tailwindcss -i cmd/web/styles/input.css -o cmd/web/assets/css/output.css @go build -o main cmd/api/main.go ``` ## Use Tailwind CSS in your project By default, simple CSS examples are included in the codebase. Update base.templ and hello.templ, then rerun templ generate to see the changes at the `localhost:PORT/web` endpoint. ![Tailwind](../public/tailwind.png) ================================================ FILE: docs/docs/advanced-flag/websocket.md ================================================ A `/websocket` endpoint is added in `routes.go` to facilitate websocket connections. Upon accessing this endpoint, the server establishes a websocket connection and begins transmitting timestamp messages at 2-second intervals. WS is utilized across all Go-blueprint supported frameworks. This simple implementation showcases how flexible a project is. ### Code Implementation ```go func (s *Server) websocketHandler(c *gin.Context) { w := c.Writer r := c.Request socket, err := websocket.Accept(w, r, nil) if err != nil { log.Printf("could not open websocket: %v", err) _, _ = w.Write([]byte("could not open websocket")) w.WriteHeader(http.StatusInternalServerError) return } defer socket.Close(websocket.StatusGoingAway, "server closing websocket") ctx := r.Context() socketCtx := socket.CloseRead(ctx) for { payload := fmt.Sprintf("server timestamp: %d", time.Now().UnixNano()) err := socket.Write(socketCtx, websocket.MessageText, []byte(payload)) if err != nil { break } time.Sleep(time.Second * 2) } } ``` ================================================ FILE: docs/docs/blueprint-core/db-drivers.md ================================================ To extend the project with database functionality, users can choose from a variety of Go database drivers. Each driver is tailored to work with specific database systems, providing flexibility based on project requirements: 1. [Mongo](https://go.mongodb.org/mongo-driver): Provides necessary tools for connecting and interacting with MongoDB databases. 2. [Mysql](https://github.com/go-sql-driver/mysql): Enables seamless integration with MySQL databases. 3. [Postgres](https://github.com/jackc/pgx/): Facilitates connectivity to PostgreSQL databases. 4. [Redis](https://github.com/redis/go-redis): Provides tools for connecting and interacting with Redis. 5. [Sqlite](https://github.com/mattn/go-sqlite3): Suitable for projects requiring a lightweight, self-contained database. 6. [ScyllaDB](https://github.com/scylladb/gocql): Facilitates connectivity to ScyllaDB databases. ## Updated Project Structure Integrating a database adds a new layer to the project structure, primarily in the `internal/database` directory: ```bash /(Root) ├── /cmd │ └── /api │ └── main.go ├── /internal │ ├── /database │ │ ├── database_test.go │ │ └── database.go │ └── /server │ ├── routes.go │ ├── routes_test.go │ └── server.go ├── go.mod ├── go.sum ├── Makefile └── README.md ``` ## Database Driver Implementation Users can select the desired database driver based on their project's specific needs. The chosen driver is then imported into the project, and the `database.go` file is adjusted accordingly to establish a connection and manage interactions with the selected database. ## Integration Tests for Database Operations For all the database drivers but `Sqlite`, integration tests are automatically generated to ensure that the database connection is working correctly. It uses [Testcontainers for Go](https://golang.testcontainers.org/) to spin up a containerized instance of the database server, run the tests, and then tear down the container. [Testcontainers for Go](https://golang.testcontainers.org/) is a Go package that makes it simple to create and clean up container-based dependencies for automated integration/smoke tests. The clean, easy-to-use API enables developers to programmatically define containers that should be run as part of a test and clean up those resources when the test is done. ### Requirements You need a container runtime installed on your machine. Testcontainers supports Docker and any other container runtime that implements the Docker APIs. To install Docker: ```bash curl -sLO get.docker.com ``` ### Running the tests Go to the `internal/database` directory and run the following command: ```bash go test -v ``` Or, just run the following command from the root directory: ```bash make itest ``` Testcontainers automatically pulls the required Docker images and start the containers. The tests run against the containers, and once the tests are done, the containers are stopped and removed. For further information, refer to the [official documentation](https://golang.testcontainers.org/). ## Docker-Compose for Quick Database Spinup To facilitate quick setup and testing, a `docker-compose.yml` file is provided. This file defines a service for the chosen database system with the necessary environment variables. Running `docker-compose up` will quickly spin up a containerized instance of the database, allowing users to test their application against a real database server. This Docker Compose approach simplifies the process of setting up a database for development or testing purposes, providing a convenient and reproducible environment for the project. ================================================ FILE: docs/docs/blueprint-core/frameworks.md ================================================ Created projects can utilize several Go web frameworks to handle HTTP routing and server functionality. The chosen frameworks are: 1. [**Chi**](https://github.com/go-chi/chi): Lightweight and flexible router for building Go HTTP services. 2. [**Echo**](https://github.com/labstack/echo): High-performance, extensible, minimalist Go web framework. 3. [**Fiber**](https://github.com/gofiber/fiber): Express-inspired web framework designed to be fast, simple, and efficient. 4. [**Gin**](https://github.com/gin-gonic/gin): A web framework with a martini-like API, but with much better performance. 5. [**Gorilla/mux**](https://github.com/gorilla/mux): A powerful URL router and dispatcher for Golang. 6. [**HttpRouter**](https://github.com/julienschmidt/httprouter): A high-performance HTTP request router that scales well. ## Project Structure The project is structured with a simple layout, focusing on the cmd, internal, and tests directories: ```bash /(Root) ├── /cmd │ └── /api │ └── main.go ├── /internal │ └── /server │ ├── routes.go │ ├── routes_test.go │ └── server.go ├── go.mod ├── go.sum ├── Makefile └── README.md ``` ================================================ FILE: docs/docs/blueprint-ui.md ================================================ The Blueprint UI is a crucial component of the Go Blueprint ecosystem, providing a user-friendly interface for creating CLI commands and visualizing project structures. By visiting the Blueprint UI website at [go-blueprint.dev](https://go-blueprint.dev), users can interact with a visual representation of their project setup before executing commands. ![BlueprintUI](public/blueprint_ui.png) This enhances the overall experience of using Go Blueprint by providing a visual representation of project setups and simplifying the command generation process. Check Blueprint UI [code](https://github.com/briancbarrow/go-blueprint-htmx). ================================================ FILE: docs/docs/creating-project/air.md ================================================ ## Air - Live Reloading Utility [Air](https://github.com/cosmtrek/air) is a live-reloading utility designed to enhance the development experience by automatically rebuilding and restarting your Go application whenever changes are detected in the source code. The Makefile provided in the project repository includes a command make watch, which triggers Air to monitor file changes and initiate rebuilds and restarts as necessary. Additionally, if Air is not installed on your machine, the Makefile provides an option to install it automatically. Air's `.air.toml` configuration file allows customization of various aspects of its behavior. ## Live Preview ```bash make watch __ _ ___ / /\ | | | |_) /_/--\ |_| |_| \_ v1.51.0, built with Go go1.22.0 mkdir /home/ujstor/code/blueprint-version-test/ws-test4/tmp watching . watching cmd watching cmd/api watching cmd/web watching cmd/web/assets watching cmd/web/assets/js watching internal watching internal/database watching internal/server watching tests !exclude tmp building... make[1]: Entering directory '/home/ujstor/code/blueprint-version-test/ws-test4' Building... Processing path: /home/ujstor/code/blueprint-version-test/ws-test4 Generated code for "/home/ujstor/code/blueprint-version-test/ws-test4/cmd/web/base.templ" in 914.556µs Generated code for "/home/ujstor/code/blueprint-version-test/ws-test4/cmd/web/hello.templ" in 963.157µs Generated code for 2 templates with 0 errors in 1.274392ms make[1]: Leaving directory '/home/ujstor/code/blueprint-version-test/ws-test4' running... internal/server/routes.go has changed building... make[1]: Entering directory '/home/ujstor/code/blueprint-version-test/ws-test4' Building... Processing path: /home/ujstor/code/blueprint-version-test/ws-test4 Generated code for "/home/ujstor/code/blueprint-version-test/ws-test4/cmd/web/base.templ" in 907.426µs Generated code for "/home/ujstor/code/blueprint-version-test/ws-test4/cmd/web/hello.templ" in 1.16142ms Generated code for 2 templates with 0 errors in 1.527556ms make[1]: Leaving directory '/home/ujstor/code/blueprint-version-test/ws-test4' running... ``` Integrating Air into your development workflow alongside the provided Makefile enables a smooth and efficient process for building, testing, and running your Go applications. With automatic live-reloading, you can focus more on coding and less on manual build and restart steps. ================================================ FILE: docs/docs/creating-project/makefile.md ================================================ ## Makefile Project Management Makefile is designed for building, running, and testing a Go project. It includes support for advanced options like HTMX and Tailwind CSS, and handles OS-specific operations for Unix-based systems (Linux/macOS) and Windows. ## Targets ***`all`*** The default target that builds and test the application by running the `build` and `test` target. ***`templ-install`*** This target installs the Go-based templating tool, `templ`, if it is not already installed. It supports: - **Unix-based systems**: Prompts the user to install `templ` if it is missing. - **Windows**: Uses PowerShell to check for and install `templ`. ***`tailwind-install`*** This target downloads and sets up `tailwindcss`, depending on the user's operating system: - **Linux**: Downloads the Linux binary. - **macOS**: Downloads the macOS binary. - **Windows**: Uses PowerShell to download the Windows executable. ***`build`*** Builds the Go application and generates assets with `templ` and `tailwind`, if the corresponding advanced options are enabled: - Uses `templ` to generate templates. - Runs `tailwindcss` to compile CSS. ***`run`*** Runs the Go application by executing the `cmd/api/main.go` file and npm install with run dev if React flag is used. ***`docker-run`*** and ***`docker-down`*** These targets manage a database container: - **Unix-based systems**: Tries Docker Compose V2 first, falls back to V1 if needed. - **Windows**: Uses Docker Compose without version fallback. ***`test`*** Runs unit tests for the application using `go test`. ***`itest`*** Runs integration tests if a database, with the exception of SQLite, is used. ***`clean`*** Removes the compiled binary (`main` or `main.exe` depending on the OS). ***`watch`*** Enables live reload for the project using the `air` tool: - **Unix-based systems**: Checks if `air` is installed and prompts for installation if missing. - **Windows**: Uses PowerShell to manage `air` installation and execution. ================================================ FILE: docs/docs/creating-project/project-init.md ================================================ ## Creating a Project After installing the Go-Blueprint CLI tool, you can create a new project with the default settings by running the following command: ```bash go-blueprint create ``` This command will interactively guide you through the project setup process, allowing you to choose the project name, framework, and database driver. ![BlueprintInteractive](../public/blueprint_1.png) ## Using Flags for Non-Interactive Setup For a non-interactive setup, you can use flags to provide the necessary information during project creation. Here's an example: ``` go-blueprint create --name my-project --framework gin --driver postgres --git commit ``` In this example: - `--name`: Specifies the name of the project (replace "my-project" with your desired project name). - `--framework`: Specifies the Go framework to be used (e.g., "gin"). - `--driver`: Specifies the database driver to be integrated (e.g., "postgres"). - `--git`: Specifies the git configuration option of the project (e.g., "commit"). Customize the flags according to your project requirements. ## Advanced Flag By including the `--advanced` flag, users can choose one or all of the advanced features, HTMX, GitHub Actions for CI/CD, Websocket, Docker and TailwindCSS support, during the project creation process. The flag enhances the simplicity of Blueprint while offering flexibility for users who require additional functionality. ```bash go-blueprint create --advanced ``` To recreate the project using the same configuration semi-interactively, use the following command: ```bash go-blueprint create --name my-project --framework chi --driver mysql --git commit --advanced ``` This approach opens interactive mode only for advanced features, which allows you to choose the one or combination of available features. ![AdvancedFlag](../public/blueprint_advanced.png) ## Non-Interactive Setup Advanced features can be enabled using the `--feature` flag along with the `--advanced` flag: HTMX: ```bash go-blueprint create --advanced --feature htmx ``` CI/CD workflow: ```bash go-blueprint create --advanced --feature githubaction ``` Websocket: ```bash go-blueprint create --advanced --feature websocket ``` TailwindCSS: ```bash go-blueprint create --advanced --feature tailwind ``` Docker: ```bash go-blueprint create --advanced --feature docker ``` Or all features at once: ```bash go-blueprint create --name my-project --framework chi --driver mysql --git commit --advanced --feature htmx --feature githubaction --feature websocket --feature tailwind --feature docker ``` ================================================ FILE: docs/docs/endpoints-test/mongo.md ================================================ To test the MongoDB Health Check endpoint, use the following curl command: ```bash curl http://localhost:PORT/health ``` ## Health Function The `Health` function checks the health of the MongoDB by pinging it. It returns a simple map containing a health message. ### Functionality **Ping MongoDB Server**: The function pings the MongoDB thru server to check its availability. - If the ping fails, it logs the error and terminates the program. - If the ping succeeds, it returns a health message indicating that the server is healthy. ### Sample Output The `Health` returns a JSON-like map structure with a single key indicating the health status: ```json { "message": "It's healthy" } ``` ## Code implementation ```go func (s *service) Health() map[string]string { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() err := s.db.Ping(ctx, nil) if err != nil { log.Fatalf("db down: %v", err) } return map[string]string{ "message": "It's healthy", } } ``` ## Note MongoDB does not support advanced health check functions like SQL databases or Redis. The implementation is basic, providing only a simple ping response to indicate if the server is reachable and DB connection healthy. ================================================ FILE: docs/docs/endpoints-test/redis.md ================================================ To test the Redis Health Check endpoint, use the following curl command: ```bash curl http://localhost:PORT/health ``` ## Health Function The `Health` function orchestrates the health assessment of the Redis server by invoking the `checkRedisHealth` function and returning the collected statistics. ### Functionality **Check Redis Health**: The function pings the Redis server to check its availability and adds the response to the stats map. - If the ping fails, it logs the error and terminates the program. - If the ping succeeds, it proceeds to retrieve additional information. **Retrieve Redis Information**: The function retrieves information about the Redis server, including version, mode, connected clients, memory usage, uptime, etc. - If an error occurs during info retrieval, it updates the health message accordingly. **Evaluate Redis Statistics**: The function evaluates the collected statistics to identify potential issues and updates the health message accordingly. - It checks for high number of connected clients, stale connections, memory usage, recent restart, high idle connections, and high connection pool utilization. ### Sample Output The `Health` function returns a JSON-like map structure with various keys representing different health metrics and their corresponding values. ```json { "redis_active_connections": "0", "redis_connected_clients": "1", "redis_hits_connections": "1", "redis_idle_connections": "1", "redis_max_memory": "0", "redis_message": "Redis has been recently restarted", "redis_misses_connections": "1", "redis_mode": "standalone", "redis_ping_response": "PONG", "redis_pool_size_percentage": "0.42%", "redis_stale_connections": "0", "redis_status": "up", "redis_timeouts_connections": "0", "redis_total_connections": "1", "redis_uptime_in_seconds": "55", "redis_used_memory": "980704", "redis_used_memory_peak": "980704", "redis_version": "7.2.4" } ``` ### Serialization/deserialization The `Sample Output` is dynamic and unstructured since it depends on the raw map. To make it structurable, it must implement `JSON serialization/deserialization` or `Other serialization/deserialization` (e.g, `XML serialization/deserialization`) to bind it. For example: - `JSON serialization/deserialization` ```json { "redis_health": { "status": "up", "message": "Redis connection pool utilization is high", "stats": { "version": "7.0.15", "mode": "standalone", "connected_clients": "10", "memory": { "used": { "mb": "22.38", "gb": "0.02" }, "peak": { "mb": "46.57", "gb": "0.05" }, "free": { "mb": "1130.00", "gb": "1.10" }, "percentage": "1.98%" }, "uptime_stats": "6 days, 3 hours, 37 minutes, 20 seconds", "uptime": [ { "day": "6" }, { "hour": "3" }, { "minute": "37" }, { "second": "20" } ], "pooling": { "figures": { "hits": "10", "misses": "2", "timeouts": "0", "total": "4", "stale": "9", "idle": "5", "active": "0", "percentage": "62.50%" }, "observed_total": "26" } } } } ``` - `XML serialization/deserialization` ```xml up Redis connection pool utilization is high 7.0.15 standalone 10 22.38 0.02 46.57 0.05 1130.00 1.10 1.98% 6 days, 3 hours, 37 minutes, 20 seconds 6 3 37 20 10 2 0 4 9 5 0 62.50% 26 ``` ## Code Implementation ```go func (s *service) Health() map[string]string { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) // Default is now 5s defer cancel() stats := make(map[string]string) stats = s.checkRedisHealth(ctx, stats) return stats } func (s *service) checkRedisHealth(ctx context.Context, stats map[string]string) map[string]string { pong, err := s.db.Ping(ctx).Result() if err != nil { log.Fatalf("db down: %v", err) } stats["redis_status"] = "up" stats["redis_message"] = "It's healthy" stats["redis_ping_response"] = pong info, err := s.db.Info(ctx).Result() if err != nil { stats["redis_message"] = fmt.Sprintf("Failed to retrieve Redis info: %v", err) return stats } redisInfo := parseRedisInfo(info) poolStats := s.db.PoolStats() stats["redis_version"] = redisInfo["redis_version"] stats["redis_mode"] = redisInfo["redis_mode"] stats["redis_connected_clients"] = redisInfo["connected_clients"] stats["redis_used_memory"] = redisInfo["used_memory"] stats["redis_used_memory_peak"] = redisInfo["used_memory_peak"] stats["redis_uptime_in_seconds"] = redisInfo["uptime_in_seconds"] stats["redis_hits_connections"] = strconv.FormatUint(uint64(poolStats.Hits), 10) stats["redis_misses_connections"] = strconv.FormatUint(uint64(poolStats.Misses), 10) stats["redis_timeouts_connections"] = strconv.FormatUint(uint64(poolStats.Timeouts), 10) stats["redis_total_connections"] = strconv.FormatUint(uint64(poolStats.TotalConns), 10) stats["redis_idle_connections"] = strconv.FormatUint(uint64(poolStats.IdleConns), 10) stats["redis_stale_connections"] = strconv.FormatUint(uint64(poolStats.StaleConns), 10) stats["redis_max_memory"] = redisInfo["maxmemory"] activeConns := uint64(math.Max(float64(poolStats.TotalConns-poolStats.IdleConns), 0)) stats["redis_active_connections"] = strconv.FormatUint(activeConns, 10) poolSize := s.db.Options().PoolSize connectedClients, _ := strconv.Atoi(redisInfo["connected_clients"]) poolSizePercentage := float64(connectedClients) / float64(poolSize) * 100 stats["redis_pool_size_percentage"] = fmt.Sprintf("%.2f%%", poolSizePercentage) return s.evaluateRedisStats(redisInfo, stats) } func (s *service) evaluateRedisStats(redisInfo, stats map[string]string) map[string]string { poolSize := s.db.Options().PoolSize poolStats := s.db.PoolStats() connectedClients, _ := strconv.Atoi(redisInfo["connected_clients"]) highConnectionThreshold := int(float64(poolSize) * 0.8) if connectedClients > highConnectionThreshold { stats["redis_message"] = "Redis has a high number of connected clients" } minStaleConnectionsThreshold := 500 if int(poolStats.StaleConns) > minStaleConnectionsThreshold { stats["redis_message"] = fmt.Sprintf("Redis has %d stale connections.", poolStats.StaleConns) } usedMemory, _ := strconv.ParseInt(redisInfo["used_memory"], 10, 64) maxMemory, _ := strconv.ParseInt(redisInfo["maxmemory"], 10, 64) if maxMemory > 0 { usedMemoryPercentage := float64(usedMemory) / float64(maxMemory) * 100 if usedMemoryPercentage >= 90 { stats["redis_message"] = "Redis is using a significant amount of memory" } } uptimeInSeconds, _ := strconv.ParseInt(redisInfo["uptime_in_seconds"], 10, 64) if uptimeInSeconds < 3600 { stats["redis_message"] = "Redis has been recently restarted" } idleConns := int(poolStats.IdleConns) highIdleConnectionThreshold := int(float64(poolSize) * 0.7) if idleConns > highIdleConnectionThreshold { stats["redis_message"] = "Redis has a high number of idle connections" } poolUtilization := float64(poolStats.TotalConns-poolStats.IdleConns) / float64(poolSize) * 100 highPoolUtilizationThreshold := 90.0 if poolUtilization > highPoolUtilizationThreshold { stats["redis_message"] = "Redis connection pool utilization is high" } return stats } ``` ================================================ FILE: docs/docs/endpoints-test/scylladb.md ================================================ To test the ScyllaDB Health Check endpoint, use the following curl command: ```bash curl http://localhost:PORT/health ``` ## Health Function The `Health` function checks the health of the ScyllaDB Cluster by pinging the [Coordinator Node](https://opensource.docs.scylladb.com/stable/architecture/architecture-fault-tolerance.html). It returns a simple map containing a health message. ### Functionality **Ping ScyllaDB Server**: The function pings the ScyllaDB through server to check its availability. - If the ping fails, it logs the error and terminates the program. - If the ping succeeds, it returns a health message indicating that the server with some . ### Sample Output The `Health` returns a JSON-like map structure with a single key indicating the health status: ```json { "message": "It's healthy", "status": "up", "scylla_cluster_nodes_up": "3", "scylla_cluster_nodes_down": "0", "scylla_cluster_size": "1", "scylla_current_datacenter": "datacenter1", "scylla_current_time": "2024-11-04 22:59:21.69 +0000 UTC", "scylla_health_check_duration": "16.896976ms", "scylla_keyspaces": "6" } ``` ## ScyllaDB Setup Before starting the cluster, ensure the [fs.aio-max-nr](https://www.kernel.org/doc/Documentation/sysctl/fs.txt) value is sufficient (e.g. `1048576` or `2097152` or more). If you prefer to configure it manually, run one of the following commands to check the current value: ```sh sysctl --all | grep --word-regexp -- 'aio-max-nr' ``` ```sh sysctl fs.aio-max-nr ``` ```sh cat /proc/sys/fs/aio-max-nr ``` If the value is lower than required, you can use one of these commands: ```sh # Update config non-persistent sysctl --write fs.aio-max-nr=1048576 ``` Here's some links for more relevant information and automation: * [Repository: gvieira/ws-scylla](https://github.com/gvieira18/ws-scylla/) - Simple ScyllaDB Cluster management with Makefiles * [ScyllaDB University: 101 Essentials Track](https://university.scylladb.com/courses/scylla-essentials-overview) - Learn the base concepts of ScyllaDB ## Code implementation Here you can check how the Health Check is done under the hood: ```go func (s *service) Health() map[string]string { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() stats := make(map[string]string) // Check ScyllaDB health and populate the stats map startedAt := time.Now() // Execute a simple query to check connectivity query := "SELECT now() FROM system.local" iter := s.Session.Query(query).WithContext(ctx).Iter() var currentTime time.Time if !iter.Scan(¤tTime) { if err := iter.Close(); err != nil { stats["status"] = "down" stats["message"] = fmt.Sprintf("Failed to execute query: %v", err) return stats } } if err := iter.Close(); err != nil { stats["status"] = "down" stats["message"] = fmt.Sprintf("Error during query execution: %v", err) return stats } // ScyllaDB is up stats["status"] = "up" stats["message"] = "It's healthy" stats["scylla_current_time"] = currentTime.String() // Retrieve cluster information // Get keyspace information getKeyspacesQuery := "SELECT keyspace_name FROM system_schema.keyspaces" keyspacesIterator := s.Session.Query(getKeyspacesQuery).Iter() stats["scylla_keyspaces"] = strconv.Itoa(keyspacesIterator.NumRows()) if err := keyspacesIterator.Close(); err != nil { log.Fatalf("Failed to close keyspaces iterator: %v", err) } // Get cluster information var currentDatacenter string var currentHostStatus bool var clusterNodesUp uint var clusterNodesDown uint var clusterSize uint clusterNodesIterator := s.Session.Query("SELECT dc, up FROM system.cluster_status").Iter() for clusterNodesIterator.Scan(¤tDatacenter, ¤tHostStatus) { clusterSize++ if currentHostStatus { clusterNodesUp++ } else { clusterNodesDown++ } } if err := clusterNodesIterator.Close(); err != nil { log.Fatalf("Failed to close cluster nodes iterator: %v", err) } stats["scylla_cluster_size"] = strconv.Itoa(int(clusterSize)) stats["scylla_cluster_nodes_up"] = strconv.Itoa(int(clusterNodesUp)) stats["scylla_cluster_nodes_down"] = strconv.Itoa(int(clusterNodesDown)) stats["scylla_current_datacenter"] = currentDatacenter // Calculate the time taken to perform the health check stats["scylla_health_check_duration"] = time.Since(startedAt).String() return stats } ``` ## Note Scylladb does not support advanced health check functions like SQL databases or Redis. The current implementation is based on queries at `system` related keyspaces. ================================================ FILE: docs/docs/endpoints-test/server.md ================================================ ## Testing Endpoints with CURL and WebSocat Testing endpoints is an essential part of ensuring the correctness and functionality of your app. Depending on what options are used for go-blueprint project creation, you have various endpoints for testing your init application status. Before proceeding, ensure you have the following tools installed: - [CURL](https://curl.se/docs/manpage.html): A command-line tool for transferring data with URLs. - [WebSocat](https://github.com/vi/websocat): A command-line WebSocket client. You can utilize alternative tools that support the WebSocket protocol to establish connections with the server. WebSocat is an open-source CLI tool, while [POSTMAN](https://www.postman.com/) serves as a GUI tool specifically designed for testing APIs and WebSocket functionality. ## Hello World Endpoint To test the Hello World endpoint, execute the following curl command: ```bash curl http://localhost:PORT ``` Sample Output: ```json {"message": "Hello World"} ``` If the server is running and it is healthy, you should see the message 'Hello World' in the response. Also, depending on the framework you are using, there will be logs in the terminal: ```bash make run [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) [GIN-debug] GET / --> websocket-test/internal/server.(*Server).HelloWorldHandler-fm (3 handlers) [GIN-debug] GET /health --> websocket-test/internal/server.(*Server).healthHandler-fm (3 handlers) [GIN-debug] GET /websocket --> websocket-test/internal/server.(*Server).websocketHandler-fm (3 handlers) [GIN] 2024/05/28 - 17:44:31 | 200 | 27.93µs | 127.0.0.1 | GET "/" ``` ================================================ FILE: docs/docs/endpoints-test/sql.md ================================================ To test the SQL DB Health Check endpoint, use the following curl command: ```bash curl http://localhost:PORT/health ``` ## Health Function The `Health` function checks the health of the database connection by pinging the database and retrieving various statistics. It returns a map with keys indicating different health metrics. ### Functionality **Ping the Database**: The function pings the database to ensure it is reachable. - If the database is down, it logs the error, sets the status to "down," and terminates the program. - If the database is up, it proceeds to gather additional statistics. **Collect Database Statistics**: The function retrieves the following statistics from the database connection: - `open_connections`: Number of open connections to the database. - `in_use`: Number of connections currently in use. - `idle`: Number of idle connections. - `wait_count`: Number of times a connection has to wait. - `wait_duration`: Total time connections have spent waiting. - `max_idle_closed`: Number of connections closed due to exceeding idle time. - `max_lifetime_closed`: Number of connections closed due to exceeding their lifetime. **Evaluate Statistics**: Evaluates the collected statistics to provide a health message. Based on predefined thresholds, it updates the health message to indicate potential issues, such as heavy load or high wait events. ### Sample Output The `Health` function returns a JSON-like map structure with the following keys and example values: ```json { "idle": "1", "in_use": "0", "max_idle_closed": "0", "max_lifetime_closed": "0", "message": "It's healthy", "open_connections": "1", "status": "up", "wait_count": "0", "wait_duration": "0s" } ``` ## Code Implementation ```go func (s *service) Health() map[string]string { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() stats := make(map[string]string) err := s.db.PingContext(ctx) if err != nil { stats["status"] = "down" stats["error"] = fmt.Sprintf("db down: %v", err) log.Fatalf("db down: %v", err) return stats } stats["status"] = "up" stats["message"] = "It's healthy" dbStats := s.db.Stats() stats["open_connections"] = strconv.Itoa(dbStats.OpenConnections) stats["in_use"] = strconv.Itoa(dbStats.InUse) stats["idle"] = strconv.Itoa(dbStats.Idle) stats["wait_count"] = strconv.FormatInt(dbStats.WaitCount, 10) stats["wait_duration"] = dbStats.WaitDuration.String() stats["max_idle_closed"] = strconv.FormatInt(dbStats.MaxIdleClosed, 10) stats["max_lifetime_closed"] = strconv.FormatInt(dbStats.MaxLifetimeClosed, 10) if dbStats.OpenConnections > 40 { stats["message"] = "The database is experiencing heavy load." } if dbStats.WaitCount > 1000 { stats["message"] = "The database has a high number of wait events, indicating potential bottlenecks." } if dbStats.MaxIdleClosed > int64(dbStats.OpenConnections)/2 { stats["message"] = "Many idle connections are being closed, consider revising the connection pool settings." } if dbStats.MaxLifetimeClosed > int64(dbStats.OpenConnections)/2 { stats["message"] = "Many connections are being closed due to max lifetime, consider increasing max lifetime or revising the connection usage pattern." } return stats } ``` ================================================ FILE: docs/docs/endpoints-test/web.md ================================================ To test the /web endpoint when HTMX and Temp are used, you can simply open it in a web browser. This endpoint serves an HTML page with a form. Navigate to `http://localhost:PORT/web` This page contains a form with a single input field and a submit button. Upon submitting the form, "Hello, [input]" will be displayed. ## Sample output ![htmx](../public/htmx.png) ## Terminal log ```bash make run 2024/05/28 20:42:06 "POST http://localhost:8070/hello HTTP/1.1" from 127.0.0.1:45494 - 200 24B in 53.23µs ``` ================================================ FILE: docs/docs/endpoints-test/websocket.md ================================================ ## Testing with WebSocat [WebSocat](https://github.com/vi/websocat) is a versatile tool for working with websockets from the command line. Below are some examples of using WebSocat to test the websocket endpoint: ```bash # Start server make run ``` ```bash # Connect to the websocket endpoint $ websocat ws://localhost:PORT/websocket ``` Replace `PORT` with the port number on which your server is running. ## Sample Output Upon successful connection, the client should start receiving timestamp messages from the server every 2 seconds. ```bash server timestamp: 1709046650354893857 server timestamp: 1709046652355956336 server timestamp: 1709046654357101642 server timestamp: 1709046656357202535 server timestamp: 1709046658358258120 server timestamp: 1709046660359338389 server timestamp: 1709046662360422533 server timestamp: 1709046664361194735 server timestamp: 1709046666362308678 server timestamp: 1709046668363390475 server timestamp: 1709046670364477838 server timestamp: 1709046672365193667 server timestamp: 1709046674366265199 server timestamp: 1709046676366564490 server timestamp: 1709046678367646090 server timestamp: 1709046680367851980 server timestamp: 1709046682368920527 ``` ================================================ FILE: docs/docs/index.md ================================================ --- hide: - toc --- ## Go Blueprint - Ultimate Golang Blueprint Library ![logo](./public/logo.png) Powerful CLI tool designed to streamline the process of creating Go projects with a robust and standardized structure. Not only does Go Blueprint facilitate project initialization, but it also offers seamless integration with popular Go frameworks, allowing you to focus on your application's code from the very beginning. ## Why Choose Go Blueprint? - **Easy Setup and Installation**: Go Blueprint simplifies the setup process, making it a breeze to install and get started with your Go projects. - **Pre-established Go Project Structure**: Save time and effort by having the entire Go project structure set up automatically. No need to worry about directory layouts or configuration files. - **HTTP Server Configuration Made Easy**: Whether you prefer Go's standard library HTTP package, Chi, Gin, Fiber, HttpRouter, Gorilla/mux or Echo, Go Blueprint caters to your server setup needs. - **Focus on Your Application Code**: With Go Blueprint handling the project scaffolding, you can dedicate more time and energy to developing your application logic. ## Project Structure Here's an overview of the project structure created by Go Blueprint when all options are utilized: ```bash / (Root) ├── .github/ │ └── workflows/ │ ├── go-test.yml # GitHub Actions workflow for running tests. │ └── release.yml # GitHub Actions workflow for releasing the application. ├── cmd/ │ ├── api/ │ │ └── main.go # Main file for starting the server. │ └── web/ │ ├── styles/ # Only for generating css will not be served public. │ │ └── input.css # Tailwind input file for compiling output.css with CLI when HTMX is used. │ ├── assets/ │ │ ├── css/ │ │ │ └── output.css # Generated CSS file. │ │ └── js/ │ │ └── htmx.min.js # HTMX library for dynamic HTML content. │ ├── base.templ # Base HTML template file. │ ├── base_templ.go # Generated Go code for base template. │ ├── efs.go # Includes assets into compiled binary. │ ├── hello.go # Logic for handling "hello" form. │ ├── hello.templ # Template file for the "hello" endpoint. │ └── hello_templ.go # Generated Go code for the "hello" template. ├── frontend/ # React advanced flag. Excludes HTMX. │ ├── node_modules/ # Node dependencies. │ ├── public/ │ │ ├── index.html │ │ └── favicon.ico │ ├── src/ # React source files. │ │ ├── App.tsx # Main React component. │ │ ├── assets/ # React assets directory. │ │ │ └── logo.svg │ │ ├── components/ # React components directory. │ │ │ ├── Header.tsx │ │ │ └── Footer.tsx │ │ ├── styles/ # CSS/SCSS styles directory. │ │ │ └── global.css │ │ └── index.tsx # Main entry point for React │ ├── eslint.config.js # ESLint configuration file. │ ├── index.html # Base HTML template. │ ├── package.json # Node.js package configuration. │ ├── package-lock.json # Lock file for Node.js dependencies. │ ├── README.md # README file for the React project. │ ├── tsconfig.app.json # TypeScript configuration for the app. │ ├── tsconfig.json # Root TypeScript configuration. │ ├── tsconfig.node.json # TypeScript configuration for Node.js. │ └── vite.config.ts # Vite configuration file. ├── internal/ │ ├── database/ │ │ ├── database_test.go # File containing integration tests for the database operations. │ │ └── database.go # File containing functions related to database operations. │ └── server/ │ ├── routes.go # File defining HTTP routes. │ ├── routes_test.go # Test file for testing HTTP handlers. │ └── server.go # Main server logic. ├── .air.toml # Configuration file for Air, a live-reload utility. ├── docker-compose.yml # Docker Compose configuration. ├── Dockerfile # Dockerfile configuration for the Go project. ├── .env # Environment configuration file. ├── .gitignore # File specifying which files and directories to ignore in Git. ├── go.mod # Go module file for managing dependencies. ├── .goreleaser.yml # Configuration file for GoReleaser, a tool for building and releasing binaries. ├── go.sum # Go module file containing checksums for dependencies. ├── Makefile # Makefile for defining and running commands. └── README.md # Project's README file containing essential information about the project. ``` This structure provides a comprehensive organization of your project, separating source code, tests, configurations and documentation. ================================================ FILE: docs/docs/installation.md ================================================ --- hide: - toc --- Go-Blueprint provides a convenient CLI tool to effortlessly set up your Go projects. Follow the steps below to install the tool on your system. ## Binary Installation To install the Go-Blueprint CLI tool as a binary, run the following command: ```sh go install github.com/melkeydev/go-blueprint@latest ``` This command installs the Go-Blueprint binary, automatically binding it to your `$GOPATH`. > If you’re using Zsh, you’ll need to add it manually to `~/.zshrc`. > After running the installation command, you need to update your `PATH` environment variable. To do this, you need to find out the correct `GOPATH` for your system. You can do this by running the following command: > Check your `GOPATH` > > ``` > go env GOPATH > ``` > > Then, add the following line to your `~/.zshrc` file: > > ``` > GOPATH=$HOME/go PATH=$PATH:/usr/local/go/bin:$GOPATH/bin > ``` > > Save the changes to your `~/.zshrc` file by running the following command: > > ``` > source ~/.zshrc > ``` ## NPM Install If you prefer using Node.js package manager, you can install Go-Blueprint via NPM. This method is convenient for developers who are already working in JavaScript/Node.js environments and want to integrate Go-Blueprint into their existing workflow. ```bash npm install -g @melkeydev/go-blueprint ``` The `-g` flag installs Go-Blueprint globally, making it accessible from any directory on your system. ## Homebrew Install For macOS and Linux users, Homebrew provides a simple way to install Go-Blueprint. Homebrew automatically handles dependencies and keeps the tool updated through its package management system. ```bash brew install go-blueprint ``` After installation via Homebrew, Go-Blueprint will be automatically added to your PATH, making it immediately available in your terminal. ## Building and Installing from Source If you prefer to build and install Go-Blueprint directly from the source code, you can follow these steps: Clone the Go-Blueprint repository from GitHub: ```sh git clone https://github.com/melkeydev/go-blueprint ``` Build the Go-Blueprint binary: ```sh go build ``` Install in your `$PATH` to make it accessible system-wide: ```sh go install ``` Verify the installation by running: ```sh go-blueprint version ``` This should display the version information of the installed Go-Blueprint. Now you have successfully built and installed Go-Blueprint from the source code. ================================================ FILE: docs/mkdocs.yml ================================================ ### Site metadata ### site_name: Go-Blueprint Docs site_description: Official documentation for Go-Blueprint project site_url: https://docs.go-blueprint.dev/ repo_url: https://github.com/Melkeydev/go-blueprint edit_uri: edit/main/docs/docs ### Build settings ### theme: name: material custom_dir: custom_theme/ theme: features: - navigation.instant - navigation.sections - navigation.footer - toc.flow palette: - scheme: default toggle: icon: material/brightness-7 name: Switch to dark mode - scheme: slate toggle: icon: material/brightness-4 name: Switch to light mode nav: - Home: index.md - Installation: installation.md - Blueprint UI: blueprint-ui.md - Project creation & default config: - Project init: creating-project/project-init.md - Makefile: creating-project/makefile.md - Air: creating-project/air.md - Blueprint Core: - Frameworks: blueprint-core/frameworks.md - DB Drivers: blueprint-core/db-drivers.md - Advanced Flag: - AF Usage: advanced-flag/advanced-flag.md - HTMX and Templ: advanced-flag/htmx-templ.md - Tailwind CSS: advanced-flag/tailwind.md - GoReleaser & GoTest CI: advanced-flag/goreleaser.md - Websocket: advanced-flag/websocket.md - Docker: advanced-flag/docker.md - React & Vite (TypeScript): advanced-flag/react-vite.md - Testing endpoints: - Server: endpoints-test/server.md - DB Health Endpoints: - SQL DBs: endpoints-test/sql.md - Redis: endpoints-test/redis.md - MongoDB: endpoints-test/mongo.md - ScyllaDB: endpoints-test/scylladb.md - Websocket: endpoints-test/websocket.md - Web Endpoint: endpoints-test/web.md extra: social: - icon: fontawesome/brands/discord link: https://discord.com/invite/HHZMSCu name: Discord - icon: fontawesome/brands/twitch link: https://www.twitch.tv/melkey name: Twitch - icon: fontawesome/brands/youtube link: https://www.youtube.com/@MelkeyDev name: YouTube - icon: fontawesome/brands/twitter link: https://x.com/MelkeyDev name: Twitter generator: false copyright: Copyright © 2025 Melkey ================================================ FILE: docs/requirements.txt ================================================ mkdocs==1.5.3 mkdocs-material==9.5.15 ================================================ FILE: go.mod ================================================ module github.com/melkeydev/go-blueprint go 1.23.0 require ( github.com/charmbracelet/bubbles v0.16.1 github.com/charmbracelet/bubbletea v0.24.2 github.com/charmbracelet/lipgloss v0.9.0 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 ) require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.18 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.2 // indirect github.com/rivo/uniseg v0.2.0 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.12.0 // indirect golang.org/x/term v0.6.0 // indirect golang.org/x/text v0.3.8 // indirect ) ================================================ FILE: go.sum ================================================ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= github.com/charmbracelet/lipgloss v0.9.0 h1:BHIM7U4vX77xGEld8GrTKspBMtSv7j0wxPCH73nrdxE= github.com/charmbracelet/lipgloss v0.9.0/go.mod h1:h8KDyaivONasw1Bhb4nWiKlk4P1wHPly+3+3v6EFMmA= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: main.go ================================================ package main import "github.com/melkeydev/go-blueprint/cmd" func main() { cmd.Execute() } ================================================ FILE: scripts/completions.sh ================================================ #!/bin/sh set -e rm -rf completions mkdir completions for sh in bash zsh fish; do go run main.go completion "$sh" >"completions/go-blueprint.$sh" done ================================================ FILE: scripts/create-npm-packages.sh ================================================ #!/bin/bash set -euo pipefail VERSION="$1" PACKAGE_NAME="@melkeydev/go-blueprint" MAIN_PACKAGE_DIR="npm-package" PLATFORM_PACKAGES_DIR="platform-packages" rm -rf "$MAIN_PACKAGE_DIR" "$PLATFORM_PACKAGES_DIR" mkdir -p "$MAIN_PACKAGE_DIR/bin" "$PLATFORM_PACKAGES_DIR" declare -A PLATFORM_MAP=( ["go-blueprint_${VERSION}_Darwin_all"]="darwin-x64,darwin-arm64" ["go-blueprint_${VERSION}_Linux_x86_64"]="linux-x64" ["go-blueprint_${VERSION}_Linux_arm64"]="linux-arm64" ["go-blueprint_${VERSION}_Windows_x86_64"]="win32-x64" ["go-blueprint_${VERSION}_Windows_arm64"]="win32-arm64" ) declare -A OS_MAP=( ["darwin-x64"]="darwin" ["darwin-arm64"]="darwin" ["linux-x64"]="linux" ["linux-arm64"]="linux" ["win32-x64"]="win32" ["win32-arm64"]="win32" ) declare -A CPU_MAP=( ["darwin-x64"]="x64" ["darwin-arm64"]="arm64" ["linux-x64"]="x64" ["linux-arm64"]="arm64" ["win32-x64"]="x64" ["win32-arm64"]="arm64" ) OPTIONAL_DEPS="" for archive in dist/*.tar.gz dist/*.zip; do if [ -f "$archive" ]; then archive_name=$(basename "$archive") archive_name="${archive_name%.tar.gz}" archive_name="${archive_name%.zip}" platform_keys="${PLATFORM_MAP[$archive_name]:-}" if [ -n "$platform_keys" ]; then echo "Processing $archive for platforms: $platform_keys" IFS=',' read -ra PLATFORM_ARRAY <<< "$platform_keys" for platform_key in "${PLATFORM_ARRAY[@]}"; do platform_key=$(echo "$platform_key" | xargs) echo " Creating package for platform: $platform_key" platform_package_dir="$PLATFORM_PACKAGES_DIR/go-blueprint-$platform_key" mkdir -p "$platform_package_dir/bin" if [[ "$archive" == *.tar.gz ]]; then tar -xzf "$archive" -C "$platform_package_dir/bin" else unzip -j "$archive" -d "$platform_package_dir/bin" fi for doc_file in README.md README README.txt LICENSE LICENSE.md LICENSE.txt; do if [ -f "$platform_package_dir/bin/$doc_file" ]; then mv "$platform_package_dir/bin/$doc_file" "$platform_package_dir/" fi done ls -l "$platform_package_dir/bin" chmod +x "$platform_package_dir/bin/"* os_value="${OS_MAP[$platform_key]}" cpu_value="${CPU_MAP[$platform_key]}" files_array='["bin/"]' for doc_file in README.md README README.txt LICENSE LICENSE.md LICENSE.txt; do if [ -f "$platform_package_dir/$doc_file" ]; then files_array="${files_array%]}, \"$doc_file\"]" fi done binary_name="go-blueprint" if [[ "$os_value" == "win32" ]]; then binary_name="go-blueprint.exe" fi cat > "$platform_package_dir/package.json" << EOF { "name": "$PACKAGE_NAME-$platform_key", "version": "$VERSION", "description": "Platform-specific binary for $PACKAGE_NAME ($platform_key)", "os": ["$os_value"], "cpu": ["$cpu_value"], "bin": { "go-blueprint": "bin/$binary_name" }, "files": $files_array, "repository": { "type": "git", "url": "https://github.com/Melkeydev/go-blueprint.git" }, "author": "Melkeydev", "license": "MIT" } EOF if [ -n "$OPTIONAL_DEPS" ]; then OPTIONAL_DEPS="$OPTIONAL_DEPS," fi OPTIONAL_DEPS="$OPTIONAL_DEPS\"$PACKAGE_NAME-$platform_key\": \"$VERSION\"" done fi fi done cat > "$MAIN_PACKAGE_DIR/bin/go-blueprint" << 'EOF' #!/usr/bin/env node const { execFileSync } = require('child_process') const packageName = '@melkeydev/go-blueprint' const platformPackages = { 'darwin-x64': `${packageName}-darwin-x64`, 'darwin-arm64': `${packageName}-darwin-arm64`, 'linux-x64': `${packageName}-linux-x64`, 'linux-arm64': `${packageName}-linux-arm64`, 'win32-x64': `${packageName}-win32-x64`, 'win32-arm64': `${packageName}-win32-arm64` } function getBinaryPath() { const platformKey = `${process.platform}-${process.arch}` const platformPackageName = platformPackages[platformKey] if (!platformPackageName) { console.error(`Platform ${platformKey} is not supported!`) process.exit(1) } try { const binaryName = process.platform === 'win32' ? 'go-blueprint.exe' : 'go-blueprint' const packagePath = platformPackageName.replace('@', '').replace('/', '-') return require.resolve(`${platformPackageName}/bin/${binaryName}`) } catch (e) { process.exit(1) } } try { const binaryPath = getBinaryPath() execFileSync(binaryPath, process.argv.slice(2), { stdio: 'inherit' }) } catch (error) { console.error('Failed to execute go-blueprint:', error.message) process.exit(1) } EOF chmod +x "$MAIN_PACKAGE_DIR/bin/go-blueprint" cat > "$MAIN_PACKAGE_DIR/package.json" << EOF { "name": "$PACKAGE_NAME", "version": "$VERSION", "description": "A CLI for scaffolding Go projects with modern tooling", "main": "index.js", "bin": { "go-blueprint": "bin/go-blueprint" }, "optionalDependencies": { $OPTIONAL_DEPS }, "keywords": ["go", "golang", "cli"], "author": "Melkeydev", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/Melkeydev/go-blueprint.git" }, "homepage": "https://github.com/Melkeydev/go-blueprint", "engines": { "node": ">=14.0.0" }, "files": [ "bin/", "index.js", "README.md" ] } EOF cat > "$MAIN_PACKAGE_DIR/index.js" << 'EOF' const { execFileSync } = require('child_process') const path = require('path') const binaryName = process.platform === 'win32' ? 'go-blueprint.exe' : 'go-blueprint' const packageName = '@melkeydev/go-blueprint' const platformPackages = { 'darwin-x64': `${packageName}-darwin-x64`, 'darwin-arm64': `${packageName}-darwin-arm64`, 'linux-x64': `${packageName}-linux-x64`, 'linux-arm64': `${packageName}-linux-arm64`, 'win32-x64': `${packageName}-win32-x64`, 'win32-arm64': `${packageName}-win32-arm64` } function getBinaryPath() { const platformKey = `${process.platform}-${process.arch}` const platformPackageName = platformPackages[platformKey] if (!platformPackageName) { throw new Error(`Platform ${platformKey} is not supported!`) } try { return require.resolve(`${platformPackageName}/bin/${binaryName}`) } catch (e) { throw new Error(`Platform-specific package ${platformPackageName} not found.`) } } module.exports = { getBinaryPath, run: function(...args) { const binaryPath = getBinaryPath() return execFileSync(binaryPath, args, { stdio: 'inherit' }) } } EOF first_platform_dir=$(ls -1d "$PLATFORM_PACKAGES_DIR"/* | head -1 2>/dev/null || echo "") if [ -n "$first_platform_dir" ] && [ -f "$first_platform_dir/README.md" ]; then cp "$first_platform_dir/README.md" "$MAIN_PACKAGE_DIR/" fi