├── .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 |  
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;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 |
--------------------------------------------------------------------------------