├── .dockerignore ├── .github └── workflows │ └── go.yml ├── .gitignore ├── .idea ├── .gitignore ├── go-http-server.iml ├── modules.xml └── vcs.xml ├── Dockerfile ├── LICENSE ├── README.md ├── docker-entrypoint.sh ├── publish.sh ├── src ├── .gitignore ├── app │ ├── app.go │ └── app_test.go ├── go.mod ├── go.sum ├── main.go ├── param │ ├── param.go │ └── param_test.go └── util │ ├── http.go │ ├── http_test.go │ ├── log.go │ ├── util.go │ └── util_test.go └── test └── frontend └── dist ├── assets ├── index.43cf8108.css ├── index.43cf8108.css.br ├── index.43cf8108.css.gz ├── index.795d9409.js ├── index.795d9409.js.br ├── index.795d9409.js.gz └── vue.5532db34.svg ├── example.html ├── index.html └── vite.svg /.dockerignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | public/ -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v3 18 | with: 19 | go-version: 1.19.1 20 | 21 | - name: Get coverage tool 22 | run: | 23 | cd src 24 | go get golang.org/x/tools/cmd/cover 25 | 26 | - name: Test 27 | run: | 28 | cd src 29 | go test ./... -coverprofile cover.out 30 | go tool cover -func cover.out > covered.txt 31 | 32 | - name: Get coverage 33 | run: | 34 | cd src 35 | for word in $(cat covered.txt); do total_percent=$word; done 36 | echo $total_percent 37 | echo "COVERAGE=$total_percent" >> $GITHUB_ENV 38 | REF=${{ github.ref }} 39 | IFS='/' read -ra PATHS <<< "$REF" 40 | BRANCH_NAME="${PATHS[1]}_${PATHS[2]}" 41 | echo $BRANCH_NAME 42 | echo "BRANCH=$(echo ${BRANCH_NAME})" >> $GITHUB_ENV 43 | 44 | - name: Create passing badge 45 | uses: schneegans/dynamic-badges-action@v1.0.0 46 | if: ${{ env.COVERAGE!=null }} 47 | with: 48 | auth: ${{ secrets.GIST_SECRET }} 49 | gistID: 7a0933f8cba0bddbcc95c8b850e32663 50 | filename: spa-to-http_units_passing__${{ env.BRANCH }}.json 51 | label: Tests 52 | message: Passed 53 | color: green 54 | namedLogo: checkmarx 55 | 56 | - name: Create coverage badge 57 | uses: schneegans/dynamic-badges-action@v1.0.0 58 | with: 59 | auth: ${{ secrets.GIST_SECRET }} 60 | gistID: 7a0933f8cba0bddbcc95c8b850e32663 61 | filename: spa-to-http_units_coverage__${{ env.BRANCH }}.json 62 | label: Test Coverage 63 | message: ${{ env.COVERAGE }} 64 | color: green 65 | namedLogo: go 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 2 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 3 | 4 | # User-specific stuff 5 | .idea/**/workspace.xml 6 | .idea/**/tasks.xml 7 | .idea/**/usage.statistics.xml 8 | .idea/**/dictionaries 9 | .idea/**/shelf 10 | 11 | # AWS User-specific 12 | .idea/**/aws.xml 13 | 14 | # Generated files 15 | .idea/**/contentModel.xml 16 | 17 | # Sensitive or high-churn files 18 | .idea/**/dataSources/ 19 | .idea/**/dataSources.ids 20 | .idea/**/dataSources.local.xml 21 | .idea/**/sqlDataSources.xml 22 | .idea/**/dynamic.xml 23 | .idea/**/uiDesigner.xml 24 | .idea/**/dbnavigator.xml 25 | 26 | # Gradle 27 | .idea/**/gradle.xml 28 | .idea/**/libraries 29 | 30 | # Gradle and Maven with auto-import 31 | # When using Gradle or Maven with auto-import, you should exclude module files, 32 | # since they will be recreated, and may cause churn. Uncomment if using 33 | # auto-import. 34 | # .idea/artifacts 35 | # .idea/compiler.xml 36 | # .idea/jarRepositories.xml 37 | # .idea/modules.xml 38 | # .idea/*.iml 39 | # .idea/modules 40 | # *.iml 41 | # *.ipr 42 | 43 | # CMake 44 | cmake-build-*/ 45 | 46 | # Mongo Explorer plugin 47 | .idea/**/mongoSettings.xml 48 | 49 | # File-based project format 50 | *.iws 51 | 52 | # IntelliJ 53 | out/ 54 | 55 | # mpeltonen/sbt-idea plugin 56 | .idea_modules/ 57 | 58 | # JIRA plugin 59 | atlassian-ide-plugin.xml 60 | 61 | # Cursive Clojure plugin 62 | .idea/replstate.xml 63 | 64 | # SonarLint plugin 65 | .idea/sonarlint/ 66 | 67 | # Crashlytics plugin (for Android Studio and IntelliJ) 68 | com_crashlytics_export_strings.xml 69 | crashlytics.properties 70 | crashlytics-build.properties 71 | fabric.properties 72 | 73 | # Editor-based Rest Client 74 | .idea/httpRequests 75 | 76 | # Android studio 3.1+ serialized cache file 77 | .idea/caches/build_file_checksums.ser 78 | 79 | public/ 80 | dist/ -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/go-http-server.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.18.2-alpine3.16 as builder 2 | 3 | WORKDIR /code/ 4 | 5 | ADD src/ . 6 | 7 | RUN go build -o dist/ -ldflags "-s -w" 8 | 9 | FROM alpine:3.16 10 | WORKDIR /code/ 11 | 12 | COPY docker-entrypoint.sh /bin/ 13 | ENTRYPOINT ["/bin/docker-entrypoint.sh"] 14 | 15 | COPY --from=builder /code/dist/go-http-server /bin/ 16 | RUN chmod +x /bin/go-http-server 17 | 18 | CMD "/bin/go-http-server" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Devforth.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Coverage Badge](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/LbP22/7a0933f8cba0bddbcc95c8b850e32663/raw/spa-to-http_units_passing__heads_main.json) ![Coverage Badge](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/LbP22/7a0933f8cba0bddbcc95c8b850e32663/raw/spa-to-http_units_coverage__heads_main.json) 2 | 3 | 4 | 5 | # World's fastest lightweight zero-configuration SPA HTTP server. 6 | 7 | Simply serves SPA bundle on HTTP port which makes it play well with Traefik / Cloudflare another reverse proxy. 8 | 9 | ## Benefits 10 | 11 | * Zero-configuration in Docker without managing additional configs 12 | * 10x times smaller then Nginx, faster startup time, a bit better or same performance 13 | * Plays well with all popular SPA frameworks and libraries: Vue, React, Angular, Svelte and bundlers: Webpack/Vite 14 | * Supports Brotly compression on original files, you don't need to archivate files by yourself, it does it for you 15 | * Written in Go, which makes it fast (no overhead on runtime) and tiny (small binary size) 16 | * Open-Source commercial friendly MIT license 17 | * Optimal statics caching out of the box: no-cache on index.html file to auto-update caches and infinite max-age for all other resources which have hash-URLs in most default SPA bundlers. 18 | * Plays well with CDNs caching (e.g. Clouflare), support for ignoring cache of fixed URLs like service worker 19 | * Created and maintained by Devforth 💪🏼 20 | 21 | ## Spa-to-http vs Nginx 22 | 23 | | | Spa-to-http | Nginx | 24 | |---|---|---| 25 | | Zero-configuration | ✅No config files, SPA serving works out of the box with most optimal settings | ❌Need to create a dedicated config file | 26 | | Ability to config settings like host, port, compression using Environment variables or CLI | ✅Yes | ❌No, only text config file | 27 | | Docker image size | ✅13.2 MiB (v1.0.3) | ❌142 MiB (v1.23.1) | 28 | | Brotli compression out-of-the-box | ✅Yes, just set env BROTLI=true | ❌You need a dedicated module like ngx_brotli | 29 | 30 | Performence accroding to [Spa-to-http vs Nginx benchmark (End of the post)](https://devforth.io/blog/deploy-react-vue-angular-in-docker-simply-and-efficiently-using-spa-to-http-and-traefik/) 31 | 32 | | | Spa-to-http | Nginx | 33 | |---|---|---| 34 | | Average time from container start to HTTP port availability (100 startups) | ✅1.358 s (11.5% faster) | ❌1.514s | 35 | | Requests-per-second on 0.5 KiB HTML file at localhost * | ✅80497 (1.6% faster) | ❌79214 | 36 | | Transfer speed on 0.5 KiB HTML file * | ❌74.16 MiB/sec | ✅75.09 MiB/sec (1.3% faster) | 37 | | Requests-per-second on 5 KiB JS file at localhost * | ✅66126 (5.2% faster) | ❌62831 | 38 | | Transfer speed on 5 KiB HTML file * | ✅301.32 MiB/sec (4.5% faster) | ❌288.4 | 39 | 40 | ## Hello world & ussage 41 | 42 | Create `Dockerfile` in yoru SPA directory (near `package.json`): 43 | 44 | ``` 45 | FROM node:20-alpine as builder 46 | WORKDIR /code/ 47 | ADD package-lock.json . 48 | ADD package.json . 49 | RUN npm ci 50 | ADD . . 51 | RUN npm run build 52 | 53 | FROM devforth/spa-to-http:latest 54 | COPY --from=builder /code/dist/ . 55 | ``` 56 | 57 | Test it locally: 58 | 59 | ```sh 60 | docker build -q . | xargs docker run --rm -p 8080:8080 61 | ``` 62 | 63 | So we built our frontend and included it into container based on Spa-to-http. This way gives us great benefits: 64 | 65 | * We build frontend in docker build time and improve build time for most changes (npm ci is not getting rebuild if there is no new packages) 66 | * Bundle has only small resulting dist folder, there is no source code and node_modules so countainer is small 67 | * When you start this container it serves SPA on HTTP port automatically with best settings. Spa-to-http already has right CMD inside which runs SPA-to-HTTP webserver with right caching 68 | 69 | 70 | # Example serving SPA with Traefik and Docker-Compose 71 | 72 | ``` 73 | version: "3.3" 74 | 75 | services: 76 | 77 | traefik: 78 | image: "traefik:v2.7" 79 | command: 80 | - "--providers.docker=true" 81 | - "--providers.docker.exposedbydefault=false" 82 | - "--entrypoints.web.address=:80" 83 | ports: 84 | - "80:80" 85 | volumes: 86 | - "/var/run/docker.sock:/var/run/docker.sock:ro" 87 | 88 | trfk-vue: 89 | build: "spa" # name of the folder where Dockerfile is located 90 | labels: 91 | - "traefik.enable=true" 92 | - "traefik.http.routers.trfk-vue.rule=Host(`trfk-vue.localhost`)" 93 | - "traefik.http.services.trfk-vue.loadbalancer.server.port=8080" # port inside of trfk-vue which should be used 94 | ``` 95 | 96 | How to enable Brotli compression: 97 | 98 | ```diff 99 | trfk-vue: 100 | build: "spa" 101 | ++ command: --brotli 102 | labels: 103 | - "traefik.enable=true" 104 | - "traefik.http.routers.trfk-vue.rule=Host(`trfk-vue.localhost`)" 105 | - "traefik.http.services.trfk-vue.loadbalancer.server.port=8080" 106 | ``` 107 | How to change thresshold of small files which should not be compressed: 108 | 109 | ```diff 110 | trfk-vue: 111 | build: "spa" 112 | -- command: --brotli 113 | ++ command: --brotli --threshold 500 114 | labels: 115 | - "traefik.enable=true" 116 | - "traefik.http.routers.trfk-vue.rule=Host(`trfk-vue.localhost`)" 117 | - "traefik.http.services.trfk-vue.loadbalancer.server.port=8080" 118 | ``` 119 | 120 | How to run container on a custom port: 121 | 122 | 123 | ```diff 124 | trfk-vue: 125 | build: "spa" 126 | ++ command: --brotli --port 8082 127 | labels: 128 | - "traefik.enable=true" 129 | - "traefik.http.routers.trfk-vue.rule=Host(`trfk-vue.localhost`)" 130 | -- - "traefik.http.services.trfk-vue.loadbalancer.server.port=8080" 131 | ++ - "traefik.http.services.trfk-vue.loadbalancer.server.port=8082" 132 | ``` 133 | 134 | Ignore caching for some specific resources, e.g. prevent Service Worker caching on CDNs like Cloudflare: 135 | 136 | 137 | 138 | ```diff 139 | trfk-vue: 140 | build: "spa" 141 | ++ command: --ignore-cache-control-paths "/sw.js" 142 | labels: 143 | - "traefik.enable=true" 144 | - "traefik.http.routers.trfk-vue.rule=Host(`trfk-vue.localhost`)" 145 | - "traefik.http.services.trfk-vue.loadbalancer.server.port=8080" 146 | ``` 147 | 148 | This is not needed for most of your assets because their filenames should contain file hash (added by default by modern bundlers). So cache naturally invalidated by referencing hashed assets from uncachable html. However some special resources like service worker must be served on fixed URL without file hash in filename 149 | 150 | 151 | 152 | ## Available Options: 153 | 154 | | Environment Variable | Command | Description | Defaults | 155 | |----------------------------|-----------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------| 156 | | ADDRESS | `-a` or `--address` | Address to use | 0.0.0.0 | 157 | | PORT | `-p` or `--port` | Port to listen on | 8080 | 158 | | GZIP | `--gzip` | When enabled it will create .gz files using gzip compression for files which size exceedes threshold and serve it instead of original one if client accepts gzip encoding. If brotli also enabled it will try to serve brotli first | `false` | 159 | | BROTLI | `--brotli` | When enabled it will create .br files using brotli compression for files which size exceedes threshold and serve it instead of original one if client accepts brotli encoding. If gzip also enabled it will try to serve brotli first | `false` | 160 | | THRESHOLD | `--threshold ` | Threshold in bytes for gzip and brotli compressions | 1024 | 161 | | DIRECTORY | `-d ` or `--directory ` | Directory to serve | `.` | 162 | | CACHE_MAX_AGE | `--cache-max-age ` | Set cache time (in seconds) for cache-control max-age header To disable cache set to -1. `.html` files are not being cached | 604800 | 163 | | IGNORE_CACHE_CONTROL_PATHS | `--ignore-cache-control-paths ` | Additional paths to set "Cache-control: no-store" via comma, example "/file1.js,/file2.js" | | 164 | | SPA_MODE | `--spa` or `--spa ` | When SPA mode if file for requested path does not exists server returns index.html from root of serving directory. SPA mode and directory listing cannot be enabled at the same time | `true` | 165 | | CACHE | `--cache` | When enabled f.Open reads are being cached using Two Queue LRU Cache in bits | `true` | 166 | | CACHE_BUFFER | `--cache-buffer ` | Specifies the maximum size of LRU cache in bytes | `51200` | 167 | | LOGGER | `--logger` | Enable requests logger | `false` | 168 | | LOG_PRETTY | `--log-pretty` | Print log messages in a pretty format instead of default JSON format | `false` | 169 | -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | _main() { 4 | # if first arg looks like a flag, assume we want to run server server 5 | if [ "${1:0:1}" = '-' ]; then 6 | set -- go-http-server "$@" 7 | fi 8 | 9 | exec "$@" 10 | } 11 | 12 | _main "$@" -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | docker buildx create --use 2 | docker buildx build --platform=linux/amd64,linux/arm64 --tag "devforth/spa-to-http:latest" --tag "devforth/spa-to-http:1.0.9" --push . 3 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | cover.out -------------------------------------------------------------------------------- /src/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "fmt" 7 | "github.com/andybalholm/brotli" 8 | lru "github.com/hashicorp/golang-lru" 9 | "go-http-server/param" 10 | "go-http-server/util" 11 | "golang.org/x/exp/slices" 12 | "io" 13 | "mime" 14 | "net/http" 15 | "os" 16 | "path" 17 | "path/filepath" 18 | "strings" 19 | "time" 20 | ) 21 | 22 | type App struct { 23 | params *param.Params 24 | server *http.Server 25 | cache *lru.TwoQueueCache 26 | } 27 | 28 | type ResponseItem struct { 29 | Name string 30 | Path string 31 | ModTime time.Time 32 | Content []byte 33 | ContentType string 34 | } 35 | 36 | type Compression int 37 | 38 | const ( 39 | None Compression = iota 40 | Gzip 41 | Brotli 42 | ) 43 | 44 | func NewApp(params *param.Params) App { 45 | var cache *lru.TwoQueueCache = nil 46 | var err error 47 | 48 | if params.CacheEnabled { 49 | cache, err = lru.New2Q(params.CacheBuffer) 50 | if err != nil { 51 | panic(err) 52 | } 53 | } 54 | 55 | return App{params: params, server: nil, cache: cache} 56 | } 57 | 58 | func (app *App) ShouldSkipCompression(filePath string) bool { 59 | ext := strings.ToLower(filepath.Ext(filePath)) 60 | for _, blocked := range app.params.NoCompress { 61 | if strings.ToLower(blocked) == ext { 62 | return true 63 | } 64 | } 65 | return false 66 | } 67 | 68 | func (app *App) CompressFiles() { 69 | if !app.params.Gzip && !app.params.Brotli { 70 | return 71 | } 72 | err := filepath.Walk(app.params.Directory, func(filePath string, info os.FileInfo, err error) error { 73 | if err != nil { 74 | return err 75 | } 76 | 77 | if info.IsDir() { 78 | return nil 79 | } 80 | 81 | ext := path.Ext(filePath) 82 | if ext == ".br" || ext == ".gz" { 83 | return nil 84 | } 85 | 86 | if app.ShouldSkipCompression(filePath) { 87 | return nil 88 | } 89 | 90 | if info.Size() > app.params.Threshold { 91 | data, _ := os.ReadFile(filePath) 92 | 93 | if app.params.Gzip { 94 | newName := filePath + ".gz" 95 | file, _ := os.Create(newName) 96 | 97 | writer := gzip.NewWriter(file) 98 | 99 | _, _ = writer.Write(data) 100 | _ = writer.Close() 101 | } 102 | 103 | if app.params.Brotli { 104 | newName := filePath + ".br" 105 | file, _ := os.Create(newName) 106 | 107 | writer := brotli.NewWriter(file) 108 | 109 | _, _ = writer.Write(data) 110 | _ = writer.Close() 111 | } 112 | } 113 | 114 | return nil 115 | }) 116 | 117 | if err != nil { 118 | panic(err) 119 | } 120 | } 121 | 122 | func (app *App) GetOrCreateResponseItem(requestedPath string, compression Compression, actualContentType *string) (*ResponseItem, int) { 123 | rootIndexPath := path.Join(app.params.Directory, "index.html") 124 | 125 | switch compression { 126 | case Gzip: 127 | requestedPath = requestedPath + ".gz" 128 | case Brotli: 129 | requestedPath = requestedPath + ".br" 130 | } 131 | 132 | if app.cache != nil { 133 | cacheValue, ok := app.cache.Get(requestedPath) 134 | if ok { 135 | responseItem, ok := cacheValue.(ResponseItem) 136 | if ok { 137 | return &responseItem, 0 138 | } else { 139 | localRedirectItem := cacheValue.(string) 140 | return app.GetOrCreateResponseItem(localRedirectItem, compression, actualContentType) 141 | } 142 | } 143 | } 144 | 145 | dirPath, fileName := filepath.Split(requestedPath) 146 | dir := http.Dir(dirPath) 147 | 148 | file, err := dir.Open(fileName) 149 | if err != nil { 150 | if app.params.SpaMode && compression == None && requestedPath != rootIndexPath { 151 | newPath := path.Join(app.params.Directory, "index.html") 152 | if app.cache != nil { 153 | app.cache.Add(requestedPath, newPath) 154 | } 155 | return app.GetOrCreateResponseItem(newPath, compression, actualContentType) 156 | } 157 | return nil, http.StatusNotFound 158 | } 159 | defer file.Close() 160 | 161 | stat, err := file.Stat() 162 | if err != nil { 163 | if app.params.SpaMode && compression == None && requestedPath != rootIndexPath { 164 | newPath := path.Join(app.params.Directory, "index.html") 165 | if app.cache != nil { 166 | app.cache.Add(requestedPath, newPath) 167 | } 168 | return app.GetOrCreateResponseItem(newPath, compression, actualContentType) 169 | } 170 | return nil, http.StatusNotFound 171 | } 172 | 173 | if stat.IsDir() && requestedPath != rootIndexPath { 174 | if app.params.SpaMode && compression == None { 175 | newPath := path.Join(app.params.Directory, "index.html") 176 | if app.cache != nil { 177 | app.cache.Add(requestedPath, newPath) 178 | } 179 | return app.GetOrCreateResponseItem(newPath, compression, actualContentType) 180 | } else if !app.params.SpaMode && compression == None { 181 | newPath := path.Join(requestedPath, "index.html") 182 | if app.cache != nil { 183 | app.cache.Add(requestedPath, newPath) 184 | } 185 | return app.GetOrCreateResponseItem(newPath, compression, actualContentType) 186 | } 187 | 188 | return nil, http.StatusNotFound 189 | } 190 | 191 | content := make([]byte, stat.Size()) 192 | _, err = io.ReadFull(file, content) 193 | if err != nil { 194 | return nil, http.StatusInternalServerError 195 | } 196 | 197 | name := stat.Name() 198 | var contentType string 199 | if compression == None { 200 | contentType = mime.TypeByExtension(filepath.Ext(name)) 201 | } else { 202 | contentType = *actualContentType 203 | } 204 | 205 | responseItem := ResponseItem{ 206 | Path: requestedPath, 207 | Name: name, 208 | ModTime: stat.ModTime(), 209 | Content: content, 210 | ContentType: contentType, 211 | } 212 | 213 | if app.cache != nil { 214 | app.cache.Add(requestedPath, responseItem) 215 | } 216 | 217 | return &responseItem, 0 218 | } 219 | 220 | func (app *App) GetFilePath(urlPath string) (string, bool) { 221 | requestedPath := path.Join(app.params.Directory, urlPath) 222 | 223 | if _, err := os.Stat(requestedPath); !os.IsNotExist(err) { 224 | requestedPath, err = filepath.EvalSymlinks(requestedPath) 225 | } 226 | 227 | if !strings.HasPrefix(requestedPath, app.params.Directory) { 228 | return "", false 229 | } 230 | 231 | return requestedPath, true 232 | } 233 | 234 | func (app *App) HandlerFuncNew(w http.ResponseWriter, r *http.Request) { 235 | requestedPath, valid := app.GetFilePath(r.URL.Path) 236 | 237 | if !valid { 238 | w.WriteHeader(http.StatusNotFound) 239 | return 240 | } 241 | 242 | responseItem, errorCode := app.GetOrCreateResponseItem(requestedPath, None, nil) 243 | if errorCode != 0 { 244 | w.WriteHeader(errorCode) 245 | return 246 | } 247 | 248 | if r.Header.Get("Range") != "" || app.ShouldSkipCompression(requestedPath) { 249 | if responseItem.ContentType != "" { 250 | w.Header().Set("Content-Type", responseItem.ContentType) 251 | } 252 | http.ServeContent(w, r, responseItem.Name, responseItem.ModTime, bytes.NewReader(responseItem.Content)) 253 | return 254 | } 255 | 256 | if slices.Contains(app.params.IgnoreCacheControlPaths, r.URL.Path) || path.Ext(responseItem.Name) == ".html" { 257 | w.Header().Set("Cache-Control", "no-store") 258 | } else { 259 | w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d", app.params.CacheControlMaxAge)) 260 | } 261 | 262 | var brotliApplicable bool 263 | var gzipApplicable bool 264 | var overThreshold = int64(len(responseItem.Content)) > app.params.Threshold 265 | 266 | acceptEncodingHeader := r.Header.Get("Accept-Encoding") 267 | if app.params.Brotli || app.params.Gzip { 268 | if strings.Contains(acceptEncodingHeader, "*") { 269 | brotliApplicable = app.params.Brotli 270 | gzipApplicable = app.params.Gzip 271 | } else { 272 | brotliApplicable = app.params.Brotli && strings.Contains(acceptEncodingHeader, "br") 273 | gzipApplicable = app.params.Gzip && strings.Contains(acceptEncodingHeader, "gzip") 274 | 275 | } 276 | } 277 | 278 | if brotliApplicable && overThreshold { 279 | brotliResponseItem, _ := app.GetOrCreateResponseItem(responseItem.Path, Brotli, &responseItem.ContentType) 280 | 281 | if brotliResponseItem != nil { 282 | responseItem = brotliResponseItem 283 | w.Header().Set("Content-Encoding", "br") 284 | } 285 | } else if gzipApplicable && overThreshold { 286 | gzipResponseItem, _ := app.GetOrCreateResponseItem(responseItem.Path, Gzip, &responseItem.ContentType) 287 | 288 | if gzipResponseItem != nil { 289 | responseItem = gzipResponseItem 290 | w.Header().Set("Content-Encoding", "gzip") 291 | } 292 | } 293 | 294 | if responseItem.ContentType != "" { 295 | w.Header().Set("Content-Type", responseItem.ContentType) 296 | } 297 | 298 | http.ServeContent(w, r, responseItem.Name, responseItem.ModTime, bytes.NewReader(responseItem.Content)) 299 | } 300 | 301 | func (app *App) Listen() { 302 | var handlerFunc http.Handler = http.HandlerFunc(app.HandlerFuncNew) 303 | if app.params.Logger { 304 | handlerFunc = util.LogRequestHandler(handlerFunc, &util.LogRequestHandlerOptions{ 305 | Pretty: app.params.LogPretty, 306 | }) 307 | } 308 | 309 | app.server = &http.Server{ 310 | Addr: fmt.Sprintf("%s:%d", app.params.Address, app.params.Port), 311 | Handler: handlerFunc, 312 | } 313 | 314 | fmt.Printf("Server listening on http://%s\n", app.server.Addr) 315 | err := app.server.ListenAndServe() 316 | if err != nil { 317 | panic(err) 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /src/app/app_test.go: -------------------------------------------------------------------------------- 1 | package app_test 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "errors" 7 | "go-http-server/app" 8 | "go-http-server/param" 9 | "io/ioutil" 10 | "net/http" 11 | "net/http/httptest" 12 | "os" 13 | "reflect" 14 | "testing" 15 | 16 | "bou.ke/monkey" 17 | "github.com/andybalholm/brotli" 18 | ) 19 | 20 | func TestNewApp(t *testing.T) { 21 | params := param.Params{ 22 | Address: "0.0.0.0", 23 | Port: 8080, 24 | Gzip: true, 25 | Brotli: true, 26 | Threshold: 1024, 27 | Directory: "../../test/frontend/dist", 28 | CacheControlMaxAge: 604800, 29 | SpaMode: true, 30 | IgnoreCacheControlPaths: nil, 31 | CacheEnabled: true, 32 | CacheBuffer: 50 * 1024, 33 | } 34 | 35 | app1 := app.NewApp(¶ms) 36 | if reflect.TypeOf(app1) == reflect.TypeOf(nil) { 37 | t.Errorf("app1 is nil") 38 | } 39 | 40 | defer func() { 41 | if r := recover(); r == nil { 42 | t.Errorf("The code did not panic") 43 | } 44 | }() 45 | params.CacheBuffer = 0 46 | app2 := app.NewApp(¶ms) 47 | if reflect.TypeOf(app2) != nil { 48 | t.Errorf("app2 is not nil") 49 | } 50 | } 51 | 52 | func TestCompressFiles(t *testing.T) { 53 | params := param.Params{ 54 | Address: "0.0.0.0", 55 | Port: 8080, 56 | Gzip: false, 57 | Brotli: false, 58 | Threshold: 1024, 59 | Directory: "../../test/frontend/dist/vite.svg", 60 | CacheControlMaxAge: 604800, 61 | SpaMode: true, 62 | IgnoreCacheControlPaths: nil, 63 | CacheEnabled: true, 64 | CacheBuffer: 50 * 1024, 65 | } 66 | app1 := app.NewApp(¶ms) 67 | app1.CompressFiles() 68 | 69 | params.Gzip = true 70 | app2 := app.NewApp(¶ms) 71 | app2.CompressFiles() 72 | 73 | info, _ := os.Stat(params.Directory) 74 | info_gz, _ := os.Stat(params.Directory + ".gz") 75 | if info.Size() < info_gz.Size() { 76 | t.Error("Original file is smaller than compressed .gz!") 77 | } 78 | 79 | params.Gzip = false 80 | params.Brotli = true 81 | app3 := app.NewApp(¶ms) 82 | app3.CompressFiles() 83 | 84 | info_br, _ := os.Stat(params.Directory + ".br") 85 | if info.Size() < info_br.Size() { 86 | t.Error("Original file is smaller than compressed .br!") 87 | } 88 | 89 | params.Directory = "fasdfa.go" 90 | app4 := app.NewApp(¶ms) 91 | defer func() { 92 | if r := recover(); r == nil { 93 | t.Errorf("The code did not panic") 94 | } 95 | 96 | params.Directory = "../app" 97 | app5 := app.NewApp(¶ms) 98 | app5.CompressFiles() 99 | os.Remove("app_test.go.br") 100 | os.Remove("app.go.br") 101 | 102 | params.Directory = "../../test/frontend/dist/vite.svg.br" 103 | app6 := app.NewApp(¶ms) 104 | app6.CompressFiles() 105 | }() 106 | app4.CompressFiles() 107 | } 108 | 109 | func TestGetOrCreateResponseItem(t *testing.T) { 110 | params := param.Params{ 111 | Address: "0.0.0.0", 112 | Port: 8000, 113 | Gzip: true, 114 | Brotli: false, 115 | Threshold: 1024, 116 | Directory: "../../test/frontend/dist/", 117 | CacheControlMaxAge: 604800, 118 | SpaMode: true, 119 | IgnoreCacheControlPaths: nil, 120 | CacheEnabled: true, 121 | CacheBuffer: 50 * 1024, 122 | } 123 | apl := app.NewApp(¶ms) 124 | text := "text/html; charset=utf-8" 125 | 126 | resp1, _ := apl.GetOrCreateResponseItem("/", 0, &text) 127 | if resp1.Name != "index.html" { 128 | t.Errorf("Expected index.html to return, got %s", resp1.Name) 129 | } 130 | 131 | params.SpaMode = false 132 | apl1 := app.NewApp(¶ms) 133 | resp11, _ := apl1.GetOrCreateResponseItem("../../test/frontend/dist/", 0, &text) 134 | if resp11.Name != "index.html" { 135 | t.Errorf("Expected index.html to return, got %s", resp11.Name) 136 | } 137 | 138 | resp2, _ := apl.GetOrCreateResponseItem("../../test/frontend/dist/vite.svg", 0, &text) 139 | if resp2.Name != "vite.svg" { 140 | t.Errorf("Expected vite.svg to return, got %s", resp2.Name) 141 | } 142 | 143 | resp3, _ := apl.GetOrCreateResponseItem("../../src/app/app_test.go", 0, &text) 144 | if resp3.Name != "app_test.go" { 145 | t.Errorf("Expected app_test.go to return, got %s", resp3.Name) 146 | } 147 | 148 | resp4, _ := apl.GetOrCreateResponseItem("../../test/frontend/dist/vite.svg", 1, &text) 149 | if resp4.Name != "vite.svg.gz" { 150 | t.Errorf("Expected vite.svg.gz to return, got %s", resp4.Name) 151 | } 152 | 153 | resp5, _ := apl.GetOrCreateResponseItem("../../test/frontend/dist/vite.svg", 2, &text) 154 | if resp5.Name != "vite.svg.br" { 155 | t.Errorf("Expected vite.svg.br to return, got %s", resp5.Name) 156 | } 157 | 158 | resp6, _ := apl.GetOrCreateResponseItem("/fdsfds.go", 0, &text) 159 | if resp6 != nil { 160 | t.Errorf("Expected nil to return, got %s", resp5) 161 | } 162 | } 163 | 164 | func TestHandlerFuncNew(t *testing.T) { 165 | params := param.Params{ 166 | Address: "0.0.0.0", 167 | Port: 8080, 168 | Gzip: false, 169 | Brotli: true, 170 | Threshold: 1024, 171 | Directory: "../../test/frontend/dist", 172 | CacheControlMaxAge: 99999999999, 173 | SpaMode: true, 174 | IgnoreCacheControlPaths: []string{"../../test/frontend/dist/example.html"}, 175 | CacheEnabled: true, 176 | CacheBuffer: 50 * 1024, 177 | } 178 | app1 := app.NewApp(¶ms) 179 | app1.CompressFiles() 180 | index_content, _ := ioutil.ReadFile("../../test/frontend/dist/index.html") 181 | example_content, _ := ioutil.ReadFile("../../test/frontend/dist/example.html") 182 | vite_content, _ := ioutil.ReadFile("../../test/frontend/dist/vite.svg") 183 | 184 | req1, _ := http.NewRequest("GET", "/", nil) 185 | recorder1 := httptest.NewRecorder() 186 | app1.HandlerFuncNew(recorder1, req1) 187 | if recorder1.HeaderMap["Cache-Control"][0] != "no-store" { 188 | t.Errorf("Expected no-store to return, got %s", recorder1.HeaderMap["Cache-Control"]) 189 | } 190 | if recorder1.Body.String() != string(index_content) { 191 | t.Errorf("Expected index.html body to return, got %s", recorder1.Body) 192 | } 193 | 194 | req2, _ := http.NewRequest("GET", "/example.html", nil) 195 | recorder2 := httptest.NewRecorder() 196 | app1.HandlerFuncNew(recorder2, req2) 197 | if recorder2.HeaderMap["Cache-Control"][0] != "no-store" { 198 | t.Errorf("Expected no-store to return, got %s", recorder2.HeaderMap["Cache-Control"]) 199 | } 200 | if recorder2.Body.String() != string(example_content) { 201 | t.Errorf("Expected exapmle.html body to return, got %s", recorder2.Body) 202 | } 203 | 204 | req3, _ := http.NewRequest("GET", "/random", nil) 205 | recorder3 := httptest.NewRecorder() 206 | app1.HandlerFuncNew(recorder3, req3) 207 | if recorder3.HeaderMap["Cache-Control"][0] != "no-store" { 208 | t.Errorf("Expected no-store to return, got %s", recorder3.HeaderMap["Cache-Control"]) 209 | } 210 | if recorder3.Body.String() != string(index_content) { 211 | t.Errorf("Expected index.html body to return, got %s", recorder3.Body) 212 | } 213 | 214 | req4, _ := http.NewRequest("GET", "/vite.svg", nil) 215 | req4.Header.Set("Accept-Encoding", "*") 216 | recorder4 := httptest.NewRecorder() 217 | app1.HandlerFuncNew(recorder4, req4) 218 | resp_body_decoded3, _ := ioutil.ReadAll(brotli.NewReader(recorder4.Body)) 219 | if recorder4.HeaderMap["Cache-Control"][0] != "max-age=99999999999" { 220 | t.Errorf("Expected Cache-Control = no-store to return, got %s", recorder4.HeaderMap["Cache-Control"]) 221 | } 222 | if recorder4.HeaderMap["Content-Encoding"][0] != "br" { 223 | t.Errorf("Expected Content-Encoding = br to return, got %s", recorder4.HeaderMap["Content-Encoding"]) 224 | } 225 | if string(resp_body_decoded3) != string(vite_content) { 226 | t.Errorf("Expected vite.svg to return, got %s", recorder4.Body) 227 | } 228 | 229 | req5, _ := http.NewRequest("GET", "/vite.svg", nil) 230 | req5.Header.Set("Accept-Encoding", "br, gzip") 231 | recorder5 := httptest.NewRecorder() 232 | app1.HandlerFuncNew(recorder5, req5) 233 | resp_body_decoded5, _ := ioutil.ReadAll(brotli.NewReader(recorder5.Body)) 234 | if recorder5.HeaderMap["Cache-Control"][0] != "max-age=99999999999" { 235 | t.Errorf("Expected no-store to return, got %s", recorder5.HeaderMap["Cache-Control"]) 236 | } 237 | if string(resp_body_decoded5) != string(vite_content) { 238 | t.Errorf("Expected vite.svg body to return, got %s", recorder5.Body) 239 | } 240 | 241 | req6, _ := http.NewRequest("GET", "/vite.svg", nil) 242 | req6.Header.Set("Accept-Encoding", "gzip") 243 | recorder6 := httptest.NewRecorder() 244 | params.Brotli = false 245 | params.Gzip = true 246 | app2 := app.NewApp(¶ms) 247 | app2.HandlerFuncNew(recorder6, req6) 248 | reader6 := bytes.NewReader(recorder6.Body.Bytes()) 249 | gzreader6, _ := gzip.NewReader(reader6) 250 | resp_body_decoded6, _ := ioutil.ReadAll(gzreader6) 251 | if recorder6.HeaderMap["Cache-Control"][0] != "max-age=99999999999" { 252 | t.Errorf("Expected no-store to return, got %s", recorder6.HeaderMap["Cache-Control"]) 253 | } 254 | if recorder6.HeaderMap["Content-Encoding"][0] != "gzip" { 255 | t.Errorf("Expected Content-Encoding = gzip to return, got %s", recorder6.HeaderMap["Cache-Control"]) 256 | } 257 | if string(resp_body_decoded6) != string(vite_content) { 258 | t.Errorf("Expected vite.svg to return, got %s", recorder6.Body) 259 | } 260 | 261 | req7, _ := http.NewRequest("GET", "/vite.svg", nil) 262 | req7.Header.Set("Accept-Encoding", "gzip") 263 | recorder7 := httptest.NewRecorder() 264 | params.Brotli = true 265 | params.Gzip = false 266 | app3 := app.NewApp(¶ms) 267 | app3.HandlerFuncNew(recorder7, req7) 268 | _, err := gzip.NewReader(bytes.NewReader(recorder7.Body.Bytes())) 269 | if err.Error() != errors.New("gzip: invalid header").Error() { 270 | t.Errorf("Expected: \"gzip: invalid header\", got %s", err) 271 | } 272 | if recorder7.HeaderMap["Cache-Control"][0] != "max-age=99999999999" { 273 | t.Errorf("Expected Cache-Control = max-age=99999999999 to return, got %s", recorder7.HeaderMap["Cache-Control"]) 274 | } 275 | if string(resp_body_decoded6) != string(vite_content) { 276 | t.Errorf("Expected vite.svg body to return, got %s", recorder7.Body) 277 | } 278 | os.Remove("../../test/frontend/dist/vite.svg.gz") 279 | os.Remove("../../test/frontend/dist/vite.svg.br") 280 | } 281 | 282 | func TestListen(t *testing.T) { 283 | params := param.Params{ 284 | Address: "0.0.0.0", 285 | Port: 8000, 286 | Gzip: false, 287 | Brotli: false, 288 | Threshold: 1024, 289 | Directory: "../../test/frontend/dist", 290 | CacheControlMaxAge: 604800, 291 | SpaMode: true, 292 | IgnoreCacheControlPaths: nil, 293 | CacheEnabled: true, 294 | CacheBuffer: 50 * 1024, 295 | } 296 | a := app.NewApp(¶ms) 297 | 298 | var l *http.Server 299 | monkey.PatchInstanceMethod(reflect.TypeOf(l), "ListenAndServe", func(*http.Server) error { 300 | return http.ErrServerClosed 301 | }) 302 | 303 | defer func() { 304 | if r := recover(); r == nil { 305 | t.Errorf("The code did not panic") 306 | } 307 | }() 308 | 309 | a.Listen() 310 | } 311 | 312 | func TestGetFilePath(t *testing.T) { 313 | params := param.Params{ 314 | Address: "0.0.0.0", 315 | Port: 8080, 316 | Gzip: false, 317 | Brotli: true, 318 | Threshold: 1024, 319 | Directory: "../../test/frontend/dist", 320 | CacheControlMaxAge: 99999999999, 321 | SpaMode: true, 322 | IgnoreCacheControlPaths: []string{"../../test/frontend/dist/example.html"}, 323 | CacheEnabled: true, 324 | CacheBuffer: 50 * 1024, 325 | } 326 | app := app.NewApp(¶ms) 327 | 328 | _, valid := app.GetFilePath("../test/index.html") 329 | if valid { 330 | t.Errorf("Expected false, got %t", valid) 331 | } 332 | 333 | _, valid = app.GetFilePath("../../test/index.html") 334 | if valid { 335 | t.Errorf("Expected false, got %t", valid) 336 | } 337 | 338 | _, valid = app.GetFilePath("test/../index.html") 339 | if !valid { 340 | t.Errorf("Expected false, got %t", valid) 341 | } 342 | 343 | _, valid = app.GetFilePath("test/../test/../index.html") 344 | if !valid { 345 | t.Errorf("Expected false, got %t", valid) 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /src/go.mod: -------------------------------------------------------------------------------- 1 | module go-http-server 2 | 3 | go 1.18 4 | 5 | require ( 6 | bou.ke/monkey v1.0.2 7 | github.com/andybalholm/brotli v1.0.4 8 | github.com/felixge/httpsnoop v1.0.3 9 | github.com/hashicorp/golang-lru v0.5.4 10 | github.com/rs/zerolog v1.29.1 11 | github.com/urfave/cli/v2 v2.16.3 12 | golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf 13 | ) 14 | 15 | require ( 16 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 17 | github.com/mattn/go-colorable v0.1.13 // indirect 18 | github.com/mattn/go-isatty v0.0.19 // indirect 19 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 20 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 21 | golang.org/x/sys v0.10.0 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /src/go.sum: -------------------------------------------------------------------------------- 1 | bou.ke/monkey v1.0.2 h1:kWcnsrCNUatbxncxR/ThdYqbytgOIArtYWqcQLQzKLI= 2 | bou.ke/monkey v1.0.2/go.mod h1:OqickVX3tNx6t33n1xvtTtu85YN5s6cKwVug+oHMaIA= 3 | github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= 4 | github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 5 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 6 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 7 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 8 | github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= 9 | github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 10 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 11 | github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= 12 | github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 13 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 14 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 15 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 16 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 17 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 18 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 19 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 20 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 21 | github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 22 | github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc= 23 | github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= 24 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 25 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 26 | github.com/urfave/cli/v2 v2.16.3 h1:gHoFIwpPjoyIMbJp/VFd+/vuD0dAgFK4B6DpEMFJfQk= 27 | github.com/urfave/cli/v2 v2.16.3/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI= 28 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= 29 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 30 | golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf h1:oXVg4h2qJDd9htKxb5SCpFBHLipW6hXmL3qpUixS2jw= 31 | golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf/go.mod h1:yh0Ynu2b5ZUe3MQfp2nM0ecK7wsgouWTDN0FNeJuIys= 32 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 33 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 34 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 35 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 36 | golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= 37 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 38 | -------------------------------------------------------------------------------- /src/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/urfave/cli/v2" 5 | "go-http-server/app" 6 | "go-http-server/param" 7 | "log" 8 | "os" 9 | ) 10 | 11 | func main() { 12 | cliApp := &cli.App{ 13 | Name: "spa-to-http", 14 | Flags: param.Flags, 15 | Action: func(c *cli.Context) error { 16 | params, err := param.ContextToParams(c) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | newApp := app.NewApp(params) 22 | go newApp.CompressFiles() 23 | newApp.Listen() 24 | 25 | return nil 26 | }, 27 | } 28 | 29 | if err := cliApp.Run(os.Args); err != nil { 30 | log.Fatal(err) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/param/param.go: -------------------------------------------------------------------------------- 1 | package param 2 | 3 | import ( 4 | "github.com/urfave/cli/v2" 5 | "path/filepath" 6 | ) 7 | 8 | var Flags = []cli.Flag{ 9 | &cli.StringFlag{ 10 | EnvVars: []string{"ADDRESS"}, 11 | Name: "address", 12 | Aliases: []string{"a"}, 13 | Value: "0.0.0.0", 14 | }, 15 | &cli.IntFlag{ 16 | EnvVars: []string{"PORT"}, 17 | Name: "port", 18 | Aliases: []string{"p"}, 19 | Value: 8080, 20 | }, 21 | &cli.BoolFlag{ 22 | EnvVars: []string{"GZIP"}, 23 | Name: "gzip", 24 | Value: false, 25 | }, 26 | &cli.BoolFlag{ 27 | EnvVars: []string{"BROTLI"}, 28 | Name: "brotli", 29 | Value: false, 30 | }, 31 | &cli.Int64Flag{ 32 | EnvVars: []string{"THRESHOLD"}, 33 | Name: "threshold", 34 | Value: 1024, 35 | }, 36 | &cli.StringFlag{ 37 | EnvVars: []string{"DIRECTORY"}, 38 | Name: "directory", 39 | Aliases: []string{"d"}, 40 | Value: ".", 41 | }, 42 | // TODO 43 | //&cli.BoolFlag{ 44 | // EnvVars: []string{"DIRECTORY_LISTING"}, 45 | // Name: "directory-listing", 46 | // Value: false, 47 | //}, 48 | &cli.Int64Flag{ 49 | EnvVars: []string{"CACHE_MAX_AGE"}, 50 | Name: "cache-max-age", 51 | Value: 604800, 52 | }, 53 | &cli.BoolFlag{ 54 | EnvVars: []string{"SPA_MODE"}, 55 | Name: "spa", 56 | Value: true, 57 | }, 58 | &cli.StringSliceFlag{ 59 | EnvVars: []string{"IGNORE_CACHE_CONTROL_PATHS"}, 60 | Name: "ignore-cache-control-paths", 61 | Value: nil, 62 | }, 63 | &cli.BoolFlag{ 64 | EnvVars: []string{"CACHE"}, 65 | Name: "cache", 66 | Value: true, 67 | }, 68 | &cli.IntFlag{ 69 | EnvVars: []string{"CACHE_BUFFER"}, 70 | Name: "cache-buffer", 71 | Value: 50 * 1024, 72 | }, 73 | &cli.BoolFlag{ 74 | EnvVars: []string{"LOGGER"}, 75 | Name: "logger", 76 | Value: false, 77 | }, 78 | &cli.BoolFlag{ 79 | EnvVars: []string{"LOG_PRETTY"}, 80 | Name: "log-pretty", 81 | Value: false, 82 | }, 83 | &cli.StringSliceFlag{ 84 | EnvVars: []string{"NO_COMPRESS"}, 85 | Name: "no-compress", 86 | Value: nil, 87 | }, 88 | } 89 | 90 | type Params struct { 91 | Address string 92 | Port int 93 | Gzip bool 94 | Brotli bool 95 | Threshold int64 96 | Directory string 97 | CacheControlMaxAge int64 98 | SpaMode bool 99 | IgnoreCacheControlPaths []string 100 | CacheEnabled bool 101 | CacheBuffer int 102 | Logger bool 103 | LogPretty bool 104 | NoCompress []string 105 | //DirectoryListing bool 106 | } 107 | 108 | func ContextToParams(c *cli.Context) (*Params, error) { 109 | directory, err := filepath.Abs(c.String("directory")) 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | return &Params{ 115 | Address: c.String("address"), 116 | Port: c.Int("port"), 117 | Gzip: c.Bool("gzip"), 118 | Brotli: c.Bool("brotli"), 119 | Threshold: c.Int64("threshold"), 120 | Directory: directory, 121 | CacheControlMaxAge: c.Int64("cache-max-age"), 122 | SpaMode: c.Bool("spa"), 123 | IgnoreCacheControlPaths: c.StringSlice("ignore-cache-control-paths"), 124 | CacheEnabled: c.Bool("cache"), 125 | CacheBuffer: c.Int("cache-buffer"), 126 | Logger: c.Bool("logger"), 127 | LogPretty: c.Bool("log-pretty"), 128 | NoCompress: c.StringSlice("no-compress"), 129 | //DirectoryListing: c.Bool("directory-listing"), 130 | }, nil 131 | } 132 | -------------------------------------------------------------------------------- /src/param/param_test.go: -------------------------------------------------------------------------------- 1 | package param_test 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "go-http-server/param" 7 | "path/filepath" 8 | "testing" 9 | 10 | "github.com/urfave/cli/v2" 11 | ) 12 | 13 | func TestContextToParams(t *testing.T) { 14 | f := flag.NewFlagSet("a", flag.ContinueOnError) 15 | e_adress := "example adr" 16 | e_port := 80 17 | e_gzip := false 18 | e_brotli := true 19 | e_threshold := int64(1024) 20 | e_directory := "example dir" 21 | e_cache_control_max_age := int64(2048) 22 | e_spa := true 23 | e_ignore_cache_control_paths := "example path1" 24 | e_cache := true 25 | e_cache_buffer := 64 26 | 27 | f.String("address", e_adress, "") 28 | f.Int("port", e_port, "") 29 | f.Bool("gzip", e_gzip, "") 30 | f.Bool("brotli", e_brotli, "") 31 | f.Int64("threshold", e_threshold, "") 32 | f.String("directory", e_directory, "") 33 | f.Int64("cache-max-age", e_cache_control_max_age, "") 34 | f.Bool("spa", e_spa, "") 35 | f.String("ignore-cache-control-paths", e_ignore_cache_control_paths, "") 36 | f.Bool("cache", e_cache, "") 37 | f.Int("cache-buffer", e_cache_buffer, "") 38 | 39 | ctx := cli.NewContext(nil, f, nil) 40 | ctx.Context = context.WithValue(context.Background(), "key", "val") 41 | params, err := param.ContextToParams(ctx) 42 | if err != nil { 43 | t.Errorf("Error: %s", err) 44 | return 45 | } 46 | 47 | if params.Address != e_adress { 48 | t.Errorf("Got %s, expected %s", params.Address, e_adress) 49 | } 50 | 51 | if params.Port != e_port { 52 | t.Errorf("Got %d, expected %d", params.Port, e_port) 53 | } 54 | 55 | if params.Gzip != e_gzip { 56 | t.Errorf("Got %t, expected %t", params.Gzip, e_gzip) 57 | } 58 | 59 | if params.Brotli != e_brotli { 60 | t.Errorf("Got %t, expected %t", params.Brotli, e_brotli) 61 | } 62 | 63 | if params.Threshold != int64(e_threshold) { 64 | t.Errorf("Got %d, expected %d", params.Threshold, e_threshold) 65 | } 66 | 67 | abs_directory, _ := filepath.Abs(e_directory) 68 | if params.Directory != abs_directory { 69 | t.Errorf("Got %s, expected %s", params.Directory, e_directory) 70 | } 71 | 72 | if params.CacheControlMaxAge != e_cache_control_max_age { 73 | t.Errorf("Got %d, expected %d", params.CacheControlMaxAge, e_cache_control_max_age) 74 | } 75 | 76 | if params.SpaMode != e_spa { 77 | t.Errorf("Got %t, expected %t", params.SpaMode, e_spa) 78 | } 79 | 80 | //TODO 81 | // fmt.Println(params.IgnoreCacheControlPaths) 82 | // if params.IgnoreCacheControlPaths[0] != e_ignore_cache_control_paths { 83 | // t.Errorf("Got %s, expected %s", params.IgnoreCacheControlPaths, e_ignore_cache_control_paths) 84 | // } 85 | 86 | if params.CacheEnabled != e_cache { 87 | t.Errorf("Got %t, expected %t", params.CacheEnabled, e_cache) 88 | } 89 | 90 | if params.CacheBuffer != e_cache_buffer { 91 | t.Errorf("Got %d, expected %d", params.CacheBuffer, e_cache_buffer) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/util/http.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | // Request.RemoteAddress contains port, which we want to remove i.e.: 10 | // "[::1]:58292" => "[::1]" 11 | func ipAddrFromRemoteAddr(s string) string { 12 | // return full string for IPv6 inputs wihtout port 13 | if strings.LastIndex(s, "]") == len(s)-1 { 14 | return s 15 | } 16 | 17 | idx := strings.LastIndex(s, ":") 18 | if idx == -1 { 19 | return s 20 | } 21 | 22 | return s[:idx] 23 | } 24 | 25 | // requestGetRemoteAddress returns ip address of the client making the request, 26 | // taking into account http proxies 27 | func requestGetRemoteAddress(r *http.Request) net.IP { 28 | hdr := r.Header 29 | 30 | hdrRealIP := hdr.Get("X-Real-Ip") 31 | hdrForwardedFor := hdr.Get("X-Forwarded-For") 32 | if hdrRealIP == "" && hdrForwardedFor == "" { 33 | return net.ParseIP(ipAddrFromRemoteAddr(r.RemoteAddr)) 34 | } 35 | 36 | if hdrForwardedFor != "" { 37 | // X-Forwarded-For is potentially a list of addresses separated with "," 38 | parts := strings.Split(hdrForwardedFor, ",") 39 | fwdIPs := make([]net.IP, len(parts)) 40 | for i, p := range parts { 41 | fwdIPs[i] = net.ParseIP(ipAddrFromRemoteAddr(strings.TrimSpace(p))) 42 | } 43 | // return first address 44 | return fwdIPs[0] 45 | } 46 | 47 | return net.ParseIP(hdrRealIP) 48 | } 49 | -------------------------------------------------------------------------------- /src/util/http_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | func TestIPAddrFromRemoteAddr(t *testing.T) { 9 | tests := []struct { 10 | remoteAddr string 11 | expected string 12 | }{ 13 | {"[::1]:58292", "[::1]"}, 14 | {"127.0.0.1:12345", "127.0.0.1"}, 15 | {"[::1]", "[::1]"}, 16 | {"127.0.0.1", "127.0.0.1"}, 17 | } 18 | 19 | for _, tt := range tests { 20 | actual := ipAddrFromRemoteAddr(tt.remoteAddr) 21 | if actual != tt.expected { 22 | t.Errorf("ipAddrFromRemoteAddr(%s): expected %s, got %s", tt.remoteAddr, tt.expected, actual) 23 | } 24 | } 25 | } 26 | 27 | func TestRequestGetRemoteAddress(t *testing.T) { 28 | tests := []struct { 29 | headerRealIP string 30 | headerForwardedFor string 31 | remoteAddr string 32 | expected string 33 | }{ 34 | {"", "", "127.0.0.1:12345", "127.0.0.1"}, 35 | {"", "192.168.0.1, 127.0.0.1", "127.0.0.1:12345", "192.168.0.1"}, 36 | {"192.168.0.1", "", "127.0.0.1:12345", "192.168.0.1"}, 37 | {"192.168.0.1", "192.168.0.2, 127.0.0.1", "127.0.0.1:12345", "192.168.0.2"}, 38 | } 39 | 40 | for _, tt := range tests { 41 | req, _ := http.NewRequest("GET", "/", nil) 42 | req.Header.Set("X-Real-Ip", tt.headerRealIP) 43 | req.Header.Set("X-Forwarded-For", tt.headerForwardedFor) 44 | req.RemoteAddr = tt.remoteAddr 45 | 46 | actual := requestGetRemoteAddress(req) 47 | if actual.String() != tt.expected { 48 | t.Errorf("requestGetRemoteAddress(%s, %s, %s): expected %s, got %s", tt.headerRealIP, tt.headerForwardedFor, tt.remoteAddr, tt.expected, actual) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/util/log.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | "os" 7 | "time" 8 | 9 | "github.com/felixge/httpsnoop" 10 | "github.com/rs/zerolog" 11 | "github.com/rs/zerolog/log" 12 | ) 13 | 14 | type LogRequestHandlerOptions struct { 15 | Pretty bool 16 | } 17 | 18 | // LogReqInfo describes info about HTTP request 19 | type HTTPReqInfo struct { 20 | // GET etc. 21 | method string 22 | // requested path 23 | path string 24 | // response code, like 200, 404 25 | code int 26 | // number of bytes of the response sent 27 | size int64 28 | // how long did it take to 29 | duration time.Duration 30 | // client IP Address 31 | ipAddress net.IP 32 | // client UserAgent 33 | userAgent string 34 | // referer header 35 | referer string 36 | } 37 | 38 | func logHTTPReqInfo(ri *HTTPReqInfo) { 39 | log.Info(). 40 | Str("method", ri.method). 41 | Str("path", ri.path). 42 | Int("code", ri.code). 43 | Int64("size", ri.size). 44 | Dur("duration", ri.duration). 45 | IPAddr("ipAddress", ri.ipAddress). 46 | Str("userAgent", ri.userAgent). 47 | Str("referer", ri.referer). 48 | Send() 49 | } 50 | 51 | func LogRequestHandler(h http.Handler, opt *LogRequestHandlerOptions) http.Handler { 52 | if opt.Pretty { 53 | log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) 54 | } 55 | 56 | zerolog.DurationFieldUnit = time.Millisecond 57 | 58 | fn := func(w http.ResponseWriter, r *http.Request) { 59 | // runs handler h and captures information about HTTP request 60 | mtr := httpsnoop.CaptureMetrics(h, w, r) 61 | 62 | logHTTPReqInfo(&HTTPReqInfo{ 63 | method: r.Method, 64 | path: r.URL.String(), 65 | code: mtr.Code, 66 | size: mtr.Written, 67 | duration: mtr.Duration, 68 | ipAddress: requestGetRemoteAddress(r), 69 | userAgent: r.Header.Get("User-Agent"), 70 | referer: r.Header.Get("Referer"), 71 | }) 72 | } 73 | 74 | return http.HandlerFunc(fn) 75 | } 76 | -------------------------------------------------------------------------------- /src/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "io/fs" 5 | "net/http" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | type FileType int 11 | 12 | const ( 13 | FileTypeNotExists = iota 14 | FileTypeFile 15 | FileTypeDirectory 16 | ) 17 | 18 | func GetFileType(path string) FileType { 19 | stat, err := os.Stat(path) 20 | 21 | if err != nil { 22 | return FileTypeNotExists 23 | } else if stat.IsDir() { 24 | return FileTypeDirectory 25 | } else { 26 | return FileTypeFile 27 | } 28 | } 29 | 30 | func GetFileWithInfoAndType(path string) (http.File, fs.FileInfo, FileType) { 31 | dirPath, fileName := filepath.Split(path) 32 | fsDir := http.Dir(dirPath) 33 | file, err := fsDir.Open(fileName) 34 | 35 | if err != nil { 36 | return nil, nil, FileTypeNotExists 37 | } 38 | 39 | stat, err := file.Stat() 40 | 41 | if err != nil { 42 | return nil, nil, FileTypeNotExists 43 | } else if stat.IsDir() { 44 | return file, stat, FileTypeDirectory 45 | } else { 46 | return file, stat, FileTypeFile 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/util/util_test.go: -------------------------------------------------------------------------------- 1 | package util_test 2 | 3 | import ( 4 | "go-http-server/util" 5 | "testing" 6 | ) 7 | 8 | func TestGetFileWithInfoAndType(t *testing.T) { 9 | f1, f_info1, f_type1 := util.GetFileWithInfoAndType("afadf") 10 | if f1 != nil { 11 | t.Errorf("Expected f1 = nil, got f1 = %s", f1) 12 | } 13 | if f_info1 != nil { 14 | t.Errorf("Expected f_info1 = nil, got f_info1 = %s", f_info1) 15 | } 16 | if f_type1 != 0 { 17 | t.Errorf("Expected f_type1 = 0, got f_type1 = %d", f_info1) 18 | } 19 | 20 | f2, f_info2, f_type2 := util.GetFileWithInfoAndType("util.go") 21 | if f2 == nil { 22 | t.Errorf("Expected f2 != nil, got f2 = nil") 23 | } 24 | if f_info2.Name() != "util.go" { 25 | t.Errorf("Expected f_info2.Name() == 'util.go', got f_info2.Name() = nil") 26 | } 27 | if f_type2 != 1 { 28 | t.Errorf("Expected f_type2 = 1, got f_type1 = %d", f_info1) 29 | } 30 | 31 | f3, f_info3, f_type3 := util.GetFileWithInfoAndType("../util") 32 | if f3 == nil { 33 | t.Errorf("Expected f3 != nil, got f3 = nil") 34 | } 35 | if f_info3.Name() != "util" { 36 | t.Errorf("Expected f_info3.Name() == 'util', got f_info3.Name() = nil") 37 | } 38 | if f_type3 != 2 { 39 | t.Errorf("Expected f_type3 = 2, got f_type3 = %d", f_info1) 40 | } 41 | } 42 | 43 | func TestGetFileType(t *testing.T) { 44 | table := []struct { 45 | name string 46 | input string 47 | expected util.FileType 48 | }{ 49 | {"wrong dir input", "../dasds", 0}, 50 | {"exsisting file", "util.go", 1}, 51 | {"directory", "../util", 2}, 52 | } 53 | 54 | for _, tc := range table { 55 | actual := util.GetFileType(tc.input) 56 | if actual != tc.expected { 57 | t.Error("expected: ", tc.expected, "\ngot: ", actual) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/frontend/dist/assets/index.43cf8108.css: -------------------------------------------------------------------------------- 1 | :root{font-family:Inter,Avenir,Helvetica,Arial,sans-serif;font-size:16px;line-height:24px;font-weight:400;color-scheme:light dark;color:#ffffffde;background-color:#242424;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-text-size-adjust:100%}a{font-weight:500;color:#646cff;text-decoration:inherit}a:hover{color:#535bf2}body{margin:0;display:flex;place-items:center;min-width:320px;min-height:100vh}h1{font-size:3.2em;line-height:1.1}button{border-radius:8px;border:1px solid transparent;padding:.6em 1.2em;font-size:1em;font-weight:500;font-family:inherit;background-color:#1a1a1a;cursor:pointer;transition:border-color .25s}button:hover{border-color:#646cff}button:focus,button:focus-visible{outline:4px auto -webkit-focus-ring-color}.card{padding:2em}#app{max-width:1280px;margin:0 auto;padding:2rem;text-align:center}@media (prefers-color-scheme: light){:root{color:#213547;background-color:#fff}a:hover{color:#747bff}button{background-color:#f9f9f9}}.read-the-docs[data-v-a1de4649]{color:#888}.logo[data-v-d2afaedd]{height:6em;padding:1.5em;will-change:filter}.logo[data-v-d2afaedd]:hover{filter:drop-shadow(0 0 2em #646cffaa)}.logo.vue[data-v-d2afaedd]:hover{filter:drop-shadow(0 0 2em #42b883aa)} 2 | -------------------------------------------------------------------------------- /test/frontend/dist/assets/index.43cf8108.css.br: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devforth/spa-to-http/c50df8bc714f3e6531ead2f404f21be70d100983/test/frontend/dist/assets/index.43cf8108.css.br -------------------------------------------------------------------------------- /test/frontend/dist/assets/index.43cf8108.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devforth/spa-to-http/c50df8bc714f3e6531ead2f404f21be70d100983/test/frontend/dist/assets/index.43cf8108.css.gz -------------------------------------------------------------------------------- /test/frontend/dist/assets/index.795d9409.js: -------------------------------------------------------------------------------- 1 | (function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))s(r);new MutationObserver(r=>{for(const i of r)if(i.type==="childList")for(const o of i.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&s(o)}).observe(document,{childList:!0,subtree:!0});function n(r){const i={};return r.integrity&&(i.integrity=r.integrity),r.referrerpolicy&&(i.referrerPolicy=r.referrerpolicy),r.crossorigin==="use-credentials"?i.credentials="include":r.crossorigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function s(r){if(r.ep)return;r.ep=!0;const i=n(r);fetch(r.href,i)}})();function bn(e,t){const n=Object.create(null),s=e.split(",");for(let r=0;r!!n[r.toLowerCase()]:r=>!!n[r]}const Er="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly",vr=bn(Er);function ys(e){return!!e||e===""}function xn(e){if(A(e)){const t={};for(let n=0;n{if(n){const s=n.split(Tr);s.length>1&&(t[s[0].trim()]=s[1].trim())}}),t}function yn(e){let t="";if(Z(e))t=e;else if(A(e))for(let n=0;nZ(e)?e:e==null?"":A(e)||z(e)&&(e.toString===ws||!F(e.toString))?JSON.stringify(e,Cs,2):String(e),Cs=(e,t)=>t&&t.__v_isRef?Cs(e,t.value):et(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[s,r])=>(n[`${s} =>`]=r,n),{})}:Es(t)?{[`Set(${t.size})`]:[...t.values()]}:z(t)&&!A(t)&&!Ts(t)?String(t):t,U={},Ge=[],me=()=>{},Ir=()=>!1,Ar=/^on[^a-z]/,Lt=e=>Ar.test(e),Cn=e=>e.startsWith("onUpdate:"),ee=Object.assign,En=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},Fr=Object.prototype.hasOwnProperty,P=(e,t)=>Fr.call(e,t),A=Array.isArray,et=e=>Ht(e)==="[object Map]",Es=e=>Ht(e)==="[object Set]",F=e=>typeof e=="function",Z=e=>typeof e=="string",vn=e=>typeof e=="symbol",z=e=>e!==null&&typeof e=="object",vs=e=>z(e)&&F(e.then)&&F(e.catch),ws=Object.prototype.toString,Ht=e=>ws.call(e),Mr=e=>Ht(e).slice(8,-1),Ts=e=>Ht(e)==="[object Object]",wn=e=>Z(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,Ot=bn(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),St=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},Pr=/-(\w)/g,st=St(e=>e.replace(Pr,(t,n)=>n?n.toUpperCase():"")),Nr=/\B([A-Z])/g,ot=St(e=>e.replace(Nr,"-$1").toLowerCase()),Os=St(e=>e.charAt(0).toUpperCase()+e.slice(1)),Yt=St(e=>e?`on${Os(e)}`:""),ht=(e,t)=>!Object.is(e,t),Xt=(e,t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,value:n})},Rr=e=>{const t=parseFloat(e);return isNaN(t)?e:t};let qn;const Lr=()=>qn||(qn=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});let xe;class Hr{constructor(t=!1){this.active=!0,this.effects=[],this.cleanups=[],!t&&xe&&(this.parent=xe,this.index=(xe.scopes||(xe.scopes=[])).push(this)-1)}run(t){if(this.active){const n=xe;try{return xe=this,t()}finally{xe=n}}}on(){xe=this}off(){xe=this.parent}stop(t){if(this.active){let n,s;for(n=0,s=this.effects.length;n{const t=new Set(e);return t.w=0,t.n=0,t},Is=e=>(e.w&Se)>0,As=e=>(e.n&Se)>0,jr=({deps:e})=>{if(e.length)for(let t=0;t{const{deps:t}=e;if(t.length){let n=0;for(let s=0;s{(d==="length"||d>=s)&&c.push(u)});else switch(n!==void 0&&c.push(o.get(n)),t){case"add":A(e)?wn(n)&&c.push(o.get("length")):(c.push(o.get(ze)),et(e)&&c.push(o.get(on)));break;case"delete":A(e)||(c.push(o.get(ze)),et(e)&&c.push(o.get(on)));break;case"set":et(e)&&c.push(o.get(ze));break}if(c.length===1)c[0]&&ln(c[0]);else{const u=[];for(const d of c)d&&u.push(...d);ln(Tn(u))}}function ln(e,t){const n=A(e)?e:[...e];for(const s of n)s.computed&&Jn(s);for(const s of n)s.computed||Jn(s)}function Jn(e,t){(e!==pe||e.allowRecurse)&&(e.scheduler?e.scheduler():e.run())}const $r=bn("__proto__,__v_isRef,__isVue"),Ps=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(vn)),Ur=In(),Dr=In(!1,!0),Kr=In(!0),Yn=Wr();function Wr(){const e={};return["includes","indexOf","lastIndexOf"].forEach(t=>{e[t]=function(...n){const s=S(this);for(let i=0,o=this.length;i{e[t]=function(...n){lt();const s=S(this)[t].apply(this,n);return ct(),s}}),e}function In(e=!1,t=!1){return function(s,r,i){if(r==="__v_isReactive")return!e;if(r==="__v_isReadonly")return e;if(r==="__v_isShallow")return t;if(r==="__v_raw"&&i===(e?t?ii:Ss:t?Hs:Ls).get(s))return s;const o=A(s);if(!e&&o&&P(Yn,r))return Reflect.get(Yn,r,i);const c=Reflect.get(s,r,i);return(vn(r)?Ps.has(r):$r(r))||(e||le(s,"get",r),t)?c:G(c)?o&&wn(r)?c:c.value:z(c)?e?js(c):Mn(c):c}}const kr=Ns(),zr=Ns(!0);function Ns(e=!1){return function(n,s,r,i){let o=n[s];if(rt(o)&&G(o)&&!G(r))return!1;if(!e&&(!Mt(r)&&!rt(r)&&(o=S(o),r=S(r)),!A(n)&&G(o)&&!G(r)))return o.value=r,!0;const c=A(n)&&wn(s)?Number(s)e,jt=e=>Reflect.getPrototypeOf(e);function Ct(e,t,n=!1,s=!1){e=e.__v_raw;const r=S(e),i=S(t);n||(t!==i&&le(r,"get",t),le(r,"get",i));const{has:o}=jt(r),c=s?An:n?Nn:pt;if(o.call(r,t))return c(e.get(t));if(o.call(r,i))return c(e.get(i));e!==r&&e.get(t)}function Et(e,t=!1){const n=this.__v_raw,s=S(n),r=S(e);return t||(e!==r&&le(s,"has",e),le(s,"has",r)),e===r?n.has(e):n.has(e)||n.has(r)}function vt(e,t=!1){return e=e.__v_raw,!t&&le(S(e),"iterate",ze),Reflect.get(e,"size",e)}function Xn(e){e=S(e);const t=S(this);return jt(t).has.call(t,e)||(t.add(e),Ae(t,"add",e,e)),this}function Zn(e,t){t=S(t);const n=S(this),{has:s,get:r}=jt(n);let i=s.call(n,e);i||(e=S(e),i=s.call(n,e));const o=r.call(n,e);return n.set(e,t),i?ht(t,o)&&Ae(n,"set",e,t):Ae(n,"add",e,t),this}function Qn(e){const t=S(this),{has:n,get:s}=jt(t);let r=n.call(t,e);r||(e=S(e),r=n.call(t,e)),s&&s.call(t,e);const i=t.delete(e);return r&&Ae(t,"delete",e,void 0),i}function Gn(){const e=S(this),t=e.size!==0,n=e.clear();return t&&Ae(e,"clear",void 0,void 0),n}function wt(e,t){return function(s,r){const i=this,o=i.__v_raw,c=S(o),u=t?An:e?Nn:pt;return!e&&le(c,"iterate",ze),o.forEach((d,m)=>s.call(r,u(d),u(m),i))}}function Tt(e,t,n){return function(...s){const r=this.__v_raw,i=S(r),o=et(i),c=e==="entries"||e===Symbol.iterator&&o,u=e==="keys"&&o,d=r[e](...s),m=n?An:t?Nn:pt;return!t&&le(i,"iterate",u?on:ze),{next(){const{value:y,done:E}=d.next();return E?{value:y,done:E}:{value:c?[m(y[0]),m(y[1])]:m(y),done:E}},[Symbol.iterator](){return this}}}}function Pe(e){return function(...t){return e==="delete"?!1:this}}function Zr(){const e={get(i){return Ct(this,i)},get size(){return vt(this)},has:Et,add:Xn,set:Zn,delete:Qn,clear:Gn,forEach:wt(!1,!1)},t={get(i){return Ct(this,i,!1,!0)},get size(){return vt(this)},has:Et,add:Xn,set:Zn,delete:Qn,clear:Gn,forEach:wt(!1,!0)},n={get(i){return Ct(this,i,!0)},get size(){return vt(this,!0)},has(i){return Et.call(this,i,!0)},add:Pe("add"),set:Pe("set"),delete:Pe("delete"),clear:Pe("clear"),forEach:wt(!0,!1)},s={get(i){return Ct(this,i,!0,!0)},get size(){return vt(this,!0)},has(i){return Et.call(this,i,!0)},add:Pe("add"),set:Pe("set"),delete:Pe("delete"),clear:Pe("clear"),forEach:wt(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach(i=>{e[i]=Tt(i,!1,!1),n[i]=Tt(i,!0,!1),t[i]=Tt(i,!1,!0),s[i]=Tt(i,!0,!0)}),[e,n,t,s]}const[Qr,Gr,ei,ti]=Zr();function Fn(e,t){const n=t?e?ti:ei:e?Gr:Qr;return(s,r,i)=>r==="__v_isReactive"?!e:r==="__v_isReadonly"?e:r==="__v_raw"?s:Reflect.get(P(n,r)&&r in s?n:s,r,i)}const ni={get:Fn(!1,!1)},si={get:Fn(!1,!0)},ri={get:Fn(!0,!1)},Ls=new WeakMap,Hs=new WeakMap,Ss=new WeakMap,ii=new WeakMap;function oi(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function li(e){return e.__v_skip||!Object.isExtensible(e)?0:oi(Mr(e))}function Mn(e){return rt(e)?e:Pn(e,!1,Rs,ni,Ls)}function ci(e){return Pn(e,!1,Xr,si,Hs)}function js(e){return Pn(e,!0,Yr,ri,Ss)}function Pn(e,t,n,s,r){if(!z(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const i=r.get(e);if(i)return i;const o=li(e);if(o===0)return e;const c=new Proxy(e,o===2?s:n);return r.set(e,c),c}function tt(e){return rt(e)?tt(e.__v_raw):!!(e&&e.__v_isReactive)}function rt(e){return!!(e&&e.__v_isReadonly)}function Mt(e){return!!(e&&e.__v_isShallow)}function Bs(e){return tt(e)||rt(e)}function S(e){const t=e&&e.__v_raw;return t?S(t):e}function $s(e){return Ft(e,"__v_skip",!0),e}const pt=e=>z(e)?Mn(e):e,Nn=e=>z(e)?js(e):e;function Us(e){Re&&pe&&(e=S(e),Ms(e.dep||(e.dep=Tn())))}function Ds(e,t){e=S(e),e.dep&&ln(e.dep)}function G(e){return!!(e&&e.__v_isRef===!0)}function fi(e){return ui(e,!1)}function ui(e,t){return G(e)?e:new ai(e,t)}class ai{constructor(t,n){this.__v_isShallow=n,this.dep=void 0,this.__v_isRef=!0,this._rawValue=n?t:S(t),this._value=n?t:pt(t)}get value(){return Us(this),this._value}set value(t){const n=this.__v_isShallow||Mt(t)||rt(t);t=n?t:S(t),ht(t,this._rawValue)&&(this._rawValue=t,this._value=n?t:pt(t),Ds(this))}}function di(e){return G(e)?e.value:e}const hi={get:(e,t,n)=>di(Reflect.get(e,t,n)),set:(e,t,n,s)=>{const r=e[t];return G(r)&&!G(n)?(r.value=n,!0):Reflect.set(e,t,n,s)}};function Ks(e){return tt(e)?e:new Proxy(e,hi)}var Ws;class pi{constructor(t,n,s,r){this._setter=n,this.dep=void 0,this.__v_isRef=!0,this[Ws]=!1,this._dirty=!0,this.effect=new On(t,()=>{this._dirty||(this._dirty=!0,Ds(this))}),this.effect.computed=this,this.effect.active=this._cacheable=!r,this.__v_isReadonly=s}get value(){const t=S(this);return Us(t),(t._dirty||!t._cacheable)&&(t._dirty=!1,t._value=t.effect.run()),t._value}set value(t){this._setter(t)}}Ws="__v_isReadonly";function gi(e,t,n=!1){let s,r;const i=F(e);return i?(s=e,r=me):(s=e.get,r=e.set),new pi(s,r,i||!r,n)}function Le(e,t,n,s){let r;try{r=s?e(...s):e()}catch(i){Bt(i,t,n)}return r}function ae(e,t,n,s){if(F(e)){const i=Le(e,t,n,s);return i&&vs(i)&&i.catch(o=>{Bt(o,t,n)}),i}const r=[];for(let i=0;i>>1;mt(Q[s])Ce&&Q.splice(t,1)}function yi(e){A(e)?nt.push(...e):(!Oe||!Oe.includes(e,e.allowRecurse?Ke+1:Ke))&&nt.push(e),zs()}function es(e,t=gt?Ce+1:0){for(;tmt(n)-mt(s)),Ke=0;Kee.id==null?1/0:e.id,Ci=(e,t)=>{const n=mt(e)-mt(t);if(n===0){if(e.pre&&!t.pre)return-1;if(t.pre&&!e.pre)return 1}return n};function Vs(e){cn=!1,gt=!0,Q.sort(Ci);const t=me;try{for(Ce=0;CeO.trim())),y&&(r=n.map(Rr))}let c,u=s[c=Yt(t)]||s[c=Yt(st(t))];!u&&i&&(u=s[c=Yt(ot(t))]),u&&ae(u,e,6,r);const d=s[c+"Once"];if(d){if(!e.emitted)e.emitted={};else if(e.emitted[c])return;e.emitted[c]=!0,ae(d,e,6,r)}}function Js(e,t,n=!1){const s=t.emitsCache,r=s.get(e);if(r!==void 0)return r;const i=e.emits;let o={},c=!1;if(!F(e)){const u=d=>{const m=Js(d,t,!0);m&&(c=!0,ee(o,m))};!n&&t.mixins.length&&t.mixins.forEach(u),e.extends&&u(e.extends),e.mixins&&e.mixins.forEach(u)}return!i&&!c?(z(e)&&s.set(e,null),null):(A(i)?i.forEach(u=>o[u]=null):ee(o,i),z(e)&&s.set(e,o),o)}function $t(e,t){return!e||!Lt(t)?!1:(t=t.slice(2).replace(/Once$/,""),P(e,t[0].toLowerCase()+t.slice(1))||P(e,ot(t))||P(e,t))}let Ee=null,Ut=null;function Pt(e){const t=Ee;return Ee=e,Ut=e&&e.type.__scopeId||null,t}function Ys(e){Ut=e}function Xs(){Ut=null}function vi(e,t=Ee,n){if(!t||e._n)return e;const s=(...r)=>{s._d&&us(-1);const i=Pt(t),o=e(...r);return Pt(i),s._d&&us(1),o};return s._n=!0,s._c=!0,s._d=!0,s}function Zt(e){const{type:t,vnode:n,proxy:s,withProxy:r,props:i,propsOptions:[o],slots:c,attrs:u,emit:d,render:m,renderCache:y,data:E,setupState:O,ctx:j,inheritAttrs:H}=e;let M,N;const ce=Pt(e);try{if(n.shapeFlag&4){const q=r||s;M=ye(m.call(q,q,y,i,O,E,j)),N=u}else{const q=t;M=ye(q.length>1?q(i,{attrs:u,slots:c,emit:d}):q(i,null)),N=t.props?u:wi(u)}}catch(q){dt.length=0,Bt(q,e,1),M=He(Ie)}let J=M;if(N&&H!==!1){const q=Object.keys(N),{shapeFlag:ne}=J;q.length&&ne&7&&(o&&q.some(Cn)&&(N=Ti(N,o)),J=je(J,N))}return n.dirs&&(J=je(J),J.dirs=J.dirs?J.dirs.concat(n.dirs):n.dirs),n.transition&&(J.transition=n.transition),M=J,Pt(ce),M}const wi=e=>{let t;for(const n in e)(n==="class"||n==="style"||Lt(n))&&((t||(t={}))[n]=e[n]);return t},Ti=(e,t)=>{const n={};for(const s in e)(!Cn(s)||!(s.slice(9)in t))&&(n[s]=e[s]);return n};function Oi(e,t,n){const{props:s,children:r,component:i}=e,{props:o,children:c,patchFlag:u}=t,d=i.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&u>=0){if(u&1024)return!0;if(u&16)return s?ts(s,o,d):!!o;if(u&8){const m=t.dynamicProps;for(let y=0;ye.__isSuspense;function Fi(e,t){t&&t.pendingBranch?A(e)?t.effects.push(...e):t.effects.push(e):yi(e)}function Mi(e,t){if(X){let n=X.provides;const s=X.parent&&X.parent.provides;s===n&&(n=X.provides=Object.create(s)),n[e]=t}}function Qt(e,t,n=!1){const s=X||Ee;if(s){const r=s.parent==null?s.vnode.appContext&&s.vnode.appContext.provides:s.parent.provides;if(r&&e in r)return r[e];if(arguments.length>1)return n&&F(t)?t.call(s.proxy):t}}const ns={};function Gt(e,t,n){return Zs(e,t,n)}function Zs(e,t,{immediate:n,deep:s,flush:r,onTrack:i,onTrigger:o}=U){const c=X;let u,d=!1,m=!1;if(G(e)?(u=()=>e.value,d=Mt(e)):tt(e)?(u=()=>e,s=!0):A(e)?(m=!0,d=e.some(N=>tt(N)||Mt(N)),u=()=>e.map(N=>{if(G(N))return N.value;if(tt(N))return Qe(N);if(F(N))return Le(N,c,2)})):F(e)?t?u=()=>Le(e,c,2):u=()=>{if(!(c&&c.isUnmounted))return y&&y(),ae(e,c,3,[E])}:u=me,t&&s){const N=u;u=()=>Qe(N())}let y,E=N=>{y=M.onStop=()=>{Le(N,c,4)}};if(bt)return E=me,t?n&&ae(t,c,3,[u(),m?[]:void 0,E]):u(),me;let O=m?[]:ns;const j=()=>{if(!!M.active)if(t){const N=M.run();(s||d||(m?N.some((ce,J)=>ht(ce,O[J])):ht(N,O)))&&(y&&y(),ae(t,c,3,[N,O===ns?void 0:O,E]),O=N)}else M.run()};j.allowRecurse=!!t;let H;r==="sync"?H=j:r==="post"?H=()=>ie(j,c&&c.suspense):(j.pre=!0,c&&(j.id=c.uid),H=()=>Ln(j));const M=new On(u,H);return t?n?j():O=M.run():r==="post"?ie(M.run.bind(M),c&&c.suspense):M.run(),()=>{M.stop(),c&&c.scope&&En(c.scope.effects,M)}}function Pi(e,t,n){const s=this.proxy,r=Z(e)?e.includes(".")?Qs(s,e):()=>s[e]:e.bind(s,s);let i;F(t)?i=t:(i=t.handler,n=t);const o=X;it(this);const c=Zs(r,i.bind(s),n);return o?it(o):qe(),c}function Qs(e,t){const n=t.split(".");return()=>{let s=e;for(let r=0;r{Qe(n,t)});else if(Ts(e))for(const n in e)Qe(e[n],t);return e}function Ni(){const e={isMounted:!1,isLeaving:!1,isUnmounting:!1,leavingVNodes:new Map};return nr(()=>{e.isMounted=!0}),sr(()=>{e.isUnmounting=!0}),e}const fe=[Function,Array],Ri={name:"BaseTransition",props:{mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:fe,onEnter:fe,onAfterEnter:fe,onEnterCancelled:fe,onBeforeLeave:fe,onLeave:fe,onAfterLeave:fe,onLeaveCancelled:fe,onBeforeAppear:fe,onAppear:fe,onAfterAppear:fe,onAppearCancelled:fe},setup(e,{slots:t}){const n=xo(),s=Ni();let r;return()=>{const i=t.default&&er(t.default(),!0);if(!i||!i.length)return;let o=i[0];if(i.length>1){for(const H of i)if(H.type!==Ie){o=H;break}}const c=S(e),{mode:u}=c;if(s.isLeaving)return en(o);const d=ss(o);if(!d)return en(o);const m=fn(d,c,s,n);un(d,m);const y=n.subTree,E=y&&ss(y);let O=!1;const{getTransitionKey:j}=d.type;if(j){const H=j();r===void 0?r=H:H!==r&&(r=H,O=!0)}if(E&&E.type!==Ie&&(!We(d,E)||O)){const H=fn(E,c,s,n);if(un(E,H),u==="out-in")return s.isLeaving=!0,H.afterLeave=()=>{s.isLeaving=!1,n.update()},en(o);u==="in-out"&&d.type!==Ie&&(H.delayLeave=(M,N,ce)=>{const J=Gs(s,E);J[String(E.key)]=E,M._leaveCb=()=>{N(),M._leaveCb=void 0,delete m.delayedLeave},m.delayedLeave=ce})}return o}}},Li=Ri;function Gs(e,t){const{leavingVNodes:n}=e;let s=n.get(t.type);return s||(s=Object.create(null),n.set(t.type,s)),s}function fn(e,t,n,s){const{appear:r,mode:i,persisted:o=!1,onBeforeEnter:c,onEnter:u,onAfterEnter:d,onEnterCancelled:m,onBeforeLeave:y,onLeave:E,onAfterLeave:O,onLeaveCancelled:j,onBeforeAppear:H,onAppear:M,onAfterAppear:N,onAppearCancelled:ce}=t,J=String(e.key),q=Gs(n,e),ne=(R,W)=>{R&&ae(R,s,9,W)},Je=(R,W)=>{const V=W[1];ne(R,W),A(R)?R.every(se=>se.length<=1)&&V():R.length<=1&&V()},Be={mode:i,persisted:o,beforeEnter(R){let W=c;if(!n.isMounted)if(r)W=H||c;else return;R._leaveCb&&R._leaveCb(!0);const V=q[J];V&&We(e,V)&&V.el._leaveCb&&V.el._leaveCb(),ne(W,[R])},enter(R){let W=u,V=d,se=m;if(!n.isMounted)if(r)W=M||u,V=N||d,se=ce||m;else return;let de=!1;const ve=R._enterCb=xt=>{de||(de=!0,xt?ne(se,[R]):ne(V,[R]),Be.delayedLeave&&Be.delayedLeave(),R._enterCb=void 0)};W?Je(W,[R,ve]):ve()},leave(R,W){const V=String(e.key);if(R._enterCb&&R._enterCb(!0),n.isUnmounting)return W();ne(y,[R]);let se=!1;const de=R._leaveCb=ve=>{se||(se=!0,W(),ve?ne(j,[R]):ne(O,[R]),R._leaveCb=void 0,q[V]===e&&delete q[V])};q[V]=e,E?Je(E,[R,de]):de()},clone(R){return fn(R,t,n,s)}};return Be}function en(e){if(Dt(e))return e=je(e),e.children=null,e}function ss(e){return Dt(e)?e.children?e.children[0]:void 0:e}function un(e,t){e.shapeFlag&6&&e.component?un(e.component.subTree,t):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function er(e,t=!1,n){let s=[],r=0;for(let i=0;i1)for(let i=0;i!!e.type.__asyncLoader,Dt=e=>e.type.__isKeepAlive;function Hi(e,t){tr(e,"a",t)}function Si(e,t){tr(e,"da",t)}function tr(e,t,n=X){const s=e.__wdc||(e.__wdc=()=>{let r=n;for(;r;){if(r.isDeactivated)return;r=r.parent}return e()});if(Kt(t,s,n),n){let r=n.parent;for(;r&&r.parent;)Dt(r.parent.vnode)&&ji(s,t,n,r),r=r.parent}}function ji(e,t,n,s){const r=Kt(t,e,s,!0);rr(()=>{En(s[t],r)},n)}function Kt(e,t,n=X,s=!1){if(n){const r=n[e]||(n[e]=[]),i=t.__weh||(t.__weh=(...o)=>{if(n.isUnmounted)return;lt(),it(n);const c=ae(t,n,e,o);return qe(),ct(),c});return s?r.unshift(i):r.push(i),i}}const Fe=e=>(t,n=X)=>(!bt||e==="sp")&&Kt(e,t,n),Bi=Fe("bm"),nr=Fe("m"),$i=Fe("bu"),Ui=Fe("u"),sr=Fe("bum"),rr=Fe("um"),Di=Fe("sp"),Ki=Fe("rtg"),Wi=Fe("rtc");function ki(e,t=X){Kt("ec",e,t)}function $e(e,t,n,s){const r=e.dirs,i=t&&t.dirs;for(let o=0;oe?mr(e)?$n(e)||e.proxy:an(e.parent):null,Nt=ee(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>an(e.parent),$root:e=>an(e.root),$emit:e=>e.emit,$options:e=>Hn(e),$forceUpdate:e=>e.f||(e.f=()=>Ln(e.update)),$nextTick:e=>e.n||(e.n=_i.bind(e.proxy)),$watch:e=>Pi.bind(e)}),qi={get({_:e},t){const{ctx:n,setupState:s,data:r,props:i,accessCache:o,type:c,appContext:u}=e;let d;if(t[0]!=="$"){const O=o[t];if(O!==void 0)switch(O){case 1:return s[t];case 2:return r[t];case 4:return n[t];case 3:return i[t]}else{if(s!==U&&P(s,t))return o[t]=1,s[t];if(r!==U&&P(r,t))return o[t]=2,r[t];if((d=e.propsOptions[0])&&P(d,t))return o[t]=3,i[t];if(n!==U&&P(n,t))return o[t]=4,n[t];dn&&(o[t]=0)}}const m=Nt[t];let y,E;if(m)return t==="$attrs"&&le(e,"get",t),m(e);if((y=c.__cssModules)&&(y=y[t]))return y;if(n!==U&&P(n,t))return o[t]=4,n[t];if(E=u.config.globalProperties,P(E,t))return E[t]},set({_:e},t,n){const{data:s,setupState:r,ctx:i}=e;return r!==U&&P(r,t)?(r[t]=n,!0):s!==U&&P(s,t)?(s[t]=n,!0):P(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(i[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:s,appContext:r,propsOptions:i}},o){let c;return!!n[o]||e!==U&&P(e,o)||t!==U&&P(t,o)||(c=i[0])&&P(c,o)||P(s,o)||P(Nt,o)||P(r.config.globalProperties,o)},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:P(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};let dn=!0;function Vi(e){const t=Hn(e),n=e.proxy,s=e.ctx;dn=!1,t.beforeCreate&&rs(t.beforeCreate,e,"bc");const{data:r,computed:i,methods:o,watch:c,provide:u,inject:d,created:m,beforeMount:y,mounted:E,beforeUpdate:O,updated:j,activated:H,deactivated:M,beforeDestroy:N,beforeUnmount:ce,destroyed:J,unmounted:q,render:ne,renderTracked:Je,renderTriggered:Be,errorCaptured:R,serverPrefetch:W,expose:V,inheritAttrs:se,components:de,directives:ve,filters:xt}=t;if(d&&Ji(d,s,null,e.appContext.config.unwrapInjectedRef),o)for(const k in o){const D=o[k];F(D)&&(s[k]=D.bind(n))}if(r){const k=r.call(n,n);z(k)&&(e.data=Mn(k))}if(dn=!0,i)for(const k in i){const D=i[k],we=F(D)?D.bind(n,n):F(D.get)?D.get.bind(n,n):me,qt=!F(D)&&F(D.set)?D.set.bind(n):me,ft=To({get:we,set:qt});Object.defineProperty(s,k,{enumerable:!0,configurable:!0,get:()=>ft.value,set:Ye=>ft.value=Ye})}if(c)for(const k in c)ir(c[k],s,n,k);if(u){const k=F(u)?u.call(n):u;Reflect.ownKeys(k).forEach(D=>{Mi(D,k[D])})}m&&rs(m,e,"c");function re(k,D){A(D)?D.forEach(we=>k(we.bind(n))):D&&k(D.bind(n))}if(re(Bi,y),re(nr,E),re($i,O),re(Ui,j),re(Hi,H),re(Si,M),re(ki,R),re(Wi,Je),re(Ki,Be),re(sr,ce),re(rr,q),re(Di,W),A(V))if(V.length){const k=e.exposed||(e.exposed={});V.forEach(D=>{Object.defineProperty(k,D,{get:()=>n[D],set:we=>n[D]=we})})}else e.exposed||(e.exposed={});ne&&e.render===me&&(e.render=ne),se!=null&&(e.inheritAttrs=se),de&&(e.components=de),ve&&(e.directives=ve)}function Ji(e,t,n=me,s=!1){A(e)&&(e=hn(e));for(const r in e){const i=e[r];let o;z(i)?"default"in i?o=Qt(i.from||r,i.default,!0):o=Qt(i.from||r):o=Qt(i),G(o)&&s?Object.defineProperty(t,r,{enumerable:!0,configurable:!0,get:()=>o.value,set:c=>o.value=c}):t[r]=o}}function rs(e,t,n){ae(A(e)?e.map(s=>s.bind(t.proxy)):e.bind(t.proxy),t,n)}function ir(e,t,n,s){const r=s.includes(".")?Qs(n,s):()=>n[s];if(Z(e)){const i=t[e];F(i)&&Gt(r,i)}else if(F(e))Gt(r,e.bind(n));else if(z(e))if(A(e))e.forEach(i=>ir(i,t,n,s));else{const i=F(e.handler)?e.handler.bind(n):t[e.handler];F(i)&&Gt(r,i,e)}}function Hn(e){const t=e.type,{mixins:n,extends:s}=t,{mixins:r,optionsCache:i,config:{optionMergeStrategies:o}}=e.appContext,c=i.get(t);let u;return c?u=c:!r.length&&!n&&!s?u=t:(u={},r.length&&r.forEach(d=>Rt(u,d,o,!0)),Rt(u,t,o)),z(t)&&i.set(t,u),u}function Rt(e,t,n,s=!1){const{mixins:r,extends:i}=t;i&&Rt(e,i,n,!0),r&&r.forEach(o=>Rt(e,o,n,!0));for(const o in t)if(!(s&&o==="expose")){const c=Yi[o]||n&&n[o];e[o]=c?c(e[o],t[o]):t[o]}return e}const Yi={data:is,props:De,emits:De,methods:De,computed:De,beforeCreate:te,created:te,beforeMount:te,mounted:te,beforeUpdate:te,updated:te,beforeDestroy:te,beforeUnmount:te,destroyed:te,unmounted:te,activated:te,deactivated:te,errorCaptured:te,serverPrefetch:te,components:De,directives:De,watch:Zi,provide:is,inject:Xi};function is(e,t){return t?e?function(){return ee(F(e)?e.call(this,this):e,F(t)?t.call(this,this):t)}:t:e}function Xi(e,t){return De(hn(e),hn(t))}function hn(e){if(A(e)){const t={};for(let n=0;n0)&&!(o&16)){if(o&8){const m=e.vnode.dynamicProps;for(let y=0;y{u=!0;const[E,O]=lr(y,t,!0);ee(o,E),O&&c.push(...O)};!n&&t.mixins.length&&t.mixins.forEach(m),e.extends&&m(e.extends),e.mixins&&e.mixins.forEach(m)}if(!i&&!u)return z(e)&&s.set(e,Ge),Ge;if(A(i))for(let m=0;m-1,O[1]=H<0||j-1||P(O,"default"))&&c.push(y)}}}const d=[o,c];return z(e)&&s.set(e,d),d}function os(e){return e[0]!=="$"}function ls(e){const t=e&&e.toString().match(/^\s*function (\w+)/);return t?t[1]:e===null?"null":""}function cs(e,t){return ls(e)===ls(t)}function fs(e,t){return A(t)?t.findIndex(n=>cs(n,e)):F(t)&&cs(t,e)?0:-1}const cr=e=>e[0]==="_"||e==="$stable",Sn=e=>A(e)?e.map(ye):[ye(e)],eo=(e,t,n)=>{if(t._n)return t;const s=vi((...r)=>Sn(t(...r)),n);return s._c=!1,s},fr=(e,t,n)=>{const s=e._ctx;for(const r in e){if(cr(r))continue;const i=e[r];if(F(i))t[r]=eo(r,i,s);else if(i!=null){const o=Sn(i);t[r]=()=>o}}},ur=(e,t)=>{const n=Sn(t);e.slots.default=()=>n},to=(e,t)=>{if(e.vnode.shapeFlag&32){const n=t._;n?(e.slots=S(t),Ft(t,"_",n)):fr(t,e.slots={})}else e.slots={},t&&ur(e,t);Ft(e.slots,Wt,1)},no=(e,t,n)=>{const{vnode:s,slots:r}=e;let i=!0,o=U;if(s.shapeFlag&32){const c=t._;c?n&&c===1?i=!1:(ee(r,t),!n&&c===1&&delete r._):(i=!t.$stable,fr(t,r)),o=t}else t&&(ur(e,t),o={default:1});if(i)for(const c in r)!cr(c)&&!(c in o)&&delete r[c]};function ar(){return{app:null,config:{isNativeTag:Ir,performance:!1,globalProperties:{},optionMergeStrategies:{},errorHandler:void 0,warnHandler:void 0,compilerOptions:{}},mixins:[],components:{},directives:{},provides:Object.create(null),optionsCache:new WeakMap,propsCache:new WeakMap,emitsCache:new WeakMap}}let so=0;function ro(e,t){return function(s,r=null){F(s)||(s=Object.assign({},s)),r!=null&&!z(r)&&(r=null);const i=ar(),o=new Set;let c=!1;const u=i.app={_uid:so++,_component:s,_props:r,_container:null,_context:i,_instance:null,version:Oo,get config(){return i.config},set config(d){},use(d,...m){return o.has(d)||(d&&F(d.install)?(o.add(d),d.install(u,...m)):F(d)&&(o.add(d),d(u,...m))),u},mixin(d){return i.mixins.includes(d)||i.mixins.push(d),u},component(d,m){return m?(i.components[d]=m,u):i.components[d]},directive(d,m){return m?(i.directives[d]=m,u):i.directives[d]},mount(d,m,y){if(!c){const E=He(s,r);return E.appContext=i,m&&t?t(E,d):e(E,d,y),c=!0,u._container=d,d.__vue_app__=u,$n(E.component)||E.component.proxy}},unmount(){c&&(e(null,u._container),delete u._container.__vue_app__)},provide(d,m){return i.provides[d]=m,u}};return u}}function gn(e,t,n,s,r=!1){if(A(e)){e.forEach((E,O)=>gn(E,t&&(A(t)?t[O]:t),n,s,r));return}if(It(s)&&!r)return;const i=s.shapeFlag&4?$n(s.component)||s.component.proxy:s.el,o=r?null:i,{i:c,r:u}=e,d=t&&t.r,m=c.refs===U?c.refs={}:c.refs,y=c.setupState;if(d!=null&&d!==u&&(Z(d)?(m[d]=null,P(y,d)&&(y[d]=null)):G(d)&&(d.value=null)),F(u))Le(u,c,12,[o,m]);else{const E=Z(u),O=G(u);if(E||O){const j=()=>{if(e.f){const H=E?m[u]:u.value;r?A(H)&&En(H,i):A(H)?H.includes(i)||H.push(i):E?(m[u]=[i],P(y,u)&&(y[u]=m[u])):(u.value=[i],e.k&&(m[e.k]=u.value))}else E?(m[u]=o,P(y,u)&&(y[u]=o)):O&&(u.value=o,e.k&&(m[e.k]=o))};o?(j.id=-1,ie(j,n)):j()}}}const ie=Fi;function io(e){return oo(e)}function oo(e,t){const n=Lr();n.__VUE__=!0;const{insert:s,remove:r,patchProp:i,createElement:o,createText:c,createComment:u,setText:d,setElementText:m,parentNode:y,nextSibling:E,setScopeId:O=me,cloneNode:j,insertStaticContent:H}=e,M=(l,f,a,p=null,h=null,b=null,C=!1,_=null,x=!!f.dynamicChildren)=>{if(l===f)return;l&&!We(l,f)&&(p=yt(l),Me(l,h,b,!0),l=null),f.patchFlag===-2&&(x=!1,f.dynamicChildren=null);const{type:g,ref:w,shapeFlag:v}=f;switch(g){case jn:N(l,f,a,p);break;case Ie:ce(l,f,a,p);break;case tn:l==null&&J(f,a,p,C);break;case ue:ve(l,f,a,p,h,b,C,_,x);break;default:v&1?Je(l,f,a,p,h,b,C,_,x):v&6?xt(l,f,a,p,h,b,C,_,x):(v&64||v&128)&&g.process(l,f,a,p,h,b,C,_,x,Xe)}w!=null&&h&&gn(w,l&&l.ref,b,f||l,!f)},N=(l,f,a,p)=>{if(l==null)s(f.el=c(f.children),a,p);else{const h=f.el=l.el;f.children!==l.children&&d(h,f.children)}},ce=(l,f,a,p)=>{l==null?s(f.el=u(f.children||""),a,p):f.el=l.el},J=(l,f,a,p)=>{[l.el,l.anchor]=H(l.children,f,a,p,l.el,l.anchor)},q=({el:l,anchor:f},a,p)=>{let h;for(;l&&l!==f;)h=E(l),s(l,a,p),l=h;s(f,a,p)},ne=({el:l,anchor:f})=>{let a;for(;l&&l!==f;)a=E(l),r(l),l=a;r(f)},Je=(l,f,a,p,h,b,C,_,x)=>{C=C||f.type==="svg",l==null?Be(f,a,p,h,b,C,_,x):V(l,f,h,b,C,_,x)},Be=(l,f,a,p,h,b,C,_)=>{let x,g;const{type:w,props:v,shapeFlag:T,transition:I,patchFlag:L,dirs:B}=l;if(l.el&&j!==void 0&&L===-1)x=l.el=j(l.el);else{if(x=l.el=o(l.type,b,v&&v.is,v),T&8?m(x,l.children):T&16&&W(l.children,x,null,p,h,b&&w!=="foreignObject",C,_),B&&$e(l,null,p,"created"),v){for(const K in v)K!=="value"&&!Ot(K)&&i(x,K,null,v[K],b,l.children,p,h,Te);"value"in v&&i(x,"value",null,v.value),(g=v.onVnodeBeforeMount)&&be(g,p,l)}R(x,l,l.scopeId,C,p)}B&&$e(l,null,p,"beforeMount");const $=(!h||h&&!h.pendingBranch)&&I&&!I.persisted;$&&I.beforeEnter(x),s(x,f,a),((g=v&&v.onVnodeMounted)||$||B)&&ie(()=>{g&&be(g,p,l),$&&I.enter(x),B&&$e(l,null,p,"mounted")},h)},R=(l,f,a,p,h)=>{if(a&&O(l,a),p)for(let b=0;b{for(let g=x;g{const _=f.el=l.el;let{patchFlag:x,dynamicChildren:g,dirs:w}=f;x|=l.patchFlag&16;const v=l.props||U,T=f.props||U;let I;a&&Ue(a,!1),(I=T.onVnodeBeforeUpdate)&&be(I,a,f,l),w&&$e(f,l,a,"beforeUpdate"),a&&Ue(a,!0);const L=h&&f.type!=="foreignObject";if(g?se(l.dynamicChildren,g,_,a,p,L,b):C||we(l,f,_,null,a,p,L,b,!1),x>0){if(x&16)de(_,f,v,T,a,p,h);else if(x&2&&v.class!==T.class&&i(_,"class",null,T.class,h),x&4&&i(_,"style",v.style,T.style,h),x&8){const B=f.dynamicProps;for(let $=0;${I&&be(I,a,f,l),w&&$e(f,l,a,"updated")},p)},se=(l,f,a,p,h,b,C)=>{for(let _=0;_{if(a!==p){for(const _ in p){if(Ot(_))continue;const x=p[_],g=a[_];x!==g&&_!=="value"&&i(l,_,g,x,C,f.children,h,b,Te)}if(a!==U)for(const _ in a)!Ot(_)&&!(_ in p)&&i(l,_,a[_],null,C,f.children,h,b,Te);"value"in p&&i(l,"value",a.value,p.value)}},ve=(l,f,a,p,h,b,C,_,x)=>{const g=f.el=l?l.el:c(""),w=f.anchor=l?l.anchor:c("");let{patchFlag:v,dynamicChildren:T,slotScopeIds:I}=f;I&&(_=_?_.concat(I):I),l==null?(s(g,a,p),s(w,a,p),W(f.children,a,w,h,b,C,_,x)):v>0&&v&64&&T&&l.dynamicChildren?(se(l.dynamicChildren,T,a,h,b,C,_),(f.key!=null||h&&f===h.subTree)&&dr(l,f,!0)):we(l,f,a,w,h,b,C,_,x)},xt=(l,f,a,p,h,b,C,_,x)=>{f.slotScopeIds=_,l==null?f.shapeFlag&512?h.ctx.activate(f,a,p,C,x):zt(f,a,p,h,b,C,x):re(l,f,x)},zt=(l,f,a,p,h,b,C)=>{const _=l.component=bo(l,p,h);if(Dt(l)&&(_.ctx.renderer=Xe),yo(_),_.asyncDep){if(h&&h.registerDep(_,k),!l.el){const x=_.subTree=He(Ie);ce(null,x,f,a)}return}k(_,l,f,a,h,b,C)},re=(l,f,a)=>{const p=f.component=l.component;if(Oi(l,f,a))if(p.asyncDep&&!p.asyncResolved){D(p,f,a);return}else p.next=f,xi(p.update),p.update();else f.el=l.el,p.vnode=f},k=(l,f,a,p,h,b,C)=>{const _=()=>{if(l.isMounted){let{next:w,bu:v,u:T,parent:I,vnode:L}=l,B=w,$;Ue(l,!1),w?(w.el=L.el,D(l,w,C)):w=L,v&&Xt(v),($=w.props&&w.props.onVnodeBeforeUpdate)&&be($,I,w,L),Ue(l,!0);const K=Zt(l),he=l.subTree;l.subTree=K,M(he,K,y(he.el),yt(he),l,h,b),w.el=K.el,B===null&&Ii(l,K.el),T&&ie(T,h),($=w.props&&w.props.onVnodeUpdated)&&ie(()=>be($,I,w,L),h)}else{let w;const{el:v,props:T}=f,{bm:I,m:L,parent:B}=l,$=It(f);if(Ue(l,!1),I&&Xt(I),!$&&(w=T&&T.onVnodeBeforeMount)&&be(w,B,f),Ue(l,!0),v&&Jt){const K=()=>{l.subTree=Zt(l),Jt(v,l.subTree,l,h,null)};$?f.type.__asyncLoader().then(()=>!l.isUnmounted&&K()):K()}else{const K=l.subTree=Zt(l);M(null,K,a,p,l,h,b),f.el=K.el}if(L&&ie(L,h),!$&&(w=T&&T.onVnodeMounted)){const K=f;ie(()=>be(w,B,K),h)}(f.shapeFlag&256||B&&It(B.vnode)&&B.vnode.shapeFlag&256)&&l.a&&ie(l.a,h),l.isMounted=!0,f=a=p=null}},x=l.effect=new On(_,()=>Ln(g),l.scope),g=l.update=()=>x.run();g.id=l.uid,Ue(l,!0),g()},D=(l,f,a)=>{f.component=l;const p=l.vnode.props;l.vnode=f,l.next=null,Gi(l,f.props,p,a),no(l,f.children,a),lt(),es(),ct()},we=(l,f,a,p,h,b,C,_,x=!1)=>{const g=l&&l.children,w=l?l.shapeFlag:0,v=f.children,{patchFlag:T,shapeFlag:I}=f;if(T>0){if(T&128){ft(g,v,a,p,h,b,C,_,x);return}else if(T&256){qt(g,v,a,p,h,b,C,_,x);return}}I&8?(w&16&&Te(g,h,b),v!==g&&m(a,v)):w&16?I&16?ft(g,v,a,p,h,b,C,_,x):Te(g,h,b,!0):(w&8&&m(a,""),I&16&&W(v,a,p,h,b,C,_,x))},qt=(l,f,a,p,h,b,C,_,x)=>{l=l||Ge,f=f||Ge;const g=l.length,w=f.length,v=Math.min(g,w);let T;for(T=0;Tw?Te(l,h,b,!0,!1,v):W(f,a,p,h,b,C,_,x,v)},ft=(l,f,a,p,h,b,C,_,x)=>{let g=0;const w=f.length;let v=l.length-1,T=w-1;for(;g<=v&&g<=T;){const I=l[g],L=f[g]=x?Ne(f[g]):ye(f[g]);if(We(I,L))M(I,L,a,null,h,b,C,_,x);else break;g++}for(;g<=v&&g<=T;){const I=l[v],L=f[T]=x?Ne(f[T]):ye(f[T]);if(We(I,L))M(I,L,a,null,h,b,C,_,x);else break;v--,T--}if(g>v){if(g<=T){const I=T+1,L=IT)for(;g<=v;)Me(l[g],h,b,!0),g++;else{const I=g,L=g,B=new Map;for(g=L;g<=T;g++){const oe=f[g]=x?Ne(f[g]):ye(f[g]);oe.key!=null&&B.set(oe.key,g)}let $,K=0;const he=T-L+1;let Ze=!1,Kn=0;const ut=new Array(he);for(g=0;g=he){Me(oe,h,b,!0);continue}let _e;if(oe.key!=null)_e=B.get(oe.key);else for($=L;$<=T;$++)if(ut[$-L]===0&&We(oe,f[$])){_e=$;break}_e===void 0?Me(oe,h,b,!0):(ut[_e-L]=g+1,_e>=Kn?Kn=_e:Ze=!0,M(oe,f[_e],a,null,h,b,C,_,x),K++)}const Wn=Ze?lo(ut):Ge;for($=Wn.length-1,g=he-1;g>=0;g--){const oe=L+g,_e=f[oe],kn=oe+1{const{el:b,type:C,transition:_,children:x,shapeFlag:g}=l;if(g&6){Ye(l.component.subTree,f,a,p);return}if(g&128){l.suspense.move(f,a,p);return}if(g&64){C.move(l,f,a,Xe);return}if(C===ue){s(b,f,a);for(let v=0;v_.enter(b),h);else{const{leave:v,delayLeave:T,afterLeave:I}=_,L=()=>s(b,f,a),B=()=>{v(b,()=>{L(),I&&I()})};T?T(b,L,B):B()}else s(b,f,a)},Me=(l,f,a,p=!1,h=!1)=>{const{type:b,props:C,ref:_,children:x,dynamicChildren:g,shapeFlag:w,patchFlag:v,dirs:T}=l;if(_!=null&&gn(_,null,a,l,!0),w&256){f.ctx.deactivate(l);return}const I=w&1&&T,L=!It(l);let B;if(L&&(B=C&&C.onVnodeBeforeUnmount)&&be(B,f,l),w&6)Cr(l.component,a,p);else{if(w&128){l.suspense.unmount(a,p);return}I&&$e(l,null,f,"beforeUnmount"),w&64?l.type.remove(l,f,a,h,Xe,p):g&&(b!==ue||v>0&&v&64)?Te(g,f,a,!1,!0):(b===ue&&v&384||!h&&w&16)&&Te(x,f,a),p&&Un(l)}(L&&(B=C&&C.onVnodeUnmounted)||I)&&ie(()=>{B&&be(B,f,l),I&&$e(l,null,f,"unmounted")},a)},Un=l=>{const{type:f,el:a,anchor:p,transition:h}=l;if(f===ue){yr(a,p);return}if(f===tn){ne(l);return}const b=()=>{r(a),h&&!h.persisted&&h.afterLeave&&h.afterLeave()};if(l.shapeFlag&1&&h&&!h.persisted){const{leave:C,delayLeave:_}=h,x=()=>C(a,b);_?_(l.el,b,x):x()}else b()},yr=(l,f)=>{let a;for(;l!==f;)a=E(l),r(l),l=a;r(f)},Cr=(l,f,a)=>{const{bum:p,scope:h,update:b,subTree:C,um:_}=l;p&&Xt(p),h.stop(),b&&(b.active=!1,Me(C,l,f,a)),_&&ie(_,f),ie(()=>{l.isUnmounted=!0},f),f&&f.pendingBranch&&!f.isUnmounted&&l.asyncDep&&!l.asyncResolved&&l.suspenseId===f.pendingId&&(f.deps--,f.deps===0&&f.resolve())},Te=(l,f,a,p=!1,h=!1,b=0)=>{for(let C=b;Cl.shapeFlag&6?yt(l.component.subTree):l.shapeFlag&128?l.suspense.next():E(l.anchor||l.el),Dn=(l,f,a)=>{l==null?f._vnode&&Me(f._vnode,null,null,!0):M(f._vnode||null,l,f,null,null,null,a),es(),qs(),f._vnode=l},Xe={p:M,um:Me,m:Ye,r:Un,mt:zt,mc:W,pc:we,pbc:se,n:yt,o:e};let Vt,Jt;return t&&([Vt,Jt]=t(Xe)),{render:Dn,hydrate:Vt,createApp:ro(Dn,Vt)}}function Ue({effect:e,update:t},n){e.allowRecurse=t.allowRecurse=n}function dr(e,t,n=!1){const s=e.children,r=t.children;if(A(s)&&A(r))for(let i=0;i>1,e[n[c]]0&&(t[s]=n[i-1]),n[i]=s)}}for(i=n.length,o=n[i-1];i-- >0;)n[i]=o,o=t[o];return n}const co=e=>e.__isTeleport,ue=Symbol(void 0),jn=Symbol(void 0),Ie=Symbol(void 0),tn=Symbol(void 0),dt=[];let ge=null;function hr(e=!1){dt.push(ge=e?null:[])}function fo(){dt.pop(),ge=dt[dt.length-1]||null}let _t=1;function us(e){_t+=e}function uo(e){return e.dynamicChildren=_t>0?ge||Ge:null,fo(),_t>0&&ge&&ge.push(e),e}function pr(e,t,n,s,r,i){return uo(Y(e,t,n,s,r,i,!0))}function ao(e){return e?e.__v_isVNode===!0:!1}function We(e,t){return e.type===t.type&&e.key===t.key}const Wt="__vInternal",gr=({key:e})=>e!=null?e:null,At=({ref:e,ref_key:t,ref_for:n})=>e!=null?Z(e)||G(e)||F(e)?{i:Ee,r:e,k:t,f:!!n}:e:null;function Y(e,t=null,n=null,s=0,r=null,i=e===ue?0:1,o=!1,c=!1){const u={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&gr(t),ref:t&&At(t),scopeId:Ut,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetAnchor:null,staticCount:0,shapeFlag:i,patchFlag:s,dynamicProps:r,dynamicChildren:null,appContext:null};return c?(Bn(u,n),i&128&&e.normalize(u)):n&&(u.shapeFlag|=Z(n)?8:16),_t>0&&!o&&ge&&(u.patchFlag>0||i&6)&&u.patchFlag!==32&&ge.push(u),u}const He=ho;function ho(e,t=null,n=null,s=0,r=null,i=!1){if((!e||e===zi)&&(e=Ie),ao(e)){const c=je(e,t,!0);return n&&Bn(c,n),_t>0&&!i&&ge&&(c.shapeFlag&6?ge[ge.indexOf(e)]=c:ge.push(c)),c.patchFlag|=-2,c}if(wo(e)&&(e=e.__vccOpts),t){t=po(t);let{class:c,style:u}=t;c&&!Z(c)&&(t.class=yn(c)),z(u)&&(Bs(u)&&!A(u)&&(u=ee({},u)),t.style=xn(u))}const o=Z(e)?1:Ai(e)?128:co(e)?64:z(e)?4:F(e)?2:0;return Y(e,t,n,s,r,o,i,!0)}function po(e){return e?Bs(e)||Wt in e?ee({},e):e:null}function je(e,t,n=!1){const{props:s,ref:r,patchFlag:i,children:o}=e,c=t?go(s||{},t):s;return{__v_isVNode:!0,__v_skip:!0,type:e.type,props:c,key:c&&gr(c),ref:t&&t.ref?n&&r?A(r)?r.concat(At(t)):[r,At(t)]:At(t):r,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:o,target:e.target,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==ue?i===-1?16:i|16:i,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:e.transition,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&je(e.ssContent),ssFallback:e.ssFallback&&je(e.ssFallback),el:e.el,anchor:e.anchor}}function Ve(e=" ",t=0){return He(jn,null,e,t)}function ye(e){return e==null||typeof e=="boolean"?He(Ie):A(e)?He(ue,null,e.slice()):typeof e=="object"?Ne(e):He(jn,null,String(e))}function Ne(e){return e.el===null||e.memo?e:je(e)}function Bn(e,t){let n=0;const{shapeFlag:s}=e;if(t==null)t=null;else if(A(t))n=16;else if(typeof t=="object")if(s&65){const r=t.default;r&&(r._c&&(r._d=!1),Bn(e,r()),r._c&&(r._d=!0));return}else{n=32;const r=t._;!r&&!(Wt in t)?t._ctx=Ee:r===3&&Ee&&(Ee.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else F(t)?(t={default:t,_ctx:Ee},n=32):(t=String(t),s&64?(n=16,t=[Ve(t)]):n=8);e.children=t,e.shapeFlag|=n}function go(...e){const t={};for(let n=0;nX||Ee,it=e=>{X=e,e.scope.on()},qe=()=>{X&&X.scope.off(),X=null};function mr(e){return e.vnode.shapeFlag&4}let bt=!1;function yo(e,t=!1){bt=t;const{props:n,children:s}=e.vnode,r=mr(e);Qi(e,n,r,t),to(e,s);const i=r?Co(e,t):void 0;return bt=!1,i}function Co(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=$s(new Proxy(e.ctx,qi));const{setup:s}=n;if(s){const r=e.setupContext=s.length>1?vo(e):null;it(e),lt();const i=Le(s,e,0,[e.props,r]);if(ct(),qe(),vs(i)){if(i.then(qe,qe),t)return i.then(o=>{as(e,o,t)}).catch(o=>{Bt(o,e,0)});e.asyncDep=i}else as(e,i,t)}else _r(e,t)}function as(e,t,n){F(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:z(t)&&(e.setupState=Ks(t)),_r(e,n)}let ds;function _r(e,t,n){const s=e.type;if(!e.render){if(!t&&ds&&!s.render){const r=s.template||Hn(e).template;if(r){const{isCustomElement:i,compilerOptions:o}=e.appContext.config,{delimiters:c,compilerOptions:u}=s,d=ee(ee({isCustomElement:i,delimiters:c},o),u);s.render=ds(r,d)}}e.render=s.render||me}it(e),lt(),Vi(e),ct(),qe()}function Eo(e){return new Proxy(e.attrs,{get(t,n){return le(e,"get","$attrs"),t[n]}})}function vo(e){const t=s=>{e.exposed=s||{}};let n;return{get attrs(){return n||(n=Eo(e))},slots:e.slots,emit:e.emit,expose:t}}function $n(e){if(e.exposed)return e.exposeProxy||(e.exposeProxy=new Proxy(Ks($s(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in Nt)return Nt[n](e)}}))}function wo(e){return F(e)&&"__vccOpts"in e}const To=(e,t)=>gi(e,t,bt),Oo="3.2.39",Io="http://www.w3.org/2000/svg",ke=typeof document<"u"?document:null,hs=ke&&ke.createElement("template"),Ao={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,s)=>{const r=t?ke.createElementNS(Io,e):ke.createElement(e,n?{is:n}:void 0);return e==="select"&&s&&s.multiple!=null&&r.setAttribute("multiple",s.multiple),r},createText:e=>ke.createTextNode(e),createComment:e=>ke.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>ke.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},cloneNode(e){const t=e.cloneNode(!0);return"_value"in e&&(t._value=e._value),t},insertStaticContent(e,t,n,s,r,i){const o=n?n.previousSibling:t.lastChild;if(r&&(r===i||r.nextSibling))for(;t.insertBefore(r.cloneNode(!0),n),!(r===i||!(r=r.nextSibling)););else{hs.innerHTML=s?`${e}`:e;const c=hs.content;if(s){const u=c.firstChild;for(;u.firstChild;)c.appendChild(u.firstChild);c.removeChild(u)}t.insertBefore(c,n)}return[o?o.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}};function Fo(e,t,n){const s=e._vtc;s&&(t=(t?[t,...s]:[...s]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}function Mo(e,t,n){const s=e.style,r=Z(n);if(n&&!r){for(const i in n)mn(s,i,n[i]);if(t&&!Z(t))for(const i in t)n[i]==null&&mn(s,i,"")}else{const i=s.display;r?t!==n&&(s.cssText=n):t&&e.removeAttribute("style"),"_vod"in e&&(s.display=i)}}const ps=/\s*!important$/;function mn(e,t,n){if(A(n))n.forEach(s=>mn(e,t,s));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const s=Po(e,t);ps.test(n)?e.setProperty(ot(s),n.replace(ps,""),"important"):e[s]=n}}const gs=["Webkit","Moz","ms"],nn={};function Po(e,t){const n=nn[t];if(n)return n;let s=st(t);if(s!=="filter"&&s in e)return nn[t]=s;s=Os(s);for(let r=0;r{let e=Date.now,t=!1;if(typeof window<"u"){Date.now()>document.createEvent("Event").timeStamp&&(e=performance.now.bind(performance));const n=navigator.userAgent.match(/firefox\/(\d+)/i);t=!!(n&&Number(n[1])<=53)}return[e,t]})();let _n=0;const Ho=Promise.resolve(),So=()=>{_n=0},jo=()=>_n||(Ho.then(So),_n=br());function Bo(e,t,n,s){e.addEventListener(t,n,s)}function $o(e,t,n,s){e.removeEventListener(t,n,s)}function Uo(e,t,n,s,r=null){const i=e._vei||(e._vei={}),o=i[t];if(s&&o)o.value=s;else{const[c,u]=Do(t);if(s){const d=i[t]=Ko(s,r);Bo(e,c,d,u)}else o&&($o(e,c,o,u),i[t]=void 0)}}const _s=/(?:Once|Passive|Capture)$/;function Do(e){let t;if(_s.test(e)){t={};let s;for(;s=e.match(_s);)e=e.slice(0,e.length-s[0].length),t[s[0].toLowerCase()]=!0}return[e[2]===":"?e.slice(3):ot(e.slice(2)),t]}function Ko(e,t){const n=s=>{const r=s.timeStamp||br();(Lo||r>=n.attached-1)&&ae(Wo(s,n.value),t,5,[s])};return n.value=e,n.attached=jo(),n}function Wo(e,t){if(A(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(s=>r=>!r._stopped&&s&&s(r))}else return t}const bs=/^on[a-z]/,ko=(e,t,n,s,r=!1,i,o,c,u)=>{t==="class"?Fo(e,s,r):t==="style"?Mo(e,n,s):Lt(t)?Cn(t)||Uo(e,t,n,s,o):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):zo(e,t,s,r))?Ro(e,t,s,i,o,c,u):(t==="true-value"?e._trueValue=s:t==="false-value"&&(e._falseValue=s),No(e,t,s,r))};function zo(e,t,n,s){return s?!!(t==="innerHTML"||t==="textContent"||t in e&&bs.test(t)&&F(n)):t==="spellcheck"||t==="draggable"||t==="translate"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA"||bs.test(t)&&Z(n)?!1:t in e}const qo={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String};Li.props;const Vo=ee({patchProp:ko},Ao);let xs;function Jo(){return xs||(xs=io(Vo))}const Yo=(...e)=>{const t=Jo().createApp(...e),{mount:n}=t;return t.mount=s=>{const r=Xo(s);if(!r)return;const i=t._component;!F(i)&&!i.render&&!i.template&&(i.template=r.innerHTML),r.innerHTML="";const o=n(r,!1,r instanceof SVGElement);return r instanceof Element&&(r.removeAttribute("v-cloak"),r.setAttribute("data-v-app","")),o},t};function Xo(e){return Z(e)?document.querySelector(e):e}const Zo="/vite.svg",Qo="/assets/vue.5532db34.svg";const xr=(e,t)=>{const n=e.__vccOpts||e;for(const[s,r]of t)n[s]=r;return n},kt=e=>(Ys("data-v-a1de4649"),e=e(),Xs(),e),Go={class:"card"},el=kt(()=>Y("p",null,[Ve(" Edit "),Y("code",null,"components/HelloWorld.vue"),Ve(" to test HMR ")],-1)),tl=kt(()=>Y("p",null,[Ve(" Check out "),Y("a",{href:"https://vuejs.org/guide/quick-start.html#local",target:"_blank"},"create-vue"),Ve(", the official Vue + Vite starter ")],-1)),nl=kt(()=>Y("p",null,[Ve(" Install "),Y("a",{href:"https://github.com/johnsoncodehk/volar",target:"_blank"},"Volar"),Ve(" in your IDE for a better DX ")],-1)),sl=kt(()=>Y("p",{class:"read-the-docs"},"Click on the Vite and Vue logos to learn more",-1)),rl={__name:"HelloWorld",props:{msg:String},setup(e){const t=fi(0);return(n,s)=>(hr(),pr(ue,null,[Y("h1",null,zn(e.msg),1),Y("div",Go,[Y("button",{type:"button",onClick:s[0]||(s[0]=r=>t.value++)},"count is "+zn(t.value),1),el]),tl,nl,sl],64))}},il=xr(rl,[["__scopeId","data-v-a1de4649"]]);const ol=e=>(Ys("data-v-d2afaedd"),e=e(),Xs(),e),ll=ol(()=>Y("div",null,[Y("a",{href:"https://vitejs.dev",target:"_blank"},[Y("img",{src:Zo,class:"logo",alt:"Vite logo"})]),Y("a",{href:"https://vuejs.org/",target:"_blank"},[Y("img",{src:Qo,class:"logo vue",alt:"Vue logo"})])],-1)),cl={__name:"App",setup(e){return(t,n)=>(hr(),pr(ue,null,[ll,He(il,{msg:"Vite + Vue"})],64))}},fl=xr(cl,[["__scopeId","data-v-d2afaedd"]]);Yo(fl).mount("#app"); 2 | -------------------------------------------------------------------------------- /test/frontend/dist/assets/index.795d9409.js.br: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devforth/spa-to-http/c50df8bc714f3e6531ead2f404f21be70d100983/test/frontend/dist/assets/index.795d9409.js.br -------------------------------------------------------------------------------- /test/frontend/dist/assets/index.795d9409.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devforth/spa-to-http/c50df8bc714f3e6531ead2f404f21be70d100983/test/frontend/dist/assets/index.795d9409.js.gz -------------------------------------------------------------------------------- /test/frontend/dist/assets/vue.5532db34.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/frontend/dist/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exapmle 5 | 6 | 7 |

Example

8 |

Example

9 |

Example

10 |

Example

11 |
Example
12 | 13 | 14 | -------------------------------------------------------------------------------- /test/frontend/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + Vue 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/frontend/dist/vite.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------