diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index c8d7ee4..3b6379d 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -71,11 +71,12 @@ Community leaders will follow these Community Impact Guidelines in determining t ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, -available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. +available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html). Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at -https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. +[FAQ](https://www.contributor-covenant.org/faq). Translations are available at +[https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 27e25db..1a32fbf 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -5,9 +5,11 @@ When contributing to this repository, please first discuss the change you wish t Please note: we have a [code of conduct](https://github.com/gofiber/fiber-cli/blob/master/.github/CODE_OF_CONDUCT.md), please follow it in all your interactions with the `Fiber` project. ## Pull Requests or Comits + Titles always we must use prefix according to below: > ๐Ÿ”ฅ Feature, โ™ป๏ธ Refactor, ๐Ÿฉน Fix, ๐Ÿšจ Test, ๐Ÿ“š Doc, ๐ŸŽจ Style + - ๐Ÿ”ฅ Feature: Add flow to add person - โ™ป๏ธ Refactor: Rename file X to Y - ๐Ÿฉน Fix: Improve flow @@ -17,7 +19,7 @@ Titles always we must use prefix according to below: All pull request that contains a feature or fix is mandatory to have unit tests. Your PR is only to be merged if you respect this flow. -# ๐Ÿ‘ Contribute +## ๐Ÿ‘ Contribute If you want to say **thank you** and/or support the active development of `Fiber`: diff --git a/.github/ISSUE_TEMPLATE/---bug.md b/.github/ISSUE_TEMPLATE/---bug.md index 262629f..9f90984 100644 --- a/.github/ISSUE_TEMPLATE/---bug.md +++ b/.github/ISSUE_TEMPLATE/---bug.md @@ -7,4 +7,4 @@ assignees: '' --- -**Issue description** +## Issue description diff --git a/.github/ISSUE_TEMPLATE/---feature.md b/.github/ISSUE_TEMPLATE/---feature.md index c3baf50..26cadbc 100644 --- a/.github/ISSUE_TEMPLATE/---feature.md +++ b/.github/ISSUE_TEMPLATE/---feature.md @@ -7,10 +7,10 @@ assignees: '' --- -**Is your feature request related to a problem?** +## Is your feature request related to a problem? -**Describe the solution you'd like** +## Describe the solution you'd like -**Describe alternatives you've considered** +## Describe alternatives you've considered -**Additional context** +## Additional context diff --git a/.github/ISSUE_TEMPLATE/---question.md b/.github/ISSUE_TEMPLATE/---question.md index 690645d..65adfea 100644 --- a/.github/ISSUE_TEMPLATE/---question.md +++ b/.github/ISSUE_TEMPLATE/---question.md @@ -7,4 +7,4 @@ assignees: '' --- -**Question description** +## Question description diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 6dcf949..271ec56 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,4 +1,6 @@ -**Please provide enough information so that others can review your pull request:** +# Pull Request Guide + +Please provide enough information so that others can review your pull request: @@ -6,6 +8,6 @@ -**Commit formatting** +## Commit formatting -Use emojis on commit messages so it provides an easy way of identifying the purpose or intention of a commit. Check out the emoji cheatsheet here: https://gitmoji.carloscuesta.me/ \ No newline at end of file +Use emojis on commit messages so it provides an easy way of identifying the purpose or intention of a commit. Check out the emoji cheatsheet here: [https://gitmoji.carloscuesta.me](https://gitmoji.carloscuesta.me) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 6df7489..041e8cf 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,37 +1,50 @@ name-template: 'v$RESOLVED_VERSION' tag-template: 'v$RESOLVED_VERSION' categories: - - title: '๐Ÿš€ New' - labels: - - 'โœ๏ธ Feature' - - title: '๐Ÿงน Updates' - labels: - - '๐Ÿงน Updates' - - '๐Ÿค– Dependencies' - - title: '๐Ÿ› Fixes' - labels: - - 'โ˜ข๏ธ Bug' - - title: '๐Ÿ“š Documentation' - labels: - - '๐Ÿ“’ Documentation' + - title: 'โ— Breaking Changes' + labels: + - 'โ— BreakingChange' + - title: '๐Ÿš€ New' + labels: + - 'โœ๏ธ Feature' + - '๐Ÿ“ Proposal' + - title: '๐Ÿงน Updates' + labels: + - '๐Ÿงน Updates' + - 'โšก๏ธ Performance' + - title: '๐Ÿ› Fixes' + labels: + - 'โ˜ข๏ธ Bug' + - title: '๐Ÿ› ๏ธ Maintenance' + labels: + - '๐Ÿค– Dependencies' + - title: '๐Ÿ“š Documentation' + labels: + - '๐Ÿ“’ Documentation' change-template: '- $TITLE (#$NUMBER)' change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. +exclude-contributors: + - dependabot + - dependabot[bot] version-resolver: - major: - labels: - - 'major' - minor: - labels: - - 'minor' - - 'โœ๏ธ Feature' - patch: - labels: - - 'patch' - - '๐Ÿ“’ Documentation' - - 'โ˜ข๏ธ Bug' - - '๐Ÿค– Dependencies' - - '๐Ÿงน Updates' - default: patch + major: + labels: + - 'major' + - 'โ— BreakingChange' + minor: + labels: + - 'minor' + - 'โœ๏ธ Feature' + - '๐Ÿ“ Proposal' + patch: + labels: + - 'patch' + - '๐Ÿ“’ Documentation' + - 'โ˜ข๏ธ Bug' + - '๐Ÿค– Dependencies' + - '๐Ÿงน Updates' + - 'โšก๏ธ Performance' + default: patch autolabeler: - label: '๐Ÿ“’ Documentation' title: diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 6ca66a2..76d68a3 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -1,16 +1,41 @@ -on: [push, pull_request] -name: Linter +name: golangci-lint + +on: + push: + branches: + - master + - main + paths-ignore: + - "**/*.md" + pull_request: + paths-ignore: + - "**/*.md" + +permissions: + # Required: allow read access to the content for analysis. + contents: read + # Optional: allow read access to pull request. Use with `only-new-issues` option. + pull-requests: read + # Optional: Allow write access to checks to allow the action to annotate code in the PR. + checks: write + jobs: - golint: - runs-on: ubuntu-latest - steps: - - name: Check out repository - uses: actions/checkout@v4 - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: 1.24.x - - name: Run Golint - uses: reviewdog/action-golangci-lint@v2 - with: - golangci_lint_flags: "--tests=false" + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + # NOTE: Keep this in sync with the version from go.mod + go-version: "1.24.x" + cache: false + + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + # NOTE: Keep this in sync with the version from .golangci.yml + version: v1.64.7 + # NOTE(ldez): temporary workaround + install-mode: goinstall diff --git a/.github/workflows/markdown.yml b/.github/workflows/markdown.yml new file mode 100644 index 0000000..4358bc8 --- /dev/null +++ b/.github/workflows/markdown.yml @@ -0,0 +1,22 @@ +name: markdownlint + +on: + push: + branches: + - master + - main + pull_request: + +jobs: + markdownlint: + runs-on: ubuntu-latest + steps: + - name: Fetch Repository + uses: actions/checkout@v4 + + - name: Run markdownlint-cli2 + uses: DavidAnson/markdownlint-cli2-action@v20 + with: + globs: | + **/*.md + #vendor diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml deleted file mode 100644 index 3bb1c3e..0000000 --- a/.github/workflows/security.yml +++ /dev/null @@ -1,12 +0,0 @@ -on: [push, pull_request] -name: Security -jobs: - Gosec: - runs-on: ubuntu-latest - steps: - - name: Check out repository - uses: actions/checkout@v4 - - name: Run Gosec - uses: securego/gosec@v2 - with: args: -exclude-dir=internal/*/ ./... - diff --git a/.github/workflows/vulncheck.yml b/.github/workflows/vulncheck.yml new file mode 100644 index 0000000..31c252e --- /dev/null +++ b/.github/workflows/vulncheck.yml @@ -0,0 +1,34 @@ +name: Run govulncheck + +on: + push: + branches: + - master + - main + paths-ignore: + - "**/*.md" + pull_request: + paths-ignore: + - "**/*.md" + +jobs: + govulncheck-check: + runs-on: ubuntu-latest + env: + GO111MODULE: on + steps: + - name: Fetch Repository + uses: actions/checkout@v4 + + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: "stable" + check-latest: true + cache: false + + - name: Install Govulncheck + run: go install golang.org/x/vuln/cmd/govulncheck@latest + + - name: Run Govulncheck + run: govulncheck ./... diff --git a/.gitignore b/.gitignore index 829ad8a..120a182 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,31 @@ +# Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib + +# Test binary, built with `go test -c` *.test -*.out -.idea/ +*.tmp + +# Output of the go coverage tool +**/*.out + +# IDE files +.vscode +.DS_Store +.idea + +# Misc +*.test.gz +*.test.zst +*.test.br +*.pprof +*.workspace + +# Dependencies +/vendor/ +vendor/ +vendor +/Godeps/ diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..8c0bc8b --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,378 @@ +# v1.2.0. Created based on golangci-lint v1.57.1 + +run: + timeout: 5m + modules-download-mode: readonly + allow-serial-runners: true + +output: + sort-results: true + +linters-settings: + depguard: + rules: + all: + list-mode: lax + deny: + - pkg: "flag" + desc: '`flag` package is only allowed in main.go' + - pkg: "io/ioutil" + desc: '`io/ioutil` package is deprecated, use the `io` and `os` package instead' + # TODO: Prevent using these without a reason + # - pkg: "reflect" + # desc: '`reflect` package is dangerous to use' + # - pkg: "unsafe" + # desc: '`unsafe` package is dangerous to use' + + errcheck: + check-type-assertions: true + check-blank: true + disable-default-exclusions: true + exclude-functions: + - '(*bytes.Buffer).Write' # always returns nil error + - '(*github.com/valyala/bytebufferpool.ByteBuffer).Write' # always returns nil error + - '(*github.com/valyala/bytebufferpool.ByteBuffer).WriteByte' # always returns nil error + - '(*github.com/valyala/bytebufferpool.ByteBuffer).WriteString' # always returns nil error + + errchkjson: + report-no-exported: true + + exhaustive: + check-generated: true + default-signifies-exhaustive: true + + forbidigo: + forbid: + - ^print(ln)?$ + - ^fmt\.Print(f|ln)?$ + - ^http\.Default(Client|ServeMux|Transport)$ + # TODO: Eventually enable these patterns + # - ^panic$ + # - ^time\.Sleep$ + analyze-types: true + + gci: + sections: + - standard + - default + - blank + - dot + # - alias + custom-order: true + + goconst: + numbers: true + + gocritic: + # TODO: Uncomment the following lines + enabled-tags: + - diagnostic + # - style + # - performance + # - experimental + # - opinionated + settings: + captLocal: + paramsOnly: false + elseif: + skipBalanced: false + underef: + skipRecvDeref: false + # NOTE: Set this option to false if other projects rely on this project's code + # unnamedResult: + # checkExported: false + + gofumpt: + module-path: github.com/gofiber/cli + extra-rules: true + + gosec: + excludes: + - G104 # TODO: Enable this again. Mostly provided by errcheck + config: + global: + # show-ignored: true # TODO: Enable this + audit: true + + govet: + enable-all: true + disable: + - shadow + + grouper: + # const-require-grouping: true # TODO: Enable this + import-require-single-import: true + import-require-grouping: true + # var-require-grouping: true # TODO: Conflicts with gofumpt + + loggercheck: + require-string-key: true + no-printf-like: true + + misspell: + locale: US + + nolintlint: + require-explanation: true + require-specific: true + + nonamedreturns: + report-error-in-defer: true + + perfsprint: + err-error: true + + predeclared: + q: true + + promlinter: + strict: true + + # TODO: Enable this + # reassign: + # patterns: + # - '.*' + + revive: + enable-all-rules: true + rules: + # Provided by gomnd linter + - name: add-constant + disabled: true + - name: argument-limit + disabled: true + # Provided by bidichk + - name: banned-characters + disabled: true + - name: cognitive-complexity + disabled: true + - name: confusing-results + disabled: true + - name: comment-spacings + arguments: + - nolint + disabled: true # TODO: Do not disable + - name: cyclomatic + disabled: true + # TODO: Enable this check. Currently disabled due to upstream bug. + # - name: enforce-repeated-arg-type-style + # arguments: + # - short + - name: enforce-slice-style + arguments: + - make + disabled: true # TODO: Do not disable + - name: exported + disabled: true + - name: file-header + disabled: true + - name: function-result-limit + arguments: [3] + - name: function-length + disabled: true + - name: line-length-limit + disabled: true + - name: max-public-structs + disabled: true + - name: modifies-parameter + disabled: true + - name: nested-structs + disabled: true # TODO: Do not disable + - name: package-comments + disabled: true + - name: optimize-operands-order + disabled: true + - name: unchecked-type-assertion + disabled: true # TODO: Do not disable + - name: unhandled-error + disabled: true + + stylecheck: + checks: + - all + - -ST1000 + - -ST1020 + - -ST1021 + - -ST1022 + + tagalign: + strict: true + + tagliatelle: + case: + rules: + json: snake + + tenv: + all: true + + testifylint: + enable-all: true + + testpackage: + skip-regexp: "^$" + + unparam: + # NOTE: Set this option to false if other projects rely on this project's code + check-exported: false + + unused: + # TODO: Uncomment these two lines + # parameters-are-used: false + # local-variables-are-used: false + # NOTE: Set these options to true if other projects rely on this project's code + field-writes-are-uses: true + # exported-is-used: true # TODO: Fix issues with this option (upstream) + exported-fields-are-used: true + + usestdlibvars: + http-method: true + http-status-code: true + time-weekday: false # TODO: Set to true + time-month: false # TODO: Set to true + time-layout: false # TODO: Set to true + crypto-hash: true + default-rpc-path: true + sql-isolation-level: true + tls-signature-scheme: true + constant-kind: true + + wrapcheck: + ignorePackageGlobs: + - github.com/gofiber/fiber/* + - github.com/valyala/fasthttp + +issues: + exclude-use-default: false + exclude-case-sensitive: true + max-issues-per-linter: 0 + max-same-issues: 0 + exclude-files: + - '_msgp\.go' + - '_msgp_test\.go' + exclude-rules: + - linters: + - err113 + text: 'do not define dynamic errors, use wrapped static errors instead*' + - path: log/.*\.go + linters: + - depguard + # Exclude some linters from running on tests files. + - path: _test\.go + linters: + - bodyclose + - err113 + - source: 'fmt.Fprintf?' + linters: + - errcheck + - revive + +linters: + enable: + - asasalint + - asciicheck + - bidichk + - bodyclose + # - containedctx TODO: Enable + - contextcheck + # - cyclop + - decorder + - depguard + - dogsled + # - dupl + - dupword + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - exhaustive + # - exhaustivestruct + # - exhaustruct + - copyloopvar + - forbidigo + - forcetypeassert + # - funlen + # - gci # TODO: Enable + - ginkgolinter + # - gocheckcompilerdirectives # TODO: Enable + # - gochecknoglobals # TODO: Enable + # - gochecknoinits # TODO: Enable + - gochecksumtype + # - gocognit + - goconst # TODO: Enable + - gocritic + # - gocyclo + # - godot + # - godox + - err113 + - gofmt + - gofumpt + # - goheader + - goimports + # - mnd # TODO: Enable + - gomoddirectives + # - gomodguard + - goprintffuncname + - gosec + - gosimple + # - gosmopolitan # TODO: Enable + - govet + - grouper + # - ifshort # TODO: Enable + # - importas + # - inamedparam + - ineffassign + # - interfacebloat + # - interfacer + # - ireturn + # - lll + - loggercheck + # - maintidx + - makezero + # - maligned + - mirror + - misspell + - musttag + - nakedret + # - nestif + - nilerr + - nilnil + # - nlreturn + - noctx + - nolintlint + # - nonamedreturns + - nosprintfhostport + # - paralleltest # TODO: Enable + - perfsprint + # - prealloc + - predeclared + - promlinter + - protogetter + - reassign + - revive + - rowserrcheck + # - scopelint # TODO: Enable + - sloglint + - spancheck + - sqlclosecheck + - staticcheck + - stylecheck + # - tagalign # TODO: Enable + - tagliatelle + - testableexamples + - testifylint + # - testpackage # TODO: Enable + - thelper + - tparallel + - typecheck + - unconvert + - unparam + - unused + - usestdlibvars + # - varnamelen + # - wastedassign # TODO: Enable + - whitespace + - wrapcheck + # - wsl + - zerologlint diff --git a/.markdownlint.yml b/.markdownlint.yml new file mode 100644 index 0000000..71d7b30 --- /dev/null +++ b/.markdownlint.yml @@ -0,0 +1,248 @@ +# Example markdownlint configuration with all properties set to their default value + +# Default state for all rules +default: true + +# Path to configuration file to extend +extends: null + +# MD001/heading-increment : Heading levels should only increment by one level at a time : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md001.md +MD001: true + +# MD003/heading-style : Heading style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md003.md +MD003: + # Heading style + style: "consistent" + +# MD004/ul-style : Unordered list style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md004.md +MD004: + # List style + style: "consistent" + +# MD005/list-indent : Inconsistent indentation for list items at the same level : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md005.md +MD005: true + +# MD007/ul-indent : Unordered list indentation : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md007.md +MD007: + # Spaces for indent + indent: + # Whether to indent the first level of the list + start_indented: false + # Spaces for first level indent (when start_indented is set) + start_indent: 2 + +# MD009/no-trailing-spaces : Trailing spaces : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md009.md +MD009: + # Spaces for line break + br_spaces: 2 + # Allow spaces for empty lines in list items + list_item_empty_lines: false + # Include unnecessary breaks + strict: true + +# MD010/no-hard-tabs : Hard tabs : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md010.md +MD010: + # Include code blocks + code_blocks: true + # Fenced code languages to ignore + ignore_code_languages: [] + # Number of spaces for each hard tab + spaces_per_tab: 4 + +# MD011/no-reversed-links : Reversed link syntax : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md011.md +MD011: true + +# MD012/no-multiple-blanks : Multiple consecutive blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md012.md +MD012: + # Consecutive blank lines + maximum: 1 + +# MD013/line-length : Line length : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md013.md +MD013: false + +# MD014/commands-show-output : Dollar signs used before commands without showing output : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md014.md +MD014: true + +# MD018/no-missing-space-atx : No space after hash on atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md018.md +MD018: true + +# MD019/no-multiple-space-atx : Multiple spaces after hash on atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md019.md +MD019: true + +# MD020/no-missing-space-closed-atx : No space inside hashes on closed atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md020.md +MD020: true + +# MD021/no-multiple-space-closed-atx : Multiple spaces inside hashes on closed atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md021.md +MD021: true + +# MD022/blanks-around-headings : Headings should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md022.md +MD022: + # Blank lines above heading + lines_above: 1 + # Blank lines below heading + lines_below: 1 + +# MD023/heading-start-left : Headings must start at the beginning of the line : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md023.md +MD023: true + +# MD024/no-duplicate-heading : Multiple headings with the same content : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md024.md +MD024: false + +# MD025/single-title/single-h1 : Multiple top-level headings in the same document : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md025.md +MD025: + # Heading level + level: 1 + # RegExp for matching title in front matter + front_matter_title: "^\\s*title\\s*[:=]" + +# MD026/no-trailing-punctuation : Trailing punctuation in heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md026.md +MD026: + # Punctuation characters + punctuation: ".,;:!ใ€‚๏ผŒ๏ผ›๏ผš๏ผ" + +# MD027/no-multiple-space-blockquote : Multiple spaces after blockquote symbol : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md027.md +MD027: true + +# MD028/no-blanks-blockquote : Blank line inside blockquote : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md028.md +MD028: true + +# MD029/ol-prefix : Ordered list item prefix : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md029.md +MD029: + # List style + style: "one_or_ordered" + +# MD030/list-marker-space : Spaces after list markers : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md030.md +MD030: + # Spaces for single-line unordered list items + ul_single: 1 + # Spaces for single-line ordered list items + ol_single: 1 + # Spaces for multi-line unordered list items + ul_multi: 1 + # Spaces for multi-line ordered list items + ol_multi: 1 + +# MD031/blanks-around-fences : Fenced code blocks should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md031.md +MD031: + # Include list items + list_items: true + +# MD032/blanks-around-lists : Lists should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md032.md +MD032: true + +# MD033/no-inline-html : Inline HTML : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md033.md +MD033: false + +# MD034/no-bare-urls : Bare URL used : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md034.md +MD034: true + +# MD035/hr-style : Horizontal rule style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md035.md +MD035: + # Horizontal rule style + style: "consistent" + +# MD036/no-emphasis-as-heading : Emphasis used instead of a heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md036.md +MD036: + # Punctuation characters + punctuation: ".,;:!?ใ€‚๏ผŒ๏ผ›๏ผš๏ผ๏ผŸ" + +# MD037/no-space-in-emphasis : Spaces inside emphasis markers : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md037.md +MD037: true + +# MD038/no-space-in-code : Spaces inside code span elements : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md038.md +MD038: true + +# MD039/no-space-in-links : Spaces inside link text : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md039.md +MD039: true + +# MD040/fenced-code-language : Fenced code blocks should have a language specified : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md040.md +MD040: + # List of languages + allowed_languages: [] + # Require language only + language_only: false + +# MD041/first-line-heading/first-line-h1 : First line in a file should be a top-level heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md041.md +MD041: + # Heading level + level: 1 + # RegExp for matching title in front matter + front_matter_title: "^\\s*title\\s*[:=]" + +# MD042/no-empty-links : No empty links : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md042.md +MD042: true + +# MD043/required-headings : Required heading structure : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md043.md +MD043: false + +# MD044/proper-names : Proper names should have the correct capitalization : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md044.md +MD044: + # List of proper names + names: [] + # Include code blocks + code_blocks: true + # Include HTML elements + html_elements: true + +# MD045/no-alt-text : Images should have alternate text (alt text) : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md045.md +MD045: false + +# MD046/code-block-style : Code block style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md046.md +MD046: + # Block style + style: "fenced" + +# MD047/single-trailing-newline : Files should end with a single newline character : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md047.md +MD047: true + +# MD048/code-fence-style : Code fence style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md048.md +MD048: + # Code fence style + style: "backtick" + +# MD049/emphasis-style : Emphasis style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md049.md +MD049: + # Emphasis style + style: "consistent" + +# MD050/strong-style : Strong style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md050.md +MD050: + # Strong style + style: "consistent" + +# MD051/link-fragments : Link fragments should be valid : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md051.md +MD051: true + +# MD052/reference-links-images : Reference links and images should use a label that is defined : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md052.md +MD052: + # Include shortcut syntax + shortcut_syntax: false + +# MD053/link-image-reference-definitions : Link and image reference definitions should be needed : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md053.md +MD053: + # Ignored definitions + ignored_definitions: + - "//" + +# MD054/link-image-style : Link and image style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md054.md +MD054: + # Allow autolinks + autolink: false + # Allow inline links and images + inline: true + # Allow full reference links and images + full: true + # Allow collapsed reference links and images + collapsed: true + # Allow shortcut reference links and images + shortcut: true + # Allow URLs as inline links + url_inline: true + +# MD055/table-pipe-style : Table pipe style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md055.md +MD055: + # Table pipe style + style: "consistent" + +# MD056/table-column-count : Table column count : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md056.md +MD056: true diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..41eb8f5 --- /dev/null +++ b/Makefile @@ -0,0 +1,65 @@ +## help: ๐Ÿ’ก Display available commands +.PHONY: help +help: + @echo 'โšก๏ธ GoFiber/Cli Development:' + @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' + +## audit: ๐Ÿš€ Conduct quality checks +.PHONY: audit +audit: + go mod verify + go vet ./... + go run golang.org/x/vuln/cmd/govulncheck@latest ./... + +## benchmark: ๐Ÿ“ˆ Benchmark code performance +.PHONY: benchmark +benchmark: + go test ./... -benchmem -bench=. -run=^Benchmark_$ + +## coverage: โ˜‚๏ธ Generate coverage report +.PHONY: coverage +coverage: + go run gotest.tools/gotestsum@latest -f testname -- ./... -race -count=1 -coverprofile=/tmp/coverage.out -covermode=atomic + go tool cover -html=/tmp/coverage.out + +## format: ๐ŸŽจ Fix code format issues +.PHONY: format +format: + go run mvdan.cc/gofumpt@latest -w -l . + +## markdown: ๐ŸŽจ Find markdown format issues (Requires markdownlint-cli) +.PHONY: markdown +markdown: + markdownlint-cli2 "**/*.md" "#vendor" + +## lint: ๐Ÿšจ Run lint checks +.PHONY: lint +lint: + go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.0 run ./... + +## test: ๐Ÿšฆ Execute all tests +.PHONY: test +test: + go run gotest.tools/gotestsum@latest -f testname -- ./... -race -count=1 -shuffle=on + +## longtest: ๐Ÿšฆ Execute all tests 10x +.PHONY: longtest +longtest: + go run gotest.tools/gotestsum@latest -f testname -- ./... -race -count=15 -shuffle=on + +## tidy: ๐Ÿ“Œ Clean and tidy dependencies +.PHONY: tidy +tidy: + go mod tidy -v + +## betteralign: ๐Ÿ“ Optimize alignment of fields in structs +.PHONY: betteralign +betteralign: + go run github.com/dkorunic/betteralign/cmd/betteralign@latest -test_files -generated_files -apply ./... + +## generate: โšก๏ธ Generate msgp && interface implementations +.PHONY: generate +generate: + go install github.com/tinylib/msgp@latest + go install github.com/vburenin/ifacemaker@975a95966976eeb2d4365a7fb236e274c54da64c + go generate ./... diff --git a/README.md b/README.md index 8e2d085..b6e2b58 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,55 @@ # cli + Fiber Command Line Interface [![Packaging status](https://repology.org/badge/vertical-allrepos/fiber-cli.svg)](https://repology.org/project/fiber-cli/versions) -# Installation +## Installation + Requires Go 1.24 or later. ```bash go install github.com/gofiber/cli/fiber@latest ``` -# Commands +## Commands + ## fiber + ### Synopsis ๐Ÿš€ Fiber is an Express inspired web framework written in Go with ๐Ÿ’– - -Learn more on https://gofiber.io - + +Learn more on [gofiber.io](https://gofiber.io) + CLI version v0.0.x ### Options -``` +```text -h, --help help for fiber ``` ## fiber dev + ### Synopsis Rerun the fiber project if watched files changed -``` +```bash fiber dev [flags] ``` - ### Examples -``` +```bash fiber dev --pre-run="command1 flag,command2 flag" Pre run specific commands before running the project ``` ### Options -``` +```text -a, --args strings arguments for exec -d, --delay duration delay to trigger rerun (default 1s) -D, --exclude_dirs strings ignore these directories (default [assets,tmp,vendor,node_modules]) @@ -58,17 +62,18 @@ fiber dev [flags] ``` ## fiber new + ### Synopsis Generate a new fiber project -``` +```bash fiber new PROJECT [module name] [flags] ``` ### Examples -``` +```bash fiber new fiber-demo Generates a project with go module name fiber-demo @@ -90,38 +95,57 @@ fiber new PROJECT [module name] [flags] ### Options -``` +```text -h, --help help for new -r, --repo string complex boilerplate repo name in github or other repo url (default "gofiber/boilerplate") -t, --template string basic|complex (default "basic") ``` +## fiber migrate + +### Synopsis + +Migrate Fiber project version to a newer version + +```bash +fiber migrate --to 3.0.0 +``` + +### Options + +```text + -t, --to string Migrate to a specific version e.g:3.0.0 Format: X.Y.Z + -h, --help help for migrate +``` + ## fiber upgrade + ### Synopsis Upgrade Fiber cli if a newer version is available -``` +```bash fiber upgrade [flags] ``` ### Options -``` +```text -h, --help help for upgrade ``` ## fiber version + ### Synopsis Print the local and released version number of fiber -``` +```bash fiber version [flags] ``` ### Options -``` +```text -h, --help help for version ``` diff --git a/cmd/dev.go b/cmd/dev.go index cdde950..b7aeccc 100644 --- a/cmd/dev.go +++ b/cmd/dev.go @@ -5,7 +5,6 @@ import ( "context" "fmt" "io" - "io/ioutil" "log" "os" "os/exec" @@ -43,6 +42,10 @@ func init() { "arguments for exec") } +const ( + windowsOS = "windows" +) + // devCmd reruns the fiber project if watched files changed var devCmd = &cobra.Command{ Use: "dev", @@ -58,19 +61,20 @@ func devRunE(_ *cobra.Command, _ []string) error { type config struct { root string target string - binPath string extensions []string excludeDirs []string excludeFiles []string - delay time.Duration preRun []string args []string + delay time.Duration } type escort struct { - config + ctx context.Context + stdoutPipe io.ReadCloser + stderrPipe io.ReadCloser + compiling atomic.Value - ctx context.Context terminate context.CancelFunc w *fsnotify.Watcher @@ -78,17 +82,17 @@ type escort struct { watcherErrors chan error sig chan os.Signal - wg sync.WaitGroup + bin *exec.Cmd + hitCh chan struct{} + hitFunc func() - binPath string - bin *exec.Cmd - stdoutPipe io.ReadCloser - stderrPipe io.ReadCloser - hitCh chan struct{} - hitFunc func() - compiling atomic.Value + binPath string preRunCommands [][]string + + config + + wg sync.WaitGroup } func newEscort(c config) *escort { @@ -99,16 +103,20 @@ func newEscort(c config) *escort { } } -func (e *escort) run() (err error) { - if err = e.init(); err != nil { - return +func (e *escort) run() error { + if err := e.init(); err != nil { + return err } log.Println("Welcome to fiber dev ๐Ÿ‘‹") defer func() { - _ = e.w.Close() - _ = os.Remove(e.binPath) + if err := e.w.Close(); err != nil { + log.Printf("Failed to close watcher: %v", err) + } + if err := os.Remove(e.binPath); err != nil { + log.Printf("Failed to remove bin: %v", err) + } }() e.wg.Add(3) @@ -128,9 +136,10 @@ func (e *escort) run() (err error) { return nil } -func (e *escort) init() (err error) { +func (e *escort) init() error { + var err error if e.w, err = fsnotify.NewWatcher(); err != nil { - return + return fmt.Errorf("failed to create watcher: %w", err) } e.watcherEvents = e.w.Events @@ -140,22 +149,20 @@ func (e *escort) init() (err error) { // normalize root if e.root, err = filepath.Abs(e.root); err != nil { - return + return fmt.Errorf("failed to get abs path for root: %w", err) } // create bin target - var f *os.File - if f, err = ioutil.TempFile("", ""); err != nil { - return + f, err := os.CreateTemp("", "") + if err != nil { + return fmt.Errorf("failed to create temp file: %w", err) + } + if cerr := f.Close(); cerr != nil { + return fmt.Errorf("failed to close temp file: %w", cerr) } - defer func() { - if e := f.Close(); e != nil { - err = e - } - }() e.binPath = f.Name() - if runtime.GOOS == "windows" { + if runtime.GOOS == windowsOS { e.binPath += ".exe" } @@ -167,7 +174,7 @@ func (e *escort) init() (err error) { e.preRunCommands = parsePreRunCommands(c.preRun) - return + return nil } func (e *escort) watchingFiles() { @@ -244,8 +251,10 @@ func (e *escort) watchingBin() { } func (e *escort) runBin() { - if ok := e.compiling.Load(); ok != nil && ok.(bool) { - return + if ok := e.compiling.Load(); ok != nil { + if val, ok := ok.(bool); ok && val { + return + } } e.doPreRun() @@ -289,10 +298,14 @@ func (e *escort) runBin() { func (e *escort) cleanOldBin() { defer func() { if e.stdoutPipe != nil { - _ = e.stdoutPipe.Close() + if err := e.stdoutPipe.Close(); err != nil { + log.Printf("Failed to close stdout pipe: %v", err) + } } if e.stderrPipe != nil { - _ = e.stderrPipe.Close() + if err := e.stderrPipe.Close(); err != nil { + log.Printf("Failed to close stderr pipe: %v", err) + } } }() @@ -300,11 +313,13 @@ func (e *escort) cleanOldBin() { log.Println("Killing old pid", pid) var err error - if runtime.GOOS == "windows" { + if runtime.GOOS == windowsOS { err = execCommand("TASKKILL", "/T", "/F", "/PID", strconv.Itoa(pid)).Run() } else { err = e.bin.Process.Kill() - _, _ = e.bin.Process.Wait() + if _, waitErr := e.bin.Process.Wait(); waitErr != nil { + log.Printf("Failed to wait for process %d: %v", pid, waitErr) + } } if err != nil { @@ -319,13 +334,21 @@ func (e *escort) watchingPipes() { if e.stdoutPipe, err = e.bin.StdoutPipe(); err != nil { log.Printf("Failed to get stdout pipe: %s", err) } else { - go func() { _, _ = io.Copy(os.Stdout, e.stdoutPipe) }() + go func() { + if _, err := io.Copy(os.Stdout, e.stdoutPipe); err != nil { + log.Printf("Failed to copy stdout: %v", err) + } + }() } if e.stderrPipe, err = e.bin.StderrPipe(); err != nil { log.Printf("Failed to get stderr pipe: %s", err) } else { - go func() { _, _ = io.Copy(os.Stderr, e.stderrPipe) }() + go func() { + if _, err := io.Copy(os.Stderr, e.stderrPipe); err != nil { + log.Printf("Failed to copy stderr: %v", err) + } + }() } } @@ -403,12 +426,20 @@ func (e *escort) doPreRun() { cmd := execCommand(command[0], command[1:]...) out, err := cmd.CombinedOutput() var buf bytes.Buffer - _, _ = buf.WriteString(fmt.Sprintf("Pre running %s... ", command)) + if _, werr := buf.WriteString(fmt.Sprintf("Pre running %s... ", command)); werr != nil { + log.Printf("Failed to write to buffer: %v", werr) + } if err != nil { - _, _ = buf.WriteString(err.Error()) - _, _ = buf.WriteString(":") + if _, werr := buf.WriteString(err.Error()); werr != nil { + log.Printf("Failed to write error to buffer: %v", werr) + } + if _, werr := buf.WriteString(":"); werr != nil { + log.Printf("Failed to write colon to buffer: %v", werr) + } + } + if _, werr := buf.Write(out); werr != nil { + log.Printf("Failed to write output to buffer: %v", werr) } - _, _ = buf.Write(out) log.Print(buf.String()) } } @@ -431,7 +462,7 @@ func parsePreRunCommands(commands []string) (list [][]string) { list = append(list, r) } } - return + return list } const ( diff --git a/cmd/dev_test.go b/cmd/dev_test.go index 44d2ac0..d1eeb9e 100644 --- a/cmd/dev_test.go +++ b/cmd/dev_test.go @@ -3,7 +3,7 @@ package cmd import ( "context" "errors" - "io/ioutil" + "io" "os" "os/exec" "path/filepath" @@ -16,6 +16,7 @@ import ( "github.com/fsnotify/fsnotify" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_Dev_Escort_New(t *testing.T) { @@ -30,28 +31,26 @@ func Test_Dev_Escort_Init(t *testing.T) { at := assert.New(t) e := getEscort() - at.Nil(e.init()) + require.NoError(t, e.init()) at.Contains(e.root, "cli") at.NotEmpty(e.binPath) - if runtime.GOOS != "windows" { - at.Nil(os.Remove(e.binPath)) + if runtime.GOOS != windowsOS { + require.NoError(t, os.Remove(e.binPath)) } } func Test_Dev_Escort_Run(t *testing.T) { - at := assert.New(t) - setupCmd() defer teardownCmd() e := getEscort() var err error - e.root, err = ioutil.TempDir("", "test_run") - at.Nil(err) + e.root, err = os.MkdirTemp("", "test_run") + require.NoError(t, err) defer func() { - at.Nil(os.RemoveAll(e.root)) + require.NoError(t, os.RemoveAll(e.root)) }() e.sig = make(chan os.Signal, 1) @@ -61,7 +60,7 @@ func Test_Dev_Escort_Run(t *testing.T) { e.sig <- syscall.SIGINT }() - at.Nil(e.run()) + require.NoError(t, e.run()) } func Test_Dev_Escort_RunBin(t *testing.T) { @@ -72,9 +71,9 @@ func Test_Dev_Escort_RunBin(t *testing.T) { e.bin = exec.Command("go", "version") _, err := e.bin.CombinedOutput() - assert.Nil(t, err) + require.NoError(t, err) - rc := ioutil.NopCloser(strings.NewReader("")) + rc := io.NopCloser(strings.NewReader("")) e.stdoutPipe = rc e.stderrPipe = rc @@ -87,7 +86,7 @@ func Test_Dev_Escort_WatchingPipes(t *testing.T) { e := getEscort() e.bin = exec.Command("go", "version") _, err := e.bin.CombinedOutput() - assert.Nil(t, err) + require.NoError(t, err) e.watchingPipes() } @@ -120,35 +119,35 @@ func Test_Dev_Escort_WatchingFiles(t *testing.T) { ) e := getEscort() - e.hitCh = make(chan struct{}) + e.hitCh = make(chan struct{}, 2) e.w, err = fsnotify.NewWatcher() - at.Nil(err) + require.NoError(t, err) + defer func() { require.NoError(t, e.w.Close()) }() e.extensions = []string{"go"} e.watcherEvents = make(chan fsnotify.Event) e.watcherErrors = make(chan error) - e.root, err = ioutil.TempDir("", "test_watch") - at.Nil(err) + e.root, err = os.MkdirTemp("", "test_watch") + require.NoError(t, err) defer func() { - _ = os.RemoveAll(e.root) + require.NoError(t, os.RemoveAll(e.root)) }() - _, err = ioutil.TempDir(e.root, ".git") - at.Nil(err) + _, err = os.MkdirTemp(e.root, ".git") + require.NoError(t, err) - newDir, err := ioutil.TempDir(e.root, "") - at.Nil(err) + newDir, err := os.MkdirTemp(e.root, "") + require.NoError(t, err) - ignoredFile, err := ioutil.TempFile(e.root, "") - at.Nil(err) - defer func() { at.Nil(ignoredFile.Close()) }() - e.excludeFiles = []string{filepath.Base(ignoredFile.Name())} + ignoredFile, err := os.MkdirTemp(e.root, "") + require.NoError(t, err) + e.excludeFiles = []string{filepath.Base(ignoredFile)} - f, err := ioutil.TempFile(e.root, "*.go") - at.Nil(err) - defer func() { at.Nil(f.Close()) }() + f, err := os.CreateTemp(e.root, "*.go") + require.NoError(t, err) + defer func() { require.NoError(t, f.Close()) }() name := f.Name() go e.watchingFiles() @@ -164,7 +163,7 @@ func Test_Dev_Escort_WatchingFiles(t *testing.T) { at.Fail("should hit") } - e.watcherEvents <- fsnotify.Event{Name: ignoredFile.Name(), Op: fsnotify.Create} + e.watcherEvents <- fsnotify.Event{Name: ignoredFile, Op: fsnotify.Create} e.watcherEvents <- fsnotify.Event{Name: name, Op: fsnotify.Create} e.terminate() @@ -247,6 +246,7 @@ func Test_Dev_IsRemoved(t *testing.T) { for _, tc := range cases { t.Run(tc.Op.String(), func(t *testing.T) { + t.Parallel() assert.Equal(t, tc.bool, isRemoved(tc.Op)) }) } @@ -268,6 +268,7 @@ func Test_Dev_IsCreated(t *testing.T) { for _, tc := range cases { t.Run(tc.Op.String(), func(t *testing.T) { + t.Parallel() assert.Equal(t, tc.bool, isCreated(tc.Op)) }) } @@ -289,6 +290,7 @@ func Test_Dev_IsChmoded(t *testing.T) { for _, tc := range cases { t.Run(tc.Op.String(), func(t *testing.T) { + t.Parallel() assert.Equal(t, tc.bool, isChmoded(tc.Op)) }) } diff --git a/cmd/helpers.go b/cmd/helpers.go index 9ddfab6..b3b3412 100644 --- a/cmd/helpers.go +++ b/cmd/helpers.go @@ -23,85 +23,108 @@ var ( ) func init() { - homeDir, _ = os.UserHomeDir() + if dir, err := os.UserHomeDir(); err == nil { + homeDir = dir + } } func runCmd(cmd *exec.Cmd) (err error) { - var ( stderr io.ReadCloser stdout io.ReadCloser ) if stderr, err = cmd.StderrPipe(); err != nil { - return + return fmt.Errorf("stderr pipe: %w", err) } defer func() { - _ = stderr.Close() + if cerr := stderr.Close(); cerr != nil { + fmt.Fprintf(os.Stderr, "close stderr pipe: %v", cerr) + } + }() + go func() { + if _, cErr := io.Copy(os.Stderr, stderr); cErr != nil { + fmt.Fprintf(os.Stderr, "copy stderr: %v", cErr) + } }() - go func() { _, _ = io.Copy(os.Stderr, stderr) }() if stdout, err = cmd.StdoutPipe(); err != nil { - return + return fmt.Errorf("stdout pipe: %w", err) } defer func() { - _ = stdout.Close() + if cerr := stdout.Close(); cerr != nil { + fmt.Fprintf(os.Stderr, "close stdout pipe: %v", cerr) + } + }() + go func() { + if _, cErr := io.Copy(os.Stdout, stdout); cErr != nil { + fmt.Fprintf(os.Stderr, "copy stdout: %v", cErr) + } }() - go func() { _, _ = io.Copy(os.Stdout, stdout) }() if err = cmd.Run(); err != nil { - err = fmt.Errorf("failed to run %s", cmd.String()) + err = fmt.Errorf("failed to run %s: %w", cmd.String(), err) } - return + return err } // replaces matching file patterns in a path, including subdirectories -func replace(path, pattern, old, new string) error { - return filepath.Walk(path, func(path string, info os.FileInfo, err error) error { +func replace(pathname, pattern, old, replacement string) error { + walkErr := filepath.Walk(pathname, func(p string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { return nil } - return replaceWalkFn(path, info, pattern, []byte(old), []byte(new)) + return replaceWalkFn(p, info, pattern, []byte(old), []byte(replacement)) }) + if walkErr != nil { + return fmt.Errorf("walk %s: %w", pathname, walkErr) + } + return nil } -func replaceWalkFn(path string, info os.FileInfo, pattern string, old, new []byte) (err error) { +func replaceWalkFn(pathname string, info os.FileInfo, pattern string, old, replacement []byte) (err error) { var matched bool if matched, err = filepath.Match(pattern, info.Name()); err != nil { - return + return fmt.Errorf("match pattern %s: %w", pattern, err) } if matched { - cleanedPath := filepath.Clean(path) + cleanedPath := filepath.Clean(pathname) - var oldContent []byte - if oldContent, err = os.ReadFile(cleanedPath); err != nil { - return + oldContent, readErr := os.ReadFile(cleanedPath) + if readErr != nil { + return fmt.Errorf("read file %s: %w", cleanedPath, readErr) } - if err = os.WriteFile(cleanedPath, bytes.Replace(oldContent, old, new, -1), 0); err != nil { - return + if err := os.WriteFile(cleanedPath, bytes.ReplaceAll(oldContent, old, replacement), 0); err != nil { + return fmt.Errorf("write file %s: %w", cleanedPath, err) } } - return + return nil } -func createFile(filePath, content string) (err error) { - var f *os.File - if f, err = os.Create(filepath.Clean(filePath)); err != nil { - return +func createFile(filePath, content string) error { + f, err := os.Create(filepath.Clean(filePath)) + if err != nil { + return fmt.Errorf("create %s: %w", filePath, err) } - defer func() { _ = f.Close() }() + defer func() { + if cerr := f.Close(); cerr != nil { + fmt.Fprintf(os.Stderr, "close file: %v", cerr) + } + }() - _, err = f.WriteString(content) + if _, err := f.WriteString(content); err != nil { + return fmt.Errorf("write %s: %w", filePath, err) + } - return + return nil } func formatLatency(d time.Duration) time.Duration { @@ -121,16 +144,16 @@ func loadConfig() (err error) { configFilePath := configFilePath() if fileExist(configFilePath) { - if err = loadJson(configFilePath, &rc); err != nil { - return + if err := loadJSON(configFilePath, &rc); err != nil { + return err } } - return + return nil } -func storeConfig() { - _ = storeJson(configFilePath(), rc) +func storeConfig() error { + return storeJSON(configFilePath(), rc) } func configFilePath() string { @@ -148,20 +171,27 @@ var fileExist = func(filename string) bool { return true } -func storeJson(filename string, v interface{}) error { +func storeJSON(filename string, v any) error { b, err := json.MarshalIndent(v, "", " ") if err != nil { - return err + return fmt.Errorf("marshal json: %w", err) + } + + if err := os.WriteFile(filename, b, 0o600); err != nil { + return fmt.Errorf("write %s: %w", filename, err) } - return os.WriteFile(filename, b, 0600) + return nil } -func loadJson(filename string, v interface{}) error { +func loadJSON(filename string, v any) error { b, err := os.ReadFile(path.Clean(filename)) if err != nil { - return err + return fmt.Errorf("read file %s: %w", filename, err) } - return json.Unmarshal(b, v) + if err := json.Unmarshal(b, v); err != nil { + return fmt.Errorf("unmarshal %s: %w", filename, err) + } + return nil } diff --git a/cmd/helpers_test.go b/cmd/helpers_test.go index 6d4f858..9f3d164 100644 --- a/cmd/helpers_test.go +++ b/cmd/helpers_test.go @@ -2,12 +2,13 @@ package cmd import ( "fmt" - "io/ioutil" "os" + "path/filepath" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_Helpers_FormatLatency(t *testing.T) { @@ -28,32 +29,31 @@ func Test_Helpers_FormatLatency(t *testing.T) { for _, tc := range cases { t.Run(tc.d.String(), func(t *testing.T) { - assert.Equal(t, formatLatency(tc.d), tc.expected) + t.Parallel() + assert.Equal(t, tc.expected, formatLatency(tc.d)) }) } } func Test_Helper_Replace(t *testing.T) { - at := assert.New(t) + t.Parallel() - dir, err := ioutil.TempDir("", "test_helper_replace") - at.Nil(err) + dir, err := os.MkdirTemp("", "test_helper_replace") + require.NoError(t, err) defer func() { - at.Nil(os.RemoveAll(dir)) + require.NoError(t, os.RemoveAll(dir)) }() - f, err := ioutil.TempFile(dir, "*.go") - at.Nil(err) - at.Nil(f.Close()) + f, err := os.CreateTemp(dir, "*.go") + require.NoError(t, err) + require.NoError(t, f.Close()) - at.Nil(replace(dir, "*.go", "old", "new")) + require.NoError(t, replace(dir, "*.go", "old", "new")) } func Test_Helper_LoadConfig(t *testing.T) { - at := assert.New(t) - t.Run("no config file", func(t *testing.T) { - at.Nil(loadConfig()) + require.NoError(t, loadConfig()) }) t.Run("has config file", func(t *testing.T) { @@ -67,18 +67,20 @@ func Test_Helper_LoadConfig(t *testing.T) { filename := fmt.Sprintf("%s%c%s", homeDir, os.PathSeparator, configName) - f, err := os.Create(filename) - at.Nil(err) - defer func() { at.Nil(f.Close()) }() + f, err := os.Create(filepath.Clean(filename)) + require.NoError(t, err) + defer func() { require.NoError(t, f.Close()) }() _, err = f.WriteString("{}") - at.Nil(err) + require.NoError(t, err) - at.Nil(loadConfig()) + require.NoError(t, loadConfig()) }) } -func Test_Helper_StoreJson(t *testing.T) { - assert.NotNil(t, storeJson("", complex(1, 1))) +func Test_Helper_StoreJSON(t *testing.T) { + t.Parallel() + + require.Error(t, storeJSON("", complex(1, 1))) } func Test_Helper_ConfigFilePath(t *testing.T) { diff --git a/cmd/internal/cmd.go b/cmd/internal/cmd.go index 75aac87..7936589 100644 --- a/cmd/internal/cmd.go +++ b/cmd/internal/cmd.go @@ -13,23 +13,26 @@ import ( "github.com/muesli/termenv" ) +// SpinnerCmd wraps an exec.Cmd and displays a spinner while it runs. type SpinnerCmd struct { - p *tea.Program + err error + p *tea.Program + cmd *exec.Cmd + + stdout chan []byte + stderr chan []byte + errCh chan error + title string + buf []byte spinnerModel spinner.Model size console.WinSize - err error - title string - cmd *exec.Cmd - - stdout chan []byte - stderr chan []byte - errCh chan error - buf []byte - done bool + done bool } +// NewSpinnerCmd returns a SpinnerCmd that runs the given command. The optional +// title is shown alongside the spinner. func NewSpinnerCmd(cmd *exec.Cmd, title ...string) *SpinnerCmd { - spinnerModel := spinner.NewModel() + spinnerModel := spinner.New() spinnerModel.Spinner = spinner.Dot c := &SpinnerCmd{ @@ -50,38 +53,40 @@ func NewSpinnerCmd(cmd *exec.Cmd, title ...string) *SpinnerCmd { return c } +// Init implements the tea.Model interface. func (t *SpinnerCmd) Init() tea.Cmd { - return tea.Batch(t.init(), spinner.Tick) + return tea.Batch(t.start(), t.spinnerModel.Tick) } -func (t *SpinnerCmd) init() tea.Cmd { +func (t *SpinnerCmd) start() tea.Cmd { return func() tea.Msg { - if p, err := t.cmd.StdoutPipe(); err != nil { - return finishedMsg{err} - } else { - go t.watchOutput(t.stdout, p) + p, err := t.cmd.StdoutPipe() + if err != nil { + return finishedError{err} } - if p, err := t.cmd.StderrPipe(); err != nil { - return finishedMsg{err} - } else { - go t.watchOutput(t.stderr, p) + go t.watchOutput(t.stdout, p) + + p, err = t.cmd.StderrPipe() + if err != nil { + return finishedError{err} } - return finishedMsg{t.cmd.Start()} + go t.watchOutput(t.stderr, p) + + return finishedError{t.cmd.Start()} } } func (t *SpinnerCmd) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { - case tea.KeyMsg: switch msg.String() { case "q", "esc", "ctrl+c": - t.err = fmt.Errorf("quit by %s\n", msg.String()) + t.err = fmt.Errorf("quit by %s", msg.String()) return t, tea.Quit default: return t, nil } - case finishedMsg: + case finishedError: if t.err = msg.error; t.err != nil { return t, tea.Quit } @@ -132,21 +137,23 @@ const spinnerCmdTemplate = ` %s %s %s (esc/q/ctrl+c to quit) - + ` +// Run executes the command and manages the spinner lifecycle. func (t *SpinnerCmd) Run() (err error) { if t.size, err = checkConsole(); err != nil { - return + return err } - if err = t.p.Start(); err != nil { - return + if _, err = t.p.Run(); err != nil { + return fmt.Errorf("program run: %w", err) } return t.err } +// UpdateOutput retrieves lines from c and stores them for display. func (t *SpinnerCmd) UpdateOutput(c <-chan []byte) { select { case b := <-c: @@ -157,8 +164,13 @@ func (t *SpinnerCmd) UpdateOutput(c <-chan []byte) { } } +// watchOutput reads lines from rc and forwards them to out until an error occurs. func (t *SpinnerCmd) watchOutput(out chan<- []byte, rc io.ReadCloser) { - defer func() { _ = rc.Close() }() + defer func() { + if err := rc.Close(); err != nil { + t.errCh <- err + } + }() br := bufio.NewReader(rc) for { b, _, err := br.ReadLine() diff --git a/cmd/internal/helpers.go b/cmd/internal/helpers.go index 3453ca9..793b10a 100644 --- a/cmd/internal/helpers.go +++ b/cmd/internal/helpers.go @@ -2,15 +2,17 @@ package internal import ( "fmt" + "os" + "path/filepath" + "strings" - tea "github.com/charmbracelet/bubbletea" "github.com/containerd/console" "github.com/muesli/termenv" ) var term = termenv.ColorProfile() -type finishedMsg struct{ error } +type finishedError struct{ error } func checkConsole() (size console.WinSize, err error) { defer func() { @@ -19,11 +21,48 @@ func checkConsole() (size console.WinSize, err error) { } }() - return console.Current().Size() + size, err = console.Current().Size() + if err != nil { + return size, fmt.Errorf("get console size: %w", err) + } + return size, nil } -func errCmd(err error) tea.Cmd { - return func() tea.Msg { - return finishedMsg{err} +// FileProcessor processes the file content and returns the modified content. +type FileProcessor func(content string) string + +// ChangeFileContent walks through cwd and applies the processorFn to every Go +// file found. Files in a vendor directory are skipped. +func ChangeFileContent(cwd string, processorFn FileProcessor) error { + err := filepath.Walk(cwd, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Skip directories named "vendor" + if info.IsDir() && info.Name() == "vendor" { + return filepath.SkipDir + } + + // Check if the file is a Go file (ending with ".go") + if info.IsDir() || !strings.HasSuffix(info.Name(), ".go") { + return nil + } + fileContent, err := os.ReadFile(path) // #nosec G304 + if err != nil { + return fmt.Errorf("read file %s: %w", path, err) + } + + // update go.mod file + if err2 := os.WriteFile(path, []byte(processorFn(string(fileContent))), 0o600); err2 != nil { + return fmt.Errorf("write file %s: %w", path, err2) + } + + return nil + }) + if err != nil { + return fmt.Errorf("error while traversing the directory tree: %w", err) } + + return nil } diff --git a/cmd/internal/migrations/common.go b/cmd/internal/migrations/common.go new file mode 100644 index 0000000..b24b35d --- /dev/null +++ b/cmd/internal/migrations/common.go @@ -0,0 +1,52 @@ +package migrations + +import ( + "fmt" + "os" + "regexp" + "strconv" + "strings" + + semver "github.com/Masterminds/semver/v3" + "github.com/spf13/cobra" + + "github.com/gofiber/cli/cmd/internal" +) + +var pkgRegex = regexp.MustCompile(`(github\.com\/gofiber\/fiber\/)(v\d+)( *?)(v[\w.-]+)`) + +func MigrateGoPkgs(cmd *cobra.Command, cwd string, curr, target *semver.Version) error { + pkgReplacer := strings.NewReplacer( + "github.com/gofiber/fiber/v"+strconv.FormatUint(curr.Major(), 10), + "github.com/gofiber/fiber/v"+strconv.FormatUint(target.Major(), 10), + ) + + err := internal.ChangeFileContent(cwd, func(content string) string { + return pkgReplacer.Replace(content) + }) + if err != nil { + return fmt.Errorf("failed to migrate Go packages: %w", err) + } + + // get go.mod file + modFile := "go.mod" + fileContent, err := os.ReadFile(modFile) + if err != nil { + return fmt.Errorf("read %s: %w", modFile, err) + } + + // replace old version with new version in go.mod file + fileContentStr := pkgRegex.ReplaceAllString( + string(fileContent), + "${1}v"+strconv.FormatUint(target.Major(), 10)+"${3}v"+target.String(), + ) + + // update go.mod file + if err := os.WriteFile(modFile, []byte(fileContentStr), 0o600); err != nil { + return fmt.Errorf("write %s: %w", modFile, err) + } + + cmd.Println("Migrating Go packages") + + return nil +} diff --git a/cmd/internal/migrations/lists.go b/cmd/internal/migrations/lists.go new file mode 100644 index 0000000..d6a4a34 --- /dev/null +++ b/cmd/internal/migrations/lists.go @@ -0,0 +1,88 @@ +package migrations + +import ( + "fmt" + + semver "github.com/Masterminds/semver/v3" + "github.com/spf13/cobra" + + v3migrations "github.com/gofiber/cli/cmd/internal/migrations/v3" +) + +// MigrationFn is a function that will be called during migration +type MigrationFn func(cmd *cobra.Command, cwd string, curr, target *semver.Version) error + +// Migration is a single migration +type Migration struct { + From string + To string + Functions []MigrationFn +} + +// Migrations is a list of all migrations +// Example structure: +// {"from": ">=2.0.0", "to": "<=3.*.*", "fn": [MigrateFN, MigrateFN]} +var Migrations = []Migration{ + {From: ">=1.0.0", To: ">=0.0.0-0", Functions: []MigrationFn{MigrateGoPkgs}}, + { + From: ">=2.0.0", + To: "<4.0.0-0", + Functions: []MigrationFn{ + v3migrations.MigrateHandlerSignatures, + v3migrations.MigrateParserMethods, + v3migrations.MigrateAllParams, + v3migrations.MigrateRedirectMethods, + v3migrations.MigrateGenericHelpers, + v3migrations.MigrateAddMethod, + v3migrations.MigrateMimeConstants, + v3migrations.MigrateLoggerTags, + v3migrations.MigrateStaticRoutes, + v3migrations.MigrateTrustedProxyConfig, + v3migrations.MigrateMount, + v3migrations.MigrateConfigListenerFields, + v3migrations.MigrateListenerCallbacks, + v3migrations.MigrateListenMethods, + v3migrations.MigrateContextMethods, + v3migrations.MigrateViewBind, + v3migrations.MigrateCORSConfig, + v3migrations.MigrateCSRFConfig, + v3migrations.MigrateMonitorImport, + v3migrations.MigrateHealthcheckConfig, + v3migrations.MigrateProxyTLSConfig, + v3migrations.MigrateAppTestConfig, + v3migrations.MigrateMiddlewareLocals, + v3migrations.MigrateFilesystemMiddleware, + v3migrations.MigrateLimiterConfig, + v3migrations.MigrateEnvVarConfig, + v3migrations.MigrateSessionConfig, + v3migrations.MigrateReqHeaderParser, + }, + }, +} + +// DoMigration runs all migrations +// It will run all migrations that match the current and target version +func DoMigration(cmd *cobra.Command, cwd string, curr, target *semver.Version) error { + for _, m := range Migrations { + toC, err := semver.NewConstraint(m.To) + if err != nil { + return fmt.Errorf("parse to constraint %s: %w", m.To, err) + } + fromC, err := semver.NewConstraint(m.From) + if err != nil { + return fmt.Errorf("parse from constraint %s: %w", m.From, err) + } + + if fromC.Check(curr) && toC.Check(target) { + for _, fn := range m.Functions { + if err := fn(cmd, cwd, curr, target); err != nil { + return err + } + } + } else { + cmd.Printf("Skipping migration from %s to %s\n", m.From, m.To) + } + } + + return nil +} diff --git a/cmd/internal/migrations/v3/common.go b/cmd/internal/migrations/v3/common.go new file mode 100644 index 0000000..688dbbd --- /dev/null +++ b/cmd/internal/migrations/v3/common.go @@ -0,0 +1,580 @@ +package v3 + +import ( + "fmt" + "regexp" + "strconv" + "strings" + + semver "github.com/Masterminds/semver/v3" + "github.com/spf13/cobra" + + "github.com/gofiber/cli/cmd/internal" +) + +func MigrateHandlerSignatures(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + sigReplacer := strings.NewReplacer("*fiber.Ctx", "fiber.Ctx") + + err := internal.ChangeFileContent(cwd, func(content string) string { + return sigReplacer.Replace(content) + }) + if err != nil { + return fmt.Errorf("failed to migrate handler signatures: %w", err) + } + + cmd.Println("Migrating handler signatures") + + return nil +} + +// MigrateParserMethods replaces deprecated parser helper methods with the new binding API +func MigrateParserMethods(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + replacer := strings.NewReplacer( + ".BodyParser(", ".Bind().Body(", + ".CookieParser(", ".Bind().Cookie(", + ".ParamsParser(", ".Bind().URI(", + ".QueryParser(", ".Bind().Query(", + ) + + err := internal.ChangeFileContent(cwd, func(content string) string { + return replacer.Replace(content) + }) + if err != nil { + return fmt.Errorf("failed to migrate parser methods: %w", err) + } + + cmd.Println("Migrating parser methods") + return nil +} + +// MigrateRedirectMethods updates redirect helper methods to the new API +func MigrateRedirectMethods(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + replacer := strings.NewReplacer( + ".RedirectBack(", ".Redirect().Back(", + ".RedirectToRoute(", ".Redirect().Route(", + ) + + err := internal.ChangeFileContent(cwd, func(content string) string { + re := regexp.MustCompile(`\.Redirect\(`) + content = re.ReplaceAllString(content, ".Redirect().To(") + return replacer.Replace(content) + }) + if err != nil { + return fmt.Errorf("failed to migrate redirect methods: %w", err) + } + + cmd.Println("Migrating redirect methods") + return nil +} + +// MigrateGenericHelpers migrates helper functions that now use generics +func MigrateGenericHelpers(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + err := internal.ChangeFileContent(cwd, func(content string) string { + reParamsInt := regexp.MustCompile(`(\w+)\.ParamsInt\(`) + content = reParamsInt.ReplaceAllString(content, "fiber.Params[int]($1, ") + + reQueryInt := regexp.MustCompile(`(\w+)\.QueryInt\(`) + content = reQueryInt.ReplaceAllString(content, "fiber.Query[int]($1, ") + + reQueryFloat := regexp.MustCompile(`(\w+)\.QueryFloat\(`) + content = reQueryFloat.ReplaceAllString(content, "fiber.Query[float64]($1, ") + + reQueryBool := regexp.MustCompile(`(\w+)\.QueryBool\(`) + content = reQueryBool.ReplaceAllString(content, "fiber.Query[bool]($1, ") + + return content + }) + if err != nil { + return fmt.Errorf("failed to migrate generic helpers: %w", err) + } + + cmd.Println("Migrating generic helpers") + return nil +} + +// MigrateContextMethods updates context related methods to the new names +func MigrateContextMethods(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + replacer := strings.NewReplacer( + ".Context()", ".RequestCtx()", + ".UserContext()", ".Context()", + ".SetUserContext(", ".SetContext(", // TODO: check if this is correct + ) + + err := internal.ChangeFileContent(cwd, func(content string) string { + return replacer.Replace(content) + }) + if err != nil { + return fmt.Errorf("failed to migrate context methods: %w", err) + } + + cmd.Println("Migrating context methods") + return nil +} + +// MigrateViewBind replaces the old Ctx.Bind view binding helper with ViewBind +func MigrateViewBind(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + replacer := strings.NewReplacer(".Bind(", ".ViewBind(") + + err := internal.ChangeFileContent(cwd, func(content string) string { + return replacer.Replace(content) + }) + if err != nil { + return fmt.Errorf("failed to migrate ViewBind calls: %w", err) + } + + cmd.Println("Migrating view binding helpers") + return nil +} + +// MigrateAllParams replaces deprecated AllParams helper with the new binding API +func MigrateAllParams(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + replacer := strings.NewReplacer( + ".AllParams(", ".Bind().URI(", + ) + + err := internal.ChangeFileContent(cwd, func(content string) string { + return replacer.Replace(content) + }) + if err != nil { + return fmt.Errorf("failed to migrate AllParams: %w", err) + } + + cmd.Println("Migrating AllParams") + return nil +} + +// MigrateMount replaces app.Mount with app.Use +func MigrateMount(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + replacer := strings.NewReplacer(".Mount(", ".Use(") + + err := internal.ChangeFileContent(cwd, func(content string) string { + return replacer.Replace(content) + }) + if err != nil { + return fmt.Errorf("failed to migrate Mount usages: %w", err) + } + + cmd.Println("Migrating Mount usages") + return nil +} + +// MigrateAddMethod adapts the Add method signature +func MigrateAddMethod(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + err := internal.ChangeFileContent(cwd, func(content string) string { + re := regexp.MustCompile(`\.Add\(\s*([^,\n]+)\s*,`) + return re.ReplaceAllString(content, ".Add([]string{$1},") + }) + if err != nil { + return fmt.Errorf("failed to migrate Add method calls: %w", err) + } + + cmd.Println("Migrating Add method calls") + return nil +} + +// MigrateCORSConfig updates cors middleware configuration fields +func MigrateCORSConfig(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + err := internal.ChangeFileContent(cwd, func(content string) string { + conv := func(src string, re *regexp.Regexp, field string) string { + return re.ReplaceAllStringFunc(src, func(s string) string { + matches := re.FindStringSubmatch(s) + if len(matches) < 2 { + return s + } + parts := strings.Split(matches[1], ",") + for i, p := range parts { + parts[i] = fmt.Sprintf("%q", strings.TrimSpace(p)) + } + return fmt.Sprintf("%s: []string{%s}", field, strings.Join(parts, ", ")) + }) + } + + reOrigins := regexp.MustCompile(`AllowOrigins:\s*"([^"]*)"`) + content = conv(content, reOrigins, "AllowOrigins") + + reMethods := regexp.MustCompile(`AllowMethods:\s*"([^"]*)"`) + content = conv(content, reMethods, "AllowMethods") + + reHeaders := regexp.MustCompile(`AllowHeaders:\s*"([^"]*)"`) + content = conv(content, reHeaders, "AllowHeaders") + + reExpose := regexp.MustCompile(`ExposeHeaders:\s*"([^"]*)"`) + content = conv(content, reExpose, "ExposeHeaders") + + return content + }) + if err != nil { + return fmt.Errorf("failed to migrate CORS configs: %w", err) + } + + cmd.Println("Migrating CORS middleware configs") + return nil +} + +// MigrateCSRFConfig updates csrf middleware configuration fields +func MigrateCSRFConfig(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + replacer := strings.NewReplacer("Expiration:", "IdleTimeout:") + + err := internal.ChangeFileContent(cwd, func(content string) string { + content = replacer.Replace(content) + re := regexp.MustCompile(`\s*SessionKey:\s*[^,]+,?\n`) + return re.ReplaceAllString(content, "") + }) + if err != nil { + return fmt.Errorf("failed to migrate CSRF configs: %w", err) + } + + cmd.Println("Migrating CSRF middleware configs") + return nil +} + +// MigrateMonitorImport updates monitor middleware import path +func MigrateMonitorImport(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + err := internal.ChangeFileContent(cwd, func(content string) string { + return strings.ReplaceAll(content, + "github.com/gofiber/fiber/v2/middleware/monitor", + "github.com/gofiber/contrib/monitor") + }) + if err != nil { + return fmt.Errorf("failed to migrate monitor import: %w", err) + } + + cmd.Println("Migrating monitor middleware import") + return nil +} + +// MigrateProxyTLSConfig updates proxy TLS helper to new client configuration +func MigrateProxyTLSConfig(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + err := internal.ChangeFileContent(cwd, func(content string) string { + re := regexp.MustCompile(`proxy\.WithTlsConfig\(([^)]+)\)`) + return re.ReplaceAllString(content, + "proxy.WithClient(&fasthttp.Client{TLSConfig: $1})") + }) + if err != nil { + return fmt.Errorf("failed to migrate proxy TLS config: %w", err) + } + + cmd.Println("Migrating proxy TLS config") + return nil +} + +// MigrateMimeConstants updates deprecated MIME constants +func MigrateMimeConstants(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + replacer := strings.NewReplacer( + "MIMEApplicationJavaScriptCharsetUTF8", "MIMETextJavaScriptCharsetUTF8", + "MIMEApplicationJavaScript", "MIMETextJavaScript", + ) + + err := internal.ChangeFileContent(cwd, func(content string) string { + return replacer.Replace(content) + }) + if err != nil { + return fmt.Errorf("failed to migrate MIME constants: %w", err) + } + + cmd.Println("Migrating MIME constants") + return nil +} + +// MigrateLoggerTags updates deprecated logger tag constants +func MigrateLoggerTags(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + err := internal.ChangeFileContent(cwd, func(content string) string { + return strings.ReplaceAll(content, "logger.TagHeader", "logger.TagReqHeader") + }) + if err != nil { + return fmt.Errorf("failed to migrate logger tags: %w", err) + } + + cmd.Println("Migrating logger tag constants") + return nil +} + +// MigrateStaticRoutes replaces app.Static calls with static middleware +func MigrateStaticRoutes(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + err := internal.ChangeFileContent(cwd, func(content string) string { + re := regexp.MustCompile(`\.Static\(\s*("[^"]*")\s*,\s*("[^"]*")(?:,\s*([^)]*))?\)`) + return re.ReplaceAllStringFunc(content, func(m string) string { + sub := re.FindStringSubmatch(m) + pathLit := sub[1] + root := sub[2] + cfg := sub[3] + + path, err := strconv.Unquote(pathLit) + if err != nil { + path = strings.Trim(pathLit, "\"") + } + + switch path { + case "/": + path = "/*" + case "*": + // keep as is + default: + path += "*" + } + + quoted := strconv.Quote(path) + + if cfg != "" { + cfg = strings.TrimSpace(cfg) + cfg = strings.Replace(cfg, "Static{", "static.Config{", 1) + reIndex := regexp.MustCompile(`Index:\s*([^,}\n]+)`) + cfg = reIndex.ReplaceAllString(cfg, "IndexNames: []string{$1}") + return fmt.Sprintf(".Get(%s, static.New(%s, %s))", quoted, root, cfg) + } + + return fmt.Sprintf(".Get(%s, static.New(%s))", quoted, root) + }) + }) + if err != nil { + return fmt.Errorf("failed to migrate static usages: %w", err) + } + + cmd.Println("Migrating app.Static usage") + return nil +} + +// MigrateTrustedProxyConfig updates trusted proxy configuration options +func MigrateTrustedProxyConfig(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + err := internal.ChangeFileContent(cwd, func(content string) string { + reEnable := regexp.MustCompile(`EnableTrustedProxyCheck`) + content = reEnable.ReplaceAllString(content, "TrustProxy") + + reProxies := regexp.MustCompile(`TrustedProxies:\s*([^,\n]+),`) + content = reProxies.ReplaceAllString(content, "TrustProxyConfig: fiber.TrustProxyConfig{Proxies: $1},") + + return content + }) + if err != nil { + return fmt.Errorf("failed to migrate trusted proxy config: %w", err) + } + + cmd.Println("Migrating trusted proxy config") + return nil +} + +// MigrateConfigListenerFields updates config fields that have been moved or renamed +// in Fiber v3. It renames Prefork and Network fields and adapts them to the new +// listener configuration fields. +func MigrateConfigListenerFields(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + err := internal.ChangeFileContent(cwd, func(content string) string { + replacer := strings.NewReplacer( + "Prefork:", "EnablePrefork:", + "Network:", "ListenerNetwork:", + ) + return replacer.Replace(content) + }) + if err != nil { + return fmt.Errorf("failed to migrate listener related config fields: %w", err) + } + + cmd.Println("Migrating listener related config fields") + return nil +} + +// MigrateListenerCallbacks removes deprecated OnShutdown callbacks from +// ListenerConfig. Fiber v3 replaces these with the OnPostShutdown hook. +func MigrateListenerCallbacks(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + err := internal.ChangeFileContent(cwd, func(content string) string { + reErr := regexp.MustCompile(`\s*OnShutdownError:\s*[^,]+,?\n`) + content = reErr.ReplaceAllString(content, "") + + reSuccess := regexp.MustCompile(`\s*OnShutdownSuccess:\s*[^,]+,?\n`) + content = reSuccess.ReplaceAllString(content, "") + + return content + }) + if err != nil { + return fmt.Errorf("failed to migrate listener callbacks: %w", err) + } + + cmd.Println("Migrating listener callbacks") + return nil +} + +// MigrateFilesystemMiddleware replaces deprecated filesystem middleware with static middleware +func MigrateFilesystemMiddleware(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + err := internal.ChangeFileContent(cwd, func(content string) string { + content = strings.ReplaceAll(content, + "github.com/gofiber/fiber/v2/middleware/filesystem", + "github.com/gofiber/fiber/v3/middleware/static") + content = strings.ReplaceAll(content, + "github.com/gofiber/fiber/v3/middleware/filesystem", + "github.com/gofiber/fiber/v3/middleware/static") + + reNew := regexp.MustCompile(`filesystem\.New\s*\(`) + content = reNew.ReplaceAllString(content, `static.New("", `) + + content = strings.ReplaceAll(content, "filesystem.Config{", "static.Config{") + + reRootHTTP := regexp.MustCompile(`Root:\s*http.Dir\(([^)]+)\)`) + content = reRootHTTP.ReplaceAllString(content, `FS: os.DirFS($1)`) + + reRoot := regexp.MustCompile(`Root:\s*([^,\n]+)`) + content = reRoot.ReplaceAllString(content, `FS: os.DirFS($1)`) + + reIndex := regexp.MustCompile(`Index:\s*([^,\n]+)`) + content = reIndex.ReplaceAllString(content, `IndexNames: []string{$1}`) + + return content + }) + if err != nil { + return fmt.Errorf("failed to migrate filesystem middleware: %w", err) + } + + cmd.Println("Migrating filesystem middleware") + return nil +} + +// MigrateEnvVarConfig removes deprecated ExcludeVars field from envvar middleware configuration +func MigrateEnvVarConfig(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + err := internal.ChangeFileContent(cwd, func(content string) string { + re := regexp.MustCompile(`\s*ExcludeVars:\s*[^,]+,?\n`) + return re.ReplaceAllString(content, "") + }) + if err != nil { + return fmt.Errorf("failed to migrate EnvVar configs: %w", err) + } + + cmd.Println("Migrating EnvVar middleware configs") + return nil +} + +// MigrateHealthcheckConfig updates healthcheck middleware configuration fields +func MigrateHealthcheckConfig(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + err := internal.ChangeFileContent(cwd, func(content string) string { + content = strings.ReplaceAll(content, "LivenessProbe:", "Probe:") + + re := regexp.MustCompile(`\s*ReadinessProbe:\s*[^,]+,?\n`) + content = re.ReplaceAllString(content, "") + + re = regexp.MustCompile(`\s*LivenessEndpoint:\s*[^,]+,?\n?`) + content = re.ReplaceAllString(content, "") + + re = regexp.MustCompile(`\s*ReadinessEndpoint:\s*[^,]+,?\n?`) + content = re.ReplaceAllString(content, "") + + return content + }) + if err != nil { + return fmt.Errorf("failed to migrate healthcheck configs: %w", err) + } + + cmd.Println("Migrating healthcheck middleware configs") + return nil +} + +// MigrateLimiterConfig updates limiter middleware configuration fields +func MigrateLimiterConfig(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + err := internal.ChangeFileContent(cwd, func(content string) string { + reConfig := regexp.MustCompile(`limiter\.Config{[^}]*}`) + return reConfig.ReplaceAllStringFunc(content, func(s string) string { + s = strings.ReplaceAll(s, "Duration:", "Expiration:") + s = strings.ReplaceAll(s, "Store:", "Storage:") + s = strings.ReplaceAll(s, "Key:", "KeyGenerator:") + return s + }) + }) + if err != nil { + return fmt.Errorf("failed to migrate limiter configs: %w", err) + } + + cmd.Println("Migrating limiter middleware configs") + return nil +} + +// MigrateSessionConfig updates session middleware configuration fields +func MigrateSessionConfig(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + err := internal.ChangeFileContent(cwd, func(content string) string { + reConfig := regexp.MustCompile(`session\.Config{[^}]*}`) + return reConfig.ReplaceAllStringFunc(content, func(s string) string { + s = strings.ReplaceAll(s, "Expiration:", "IdleTimeout:") + return s + }) + }) + if err != nil { + return fmt.Errorf("failed to migrate session configs: %w", err) + } + + cmd.Println("Migrating session middleware configs") + return nil +} + +// MigrateAppTestConfig updates app.Test calls to use the new TestConfig parameter +func MigrateAppTestConfig(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + err := internal.ChangeFileContent(cwd, func(content string) string { + re := regexp.MustCompile(`\.Test\(([^,\n]+),\s*([^\n)]+)\)`) + return re.ReplaceAllString(content, `.Test($1, fiber.TestConfig{Timeout: $2})`) + }) + if err != nil { + return fmt.Errorf("failed to migrate app.Test calls: %w", err) + } + + cmd.Println("Migrating app.Test usages") + return nil +} + +// MigrateMiddlewareLocals replaces Locals lookups for middleware data with helper functions +func MigrateMiddlewareLocals(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + err := internal.ChangeFileContent(cwd, func(content string) string { + replacements := []struct { + re *regexp.Regexp + repl string + }{ + {regexp.MustCompile(`(\w+)\.Locals\("requestid"\)`), `requestid.FromContext($1)`}, + {regexp.MustCompile(`(\w+)\.Locals\("csrf"\)`), `csrf.TokenFromContext($1)`}, + {regexp.MustCompile(`(\w+)\.Locals\("csrf_handler"\)`), `csrf.HandlerFromContext($1)`}, + {regexp.MustCompile(`(\w+)\.Locals\("session"\)`), `session.FromContext($1)`}, + {regexp.MustCompile(`(\w+)\.Locals\("username"\)`), `basicauth.UsernameFromContext($1)`}, + {regexp.MustCompile(`(\w+)\.Locals\("password"\)`), `basicauth.PasswordFromContext($1)`}, + {regexp.MustCompile(`(\w+)\.Locals\("token"\)`), `keyauth.TokenFromContext($1)`}, + } + for _, r := range replacements { + content = r.re.ReplaceAllString(content, r.repl) + } + return content + }) + if err != nil { + return fmt.Errorf("failed to migrate middleware locals: %w", err) + } + + cmd.Println("Migrating middleware locals") + return nil +} + +// MigrateListenMethods replaces removed Listen helpers with Listen +func MigrateListenMethods(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + replacer := strings.NewReplacer( + ".ListenTLSWithCertificate(", ".Listen(", + ".ListenTLS(", ".Listen(", + ".ListenMutualTLSWithCertificate(", ".Listen(", + ".ListenMutualTLS(", ".Listen(", + ) + + err := internal.ChangeFileContent(cwd, func(content string) string { + return replacer.Replace(content) + }) + if err != nil { + return fmt.Errorf("failed to migrate listen methods: %w", err) + } + + cmd.Println("Migrating listen methods") + return nil +} + +// MigrateReqHeaderParser replaces the deprecated ReqHeaderParser helper with the new binding API +func MigrateReqHeaderParser(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + replacer := strings.NewReplacer( + ".ReqHeaderParser(", ".Bind().Header(", + ) + + err := internal.ChangeFileContent(cwd, func(content string) string { + return replacer.Replace(content) + }) + if err != nil { + return fmt.Errorf("failed to migrate ReqHeaderParser: %w", err) + } + + cmd.Println("Migrating request header parser helper") + return nil +} diff --git a/cmd/internal/migrations/v3/common_test.go b/cmd/internal/migrations/v3/common_test.go new file mode 100644 index 0000000..55c42ba --- /dev/null +++ b/cmd/internal/migrations/v3/common_test.go @@ -0,0 +1,777 @@ +package v3 + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func writeTempFile(t *testing.T, dir, content string) string { + t.Helper() + path := filepath.Join(dir, "main.go") + err := os.WriteFile(path, []byte(content), 0o600) + require.NoError(t, err) + return path +} + +func readFile(t *testing.T, path string) string { + t.Helper() + b, err := os.ReadFile(path) // #nosec G304 + require.NoError(t, err) + return string(b) +} + +func newCmd(buf *bytes.Buffer) *cobra.Command { + cmd := &cobra.Command{} + cmd.SetOut(buf) + cmd.SetErr(buf) + return cmd +} + +func Test_MigrateHandlerSignatures(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mhstest") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import "github.com/gofiber/fiber/v2" +func handler(c *fiber.Ctx) error { return nil } +`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, MigrateHandlerSignatures(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.NotContains(t, content, "*fiber.Ctx") + assert.Contains(t, content, "fiber.Ctx") + assert.Contains(t, buf.String(), "Migrating handler signatures") +} + +func Test_MigrateParserMethods(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mptest") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import "github.com/gofiber/fiber/v2" +func handler(c fiber.Ctx) error { + var v any + c.BodyParser(&v) + c.CookieParser(&v) + c.ParamsParser(&v) + c.QueryParser(&v) + return nil +} +`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, MigrateParserMethods(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Contains(t, content, ".Bind().Body(&v)") + assert.Contains(t, content, ".Bind().Cookie(&v)") + assert.Contains(t, content, ".Bind().URI(&v)") + assert.Contains(t, content, ".Bind().Query(&v)") + assert.Contains(t, buf.String(), "Migrating parser methods") +} + +func Test_MigrateRedirectMethods(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mrtest") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import "github.com/gofiber/fiber/v2" +func handler(c fiber.Ctx) error { + c.Redirect("/foo") + c.RedirectBack() + c.RedirectToRoute("home") + return nil +} +`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, MigrateRedirectMethods(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Contains(t, content, ".Redirect().To(\"/foo\")") + assert.Contains(t, content, ".Redirect().Back()") + assert.Contains(t, content, ".Redirect().Route(\"home\")") + assert.Contains(t, buf.String(), "Migrating redirect methods") +} + +func Test_MigrateGenericHelpers(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mghtest") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import "github.com/gofiber/fiber/v2" +func handler(c fiber.Ctx) error { + _ = c.ParamsInt("id", 0) + _ = c.QueryInt("age", 0) + _ = c.QueryFloat("score", 0.5) + _ = c.QueryBool("ok", true) + return nil +} +`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, MigrateGenericHelpers(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Contains(t, content, "fiber.Params[int](c, \"id\"") + assert.Contains(t, content, "fiber.Query[int](c, \"age\"") + assert.Contains(t, content, "fiber.Query[float64](c, \"score\"") + assert.Contains(t, content, "fiber.Query[bool](c, \"ok\"") + assert.Contains(t, buf.String(), "Migrating generic helpers") +} + +func Test_MigrateContextMethods(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mcmtest") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import "github.com/gofiber/fiber/v2" +func handler(c fiber.Ctx) error { + ctx := c.Context() + uc := c.UserContext() + c.SetUserContext(ctx) + _ = uc + return nil +} +`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, MigrateContextMethods(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Contains(t, content, ".RequestCtx()") + assert.Contains(t, content, ".Context()") + assert.Contains(t, content, ".SetContext(") + assert.Contains(t, buf.String(), "Migrating context methods") +} + +func Test_MigrateViewBind(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mvbtest") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import "github.com/gofiber/fiber/v2" +func handler(c fiber.Ctx) error { + return c.Bind("index", fiber.Map{}) +}`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, MigrateViewBind(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Contains(t, content, ".ViewBind(") + assert.NotContains(t, content, "c.Bind(") + assert.Contains(t, buf.String(), "Migrating view binding helpers") +} + +func Test_MigrateAllParams(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "maptest") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import "github.com/gofiber/fiber/v2" +func handler(c fiber.Ctx) error { + var p any + c.AllParams(&p) + return nil +}`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, MigrateAllParams(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Contains(t, content, ".Bind().URI(&p)") + assert.Contains(t, buf.String(), "Migrating AllParams") +} + +func Test_MigrateMount(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mmtest") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import "github.com/gofiber/fiber/v2" +func main() { + app := fiber.New() + api := fiber.New() + app.Mount("/api", api) +}`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, MigrateMount(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Contains(t, content, ".Use(\"/api\", api)") + assert.Contains(t, buf.String(), "Migrating Mount usages") +} + +func Test_MigrateAddMethod(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "maddtest") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import "github.com/gofiber/fiber/v2" +func main() { + app := fiber.New() + app.Add(fiber.MethodGet, "/foo", func(c fiber.Ctx) error { return nil }) +} +`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, MigrateAddMethod(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Contains(t, content, `Add([]string{fiber.MethodGet}, "/foo"`) + assert.Contains(t, buf.String(), "Migrating Add method calls") +} + +func Test_MigrateMimeConstants(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mmimetest") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import "github.com/gofiber/fiber/v2" +const mime = fiber.MIMEApplicationJavaScript +`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, MigrateMimeConstants(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.NotContains(t, content, "MIMEApplicationJavaScript") + assert.Contains(t, content, "MIMETextJavaScript") + assert.Contains(t, buf.String(), "Migrating MIME constants") +} + +func Test_MigrateLoggerTags(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mloggertest") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import ( + "github.com/gofiber/fiber/v2/middleware/logger" +) +var _ = logger.TagHeader +`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, MigrateLoggerTags(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.NotContains(t, content, "TagHeader") + assert.Contains(t, content, "TagReqHeader") + assert.Contains(t, buf.String(), "Migrating logger tag constants") +} + +func Test_MigrateStaticRoutes(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "msrtest") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import "github.com/gofiber/fiber/v2" +func main() { + app := fiber.New() + app.Static("/", "./public") + app.Static("/prefix", "./public", Static{Index: "index.htm"}) +}`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, MigrateStaticRoutes(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Contains(t, content, `.Get("/*", static.New("./public"))`) + assert.Contains(t, content, `static.New("./public", static.Config{IndexNames: []string{"index.htm"}})`) + assert.Contains(t, buf.String(), "Migrating app.Static usage") +} + +func Test_MigrateTrustedProxyConfig(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mtpctest") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import "github.com/gofiber/fiber/v2" +func main() { + app := fiber.New(fiber.Config{ + EnableTrustedProxyCheck: true, + TrustedProxies: []string{"0.8.0.0"}, + }) + _ = app +}`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, MigrateTrustedProxyConfig(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Contains(t, content, "TrustProxy: true") + assert.Contains(t, content, "TrustProxyConfig: fiber.TrustProxyConfig{Proxies: []string{\"0.8.0.0\"}},") + assert.Contains(t, buf.String(), "Migrating trusted proxy config") +} + +func Test_MigrateCORSConfig(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mcors") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import "github.com/gofiber/fiber/v2/middleware/cors" +var _ = cors.New(cors.Config{ + AllowOrigins: "https://a.com,https://b.com", + AllowMethods: "GET,POST", + AllowHeaders: "Content-Type", + ExposeHeaders: "Content-Length", +})`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, MigrateCORSConfig(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Contains(t, content, `AllowOrigins: []string{"https://a.com", "https://b.com"}`) + assert.Contains(t, content, `AllowMethods: []string{"GET", "POST"}`) + assert.Contains(t, content, `AllowHeaders: []string{"Content-Type"}`) + assert.Contains(t, content, `ExposeHeaders: []string{"Content-Length"}`) + assert.Contains(t, buf.String(), "Migrating CORS middleware configs") +} + +func Test_MigrateCSRFConfig(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mcsrf") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import ( + "github.com/gofiber/fiber/v2/middleware/csrf" + "time" +) +var _ = csrf.New(csrf.Config{ + Expiration: 10 * time.Minute, + SessionKey: "csrf", +})`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, MigrateCSRFConfig(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Contains(t, content, "IdleTimeout:") + assert.NotContains(t, content, "Expiration:") + assert.NotContains(t, content, "SessionKey") + assert.Contains(t, buf.String(), "Migrating CSRF middleware configs") +} + +func Test_MigrateMonitorImport(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mmonitor") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import "github.com/gofiber/fiber/v2/middleware/monitor" +var _ = monitor.New()`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, MigrateMonitorImport(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Contains(t, content, "github.com/gofiber/contrib/monitor") + assert.NotContains(t, content, "fiber/v2/middleware/monitor") + assert.Contains(t, buf.String(), "Migrating monitor middleware import") +} + +func Test_MigrateProxyTLSConfig(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mproxy") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import ( + "github.com/gofiber/fiber/v2/middleware/proxy" + "crypto/tls" +) +func main() { + proxy.WithTlsConfig(&tls.Config{InsecureSkipVerify: true}) +}`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, MigrateProxyTLSConfig(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Contains(t, content, "proxy.WithClient(&fasthttp.Client{TLSConfig: &tls.Config{InsecureSkipVerify: true}})") + assert.Contains(t, buf.String(), "Migrating proxy TLS config") +} + +func Test_MigrateConfigListenerFields(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mconf") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import "github.com/gofiber/fiber/v2" +func main() { + app := fiber.New(fiber.Config{ + Prefork: true, + Network: "tcp", + }) + _ = app +}`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, MigrateConfigListenerFields(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Contains(t, content, "EnablePrefork: true") + assert.Contains(t, content, "ListenerNetwork: \"tcp\"") + assert.Contains(t, buf.String(), "Migrating listener related config fields") +} + +func Test_MigrateListenerCallbacks(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mlistener") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import ( + "github.com/gofiber/fiber/v2" + "log" +) +func main() { + app := fiber.New() + app.Listen(":3000", fiber.ListenerConfig{ + OnShutdownError: func(err error) { + log.Print(err) + }, + OnShutdownSuccess: func() { + log.Print("ok") + }, + }) +}`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, MigrateListenerCallbacks(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.NotContains(t, content, "OnShutdownError") + assert.NotContains(t, content, "OnShutdownSuccess") + assert.Contains(t, buf.String(), "Migrating listener callbacks") +} + +func Test_MigrateListenMethods(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mlisten") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import ( + "github.com/gofiber/fiber/v2" + "crypto/tls" +) +func main() { + app := fiber.New() + cert, _ := tls.LoadX509KeyPair("cert.pem", "key.pem") + app.ListenTLS(":443", "cert.pem", "key.pem") + app.ListenTLSWithCertificate(":443", cert) + app.ListenMutualTLS(":443", "cert.pem", "key.pem", "ca.pem") + app.ListenMutualTLSWithCertificate(":443", cert, "ca.pem") +}`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, MigrateListenMethods(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.NotContains(t, content, "ListenTLS(") + assert.NotContains(t, content, "ListenTLSWithCertificate(") + assert.NotContains(t, content, "ListenMutualTLS(") + assert.NotContains(t, content, "ListenMutualTLSWithCertificate(") + assert.Equal(t, 4, strings.Count(content, ".Listen(")) + assert.Contains(t, buf.String(), "Migrating listen methods") +} + +func Test_MigrateFilesystemMiddleware(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mfs") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import ( + "github.com/gofiber/fiber/v2/middleware/filesystem" + "net/http" +) +func main() { + _ = filesystem.New(filesystem.Config{ + Root: http.Dir("./assets"), + Index: "index.html", + }) +}`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, MigrateFilesystemMiddleware(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Contains(t, content, `static.New("", static.Config{`) + assert.Contains(t, content, `FS: os.DirFS("./assets")`) + assert.Contains(t, content, `IndexNames: []string{"index.html"}`) + assert.Contains(t, buf.String(), "Migrating filesystem middleware") +} + +func Test_MigrateEnvVarConfig(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "menvvar") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import "github.com/gofiber/fiber/v2/middleware/envvar" +var _ = envvar.New(envvar.Config{ + ExcludeVars: []string{"SECRET"}, +})`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, MigrateEnvVarConfig(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.NotContains(t, content, "ExcludeVars") + assert.Contains(t, buf.String(), "Migrating EnvVar middleware configs") +} + +func Test_MigrateLimiterConfig(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mlimiter") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import ( + "github.com/gofiber/fiber/v2/middleware/limiter" + "time" +) +var _ = limiter.New(limiter.Config{ + Duration: time.Minute, + Store: nil, + Key: func(c fiber.Ctx) string { return "a" }, +})`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, MigrateLimiterConfig(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Contains(t, content, "Expiration:") + assert.Contains(t, content, "Storage:") + assert.Contains(t, content, "KeyGenerator:") + assert.Contains(t, buf.String(), "Migrating limiter middleware configs") +} + +func Test_MigrateHealthcheckConfig(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mhealth") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import "github.com/gofiber/fiber/v2/middleware/healthcheck" +var _ = healthcheck.New(healthcheck.Config{ + LivenessProbe: func(c fiber.Ctx) bool { return true }, + LivenessEndpoint: "/live", + ReadinessProbe: func(c fiber.Ctx) bool { return true }, + ReadinessEndpoint: "/ready", +})`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, MigrateHealthcheckConfig(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Contains(t, content, "Probe:") + assert.NotContains(t, content, "LivenessProbe") + assert.NotContains(t, content, "ReadinessProbe") + assert.NotContains(t, content, "LivenessEndpoint") + assert.NotContains(t, content, "ReadinessEndpoint") + assert.Contains(t, buf.String(), "Migrating healthcheck middleware configs") +} + +func Test_MigrateAppTestConfig(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mtestcfg") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import ( + "github.com/gofiber/fiber/v2" + "net/http/httptest" + "time" +) +func main() { + app := fiber.New() + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + _ = app.Test(req, 2*time.Second) +}`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, MigrateAppTestConfig(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Contains(t, content, `app.Test(req, fiber.TestConfig{Timeout: 2*time.Second})`) + assert.Contains(t, buf.String(), "Migrating app.Test usages") +} + +func Test_MigrateMiddlewareLocals(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mlocals") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import "github.com/gofiber/fiber/v2" +func handler(c fiber.Ctx) error { + id := c.Locals("requestid") + _ = id + return nil +}`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, MigrateMiddlewareLocals(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Contains(t, content, `requestid.FromContext(c)`) + assert.Contains(t, buf.String(), "Migrating middleware locals") +} + +func Test_MigrateReqHeaderParser(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mrhp") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import "github.com/gofiber/fiber/v2" +func handler(c fiber.Ctx) error { + var v any + c.ReqHeaderParser(&v) + return nil +}`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, MigrateReqHeaderParser(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Contains(t, content, `.Bind().Header(&v)`) + assert.Contains(t, buf.String(), "Migrating request header parser helper") +} + +func Test_MigrateSessionConfig(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "msession") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import ( + "github.com/gofiber/fiber/v2/middleware/session" + "time" +) +var _ = session.New(session.Config{ + Expiration: 5 * time.Minute, +})`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, MigrateSessionConfig(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Contains(t, content, "IdleTimeout:") + assert.NotContains(t, content, "Expiration:") + assert.Contains(t, buf.String(), "Migrating session middleware configs") +} diff --git a/cmd/internal/prompt.go b/cmd/internal/prompt.go index dd32985..72bf145 100644 --- a/cmd/internal/prompt.go +++ b/cmd/internal/prompt.go @@ -11,18 +11,20 @@ import ( type errMsg error +// Prompt represents a small interactive input prompt used in the CLI. type Prompt struct { - p *tea.Program - textInput input.Model err error + p *tea.Program title string answer string + textInput input.Model } +// NewPrompt initializes a new Prompt with an optional placeholder value. func NewPrompt(title string, placeholder ...string) *Prompt { p := &Prompt{ title: title, - textInput: input.NewModel(), + textInput: input.New(), } if len(placeholder) > 0 { @@ -34,6 +36,7 @@ func NewPrompt(title string, placeholder ...string) *Prompt { return p } +// YesOrNo runs the prompt and returns true if the answer resembles "yes". func (p *Prompt) YesOrNo() (bool, error) { answer, err := p.Answer() if err != nil { @@ -43,6 +46,7 @@ func (p *Prompt) YesOrNo() (bool, error) { return parseBool(answer), nil } +// parseBool returns true if the provided string represents a truthy value. func parseBool(str string) bool { switch str { case "1", "t", "T", "true", "TRUE", "True", "y", "Y", "yes", "Yes": @@ -51,23 +55,26 @@ func parseBool(str string) bool { return false } +// Answer displays the prompt and returns the user's input. func (p *Prompt) Answer() (result string, err error) { if _, err = checkConsole(); err != nil { - return + return "", fmt.Errorf("check console: %w", err) } - if err := p.p.Start(); err != nil { - return "", err + if _, err := p.p.Run(); err != nil { + return "", fmt.Errorf("run prompt: %w", err) } return p.answer, nil } +// Init initializes the bubbletea program for the prompt. func (p *Prompt) Init() tea.Cmd { p.textInput.Focus() return input.Blink } +// Update handles prompt events and updates its state. func (p *Prompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd @@ -81,6 +88,8 @@ func (p *Prompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyEnter: p.answer = p.textInput.Value() return p, tea.Quit + default: + // ignore other keys } // We handle errors just like any other message @@ -93,6 +102,7 @@ func (p *Prompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return p, cmd } +// View renders the prompt UI. func (p *Prompt) View() string { return fmt.Sprintf( "%s\n\n%s\n\n%s\n\n", diff --git a/cmd/internal/prompt_test.go b/cmd/internal/prompt_test.go index e96308b..717b8e3 100644 --- a/cmd/internal/prompt_test.go +++ b/cmd/internal/prompt_test.go @@ -6,6 +6,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_Prompt_New(t *testing.T) { @@ -26,7 +27,7 @@ func Test_Prompt_Answer(t *testing.T) { p := NewPrompt("") _, err := p.Answer() - assert.NotNil(t, err) + require.Error(t, err) } func Test_Prompt_YesOrNo(t *testing.T) { @@ -34,7 +35,7 @@ func Test_Prompt_YesOrNo(t *testing.T) { p := NewPrompt("") _, err := p.YesOrNo() - assert.NotNil(t, err) + require.Error(t, err) } func Test_Prompt_ParseBool(t *testing.T) { @@ -67,7 +68,7 @@ func Test_Prompt_Update(t *testing.T) { at.Nil(cmd) _, cmd = p.Update(errMsg(errors.New("fake error"))) - at.NotNil(p.err) + require.Error(t, p.err) at.Nil(cmd) _, cmd = p.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'a'}}) diff --git a/cmd/internal/task.go b/cmd/internal/task.go index 6f3b4b4..eafb2be 100644 --- a/cmd/internal/task.go +++ b/cmd/internal/task.go @@ -8,18 +8,21 @@ import ( "github.com/muesli/termenv" ) +// Task is a function executed while the spinner is displayed. type Task func() error +// SpinnerTask runs a Task while showing a spinner animation. type SpinnerTask struct { - p *tea.Program - spinnerModel spinner.Model err error - title string + p *tea.Program task Task + title string + spinnerModel spinner.Model } +// NewSpinnerTask creates a SpinnerTask with the provided title and Task. func NewSpinnerTask(title string, task Task) *SpinnerTask { - spinnerModel := spinner.NewModel() + spinnerModel := spinner.New() spinnerModel.Spinner = spinner.Dot at := &SpinnerTask{ @@ -33,26 +36,27 @@ func NewSpinnerTask(title string, task Task) *SpinnerTask { return at } +// Init implements the tea.Model interface. func (t *SpinnerTask) Init() tea.Cmd { return tea.Batch( func() tea.Msg { - return finishedMsg{t.task()} - }, spinner.Tick) + return finishedError{t.task()} + }, t.spinnerModel.Tick) } +// Update handles spinner events and updates its state. func (t *SpinnerTask) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { - case tea.KeyMsg: switch msg.String() { case "q", "esc", "ctrl+c": - t.err = fmt.Errorf("quit by %s\n", msg.String()) + t.err = fmt.Errorf("quit by %s", msg.String()) return t, tea.Quit default: return t, nil } - case finishedMsg: + case finishedError: t.err = msg.error return t, tea.Quit @@ -61,9 +65,9 @@ func (t *SpinnerTask) Update(msg tea.Msg) (tea.Model, tea.Cmd) { t.spinnerModel, cmd = t.spinnerModel.Update(msg) return t, cmd } - } +// View renders the spinner view. func (t *SpinnerTask) View() string { if t.err != nil { return "" @@ -77,13 +81,14 @@ func (t *SpinnerTask) View() string { return fmt.Sprintf("\n %s %s\n\n(esc/q/ctrl+c to quit)\n\n", s, t.title) } +// Run executes the task while showing a spinner. func (t *SpinnerTask) Run() (err error) { if _, err = checkConsole(); err != nil { - return + return err } - if err = t.p.Start(); err != nil { - return + if _, err = t.p.Run(); err != nil { + return fmt.Errorf("run spinner: %w", err) } return t.err diff --git a/cmd/migrate.go b/cmd/migrate.go new file mode 100644 index 0000000..a9508a9 --- /dev/null +++ b/cmd/migrate.go @@ -0,0 +1,67 @@ +package cmd + +import ( + "fmt" + "os" + "strings" + + "github.com/Masterminds/semver/v3" + "github.com/gofiber/cli/cmd/internal/migrations" + "github.com/muesli/termenv" + "github.com/spf13/cobra" +) + +var targetVersionS string + +func init() { + latestFiberVersion, err := LatestFiberVersion() + if err != nil { + latestFiberVersion = "" + } + + migrateCmd.Flags().StringVarP(&targetVersionS, "to", "t", "", "Migrate to a specific version e.g:"+latestFiberVersion+" Format: X.Y.Z") + if err := migrateCmd.MarkFlagRequired("to"); err != nil { + panic(err) + } +} + +var migrateCmd = &cobra.Command{ + Use: "migrate", + Short: "Migrate Fiber project version to a newer version", + RunE: migrateRunE, +} + +func migrateRunE(cmd *cobra.Command, _ []string) error { + currentVersionS, err := currentVersion() + if err != nil { + return fmt.Errorf("current fiber project version not found: %w", err) + } + currentVersionS = strings.TrimPrefix(currentVersionS, "v") + currentVersion := semver.MustParse(currentVersionS) + + targetVersionS = strings.TrimPrefix(targetVersionS, "v") + targetVersion, err := semver.NewVersion(targetVersionS) + if err != nil { + return fmt.Errorf("invalid version for \"%s\": %w", targetVersionS, err) + } + + if !targetVersion.GreaterThan(currentVersion) { + return fmt.Errorf("target version v%s is not greater than current version v%s", targetVersionS, currentVersionS) + } + + wd, err := os.Getwd() + if err != nil { + return fmt.Errorf("cannot get current working directory: %w", err) + } + + err = migrations.DoMigration(cmd, wd, currentVersion, targetVersion) + if err != nil { + return fmt.Errorf("migration failed %w", err) + } + + msg := fmt.Sprintf("Migration from Fiber %s to %s", currentVersionS, targetVersionS) + cmd.Println(termenv.String(msg). + Foreground(termenv.ANSIBrightBlue)) + + return nil +} diff --git a/cmd/new.go b/cmd/new.go index 21f2aff..8de0105 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -37,15 +37,20 @@ func newRunE(cmd *cobra.Command, args []string) (err error) { modName = args[1] } - wd, _ := os.Getwd() + wd, err := os.Getwd() + if err != nil { + return fmt.Errorf("getwd: %w", err) + } projectPath := fmt.Sprintf("%s%c%s", wd, os.PathSeparator, projectName) - if err = createProject(projectPath); err != nil { - return + if err := createProject(projectPath); err != nil { + return err } defer func() { if err != nil { - _ = os.RemoveAll(projectPath) + if rmErr := os.RemoveAll(projectPath); rmErr != nil { + fmt.Fprintf(cmd.ErrOrStderr(), "failed to remove project dir: %v", rmErr) + } } }() @@ -64,32 +69,37 @@ func newRunE(cmd *cobra.Command, args []string) (err error) { return create(projectPath, modName) } -func createProject(projectPath string) (err error) { - if err = os.Mkdir(projectPath, 0750); err != nil { - return +func createProject(projectPath string) error { + if err := os.Mkdir(projectPath, 0o750); err != nil { + return fmt.Errorf("create directory: %w", err) + } + + if err := os.Chdir(projectPath); err != nil { + return fmt.Errorf("change directory: %w", err) } - return os.Chdir(projectPath) + return nil } -func createBasic(projectPath, modName string) (err error) { - // create main.go - if err = createFile(fmt.Sprintf("%s%cmain.go", projectPath, os.PathSeparator), newBasicTemplate); err != nil { - return +func createBasic(projectPath, modName string) error { + if err := createFile(fmt.Sprintf("%s%cmain.go", projectPath, os.PathSeparator), newBasicTemplate); err != nil { + return err } return runCmd(execCommand("go", "mod", "init", modName)) } -const githubPrefix = "https://github.com/" -const defaultRepo = "gofiber/boilerplate" +const ( + githubPrefix = "https://github.com/" + defaultRepo = "gofiber/boilerplate" +) var fullPathRegex = regexp.MustCompile(`^(http|https|git)`) -func createComplex(projectPath, modName string) (err error) { - var git string - if git, err = execLookPath("git"); err != nil { - return +func createComplex(projectPath, modName string) error { + git, err := execLookPath("git") + if err != nil { + return err } toClone := githubPrefix + repo @@ -97,20 +107,20 @@ func createComplex(projectPath, modName string) (err error) { toClone = repo } - if err = runCmd(execCommand(git, "clone", toClone, projectPath)); err != nil { - return + if err := runCmd(execCommand(git, "clone", toClone, projectPath)); err != nil { + return err } if repo == defaultRepo { - if err = replace(projectPath, "go.mod", "boilerplate", modName); err != nil { - return + if err := replace(projectPath, "go.mod", "boilerplate", modName); err != nil { + return err } - if err = replace(projectPath, "*.go", "boilerplate", modName); err != nil { - return + if err := replace(projectPath, "*.go", "boilerplate", modName); err != nil { + return err } } - return + return nil } var ( diff --git a/cmd/new_test.go b/cmd/new_test.go index 5dae83d..977b3ec 100644 --- a/cmd/new_test.go +++ b/cmd/new_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_New_Run(t *testing.T) { @@ -12,8 +13,8 @@ func Test_New_Run(t *testing.T) { t.Run("new project", func(t *testing.T) { defer func() { - at.Nil(os.Chdir("../")) - _ = os.RemoveAll("normal") + require.NoError(t, os.Chdir("../")) + require.NoError(t, os.RemoveAll("normal")) }() setupCmd() @@ -21,14 +22,14 @@ func Test_New_Run(t *testing.T) { out, err := runCobraCmd(newCmd, "normal") - at.Nil(err) + require.NoError(t, err) at.Contains(out, "Done") }) t.Run("custom mod name", func(t *testing.T) { defer func() { - at.Nil(os.Chdir("../")) - at.Nil(os.RemoveAll("custom_mod_name")) + require.NoError(t, os.Chdir("../")) + require.NoError(t, os.RemoveAll("custom_mod_name")) }() setupCmd() @@ -36,28 +37,28 @@ func Test_New_Run(t *testing.T) { out, err := runCobraCmd(newCmd, "custom_mod_name", "name") - at.Nil(err) + require.NoError(t, err) at.Contains(out, "name") }) t.Run("create complex project", func(t *testing.T) { defer func() { - at.Nil(os.Chdir("../")) - at.Nil(os.RemoveAll("complex")) + require.NoError(t, os.Chdir("../")) + require.NoError(t, os.RemoveAll("complex")) }() setupCmd() defer teardownCmd() out, err := runCobraCmd(newCmd, "complex", "-t=complex") - at.Nil(err) + require.NoError(t, err) at.Contains(out, "Done") }) t.Run("failed to create complex project", func(t *testing.T) { defer func() { - at.Nil(os.Chdir("../")) - at.Nil(os.RemoveAll("complex_failed")) + require.NoError(t, os.Chdir("../")) + require.NoError(t, os.RemoveAll("complex_failed")) }() setupCmd(errFlag) @@ -65,30 +66,28 @@ func Test_New_Run(t *testing.T) { out, err := runCobraCmd(newCmd, "complex_failed", "-t=complex") - at.NotNil(err) + require.Error(t, err) at.Contains(out, "failed to run") }) t.Run("invalid project name", func(t *testing.T) { out, err := runCobraCmd(newCmd, ".") - at.NotNil(err) + require.Error(t, err) at.Contains(out, ".") }) } func Test_New_CreateBasic(t *testing.T) { - assert.NotNil(t, createBasic(" ", "name")) + require.Error(t, createBasic(" ", "name")) } func Test_New_CreateComplex(t *testing.T) { - at := assert.New(t) - t.Run("look path error", func(t *testing.T) { setupLookPath(errFlag) defer teardownLookPath() - at.NotNil(createComplex(" ", "name")) + require.Error(t, createComplex(" ", "name")) }) t.Run("failed to replace pattern", func(t *testing.T) { @@ -99,6 +98,6 @@ func Test_New_CreateComplex(t *testing.T) { repo = "git@any.provider.com:id/repo.git" - at.NotNil(createComplex(" ", "name")) + require.Error(t, createComplex(" ", "name")) }) } diff --git a/cmd/root.go b/cmd/root.go index 2d5d16d..f1da5d3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "os" "time" "github.com/gofiber/cli/cmd/internal" @@ -9,15 +10,15 @@ import ( "github.com/spf13/cobra" ) -const version = "0.0.9" -const configName = ".fiberconfig" - -var ( - rc = rootConfig{ - CliVersionCheckInterval: int64((time.Hour * 12) / time.Second), - } +const ( + version = "0.0.9" + configName = ".fiberconfig" ) +var rc = rootConfig{ + CliVersionCheckInterval: int64((time.Hour * 12) / time.Second), +} + type rootConfig struct { CliVersionCheckInterval int64 `json:"cli_version_check_interval"` CliVersionCheckedAt int64 `json:"cli_version_checked_at"` @@ -25,7 +26,7 @@ type rootConfig struct { func init() { rootCmd.AddCommand( - versionCmd, newCmd, devCmd, upgradeCmd, + versionCmd, newCmd, devCmd, upgradeCmd, migrateCmd, ) } @@ -49,7 +50,7 @@ func Execute() { } func rootRunE(cmd *cobra.Command, _ []string) error { - return cmd.Help() + return fmt.Errorf("help: %w", cmd.Help()) } func rootPersistentPreRun(cmd *cobra.Command, _ []string) { @@ -68,7 +69,7 @@ func checkCliVersion(cmd *cobra.Command) { return } - cliLatestVersion, err := latestVersion(true) + cliLatestVersion, err := LatestCliVersion() if err != nil { return } @@ -95,7 +96,11 @@ func checkCliVersion(cmd *cobra.Command) { func updateVersionCheckedAt() { rc.CliVersionCheckedAt = time.Now().Unix() - storeConfig() + if err := storeConfig(); err != nil { + if _, pErr := fmt.Fprintf(os.Stdout, "failed to store config: %v\n", err); pErr != nil { + fmt.Fprintf(os.Stderr, "print error: %v", pErr) + } + } } func needCheckCliVersion() bool { diff --git a/cmd/root_test.go b/cmd/root_test.go index 094f785..7c38b91 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -11,6 +11,7 @@ import ( "github.com/jarcoal/httpmock" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_Root_Execute(t *testing.T) { @@ -22,7 +23,7 @@ func Test_Root_Execute(t *testing.T) { oldRunE := rootCmd.RunE rootCmd.RunE = func(_ *cobra.Command, _ []string) error { - return fmt.Errorf("fake error") + return errors.New("fake error") } Execute() @@ -35,7 +36,8 @@ func Test_Root_Execute(t *testing.T) { func Test_Root_RunE(t *testing.T) { at, b := setupRootCmd(t) - at.Nil(rootRunE(rootCmd, nil)) + err := rootRunE(rootCmd, nil) + require.Error(t, err) at.Contains(b.String(), "fiber") } @@ -81,7 +83,7 @@ func Test_Root_CheckCliVersion(t *testing.T) { httpmock.Activate() defer httpmock.DeactivateAndReset() - httpmock.RegisterResponder(http.MethodGet, latestCliVersionUrl, httpmock.NewErrorResponder(errors.New("network error"))) + httpmock.RegisterResponder(http.MethodGet, latestCliVersionURL, httpmock.NewErrorResponder(errors.New("network error"))) checkCliVersion(rootCmd) @@ -95,7 +97,7 @@ func Test_Root_CheckCliVersion(t *testing.T) { teardownHomeDir(tempHome) }() - httpmock.RegisterResponder(http.MethodGet, latestCliVersionUrl, httpmock.NewBytesResponder(200, fakeCliVersionResponse())) + httpmock.RegisterResponder(http.MethodGet, latestCliVersionURL, httpmock.NewBytesResponder(200, fakeCliVersionResponse())) checkCliVersion(rootCmd) @@ -113,6 +115,7 @@ func Test_Root_NeedCheckCliVersion(t *testing.T) { } func setupRootCmd(t *testing.T) (*assert.Assertions, *bytes.Buffer) { + t.Helper() at := assert.New(t) b := &bytes.Buffer{} @@ -122,7 +125,7 @@ func setupRootCmd(t *testing.T) (*assert.Assertions, *bytes.Buffer) { return at, b } -var latestCliVersionUrl = "https://api.github.com/repos/gofiber/cli/releases/latest" +var latestCliVersionURL = "https://api.github.com/repos/gofiber/cli/releases/latest" var fakeCliVersionResponse = func(version ...string) []byte { v := "99.99.99" diff --git a/cmd/tester_test.go b/cmd/tester_test.go index 5686831..c36bef4 100644 --- a/cmd/tester_test.go +++ b/cmd/tester_test.go @@ -4,7 +4,6 @@ import ( "bytes" "errors" "fmt" - "io/ioutil" "os" "os/exec" "testing" @@ -17,11 +16,13 @@ import ( var ( needError bool errFlag = struct{}{} + testExit = os.Exit // for testing exit ) func fakeExecCommand(command string, args ...string) *exec.Cmd { cs := []string{"-test.run=TestHelperProcess", "--", command} cs = append(cs, args...) + // #nosec G204 -- safe for test, args are controlled cmd := exec.Command(os.Args[0], cs...) cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"} if needError { @@ -31,6 +32,7 @@ func fakeExecCommand(command string, args ...string) *exec.Cmd { } func TestHelperProcess(t *testing.T) { + t.Helper() if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { return } @@ -45,15 +47,17 @@ func TestHelperProcess(t *testing.T) { if len(args) == 0 { _, _ = fmt.Fprintf(os.Stderr, "No command") - os.Exit(2) + testExit(2) + return } if os.Getenv("GO_WANT_HELPER_NEED_ERR") == "1" { _, _ = fmt.Fprintf(os.Stderr, "fake error") - os.Exit(1) + testExit(1) + return } - os.Exit(0) + testExit(0) } func setupCmd(flag ...struct{}) { @@ -69,11 +73,11 @@ func teardownCmd() { } func setupLookPath(flag ...struct{}) { - execLookPath = func(file string) (s string, err error) { + execLookPath = func(_ string) (s string, err error) { if len(flag) > 0 { err = errors.New("fake look path error") } - return + return "", err } } @@ -87,10 +91,12 @@ func setupOsExit(override ...func(int)) { fn = override[0] } osExit = fn + testExit = fn } func teardownOsExit() { osExit = os.Exit + testExit = os.Exit } func runCobraCmd(cmd *cobra.Command, args ...string) (string, error) { @@ -106,13 +112,17 @@ func runCobraCmd(cmd *cobra.Command, args ...string) (string, error) { } func setupHomeDir(t *testing.T, pattern string) string { - homeDir, err := ioutil.TempDir("", "test_"+pattern) - assert.Nil(t, err) + t.Helper() + homeDir, err := os.MkdirTemp("", "test_"+pattern) + assert.NoError(t, err) return homeDir } func teardownHomeDir(dir string) { - _ = os.RemoveAll(dir) + err := os.RemoveAll(dir) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to remove temp dir: %v", err) + } } func setupSpinner() { diff --git a/cmd/upgrade.go b/cmd/upgrade.go index 128d641..a2b338a 100644 --- a/cmd/upgrade.go +++ b/cmd/upgrade.go @@ -18,7 +18,7 @@ var upgradeCmd = &cobra.Command{ var upgraded bool func upgradeRunE(cmd *cobra.Command, _ []string) error { - cliLatestVersion, err := latestVersion(true) + cliLatestVersion, err := LatestCliVersion() if err != nil { return err } diff --git a/cmd/upgrade_test.go b/cmd/upgrade_test.go index 2b8d9cc..ac35b2e 100644 --- a/cmd/upgrade_test.go +++ b/cmd/upgrade_test.go @@ -8,6 +8,7 @@ import ( "github.com/jarcoal/httpmock" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_Upgrade_upgradeRunE(t *testing.T) { @@ -20,24 +21,24 @@ func Test_Upgrade_upgradeRunE(t *testing.T) { httpmock.Activate() defer httpmock.DeactivateAndReset() - httpmock.RegisterResponder(http.MethodGet, latestCliVersionUrl, httpmock.NewErrorResponder(errors.New("network error"))) + httpmock.RegisterResponder(http.MethodGet, latestCliVersionURL, httpmock.NewErrorResponder(errors.New("network error"))) - at.NotNil(upgradeRunE(upgradeCmd, nil)) + require.Error(t, upgradeRunE(upgradeCmd, nil)) - httpmock.RegisterResponder(http.MethodGet, latestCliVersionUrl, httpmock.NewBytesResponder(200, fakeCliVersionResponse())) + httpmock.RegisterResponder(http.MethodGet, latestCliVersionURL, httpmock.NewBytesResponder(200, fakeCliVersionResponse())) setupSpinner() defer teardownSpinner() - at.Nil(upgradeRunE(upgradeCmd, nil)) + require.NoError(t, upgradeRunE(upgradeCmd, nil)) at.Contains(b.String(), "99.99.99") - httpmock.RegisterResponder(http.MethodGet, latestCliVersionUrl, httpmock.NewBytesResponder(200, fakeCliVersionResponse(version))) + httpmock.RegisterResponder(http.MethodGet, latestCliVersionURL, httpmock.NewBytesResponder(200, fakeCliVersionResponse(version))) b.Reset() - at.Nil(upgradeRunE(upgradeCmd, nil)) + require.NoError(t, upgradeRunE(upgradeCmd, nil)) at.Contains(b.String(), "Currently") } diff --git a/cmd/version.go b/cmd/version.go index b94a107..67133bd 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -1,11 +1,14 @@ package cmd import ( + "context" "errors" "fmt" - "io/ioutil" + "io" "net/http" + "os" "regexp" + "time" "github.com/spf13/cobra" ) @@ -27,7 +30,7 @@ func versionRun(cmd *cobra.Command, _ []string) { cur = err.Error() } - if latest, err = latestVersion(false); err != nil { + if latest, err = LatestFiberVersion(); err != nil { _, _ = fmt.Fprintf(w, "fiber version: %v\n", err) return } @@ -35,13 +38,15 @@ func versionRun(cmd *cobra.Command, _ []string) { _, _ = fmt.Fprintf(w, "fiber version: %s (latest %s)\n", cur, latest) } -var currentVersionRegexp = regexp.MustCompile(`github\.com/gofiber/fiber[^\n]*? (.*)\n`) -var currentVersionFile = "go.mod" +var ( + currentVersionRegexp = regexp.MustCompile(`github\.com/gofiber/fiber[^\n]*? (.*)\n`) + currentVersionFile = "go.mod" +) func currentVersion() (string, error) { - b, err := ioutil.ReadFile(currentVersionFile) + b, err := os.ReadFile(currentVersionFile) if err != nil { - return "", err + return "", fmt.Errorf("read current version file: %w", err) } if submatch := currentVersionRegexp.FindSubmatch(b); len(submatch) == 2 { @@ -53,28 +58,45 @@ func currentVersion() (string, error) { var latestVersionRegexp = regexp.MustCompile(`"name":\s*?"v(.*?)"`) -func latestVersion(isCli bool) (v string, err error) { +// LatestFiberVersion retrieves the most recent Fiber release version from GitHub. +func LatestFiberVersion() (string, error) { + return latestVersionByURL("https://api.github.com/repos/gofiber/fiber/releases/latest") +} + +// LatestCliVersion retrieves the latest Fiber CLI release version from GitHub. +func LatestCliVersion() (string, error) { + return latestVersionByURL("https://api.github.com/repos/gofiber/cli/releases/latest") +} + +func latestVersionByURL(url string) (string, error) { var ( res *http.Response b []byte ) - if isCli { - res, err = http.Get("https://api.github.com/repos/gofiber/cli/releases/latest") - } else { - res, err = http.Get("https://api.github.com/repos/gofiber/fiber/releases/latest") + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return "", fmt.Errorf("create http request: %w", err) } + client := &http.Client{} + res, err = client.Do(req) if err != nil { - return + return "", fmt.Errorf("http request failed: %w", err) } defer func() { - _ = res.Body.Close() + if cerr := res.Body.Close(); cerr != nil && err == nil { + err = cerr + } }() - if b, err = ioutil.ReadAll(res.Body); err != nil { - return + b, err = io.ReadAll(res.Body) + if err != nil { + return "", fmt.Errorf("read response body: %w", err) } if submatch := latestVersionRegexp.FindSubmatch(b); len(submatch) == 2 { diff --git a/cmd/version_test.go b/cmd/version_test.go index 05271e9..1746f1d 100644 --- a/cmd/version_test.go +++ b/cmd/version_test.go @@ -2,13 +2,13 @@ package cmd import ( "errors" - "io/ioutil" "net/http" "os" "testing" "github.com/jarcoal/httpmock" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_Version_Printer(t *testing.T) { @@ -17,10 +17,10 @@ func Test_Version_Printer(t *testing.T) { httpmock.Activate() defer httpmock.DeactivateAndReset() - httpmock.RegisterResponder(http.MethodGet, latestVersionUrl, httpmock.NewBytesResponder(200, fakeVersionResponse)) + httpmock.RegisterResponder(http.MethodGet, latestVersionURL, httpmock.NewBytesResponder(200, fakeVersionResponse)) out, err := runCobraCmd(versionCmd) - at.Nil(err) + require.NoError(t, err) at.Contains(out, "2.0.6") }) @@ -28,10 +28,10 @@ func Test_Version_Printer(t *testing.T) { httpmock.Activate() defer httpmock.DeactivateAndReset() - httpmock.RegisterResponder(http.MethodGet, latestVersionUrl, httpmock.NewBytesResponder(200, []byte("no version"))) + httpmock.RegisterResponder(http.MethodGet, latestVersionURL, httpmock.NewBytesResponder(200, []byte("no version"))) out, err := runCobraCmd(versionCmd) - at.Nil(err) + require.NoError(t, err) at.Contains(out, "no version") }) } @@ -44,7 +44,7 @@ func Test_Version_Current(t *testing.T) { defer teardownCurrentVersionFile() _, err := currentVersion() - at.NotNil(err) + require.Error(t, err) }) t.Run("match version", func(t *testing.T) { @@ -59,7 +59,7 @@ require ( defer teardownCurrentVersionFile() v, err := currentVersion() - at.Nil(err) + require.NoError(t, err) at.Equal("v2.0.6", v) }) @@ -75,7 +75,7 @@ require ( defer teardownCurrentVersionFile() v, err := currentVersion() - at.Nil(err) + require.NoError(t, err) at.Equal("v0.0.0-20200926082917-55763e7e6ee3", v) }) @@ -90,19 +90,23 @@ require ( defer teardownCurrentVersionFile() _, err := currentVersion() - at.NotNil(err) + require.Error(t, err) }) } func setupCurrentVersionFile(content ...string) { currentVersionFile = "current-version" if len(content) > 0 { - _ = ioutil.WriteFile(currentVersionFile, []byte(content[0]), 0600) + if err := os.WriteFile(currentVersionFile, []byte(content[0]), 0o600); err != nil { + panic(err) + } } } func teardownCurrentVersionFile() { - _ = os.Remove(currentVersionFile) + if err := os.Remove(currentVersionFile); err != nil && !os.IsNotExist(err) { + panic(err) + } } func Test_Version_Latest(t *testing.T) { @@ -111,20 +115,20 @@ func Test_Version_Latest(t *testing.T) { httpmock.Activate() defer httpmock.DeactivateAndReset() - httpmock.RegisterResponder(http.MethodGet, latestVersionUrl, httpmock.NewErrorResponder(errors.New("network error"))) + httpmock.RegisterResponder(http.MethodGet, latestVersionURL, httpmock.NewErrorResponder(errors.New("network error"))) - _, err := latestVersion(false) - at.NotNil(err) + _, err := LatestFiberVersion() + require.Error(t, err) }) t.Run("version matched", func(t *testing.T) { httpmock.Activate() defer httpmock.DeactivateAndReset() - httpmock.RegisterResponder(http.MethodGet, latestVersionUrl, httpmock.NewBytesResponder(200, fakeVersionResponse)) + httpmock.RegisterResponder(http.MethodGet, latestVersionURL, httpmock.NewBytesResponder(200, fakeVersionResponse)) - v, err := latestVersion(false) - at.Nil(err) + v, err := LatestFiberVersion() + require.NoError(t, err) at.Equal("2.0.6", v) }) @@ -132,13 +136,13 @@ func Test_Version_Latest(t *testing.T) { httpmock.Activate() defer httpmock.DeactivateAndReset() - httpmock.RegisterResponder(http.MethodGet, latestVersionUrl, httpmock.NewBytesResponder(200, []byte("no version"))) + httpmock.RegisterResponder(http.MethodGet, latestVersionURL, httpmock.NewBytesResponder(200, []byte("no version"))) - _, err := latestVersion(false) - at.NotNil(err) + _, err := LatestFiberVersion() + require.Error(t, err) }) } -var latestVersionUrl = "https://api.github.com/repos/gofiber/fiber/releases/latest" +var latestVersionURL = "https://api.github.com/repos/gofiber/fiber/releases/latest" var fakeVersionResponse = []byte(`{ "url": "https://api.github.com/repos/gofiber/fiber/releases/32189569", "assets_url": "https://api.github.com/repos/gofiber/fiber/releases/32189569/assets", "upload_url": "https://uploads.github.com/repos/gofiber/fiber/releases/32189569/assets{?name,label}", "html_url": "https://github.com/gofiber/fiber/releases/tag/v2.0.6", "id": 32189569, "node_id": "MDc6UmVsZWFzZTMyMTg5NTY5", "tag_name": "v2.0.6", "target_commitish": "master", "name": "v2.0.6", "draft": false, "author": { "login": "Fenny", "id": 25108519, "node_id": "MDQ6VXNlcjI1MTA4NTE5", "avatar_url": "https://avatars1.githubusercontent.com/u/25108519?v=4", "gravatar_id": "", "url": "https://api.github.com/users/Fenny", "html_url": "https://github.com/Fenny", "followers_url": "https://api.github.com/users/Fenny/followers", "following_url": "https://api.github.com/users/Fenny/following{/other_user}", "gists_url": "https://api.github.com/users/Fenny/gists{/gist_id}", "starred_url": "https://api.github.com/users/Fenny/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/Fenny/subscriptions", "organizations_url": "https://api.github.com/users/Fenny/orgs", "repos_url": "https://api.github.com/users/Fenny/repos", "events_url": "https://api.github.com/users/Fenny/events{/privacy}", "received_events_url": "https://api.github.com/users/Fenny/received_events", "type": "User", "site_admin": false }, "prerelease": false, "created_at": "2020-10-05T19:54:02Z", "published_at": "2020-10-05T22:10:27Z", "assets": [], "tarball_url": "https://api.github.com/repos/gofiber/fiber/tarball/v2.0.6", "zipball_url": "https://api.github.com/repos/gofiber/fiber/zipball/v2.0.6" }`) diff --git a/development.md b/development.md new file mode 100644 index 0000000..44c3c92 --- /dev/null +++ b/development.md @@ -0,0 +1,20 @@ +# Local Development + +Install [air](https://github.com/cosmtrek/air) + +```bash +go install github.com/cosmtrek/air@latest +``` + +Use air to watch for changes in the project and recompile the binary + +```bash +air --build.cmd="go install ./fiber" +``` + +Test the binary in fiber project + +```bash +cd my-fiber-project +fiber version +``` diff --git a/fiber/main.go b/fiber/main.go index fe403aa..887b917 100644 --- a/fiber/main.go +++ b/fiber/main.go @@ -1,6 +1,8 @@ package main -import "github.com/gofiber/cli/cmd" +import ( + "github.com/gofiber/cli/cmd" +) func main() { cmd.Execute() diff --git a/go.mod b/go.mod index 9fb64f3..ad22504 100644 --- a/go.mod +++ b/go.mod @@ -3,38 +3,43 @@ module github.com/gofiber/cli go 1.24 require ( - github.com/charmbracelet/bubbles v0.18.0 - github.com/charmbracelet/bubbletea v0.25.0 - github.com/containerd/console v1.0.4 - github.com/fsnotify/fsnotify v1.7.0 - github.com/jarcoal/httpmock v1.3.1 - github.com/muesli/termenv v0.15.2 - github.com/spf13/cobra v1.8.0 - github.com/stretchr/testify v1.9.0 + github.com/Masterminds/semver/v3 v3.4.0 + github.com/charmbracelet/bubbles v0.21.0 + github.com/charmbracelet/bubbletea v1.3.6 + github.com/containerd/console v1.0.5 + github.com/fsnotify/fsnotify v1.9.0 + github.com/jarcoal/httpmock v1.4.0 + github.com/muesli/termenv v0.16.0 + github.com/spf13/cobra v1.9.1 + github.com/stretchr/testify v1.10.0 ) require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/lipgloss v0.9.1 // indirect + github.com/charmbracelet/colorprofile v0.3.1 // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.9.3 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13 // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kr/text v0.2.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-isatty v0.0.20 // 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/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/reflow v0.3.0 // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rivo/uniseg v0.4.6 // indirect - github.com/spf13/pflag v1.0.5 // 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 + github.com/rivo/uniseg v0.4.7 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/text v0.27.0 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 76f7888..4991c07 100644 --- a/go.sum +++ b/go.sum @@ -1,73 +1,84 @@ +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 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.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= -github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= -github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= -github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= -github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= -github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= -github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= -github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= +github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= +github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU= +github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc= +github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40= +github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0= +github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= +github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= +github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= -github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= +github.com/jarcoal/httpmock v1.4.0 h1:BvhqnH0JAYbNudL2GMJKgOHe2CtKlzJ/5rWKyp+hc2k= +github.com/jarcoal/httpmock v1.4.0/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/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/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= -github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= -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/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI= +github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= 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/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg= -github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=