├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── app └── lib │ └── components │ ├── Layout.svelte │ └── views │ ├── Welcome.svelte │ ├── client.ts │ └── server.ts ├── bun.lock ├── cli ├── aot.go ├── flags.go ├── project.go ├── run.go ├── utilities.go └── utilities │ ├── components │ ├── Action.svelte │ ├── ClientView.svelte │ ├── ClientViewLoader.svelte │ └── ServerView.svelte │ ├── scripts │ ├── action.ts │ ├── client.ts │ ├── href.ts │ ├── route.ts │ ├── server.ts │ ├── swaps.ts │ └── uuid.ts │ └── types.ts ├── eslint.config.js ├── frz ├── libalphabet.go ├── libarchive.go ├── libcon.go ├── libglobals.go ├── libglobals_test.go ├── libhttp.go ├── libjs.go ├── libjs_test.go ├── libnotifier.go ├── libnumbers.go ├── libnumbers_test.go ├── libroad.go ├── libserver.go ├── libserver_test.go ├── libsession.go ├── libview.go └── libview_test.go ├── fs ├── actions.go ├── mimes.go ├── mimes_test.go ├── readers.go ├── status.go ├── status_test.go └── test │ └── .gitkeep ├── go.mod ├── go.sum ├── index.html ├── main.go ├── makefile ├── package.json ├── public └── .gitkeep ├── svelte.config.js ├── tsconfig.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | *.local 12 | 13 | # Editor directories and files 14 | .vscode/* 15 | !.vscode/extensions.json 16 | .idea 17 | .DS_Store 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | 24 | # If you prefer the allow list template instead of the deny list, see community template: 25 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 26 | # 27 | # Binaries for programs and plugins 28 | *.exe 29 | *.exe~ 30 | *.dll 31 | *.so 32 | *.dylib 33 | 34 | # Test binary, built with `go test -c` 35 | *.test 36 | 37 | # Output of the go coverage tool, specifically when used with LiteIDE 38 | *.out 39 | 40 | # Dependency directories (remove the comment below to include it) 41 | # vendor/ 42 | 43 | # Go workspace file 44 | go.work 45 | go.work.sum 46 | 47 | # env file 48 | .env 49 | 50 | # Certificates 51 | *.pem 52 | *.crt 53 | *.key 54 | 55 | # Frizzante 56 | /tmp 57 | /bin 58 | /app/frz 59 | /frz/dist -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Get started 2 | 3 | Clone the basic starter template. 4 | ```sh 5 | git clone https://github.com/razshare/frizzante-starter && \ 6 | rm frizzante-starter/.git -fr && \ 7 | cd frizzante-starter 8 | ``` 9 | 10 | > [!NOTE] 11 | > Make sure [Bun](https://bun.sh/) and [build-essential](https://askubuntu.com/questions/398489/how-to-install-build-essential) are installed. 12 | 13 | Start development with 14 | 15 | ```sh 16 | make dev 17 | ``` 18 | 19 | > [!TIP] 20 | > The default makefile uses [Bun](https://bun.sh/) to install 21 | > dependencies while in development mode.\ 22 | > \ 23 | > You can swap Bun out with whatever runtime you want to use to 24 | > develop by replacing all `bun` and `bunx` references within 25 | > the makefile with the equivalent of whatever runtime you'd like to use.\ 26 | > \ 27 | > Note that this will have no impact on your application's performance, 28 | > Bun is only used in development mode 29 | > in order to be able to run the Vite server. 30 | 31 | Build with 32 | 33 | ```sh 34 | make build 35 | ``` 36 | 37 | This will create a `bin/app` standalone executable. 38 | 39 | > [!NOTE] 40 | > The final executable uses [V8](https://v8.dev/) bindings to run JavaScript code on the server in order to render svelte components.\ 41 | > \ 42 | > For that reason, the first build or launch may take some time, just be patient.\ 43 | > Once that is done, subsequent builds or launches will take considerably less time. 44 | 45 | # Documentation 46 | 47 | Visit [https://razshare.github.io/frizzante-docs/guides/get-started/](https://razshare.github.io/frizzante-docs/guides/get-started/) for a detailed documentation. -------------------------------------------------------------------------------- /app/lib/components/Layout.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 20 | 21 | 22 | 23 | 25 | 26 | 27 |
28 | {@render children()} 29 |
-------------------------------------------------------------------------------- /app/lib/components/views/Welcome.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | Welcome 11 | 12 | 13 | 14 |

Hello {view.data.name}.

15 |
-------------------------------------------------------------------------------- /app/lib/components/views/client.ts: -------------------------------------------------------------------------------- 1 | export const views: Record> = { 2 | "Welcome": import('./Welcome.svelte'), 3 | } -------------------------------------------------------------------------------- /app/lib/components/views/server.ts: -------------------------------------------------------------------------------- 1 | import Welcome from './Welcome.svelte' 2 | import type {Component} from "svelte"; 3 | 4 | export const views: Record = { 5 | "Welcome": Welcome, 6 | } -------------------------------------------------------------------------------- /bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "workspaces": { 4 | "": { 5 | "name": "www", 6 | "devDependencies": { 7 | "@eslint/compat": "^1.2.9", 8 | "@sveltejs/vite-plugin-svelte": "^5.1.0", 9 | "@tsconfig/svelte": "^5.0.4", 10 | "@types/node": "^22.15.29", 11 | "eslint": "^9.28.0", 12 | "eslint-config-prettier": "^10.1.5", 13 | "eslint-plugin-svelte": "^3.9.1", 14 | "svelte": "^5.33.14", 15 | "svelte-check": "^4.2.1", 16 | "typescript": "~5.8.3", 17 | "typescript-eslint": "^8.33.1", 18 | "vite": "^6.3.5", 19 | }, 20 | }, 21 | }, 22 | "packages": { 23 | "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], 24 | 25 | "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ=="], 26 | 27 | "@esbuild/android-arm": ["@esbuild/android-arm@0.25.0", "", { "os": "android", "cpu": "arm" }, "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g=="], 28 | 29 | "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.0", "", { "os": "android", "cpu": "arm64" }, "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g=="], 30 | 31 | "@esbuild/android-x64": ["@esbuild/android-x64@0.25.0", "", { "os": "android", "cpu": "x64" }, "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg=="], 32 | 33 | "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw=="], 34 | 35 | "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg=="], 36 | 37 | "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w=="], 38 | 39 | "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A=="], 40 | 41 | "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.0", "", { "os": "linux", "cpu": "arm" }, "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg=="], 42 | 43 | "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg=="], 44 | 45 | "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg=="], 46 | 47 | "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.0", "", { "os": "linux", "cpu": "none" }, "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw=="], 48 | 49 | "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.0", "", { "os": "linux", "cpu": "none" }, "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ=="], 50 | 51 | "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw=="], 52 | 53 | "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.0", "", { "os": "linux", "cpu": "none" }, "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA=="], 54 | 55 | "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA=="], 56 | 57 | "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.0", "", { "os": "linux", "cpu": "x64" }, "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw=="], 58 | 59 | "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.0", "", { "os": "none", "cpu": "arm64" }, "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw=="], 60 | 61 | "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.0", "", { "os": "none", "cpu": "x64" }, "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA=="], 62 | 63 | "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw=="], 64 | 65 | "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg=="], 66 | 67 | "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg=="], 68 | 69 | "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw=="], 70 | 71 | "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA=="], 72 | 73 | "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ=="], 74 | 75 | "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="], 76 | 77 | "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], 78 | 79 | "@eslint/compat": ["@eslint/compat@1.2.9", "", { "peerDependencies": { "eslint": "^9.10.0" }, "optionalPeers": ["eslint"] }, "sha512-gCdSY54n7k+driCadyMNv8JSPzYLeDVM/ikZRtvtROBpRdFSkS8W9A82MqsaY7lZuwL0wiapgD0NT1xT0hyJsA=="], 80 | 81 | "@eslint/config-array": ["@eslint/config-array@0.20.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ=="], 82 | 83 | "@eslint/config-helpers": ["@eslint/config-helpers@0.2.2", "", {}, "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg=="], 84 | 85 | "@eslint/core": ["@eslint/core@0.14.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg=="], 86 | 87 | "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], 88 | 89 | "@eslint/js": ["@eslint/js@9.28.0", "", {}, "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg=="], 90 | 91 | "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], 92 | 93 | "@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.1", "", { "dependencies": { "@eslint/core": "^0.14.0", "levn": "^0.4.1" } }, "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w=="], 94 | 95 | "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], 96 | 97 | "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], 98 | 99 | "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], 100 | 101 | "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], 102 | 103 | "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], 104 | 105 | "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], 106 | 107 | "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="], 108 | 109 | "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], 110 | 111 | "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], 112 | 113 | "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], 114 | 115 | "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], 116 | 117 | "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], 118 | 119 | "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.40.0", "", { "os": "android", "cpu": "arm" }, "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg=="], 120 | 121 | "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.40.0", "", { "os": "android", "cpu": "arm64" }, "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w=="], 122 | 123 | "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.40.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ=="], 124 | 125 | "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.40.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA=="], 126 | 127 | "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.40.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg=="], 128 | 129 | "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.40.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw=="], 130 | 131 | "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.40.0", "", { "os": "linux", "cpu": "arm" }, "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA=="], 132 | 133 | "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.40.0", "", { "os": "linux", "cpu": "arm" }, "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg=="], 134 | 135 | "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg=="], 136 | 137 | "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ=="], 138 | 139 | "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg=="], 140 | 141 | "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.40.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw=="], 142 | 143 | "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA=="], 144 | 145 | "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ=="], 146 | 147 | "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.40.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw=="], 148 | 149 | "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ=="], 150 | 151 | "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw=="], 152 | 153 | "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.40.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ=="], 154 | 155 | "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.40.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA=="], 156 | 157 | "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ=="], 158 | 159 | "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.5", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ=="], 160 | 161 | "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@5.1.0", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "debug": "^4.4.1", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.17", "vitefu": "^1.0.6" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-wojIS/7GYnJDYIg1higWj2ROA6sSRWvcR1PO/bqEyFr/5UZah26c8Cz4u0NaqjPeVltzsVpt2Tm8d2io0V+4Tw=="], 162 | 163 | "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@4.0.1", "", { "dependencies": { "debug": "^4.3.7" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^5.0.0", "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw=="], 164 | 165 | "@tsconfig/svelte": ["@tsconfig/svelte@5.0.4", "", {}, "sha512-BV9NplVgLmSi4mwKzD8BD/NQ8erOY/nUE/GpgWe2ckx+wIQF5RyRirn/QsSSCPeulVpc3RA/iJt6DpfTIZps0Q=="], 166 | 167 | "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], 168 | 169 | "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], 170 | 171 | "@types/node": ["@types/node@22.15.29", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ=="], 172 | 173 | "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.33.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.33.1", "@typescript-eslint/type-utils": "8.33.1", "@typescript-eslint/utils": "8.33.1", "@typescript-eslint/visitor-keys": "8.33.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.33.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-TDCXj+YxLgtvxvFlAvpoRv9MAncDLBV2oT9Bd7YBGC/b/sEURoOYuIwLI99rjWOfY3QtDzO+mk0n4AmdFExW8A=="], 174 | 175 | "@typescript-eslint/parser": ["@typescript-eslint/parser@8.33.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.33.1", "@typescript-eslint/types": "8.33.1", "@typescript-eslint/typescript-estree": "8.33.1", "@typescript-eslint/visitor-keys": "8.33.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-qwxv6dq682yVvgKKp2qWwLgRbscDAYktPptK4JPojCwwi3R9cwrvIxS4lvBpzmcqzR4bdn54Z0IG1uHFskW4dA=="], 176 | 177 | "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.33.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.33.1", "@typescript-eslint/types": "^8.33.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-DZR0efeNklDIHHGRpMpR5gJITQpu6tLr9lDJnKdONTC7vvzOlLAG/wcfxcdxEWrbiZApcoBCzXqU/Z458Za5Iw=="], 178 | 179 | "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.33.1", "", { "dependencies": { "@typescript-eslint/types": "8.33.1", "@typescript-eslint/visitor-keys": "8.33.1" } }, "sha512-dM4UBtgmzHR9bS0Rv09JST0RcHYearoEoo3pG5B6GoTR9XcyeqX87FEhPo+5kTvVfKCvfHaHrcgeJQc6mrDKrA=="], 180 | 181 | "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.33.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-STAQsGYbHCF0/e+ShUQ4EatXQ7ceh3fBCXkNU7/MZVKulrlq1usH7t2FhxvCpuCi5O5oi1vmVaAjrGeL71OK1g=="], 182 | 183 | "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.33.1", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.33.1", "@typescript-eslint/utils": "8.33.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-1cG37d9xOkhlykom55WVwG2QRNC7YXlxMaMzqw2uPeJixBFfKWZgaP/hjAObqMN/u3fr5BrTwTnc31/L9jQ2ww=="], 184 | 185 | "@typescript-eslint/types": ["@typescript-eslint/types@8.33.1", "", {}, "sha512-xid1WfizGhy/TKMTwhtVOgalHwPtV8T32MS9MaH50Cwvz6x6YqRIPdD2WvW0XaqOzTV9p5xdLY0h/ZusU5Lokg=="], 186 | 187 | "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.33.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.33.1", "@typescript-eslint/tsconfig-utils": "8.33.1", "@typescript-eslint/types": "8.33.1", "@typescript-eslint/visitor-keys": "8.33.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-+s9LYcT8LWjdYWu7IWs7FvUxpQ/DGkdjZeE/GGulHvv8rvYwQvVaUZ6DE+j5x/prADUgSbbCWZ2nPI3usuVeOA=="], 188 | 189 | "@typescript-eslint/utils": ["@typescript-eslint/utils@8.33.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.33.1", "@typescript-eslint/types": "8.33.1", "@typescript-eslint/typescript-estree": "8.33.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-52HaBiEQUaRYqAXpfzWSR2U3gxk92Kw006+xZpElaPMg3C4PgM+A5LqwoQI1f9E5aZ/qlxAZxzm42WX+vn92SQ=="], 190 | 191 | "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.33.1", "", { "dependencies": { "@typescript-eslint/types": "8.33.1", "eslint-visitor-keys": "^4.2.0" } }, "sha512-3i8NrFcZeeDHJ+7ZUuDkGT+UHq+XoFGsymNK2jZCOHcfEzRQ0BdpRtdpSx/Iyf3MHLWIcLS0COuOPibKQboIiQ=="], 192 | 193 | "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], 194 | 195 | "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], 196 | 197 | "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], 198 | 199 | "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], 200 | 201 | "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], 202 | 203 | "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], 204 | 205 | "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], 206 | 207 | "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], 208 | 209 | "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], 210 | 211 | "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], 212 | 213 | "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], 214 | 215 | "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], 216 | 217 | "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], 218 | 219 | "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], 220 | 221 | "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], 222 | 223 | "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], 224 | 225 | "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], 226 | 227 | "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], 228 | 229 | "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], 230 | 231 | "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], 232 | 233 | "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], 234 | 235 | "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], 236 | 237 | "detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="], 238 | 239 | "esbuild": ["esbuild@0.25.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.0", "@esbuild/android-arm": "0.25.0", "@esbuild/android-arm64": "0.25.0", "@esbuild/android-x64": "0.25.0", "@esbuild/darwin-arm64": "0.25.0", "@esbuild/darwin-x64": "0.25.0", "@esbuild/freebsd-arm64": "0.25.0", "@esbuild/freebsd-x64": "0.25.0", "@esbuild/linux-arm": "0.25.0", "@esbuild/linux-arm64": "0.25.0", "@esbuild/linux-ia32": "0.25.0", "@esbuild/linux-loong64": "0.25.0", "@esbuild/linux-mips64el": "0.25.0", "@esbuild/linux-ppc64": "0.25.0", "@esbuild/linux-riscv64": "0.25.0", "@esbuild/linux-s390x": "0.25.0", "@esbuild/linux-x64": "0.25.0", "@esbuild/netbsd-arm64": "0.25.0", "@esbuild/netbsd-x64": "0.25.0", "@esbuild/openbsd-arm64": "0.25.0", "@esbuild/openbsd-x64": "0.25.0", "@esbuild/sunos-x64": "0.25.0", "@esbuild/win32-arm64": "0.25.0", "@esbuild/win32-ia32": "0.25.0", "@esbuild/win32-x64": "0.25.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw=="], 240 | 241 | "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], 242 | 243 | "eslint": ["eslint@9.28.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.28.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ=="], 244 | 245 | "eslint-config-prettier": ["eslint-config-prettier@10.1.5", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw=="], 246 | 247 | "eslint-plugin-svelte": ["eslint-plugin-svelte@3.9.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.6.1", "@jridgewell/sourcemap-codec": "^1.5.0", "esutils": "^2.0.3", "globals": "^16.0.0", "known-css-properties": "^0.36.0", "postcss": "^8.4.49", "postcss-load-config": "^3.1.4", "postcss-safe-parser": "^7.0.0", "semver": "^7.6.3", "svelte-eslint-parser": "^1.2.0" }, "peerDependencies": { "eslint": "^8.57.1 || ^9.0.0", "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-mXFulSdD/0/p+zwENjPNsiVwAqmSRp90sy5zvVQBX1yAXhJbdhIn6C/tn8BZYjU94Ia7Y87d1Xdbvi49DeWyHQ=="], 248 | 249 | "eslint-scope": ["eslint-scope@8.3.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ=="], 250 | 251 | "eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], 252 | 253 | "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], 254 | 255 | "espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="], 256 | 257 | "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], 258 | 259 | "esrap": ["esrap@1.4.6", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw=="], 260 | 261 | "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], 262 | 263 | "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], 264 | 265 | "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], 266 | 267 | "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], 268 | 269 | "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], 270 | 271 | "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], 272 | 273 | "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], 274 | 275 | "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], 276 | 277 | "fdir": ["fdir@6.4.4", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg=="], 278 | 279 | "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], 280 | 281 | "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], 282 | 283 | "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], 284 | 285 | "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], 286 | 287 | "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], 288 | 289 | "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], 290 | 291 | "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], 292 | 293 | "globals": ["globals@16.2.0", "", {}, "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg=="], 294 | 295 | "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], 296 | 297 | "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], 298 | 299 | "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], 300 | 301 | "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], 302 | 303 | "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], 304 | 305 | "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], 306 | 307 | "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], 308 | 309 | "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], 310 | 311 | "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], 312 | 313 | "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], 314 | 315 | "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], 316 | 317 | "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], 318 | 319 | "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], 320 | 321 | "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], 322 | 323 | "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], 324 | 325 | "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], 326 | 327 | "known-css-properties": ["known-css-properties@0.36.0", "", {}, "sha512-A+9jP+IUmuQsNdsLdcg6Yt7voiMF/D4K83ew0OpJtpu+l34ef7LaohWV0Rc6KNvzw6ZDizkqfyB5JznZnzuKQA=="], 328 | 329 | "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], 330 | 331 | "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], 332 | 333 | "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], 334 | 335 | "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="], 336 | 337 | "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="], 338 | 339 | "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="], 340 | 341 | "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="], 342 | 343 | "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="], 344 | 345 | "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="], 346 | 347 | "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="], 348 | 349 | "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="], 350 | 351 | "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="], 352 | 353 | "lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], 354 | 355 | "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], 356 | 357 | "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], 358 | 359 | "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], 360 | 361 | "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], 362 | 363 | "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], 364 | 365 | "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], 366 | 367 | "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], 368 | 369 | "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], 370 | 371 | "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 372 | 373 | "nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="], 374 | 375 | "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], 376 | 377 | "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], 378 | 379 | "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], 380 | 381 | "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], 382 | 383 | "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], 384 | 385 | "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], 386 | 387 | "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], 388 | 389 | "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], 390 | 391 | "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], 392 | 393 | "postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="], 394 | 395 | "postcss-load-config": ["postcss-load-config@3.1.4", "", { "dependencies": { "lilconfig": "^2.0.5", "yaml": "^1.10.2" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg=="], 396 | 397 | "postcss-safe-parser": ["postcss-safe-parser@7.0.1", "", { "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A=="], 398 | 399 | "postcss-scss": ["postcss-scss@4.0.9", "", { "peerDependencies": { "postcss": "^8.4.29" } }, "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A=="], 400 | 401 | "postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="], 402 | 403 | "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], 404 | 405 | "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], 406 | 407 | "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], 408 | 409 | "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], 410 | 411 | "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], 412 | 413 | "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], 414 | 415 | "rollup": ["rollup@4.40.0", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.40.0", "@rollup/rollup-android-arm64": "4.40.0", "@rollup/rollup-darwin-arm64": "4.40.0", "@rollup/rollup-darwin-x64": "4.40.0", "@rollup/rollup-freebsd-arm64": "4.40.0", "@rollup/rollup-freebsd-x64": "4.40.0", "@rollup/rollup-linux-arm-gnueabihf": "4.40.0", "@rollup/rollup-linux-arm-musleabihf": "4.40.0", "@rollup/rollup-linux-arm64-gnu": "4.40.0", "@rollup/rollup-linux-arm64-musl": "4.40.0", "@rollup/rollup-linux-loongarch64-gnu": "4.40.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0", "@rollup/rollup-linux-riscv64-gnu": "4.40.0", "@rollup/rollup-linux-riscv64-musl": "4.40.0", "@rollup/rollup-linux-s390x-gnu": "4.40.0", "@rollup/rollup-linux-x64-gnu": "4.40.0", "@rollup/rollup-linux-x64-musl": "4.40.0", "@rollup/rollup-win32-arm64-msvc": "4.40.0", "@rollup/rollup-win32-ia32-msvc": "4.40.0", "@rollup/rollup-win32-x64-msvc": "4.40.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w=="], 416 | 417 | "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], 418 | 419 | "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], 420 | 421 | "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], 422 | 423 | "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], 424 | 425 | "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], 426 | 427 | "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], 428 | 429 | "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], 430 | 431 | "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], 432 | 433 | "svelte": ["svelte@5.33.14", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^1.4.6", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-kRlbhIlMTijbFmVDQFDeKXPLlX1/ovXwV0I162wRqQhRcygaqDIcu1d/Ese3H2uI+yt3uT8E7ndgDthQv5v5BA=="], 434 | 435 | "svelte-check": ["svelte-check@4.2.1", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-e49SU1RStvQhoipkQ/aonDhHnG3qxHSBtNfBRb9pxVXoa+N7qybAo32KgA9wEb2PCYFNaDg7bZCdhLD1vHpdYA=="], 436 | 437 | "svelte-eslint-parser": ["svelte-eslint-parser@1.2.0", "", { "dependencies": { "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.0.0", "espree": "^10.0.0", "postcss": "^8.4.49", "postcss-scss": "^4.0.9", "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-mbPtajIeuiyU80BEyGvwAktBeTX7KCr5/0l+uRGLq1dafwRNrjfM5kHGJScEBlPG3ipu6dJqfW/k0/fujvIEVw=="], 438 | 439 | "tinyglobby": ["tinyglobby@0.2.13", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw=="], 440 | 441 | "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], 442 | 443 | "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], 444 | 445 | "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], 446 | 447 | "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], 448 | 449 | "typescript-eslint": ["typescript-eslint@8.33.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.33.1", "@typescript-eslint/parser": "8.33.1", "@typescript-eslint/utils": "8.33.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-AgRnV4sKkWOiZ0Kjbnf5ytTJXMUZQ0qhSVdQtDNYLPLnjsATEYhaO94GlRQwi4t4gO8FfjM6NnikHeKjUm8D7A=="], 450 | 451 | "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], 452 | 453 | "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], 454 | 455 | "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], 456 | 457 | "vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="], 458 | 459 | "vitefu": ["vitefu@1.0.6", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["vite"] }, "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA=="], 460 | 461 | "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], 462 | 463 | "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], 464 | 465 | "yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], 466 | 467 | "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], 468 | 469 | "zimmerframe": ["zimmerframe@1.1.2", "", {}, "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w=="], 470 | 471 | "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], 472 | 473 | "@eslint/config-array/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], 474 | 475 | "@eslint/eslintrc/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], 476 | 477 | "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], 478 | 479 | "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], 480 | 481 | "@sveltejs/vite-plugin-svelte-inspector/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], 482 | 483 | "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], 484 | 485 | "@typescript-eslint/parser/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], 486 | 487 | "@typescript-eslint/project-service/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], 488 | 489 | "@typescript-eslint/type-utils/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], 490 | 491 | "@typescript-eslint/typescript-estree/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], 492 | 493 | "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], 494 | 495 | "eslint/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], 496 | 497 | "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], 498 | 499 | "is-reference/@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], 500 | 501 | "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], 502 | 503 | "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], 504 | } 505 | } 506 | -------------------------------------------------------------------------------- /cli/aot.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "embed" 5 | "github.com/razshare/frizzante/fs" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | ) 11 | 12 | //go:embed utilities/* 13 | var utilitiesEfs embed.FS 14 | 15 | type AotUtilities struct { 16 | efs embed.FS 17 | } 18 | 19 | func NewAotUtilities() *AotUtilities { 20 | return &AotUtilities{ 21 | efs: utilitiesEfs, 22 | } 23 | } 24 | 25 | func (assets *AotUtilities) CreateOnDisk(to string) error { 26 | var from = "utilities" 27 | //var to = filepath.Join("frz/.generated", "utilities") 28 | var create func(from string, to string) error 29 | 30 | create = func(from string, to string) error { 31 | embeddedFiles, readDirError := assets.efs.ReadDir(from) 32 | if readDirError != nil { 33 | return readDirError 34 | } 35 | 36 | if !fs.IsDirectory(to) { 37 | mkdirError := os.MkdirAll(to, os.ModePerm) 38 | if mkdirError != nil { 39 | return mkdirError 40 | } 41 | } 42 | 43 | for _, embeddedFile := range embeddedFiles { 44 | fromFileName := strings.Join([]string{from, embeddedFile.Name()}, "/") 45 | toFileName := filepath.Join(to, strings.ReplaceAll(embeddedFile.Name(), "/", string(filepath.Separator))) 46 | if embeddedFile.IsDir() { 47 | createError := create(fromFileName, toFileName) 48 | if createError != nil { 49 | return createError 50 | } 51 | continue 52 | } 53 | 54 | embeddedContents, readError := utilitiesEfs.ReadFile(fromFileName) 55 | if readError != nil { 56 | return readError 57 | } 58 | 59 | writeError := os.WriteFile(toFileName, embeddedContents, os.ModePerm) 60 | if writeError != nil { 61 | return writeError 62 | } 63 | } 64 | return nil 65 | } 66 | 67 | return create(from, to) 68 | } 69 | 70 | func CreateAotUtilitiesOnDisk(to string) { 71 | utilities := NewAotUtilities() 72 | utilitiesError := utilities.CreateOnDisk(to) 73 | if utilitiesError != nil { 74 | log.Fatal(utilitiesError) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /cli/flags.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import "flag" 4 | 5 | var FlagGenerate = flag.Bool("generate", true, "") 6 | var FlagProject = flag.Bool("project", false, "") 7 | var FlagUtilities = flag.Bool("utilities", false, "") 8 | var FlagOut = flag.String("out", "", "") 9 | -------------------------------------------------------------------------------- /cli/project.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "archive/zip" 5 | "github.com/pterm/pterm" 6 | "github.com/razshare/frizzante/fs" 7 | "io" 8 | "log" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | ) 14 | 15 | func Project() { 16 | if !*FlagGenerate || !*FlagProject { 17 | return 18 | } 19 | 20 | var showError error 21 | 22 | if "" == *FlagOut { 23 | *FlagOut, showError = pterm. 24 | DefaultInteractiveTextInput. 25 | Show("The name of the project is") 26 | if showError != nil { 27 | log.Fatal(showError) 28 | } 29 | } 30 | 31 | response, getError := http.Get("https://github.com/razshare/frizzante-starter/archive/refs/heads/main.zip") 32 | if getError != nil { 33 | log.Fatal(getError) 34 | } 35 | 36 | zipData, zipReadError := io.ReadAll(response.Body) 37 | if zipReadError != nil { 38 | log.Fatal(zipReadError) 39 | } 40 | 41 | zipFileName := *FlagOut + ".zip" 42 | 43 | zipWriteError := os.WriteFile(zipFileName, zipData, os.ModePerm) 44 | if zipWriteError != nil { 45 | log.Fatal(zipWriteError) 46 | } 47 | 48 | zipReader, zipOpenError := zip.OpenReader(zipFileName) 49 | if zipOpenError != nil { 50 | log.Fatal(zipOpenError) 51 | } 52 | defer func(zipReader *zip.ReadCloser) { 53 | err := zipReader.Close() 54 | if err != nil { 55 | log.Fatal(err) 56 | } 57 | }(zipReader) 58 | 59 | for _, file := range zipReader.File { 60 | fileName := filepath.Join(*FlagOut, strings.TrimPrefix(file.Name, "frizzante-starter-main")) 61 | fileIsDirectory := file.FileInfo().IsDir() 62 | fileIsDirectoryOnDisk := fs.IsDirectory(fileName) 63 | 64 | if fileIsDirectory && !fileIsDirectoryOnDisk { 65 | mkdirError := os.MkdirAll(fileName, os.ModePerm) 66 | if mkdirError != nil { 67 | log.Fatal(mkdirError) 68 | } 69 | continue 70 | } 71 | 72 | directoryName := filepath.Dir(fileName) 73 | 74 | if "." == directoryName { 75 | continue 76 | } 77 | 78 | parentExists := fs.IsDirectory(directoryName) 79 | 80 | if !parentExists { 81 | mkdirError := os.MkdirAll(directoryName, os.ModePerm) 82 | if mkdirError != nil { 83 | log.Fatal(mkdirError) 84 | } 85 | } 86 | 87 | dstFile, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode()) 88 | if err != nil { 89 | panic(err) 90 | } 91 | 92 | fileInArchive, err := file.Open() 93 | if err != nil { 94 | panic(err) 95 | } 96 | 97 | if _, copyError := io.Copy(dstFile, fileInArchive); copyError != nil { 98 | panic(copyError) 99 | } 100 | 101 | _ = dstFile.Close() 102 | _ = fileInArchive.Close() 103 | } 104 | 105 | _ = os.Remove(zipFileName) 106 | 107 | os.Exit(0) 108 | } 109 | -------------------------------------------------------------------------------- /cli/run.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "flag" 5 | "github.com/pterm/pterm" 6 | "log" 7 | ) 8 | 9 | func Run() { 10 | 11 | flag.Parse() 12 | 13 | if !*FlagProject && 14 | !*FlagUtilities { 15 | 16 | generator, showError := pterm. 17 | DefaultInteractiveSelect. 18 | WithOptions([]string{ 19 | "Project", 20 | "Utilities", 21 | }). 22 | Show("Generate") 23 | 24 | if showError != nil { 25 | log.Fatal(showError) 26 | } 27 | 28 | if "Project" == generator { 29 | *FlagProject = true 30 | } 31 | 32 | if "Utilities" == generator { 33 | *FlagUtilities = true 34 | } 35 | } 36 | 37 | Project() 38 | Utilities() 39 | } 40 | -------------------------------------------------------------------------------- /cli/utilities.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/pterm/pterm" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | func Utilities() { 11 | if !*FlagGenerate || !*FlagUtilities { 12 | return 13 | } 14 | 15 | var e error 16 | 17 | if "" == *FlagOut { 18 | *FlagOut, e = pterm. 19 | DefaultInteractiveTextInput. 20 | Show("Drop utilities in") 21 | if e != nil { 22 | log.Fatal(e) 23 | } 24 | } 25 | 26 | u := NewAotUtilities() 27 | 28 | if e = u.CreateOnDisk(filepath.Join(*FlagOut)); e != nil { 29 | log.Fatal(e) 30 | } 31 | 32 | os.Exit(0) 33 | } 34 | -------------------------------------------------------------------------------- /cli/utilities/components/Action.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 31 | 32 |
33 | {#each Object.keys(using ?? {}) as key(key)} 34 | {@const value = (using??{})[key]??''} 35 | 36 | {/each} 37 | 38 | 39 | 40 | 43 |
44 | -------------------------------------------------------------------------------- /cli/utilities/components/ClientView.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | {#each Object.keys(views) as key(key)} 13 | {#if key === view.name} 14 | 15 | {/if} 16 | {/each} 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /cli/utilities/components/ClientViewLoader.svelte: -------------------------------------------------------------------------------- 1 | 6 | 12 | 13 | {#await from} 14 | {#if PreviousComponent} 15 | 16 | {/if} 17 | {:then Component} 18 | 19 | {/await} -------------------------------------------------------------------------------- /cli/utilities/components/ServerView.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | {#each Object.keys(views) as key(key)} 12 | {@const Component = views[key]} 13 | {#if key === name} 14 | 15 | {/if} 16 | {/each} -------------------------------------------------------------------------------- /cli/utilities/scripts/action.ts: -------------------------------------------------------------------------------- 1 | import {getContext} from "svelte"; 2 | import type {View} from "../types.ts"; 3 | import {route} from "./route.ts"; 4 | import {swaps} from "./swaps.ts"; 5 | 6 | export function action(path = ""): { 7 | method: "POST" 8 | action: string 9 | onsubmit: (e: Event) => Promise 10 | } { 11 | const view = getContext("view") as View 12 | route(view) 13 | return { 14 | method: "POST", 15 | action: path, 16 | async onsubmit(e: Event) { 17 | e.preventDefault() 18 | const form = e.target as HTMLFormElement 19 | const body = new FormData(form) 20 | 21 | await swaps 22 | .swap(view) 23 | .withMethod("POST") 24 | .withPath(path) 25 | .withBody(body) 26 | .play(true).then(function done() { 27 | form.reset() 28 | }) 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /cli/utilities/scripts/client.ts: -------------------------------------------------------------------------------- 1 | import { hydrate } from "svelte"; 2 | import ClientView from "$frz/components/ClientView.svelte"; 3 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 4 | // @ts-expect-error 5 | target().innerHTML = ""; 6 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 7 | // @ts-expect-error 8 | hydrate(ClientView, { target: target(), props: props() }); -------------------------------------------------------------------------------- /cli/utilities/scripts/href.ts: -------------------------------------------------------------------------------- 1 | import {getContext} from "svelte"; 2 | import type {View} from "../types.ts"; 3 | import {route} from "./route.ts"; 4 | import {swaps} from "./swaps.ts"; 5 | 6 | export function href(path = ""): { 7 | href: string, 8 | onclick: (e: MouseEvent) => void 9 | } { 10 | const view = getContext("view") as View 11 | route(view) 12 | return { 13 | href: path, 14 | async onclick(e: MouseEvent) { 15 | e.preventDefault() 16 | await swaps 17 | .swap(view) 18 | .withPath(path) 19 | .play(true) 20 | return false 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /cli/utilities/scripts/route.ts: -------------------------------------------------------------------------------- 1 | import type {View} from "../types.ts"; 2 | import {swaps} from "./swaps.ts"; 3 | 4 | let started = false 5 | const IS_BROWSER = typeof document !== 'undefined' 6 | 7 | export function route(view: View): void { 8 | if (!IS_BROWSER || started) { 9 | return 10 | } 11 | 12 | const listener = async function pop(e: PopStateEvent) { 13 | e.preventDefault(); 14 | 15 | const id = e.state ?? "" 16 | const current = swaps.find(id) 17 | 18 | if (!current) { 19 | await swaps.swap(view).withPath("/").play(false) 20 | return 21 | } 22 | 23 | if (current.position() + 1 != swaps.position()) { 24 | swaps.teleport(current.position() + 1) 25 | await current.play(false) 26 | } else { 27 | await current.play(true) 28 | } 29 | } 30 | window.addEventListener("popstate", listener); 31 | started = true 32 | } 33 | -------------------------------------------------------------------------------- /cli/utilities/scripts/server.ts: -------------------------------------------------------------------------------- 1 | import {render as _render} from "svelte/server"; 2 | import ServerView from "$frz/components/ServerView.svelte"; 3 | export async function render(props: never) { 4 | return _render(ServerView, {props}); 5 | } -------------------------------------------------------------------------------- /cli/utilities/scripts/swaps.ts: -------------------------------------------------------------------------------- 1 | import type {View} from "../types.ts"; 2 | import {uuid} from './uuid.ts' 3 | 4 | type SwapAction = { 5 | method: () => "GET" | "POST" 6 | path: () => string 7 | body: () => unknown 8 | position: () => number 9 | withMethod: (method: "GET" | "POST") => SwapAction 10 | withPath: (path: string) => SwapAction 11 | withBody: (body: unknown) => SwapAction 12 | play: (update: boolean) => Promise 13 | } 14 | 15 | let nextPosition = 0 16 | const record = {} as Record 17 | 18 | function find(id: string): false | SwapAction { 19 | return record[id] ?? false 20 | } 21 | 22 | function swap(view: View): SwapAction { 23 | let swapMethod = 'GET' as "GET" | "POST" 24 | let swapPath = location.pathname 25 | let swapBody: unknown 26 | const swapPosition = nextPosition++ 27 | 28 | return { 29 | method() { 30 | return swapMethod 31 | }, 32 | path() { 33 | return swapPath 34 | }, 35 | body() { 36 | return swapBody 37 | }, 38 | position() { 39 | return swapPosition 40 | }, 41 | withMethod(method: "GET" | "POST") { 42 | swapMethod = method 43 | return this 44 | }, 45 | withPath(path: string) { 46 | swapPath = path 47 | return this 48 | }, 49 | withBody(body: unknown) { 50 | swapBody = body 51 | return this 52 | }, 53 | 54 | async play(update: boolean) { 55 | const response = await fetch(swapPath, { 56 | method: swapMethod, 57 | headers: {Accept: "application/json"}, 58 | body: swapBody as BodyInit 59 | }); 60 | 61 | const json = await response.json(); 62 | 63 | view.data = json.data 64 | view.name = json.name; 65 | view.error = json.error; 66 | 67 | if (update) { 68 | const id = uuid() 69 | record[id] = this 70 | window.history.pushState(id, "", response.url); 71 | } 72 | } 73 | } 74 | } 75 | 76 | function position(): number { 77 | return nextPosition 78 | } 79 | 80 | function teleport(position: number) { 81 | nextPosition = position 82 | } 83 | 84 | export const swaps = { 85 | swap, 86 | find, 87 | position, 88 | teleport, 89 | } -------------------------------------------------------------------------------- /cli/utilities/scripts/uuid.ts: -------------------------------------------------------------------------------- 1 | export function uuid(short = false): string { 2 | let dt = new Date().getTime() 3 | const BLUEPRINT = short ? 'xyxxyxyx' : 'xxxxxxxx-xxxx-yxxx-yxxx-xxxxxxxxxxxx' 4 | return BLUEPRINT.replace(/[xy]/g, function check(c) { 5 | const r = (dt + Math.random() * 16) % 16 | 0 6 | dt = Math.floor(dt / 16) 7 | return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16) 8 | }) 9 | } -------------------------------------------------------------------------------- /cli/utilities/types.ts: -------------------------------------------------------------------------------- 1 | export type View = { 2 | name: string 3 | data: T 4 | error: string 5 | renderMode: number 6 | } -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import prettier from 'eslint-config-prettier'; 2 | import js from '@eslint/js'; 3 | import { includeIgnoreFile } from '@eslint/compat'; 4 | import svelte from 'eslint-plugin-svelte'; 5 | import globals from 'globals'; 6 | import { fileURLToPath } from 'node:url'; 7 | import ts from 'typescript-eslint'; 8 | import svelteConfig from './svelte.config.js'; 9 | 10 | const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); 11 | 12 | export default ts.config( 13 | includeIgnoreFile(gitignorePath), 14 | js.configs.recommended, 15 | ...ts.configs.recommended, 16 | ...svelte.configs.recommended, 17 | prettier, 18 | ...svelte.configs.prettier, 19 | { 20 | languageOptions: { 21 | globals: { ...globals.browser, ...globals.node } 22 | }, 23 | rules: { 'no-undef': 'off' } 24 | }, 25 | { 26 | files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], 27 | languageOptions: { 28 | parserOptions: { 29 | projectService: true, 30 | extraFileExtensions: ['.svelte'], 31 | parser: ts.parser, 32 | svelteConfig 33 | } 34 | } 35 | } 36 | ); 37 | -------------------------------------------------------------------------------- /frz/libalphabet.go: -------------------------------------------------------------------------------- 1 | package frz 2 | 3 | import "slices" 4 | 5 | var alphabet = []rune{ 6 | 'A', 7 | 'a', 8 | 'B', 9 | 'b', 10 | 'C', 11 | 'c', 12 | 'D', 13 | 'd', 14 | 'E', 15 | 'e', 16 | 'F', 17 | 'f', 18 | 'G', 19 | 'g', 20 | 'H', 21 | 'h', 22 | 'I', 23 | 'i', 24 | 'J', 25 | 'j', 26 | 'K', 27 | 'k', 28 | 'L', 29 | 'l', 30 | 'M', 31 | 'm', 32 | 'N', 33 | 'n', 34 | 'O', 35 | 'o', 36 | 'P', 37 | 'p', 38 | 'Q', 39 | 'q', 40 | 'R', 41 | 'r', 42 | 'S', 43 | 's', 44 | 'T', 45 | 't', 46 | 'U', 47 | 'u', 48 | 'V', 49 | 'v', 50 | 'W', 51 | 'w', 52 | 'X', 53 | 'x', 54 | 'Y', 55 | 'y', 56 | 'Z', 57 | 'z', 58 | '1', 59 | '2', 60 | '3', 61 | '4', 62 | '5', 63 | '6', 64 | '7', 65 | '8', 66 | '9', 67 | '0', 68 | '_', 69 | '-', 70 | '.', 71 | } 72 | 73 | // KeyIsSafe checks if a key is accepted by the safe alphabet. 74 | // 75 | // The safe alphabet accepts only "_" (underscore), "-" (hyphen), "." (period), 76 | // english letters (uppercase and lowercase) and numeric digits (from 0 to 9). 77 | func KeyIsSafe(key string) bool { 78 | for _, char := range key { 79 | if slices.Contains(alphabet, char) { 80 | continue 81 | } 82 | return false 83 | } 84 | 85 | return true 86 | } 87 | -------------------------------------------------------------------------------- /frz/libarchive.go: -------------------------------------------------------------------------------- 1 | package frz 2 | 3 | import ( 4 | "errors" 5 | "github.com/razshare/frizzante/fs" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | type Archive interface { 11 | Get(domain string, key string) ([]byte, error) 12 | Set(domain string, key string, value []byte) error 13 | Has(domain string, key string) (bool, error) 14 | Remove(domain string, key string) error 15 | HasDomain(domain string) (bool, error) 16 | RemoveDomain(domain string) error 17 | } 18 | 19 | type DiskArchive struct { 20 | name string 21 | road *Road 22 | } 23 | 24 | func NewDiskArchive() *DiskArchive { 25 | return &DiskArchive{ 26 | name: ".archive", 27 | road: NewRoad(), 28 | } 29 | } 30 | 31 | // WithName sets the name of the archive and thus the directory. 32 | func (archive *DiskArchive) WithName(name string) *DiskArchive { 33 | archive.name = name 34 | return archive 35 | } 36 | 37 | func (archive *DiskArchive) Get(domain string, key string) ([]byte, error) { 38 | if "" == archive.name { 39 | return make([]byte, 0), errors.New("disk archive name is blank") 40 | } 41 | lane := archive.road.WithLane(domain, key) 42 | lane.Lock() 43 | defer lane.Unlock() 44 | fileName := filepath.Join(archive.name, domain, key) 45 | value, readError := os.ReadFile(fileName) 46 | if nil != readError { 47 | return make([]byte, 0), readError 48 | } 49 | return value, nil 50 | } 51 | 52 | func (archive *DiskArchive) Set(domain string, key string, value []byte) error { 53 | if "" == archive.name { 54 | return errors.New("disk archive name is blank") 55 | } 56 | lane := archive.road.WithLane(domain, key) 57 | lane.Lock() 58 | defer lane.Unlock() 59 | directoryName := filepath.Join(archive.name, domain) 60 | if !fs.FileExists(directoryName) { 61 | mkdirError := os.MkdirAll(directoryName, os.ModePerm) 62 | if nil != mkdirError { 63 | return mkdirError 64 | } 65 | } 66 | fileName := filepath.Join(directoryName, key) 67 | writeError := os.WriteFile(fileName, value, os.ModePerm) 68 | if nil != writeError { 69 | return writeError 70 | } 71 | return nil 72 | } 73 | 74 | func (archive *DiskArchive) Has(domain string, key string) (bool, error) { 75 | if "" == archive.name { 76 | return false, errors.New("disk archive name is blank") 77 | } 78 | lane := archive.road.WithLane(domain, key) 79 | lane.Lock() 80 | defer lane.Unlock() 81 | fileName := filepath.Join(archive.name, domain, key) 82 | return fs.FileExists(fileName), nil 83 | } 84 | 85 | func (archive *DiskArchive) Remove(domain string, key string) error { 86 | if "" == archive.name { 87 | return errors.New("disk archive name is blank") 88 | } 89 | lane := archive.road.WithLane(domain, key) 90 | lane.Lock() 91 | defer lane.Unlock() 92 | fileName := filepath.Join(archive.name, domain, key) 93 | removeError := os.Remove(fileName) 94 | if nil != removeError { 95 | return removeError 96 | } 97 | return nil 98 | } 99 | 100 | func (archive *DiskArchive) HasDomain(domain string) (bool, error) { 101 | if "" == archive.name { 102 | return false, errors.New("disk archive name is blank") 103 | } 104 | lane := archive.road.WithLane(domain) 105 | lane.Lock() 106 | defer lane.Unlock() 107 | return fs.FileExists(filepath.Join(archive.name, domain)), nil 108 | } 109 | 110 | func (archive *DiskArchive) RemoveDomain(domain string) error { 111 | if "" == archive.name { 112 | return errors.New("disk archive name is blank") 113 | } 114 | lane := archive.road.WithLane(domain) 115 | lane.Lock() 116 | defer lane.Unlock() 117 | directoryName := filepath.Join(archive.name, domain) 118 | removeError := os.RemoveAll(directoryName) 119 | if nil != removeError { 120 | return removeError 121 | } 122 | return nil 123 | } 124 | -------------------------------------------------------------------------------- /frz/libcon.go: -------------------------------------------------------------------------------- 1 | package frz 2 | 3 | import ( 4 | "bytes" 5 | "embed" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "github.com/gorilla/websocket" 10 | "github.com/razshare/frizzante/fs" 11 | "io" 12 | "net/http" 13 | "net/url" 14 | "path/filepath" 15 | "strings" 16 | ) 17 | 18 | type Connection struct { 19 | server *Server 20 | request *http.Request 21 | writer http.ResponseWriter 22 | locked bool 23 | status int 24 | header http.Header 25 | webSocket *websocket.Conn 26 | eventName string 27 | eventId int64 28 | sessionId string 29 | } 30 | 31 | ///////////////////////////////////////////////////////////// 32 | ///////////////////////////////////////////////////////////// 33 | ///////////////////////////////////////////////////////////// 34 | ///////////////////////////////////////////////////////////// 35 | ////////////////////////// RECEIVE ////////////////////////// 36 | ///////////////////////////////////////////////////////////// 37 | ///////////////////////////////////////////////////////////// 38 | ///////////////////////////////////////////////////////////// 39 | ///////////////////////////////////////////////////////////// 40 | 41 | // ReceiveCancellation returns a channel that closes when the request gets cancelled. 42 | func (connection *Connection) ReceiveCancellation() <-chan struct{} { 43 | return connection.request.Context().Done() 44 | } 45 | 46 | // IsAlive returns a reference to a bool which is initially set to `true`. 47 | // 48 | // This bool updates to `false` when the request gets cancelled. 49 | func (connection *Connection) IsAlive() *bool { 50 | value := true 51 | go func() { 52 | <-connection.ReceiveCancellation() 53 | value = false 54 | }() 55 | return &value 56 | } 57 | 58 | // ReceiveCookie reads the contents of a cookie from the message and returns the value. 59 | // 60 | // It silently discards malformed values. 61 | // 62 | // Compatible with web sockets. 63 | func (connection *Connection) ReceiveCookie(key string) string { 64 | cookie, cookieError := connection.request.Cookie(key) 65 | if nil != cookieError { 66 | return "" 67 | } 68 | value, unescapeError := url.QueryUnescape(cookie.Value) 69 | if nil != unescapeError { 70 | return "" 71 | } 72 | 73 | return value 74 | } 75 | 76 | // ReceiveMessage reads the contents of the message and returns the value. 77 | // 78 | // Compatible with web sockets. 79 | func ReceiveMessage(connection *Connection) (string, error) { 80 | if connection.webSocket != nil { 81 | _, readBytes, readError := connection.webSocket.ReadMessage() 82 | if nil != readError { 83 | return "", readError 84 | } 85 | return string(readBytes), nil 86 | } 87 | 88 | readBytes, readAllError := io.ReadAll(connection.request.Body) 89 | if nil != readAllError { 90 | return "", readAllError 91 | } 92 | return string(readBytes), nil 93 | } 94 | 95 | // ReceiveJson reads the next JSON-encoded message from the 96 | // connection and stores it in the value pointed to by v. 97 | // 98 | // ReceiveJson returns true on success or false on failure. 99 | // 100 | // Compatible with web sockets. 101 | func (connection *Connection) ReceiveJson(v any) bool { 102 | if connection.webSocket != nil { 103 | jsonError := connection.webSocket.ReadJSON(v) 104 | if nil != jsonError { 105 | connection.server.notifier.SendErrorAndTrace(jsonError, 1) 106 | return false 107 | } 108 | return true 109 | } 110 | 111 | readBytes, readAllError := io.ReadAll(connection.request.Body) 112 | if nil != readAllError { 113 | connection.server.notifier.SendErrorAndTrace(readAllError, 1) 114 | return false 115 | } 116 | unmarshalError := json.Unmarshal(readBytes, v) 117 | if nil != unmarshalError { 118 | connection.server.notifier.SendErrorAndTrace(unmarshalError, 1) 119 | return false 120 | } 121 | return true 122 | } 123 | 124 | // ReceiveForm reads the message as a form and returns the value. 125 | // 126 | // It silently discards malformed values. 127 | func (connection *Connection) ReceiveForm() url.Values { 128 | return connection.ReceiveFormWithMaxMemory(2 * MB) 129 | } 130 | 131 | // ReceiveFormWithMaxMemory reads the message as a form and returns the value. 132 | // 133 | // It silently discards malformed values. 134 | func (connection *Connection) ReceiveFormWithMaxMemory(maxMemory int64) url.Values { 135 | if connection.webSocket != nil { 136 | return url.Values{} 137 | } 138 | 139 | parseMultipartFormError := connection.request.ParseMultipartForm(maxMemory) 140 | if nil != parseMultipartFormError { 141 | if !errors.Is(parseMultipartFormError, http.ErrNotMultipart) { 142 | return url.Values{} 143 | } 144 | 145 | parseFormError := connection.request.ParseForm() 146 | if nil != parseFormError { 147 | return url.Values{} 148 | } 149 | } 150 | 151 | return connection.request.Form 152 | } 153 | 154 | // ReceiveQuery reads a query field and returns the value. 155 | // 156 | // Compatible with web sockets. 157 | func (connection *Connection) ReceiveQuery(name string) string { 158 | return connection.request.URL.Query().Get(name) 159 | } 160 | 161 | // ReceivePath reads a parameters fields and returns the value. 162 | // 163 | // Compatible with web sockets. 164 | func (connection *Connection) ReceivePath(name string) string { 165 | return connection.request.PathValue(name) 166 | } 167 | 168 | // ReceiveHeader reads a header field and returns the value. 169 | // 170 | // Compatible with web sockets. 171 | func (connection *Connection) ReceiveHeader(key string) string { 172 | return connection.request.Header.Get(key) 173 | } 174 | 175 | // ReceiveContentType reads the Content-Type header field and returns the value. 176 | // 177 | // Compatible with web sockets. 178 | func ReceiveContentType(connection *Connection) string { 179 | return connection.request.Header.Get("Content-Type") 180 | } 181 | 182 | // VerifyContentType checks if the incoming request has any of the given content-types. 183 | func VerifyContentType(connection *Connection, contentTypes ...string) bool { 184 | requestedMime := connection.request.Header.Get("Content-Type") 185 | for _, acceptedMime := range contentTypes { 186 | if acceptedMime == "*" || strings.HasPrefix(requestedMime, acceptedMime) { 187 | return true 188 | } 189 | } 190 | 191 | return false 192 | } 193 | 194 | // VerifyAccept checks if the incoming request accepts any of the given content-types. 195 | func (connection *Connection) VerifyAccept(contentTypes ...string) bool { 196 | requestedAcceptMime := connection.request.Header.Get("Accept") 197 | for _, acceptedMime := range contentTypes { 198 | if acceptedMime == "*" || strings.Contains(requestedAcceptMime, acceptedMime) { 199 | return true 200 | } 201 | } 202 | 203 | return false 204 | } 205 | 206 | ///////////////////////////////////////////////////////////// 207 | ///////////////////////////////////////////////////////////// 208 | ///////////////////////////////////////////////////////////// 209 | ///////////////////////////////////////////////////////////// 210 | //////////////////////////// SEND /////////////////////////// 211 | ///////////////////////////////////////////////////////////// 212 | ///////////////////////////////////////////////////////////// 213 | ///////////////////////////////////////////////////////////// 214 | ///////////////////////////////////////////////////////////// 215 | 216 | // SendEventContent sends content using the `server sent events` format. 217 | // 218 | // Usually this should be used internally in order to send content to a Server sent event. 219 | // 220 | // That being said, other than the format, there is nothing else different between this function and ResponseSendContent. 221 | // 222 | // See https://html.spec.whatwg.org/multipage/server-sent-events.html for more details on the format. 223 | func (connection *Connection) SendEventContent(content []byte) { 224 | header := fmt.Sprintf("id: %d\r\nevent: %s\r\n", connection.eventId, connection.eventName) 225 | 226 | _, writeEventError := connection.writer.Write([]byte(header)) 227 | if nil != writeEventError { 228 | connection.server.notifier.SendErrorAndTrace(writeEventError, 1) 229 | return 230 | } 231 | 232 | for _, line := range bytes.Split(content, []byte("\r\n")) { 233 | _, writeEventError = connection.writer.Write([]byte("data: ")) 234 | if nil != writeEventError { 235 | connection.server.notifier.SendErrorAndTrace(writeEventError, 1) 236 | return 237 | } 238 | 239 | _, writeEventError = connection.writer.Write(line) 240 | if nil != writeEventError { 241 | connection.server.notifier.SendErrorAndTrace(writeEventError, 1) 242 | return 243 | } 244 | 245 | _, writeEventError = connection.writer.Write([]byte("\r\n")) 246 | if nil != writeEventError { 247 | connection.server.notifier.SendErrorAndTrace(writeEventError, 1) 248 | return 249 | } 250 | } 251 | 252 | _, writeEventError = connection.writer.Write([]byte("\r\n")) 253 | if nil != writeEventError { 254 | connection.server.notifier.SendErrorAndTrace(writeEventError, 1) 255 | return 256 | } 257 | 258 | flusher, flushedOk := connection.writer.(http.Flusher) 259 | if !flushedOk { 260 | connection.server.notifier.SendErrorAndTrace(errors.New("could not retrieve flusher"), 1) 261 | return 262 | } 263 | 264 | flusher.Flush() 265 | 266 | connection.eventId++ 267 | } 268 | 269 | // SendNavigate redirects the request with status 302. 270 | func (connection *Connection) SendNavigate(location string) { 271 | connection.SendRedirect(location, 302) 272 | connection.SendMessage("") 273 | } 274 | 275 | // SendRedirect redirects the request. 276 | func (connection *Connection) SendRedirect(location string, statusCode int) { 277 | connection.SendStatus(statusCode) 278 | connection.SendHeader("Location", location) 279 | } 280 | 281 | // SendStatus sets the status code. 282 | // 283 | // This will lock the status, which makes it 284 | // so that the increaseIndex time you invoke this 285 | // function it will fail with an error. 286 | // 287 | // All errors are sent to the server notifier. 288 | func (connection *Connection) SendStatus(code int) { 289 | if connection.locked { 290 | connection.server.notifier.SendErrorAndTrace(errors.New("status is locked"), 1) 291 | } 292 | connection.status = code 293 | } 294 | 295 | // SendHeader sets a header field. 296 | // 297 | // If the status has not been sent already, a default "200 OK" status will be sent immediately. 298 | // 299 | // This means the status will become locked and further attempts to send the status will fail with an error. 300 | // 301 | // All errors are sent to the server notifier. 302 | func (connection *Connection) SendHeader(key string, value string) { 303 | if connection.locked { 304 | connection.server.notifier.SendErrorAndTrace(errors.New("header is locked"), 1) 305 | } 306 | 307 | connection.header.Set(key, value) 308 | } 309 | 310 | // SendContentType sets the Content-Type header field. 311 | func (connection *Connection) SendContentType(contentType string) { 312 | connection.SendHeader("Content-Type", contentType) 313 | } 314 | 315 | // SendCookie sends a cookies to the client. 316 | func (connection *Connection) SendCookie(key string, value string) { 317 | connection.SendHeader( 318 | "Set-Cookie", 319 | fmt.Sprintf("%s=%s; Path=/; HttpOnly", url.QueryEscape(key), url.QueryEscape(value)), 320 | ) 321 | } 322 | 323 | // SendContent sends binary safe content. 324 | // 325 | // If the status code or the header have not been sent already, a default status of "200 OK" will be sent immediately along with whatever headers you've previously defined. 326 | // 327 | // The status code and the header will become locked and further attempts to send either of them will fail with an error. 328 | // 329 | // All errors are sent to the server notifier. 330 | // 331 | // Compatible with web sockets. 332 | func (connection *Connection) SendContent(content []byte) { 333 | if !connection.locked { 334 | connection.writer.WriteHeader(connection.status) 335 | connection.locked = true 336 | } 337 | 338 | if connection.webSocket != nil { 339 | writeError := connection.webSocket.WriteMessage(websocket.TextMessage, content) 340 | if nil != writeError { 341 | connection.server.notifier.SendErrorAndTrace(writeError, 1) 342 | } 343 | return 344 | } 345 | 346 | if "" != connection.eventName { 347 | connection.SendEventContent(content) 348 | return 349 | } 350 | 351 | _, writeError := connection.writer.Write(content) 352 | if nil != writeError { 353 | connection.server.notifier.SendErrorAndTrace(writeError, 1) 354 | } 355 | } 356 | 357 | // SendMessage sends utf-8 safe content. 358 | // 359 | // If the status code or the header have not been sent already, a default status of "200 OK" will be sent immediately along with whatever headers you've previously defined. 360 | // 361 | // The status code and the header will become locked and further attempts to send either of them will fail with an error. 362 | // 363 | // All errors are sent to the server notifier. 364 | // 365 | // Compatible with web sockets. 366 | func (connection *Connection) SendMessage(message string) { 367 | connection.SendContent([]byte(message)) 368 | } 369 | 370 | // SendNotFound sends a message with status 404 Not Found. 371 | func (connection *Connection) SendNotFound(message string) { 372 | connection.SendStatus(http.StatusNotFound) 373 | connection.SendMessage(message) 374 | } 375 | 376 | // SendUnauthorized sends a message with status 401 Unauthorized. 377 | func (connection *Connection) SendUnauthorized(message string) { 378 | connection.SendStatus(http.StatusUnauthorized) 379 | connection.SendMessage(message) 380 | } 381 | 382 | // SendBadRequest sends a message with status 400 Bad Request. 383 | func (connection *Connection) SendBadRequest(message string) { 384 | connection.SendStatus(http.StatusBadRequest) 385 | connection.SendMessage(message) 386 | } 387 | 388 | // SendInternalServerError sends a message with status 500 Internal server Error 389 | // and also sends the error to the server notifier. 390 | func (connection *Connection) SendInternalServerError(err error) { 391 | connection.SendStatus(http.StatusBadRequest) 392 | connection.SendMessage(err.Error()) 393 | } 394 | 395 | // SendForbidden sends a message with status 403 Forbidden. 396 | func (connection *Connection) SendForbidden(message string) { 397 | connection.SendStatus(http.StatusForbidden) 398 | connection.SendMessage(message) 399 | } 400 | 401 | // SendTooManyRequests sends a message with status 403 Forbidden. 402 | func (connection *Connection) SendTooManyRequests(message string) { 403 | connection.SendStatus(http.StatusTooManyRequests) 404 | connection.SendMessage(message) 405 | } 406 | 407 | // SendJson sends json content. 408 | // 409 | // If the status code or the header have not been sent already, a default status of "200 OK" will be sent immediately along with whatever headers you've previously defined. 410 | // 411 | // The status code and the header will become locked and further attempts to send either of them will fail with an error. 412 | // 413 | // All errors are sent to the server notifier. 414 | // 415 | // Compatible with web sockets. 416 | func (connection *Connection) SendJson(payload any) { 417 | content, marshalError := json.Marshal(payload) 418 | if nil != marshalError { 419 | connection.server.notifier.SendErrorAndTrace(marshalError, 1) 420 | return 421 | } 422 | 423 | if nil == connection.webSocket { 424 | contentType := connection.header.Get("Content-Type") 425 | if "" == contentType { 426 | connection.header.Set("Content-Type", "application/json") 427 | } 428 | } 429 | 430 | connection.SendContent(content) 431 | } 432 | 433 | // SendEmbeddedFileOrElse sends the embedded file requested by the client, 434 | // or the closest index.html embedded file, or else falls back. 435 | func (connection *Connection) SendEmbeddedFileOrElse(efs embed.FS, orElse func()) { 436 | fileName := connection.server.publicRoot + connection.request.RequestURI 437 | fileName = strings.Split(fileName, "?")[0] 438 | fileName = strings.Split(fileName, "&")[0] 439 | 440 | if !fs.ExistsInEmbeddedFileSystem(efs, fileName) || fs.IsEmbeddedDirectory(efs, fileName) { 441 | orElse() 442 | return 443 | } 444 | 445 | reader, info, readerError := fs.ReaderFromEmbeddedFileName(efs, fileName) 446 | if nil != readerError { 447 | connection.server.notifier.SendErrorAndTrace(readerError, 1) 448 | return 449 | } 450 | 451 | if connection.webSocket != nil { 452 | content, readError := io.ReadAll(reader) 453 | if nil != readError { 454 | connection.server.notifier.SendErrorAndTrace(readError, 1) 455 | return 456 | } 457 | writeError := connection.webSocket.WriteMessage(websocket.TextMessage, content) 458 | if nil != writeError { 459 | connection.server.notifier.SendErrorAndTrace(writeError, 1) 460 | } 461 | return 462 | } 463 | 464 | if "" != connection.eventName { 465 | content, readError := io.ReadAll(reader) 466 | if nil != readError { 467 | connection.server.notifier.SendErrorAndTrace(readError, 1) 468 | } 469 | connection.SendEventContent(content) 470 | return 471 | } 472 | 473 | if "" == connection.header.Get("Content-Type") { 474 | connection.SendHeader("Content-Type", fs.Mime(fileName)) 475 | } 476 | 477 | if "" == connection.header.Get("Content-Length") { 478 | connection.SendHeader("Content-Length", fmt.Sprintf("%d", (*info).Size())) 479 | } 480 | http.ServeContent(connection.writer, connection.request, fileName, (*info).ModTime(), reader) 481 | } 482 | 483 | // SendFileOrElse sends the file requested by the client, or else falls back. 484 | func (connection *Connection) SendFileOrElse(orElse func()) { 485 | fileName := filepath.Join(connection.server.publicRoot, connection.request.RequestURI) 486 | 487 | if !fs.FileExists(fileName) || fs.IsDirectory(fileName) { 488 | connection.SendEmbeddedFileOrElse(connection.server.dist, orElse) 489 | return 490 | } 491 | 492 | reader, info, readerError := fs.ReaderFromFileName(fileName) 493 | if nil != readerError { 494 | connection.server.notifier.SendErrorAndTrace(readerError, 1) 495 | return 496 | } 497 | 498 | if connection.webSocket != nil { 499 | content, readError := io.ReadAll(reader) 500 | if nil != readError { 501 | connection.server.notifier.SendErrorAndTrace(readError, 1) 502 | return 503 | } 504 | writeError := connection.webSocket.WriteMessage(websocket.TextMessage, content) 505 | if nil != writeError { 506 | connection.server.notifier.SendErrorAndTrace(writeError, 1) 507 | } 508 | return 509 | } 510 | 511 | if "" != connection.eventName { 512 | content, readError := io.ReadAll(reader) 513 | if nil != readError { 514 | connection.server.notifier.SendErrorAndTrace(readError, 1) 515 | return 516 | } 517 | connection.SendEventContent(content) 518 | } 519 | 520 | if "" == connection.header.Get("Content-Type") { 521 | connection.SendHeader("Content-Type", fs.Mime(fileName)) 522 | } 523 | 524 | if "" == connection.header.Get("Content-Length") { 525 | connection.SendHeader("Content-Length", fmt.Sprintf("%d", (*info).Size())) 526 | } 527 | http.ServeContent(connection.writer, connection.request, fileName, (*info).ModTime(), reader) 528 | } 529 | 530 | // SendSseUpgrade upgrades the http connection to server sent events 531 | // and returns a function that sets the name of the current event. 532 | // 533 | // The default event is "message". 534 | func SendSseUpgrade(connection *Connection) func(eventName string) { 535 | connection.SendHeader("Access-Control-Allow-Origin", "*") 536 | connection.SendHeader("Access-Control-Expose-Headers", "Content-Type") 537 | connection.SendHeader("Content-Type", "text/event-stream") 538 | connection.SendHeader("MemoryCache-Control", "no-cache") 539 | connection.SendHeader("Connection", "keep-alive") 540 | connection.eventName = "message" 541 | return func(eventName string) { 542 | if "" == eventName { 543 | connection.server.notifier.SendErrorAndTrace( 544 | fmt.Errorf("renaming a server sent event (`%s`) to an empty string is not allowed", connection.eventName), 545 | 1, 546 | ) 547 | } 548 | connection.eventName = eventName 549 | } 550 | } 551 | 552 | // SendWsUpgrade upgrades to web sockets. 553 | func (connection *Connection) SendWsUpgrade() { 554 | connection.SendConfiguredWsUpgrade(websocket.Upgrader{ 555 | ReadBufferSize: 10 * KB, 556 | WriteBufferSize: 10 * KB, 557 | }) 558 | } 559 | 560 | // SendConfiguredWsUpgrade upgrades to web sockets. 561 | func (connection *Connection) SendConfiguredWsUpgrade(upgrader websocket.Upgrader) { 562 | conn, upgradeError := upgrader.Upgrade(connection.writer, connection.request, nil) 563 | if nil != upgradeError { 564 | connection.server.notifier.SendErrorAndTrace(upgradeError, 1) 565 | return 566 | } 567 | defer func(conn *websocket.Conn) { 568 | closeError := conn.Close() 569 | if nil != closeError { 570 | connection.server.notifier.SendErrorAndTrace(closeError, 1) 571 | } 572 | }(conn) 573 | connection.webSocket = conn 574 | connection.locked = true 575 | return 576 | } 577 | 578 | // SendView sends a view. 579 | func (connection *Connection) SendView(view View) { 580 | if "" != connection.header.Get("Location") { 581 | return 582 | } 583 | 584 | if nil == view.Data { 585 | view.Data = map[string]string{} 586 | } 587 | 588 | if connection.VerifyAccept("application/json") { 589 | connection.SendJson(view) 590 | return 591 | } 592 | 593 | if view.server == nil { 594 | view.server = connection.server.viewServer 595 | } 596 | 597 | if view.index == nil { 598 | view.index = connection.server.viewIndex 599 | } 600 | 601 | content, compileError := view.Render() 602 | if nil != compileError { 603 | connection.server.notifier.SendErrorAndTrace(compileError, 1) 604 | return 605 | } 606 | 607 | if "" == connection.header.Get("Content-Type") { 608 | connection.SendHeader("Content-Type", "text/html") 609 | } 610 | 611 | connection.SendMessage(content) 612 | } 613 | -------------------------------------------------------------------------------- /frz/libglobals.go: -------------------------------------------------------------------------------- 1 | package frz 2 | 3 | import "path/filepath" 4 | 5 | const KB = 1024 6 | const MB = 1024 * KB 7 | const GB = 1024 * MB 8 | const TB = 1024 * GB 9 | const PB = 1024 * TB 10 | const EB = 1024 * PB 11 | 12 | const SessionKey = "session.json" 13 | 14 | var ViewsLocation = filepath.Join("lib", "components", "views") 15 | -------------------------------------------------------------------------------- /frz/libglobals_test.go: -------------------------------------------------------------------------------- 1 | package frz 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestKB(test *testing.T) { 8 | if 1024 != KB { 9 | test.Fatalf("KB constant is not %d", 1024) 10 | } 11 | } 12 | 13 | func TestMB(test *testing.T) { 14 | expected := 1024 * 1024 15 | if expected != MB { 16 | test.Fatalf("MB constant is not %d", expected) 17 | } 18 | } 19 | 20 | func TestGB(test *testing.T) { 21 | expected := 1024 * 1024 * 1024 22 | if expected != GB { 23 | test.Fatalf("GB constant is not %d", expected) 24 | } 25 | } 26 | 27 | func TestTB(test *testing.T) { 28 | expected := 1024 * 1024 * 1024 * 1024 29 | if expected != TB { 30 | test.Fatalf("TB constant is not %d", expected) 31 | } 32 | } 33 | 34 | func TestPB(test *testing.T) { 35 | expected := 1024 * 1024 * 1024 * 1024 * 1024 36 | if expected != PB { 37 | test.Fatalf("PB constant is not %d", expected) 38 | } 39 | } 40 | 41 | func TestEB(test *testing.T) { 42 | expected := 1024 * 1024 * 1024 * 1024 * 1024 * 1024 43 | if expected != EB { 44 | test.Fatalf("EB constant is not %d", expected) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /frz/libhttp.go: -------------------------------------------------------------------------------- 1 | package frz 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | ) 9 | 10 | var client http.Client 11 | 12 | // HttpGet sends an http request using the GET verb. 13 | func HttpGet(path string, header map[string]string) (res string, err error) { 14 | if nil == header { 15 | header = map[string]string{} 16 | } 17 | 18 | request, requestError := http.NewRequest("GET", path, nil) 19 | if requestError != nil { 20 | return "", requestError 21 | } 22 | 23 | for key, value := range header { 24 | request.Header.Set(key, value) 25 | } 26 | 27 | response, doError := client.Do(request) 28 | if doError != nil { 29 | return "", doError 30 | } 31 | defer func(Body io.ReadCloser) { err = Body.Close() }(response.Body) 32 | 33 | bodyBytes, readError := io.ReadAll(response.Body) 34 | if readError != nil { 35 | return "", readError 36 | } 37 | result := string(bodyBytes) 38 | 39 | if response.StatusCode >= 300 { 40 | return result, fmt.Errorf("server responded with status code '%d'", response.StatusCode) 41 | } 42 | 43 | return result, nil 44 | } 45 | 46 | // HttpDelete sends an http request using the DELETE verb. 47 | func HttpDelete(path string, header map[string]string) (err error) { 48 | if nil == header { 49 | header = map[string]string{} 50 | } 51 | 52 | request, requestError := http.NewRequest("DELETE", path, nil) 53 | if requestError != nil { 54 | return requestError 55 | } 56 | 57 | for key, value := range header { 58 | request.Header.Set(key, value) 59 | } 60 | 61 | response, doError := client.Do(request) 62 | if doError != nil { 63 | return doError 64 | } 65 | defer func(Body io.ReadCloser) { err = Body.Close() }(response.Body) 66 | 67 | if 200 != response.StatusCode { 68 | return fmt.Errorf("server responded with status code '%d'", response.StatusCode) 69 | } 70 | 71 | return nil 72 | } 73 | 74 | // HttpPost sends an http request using the POST verb. 75 | func HttpPost(path string, contents string, header map[string]string) (res string, err error) { 76 | if nil == header { 77 | header = map[string]string{} 78 | } 79 | 80 | request, requestError := http.NewRequest("POST", path, bytes.NewBuffer([]byte(contents))) 81 | if requestError != nil { 82 | return "", requestError 83 | } 84 | 85 | for key, value := range header { 86 | request.Header.Set(key, value) 87 | } 88 | 89 | response, doError := client.Do(request) 90 | if doError != nil { 91 | return "", doError 92 | } 93 | defer func(Body io.ReadCloser) { err = Body.Close() }(response.Body) 94 | 95 | if 200 != response.StatusCode { 96 | return "", fmt.Errorf("server responded with status code '%d'", response.StatusCode) 97 | } 98 | 99 | bodyBytes, readError := io.ReadAll(response.Body) 100 | if readError != nil { 101 | return "", readError 102 | } 103 | 104 | result := string(bodyBytes) 105 | 106 | return result, nil 107 | } 108 | 109 | // HttpPut sends an http request using the PUT verb. 110 | func HttpPut(path string, header map[string]string, contents string) (res string, err error) { 111 | if nil == header { 112 | header = map[string]string{} 113 | } 114 | 115 | request, requestError := http.NewRequest("PUT", path, bytes.NewBuffer([]byte(contents))) 116 | if requestError != nil { 117 | return "", requestError 118 | } 119 | 120 | for key, value := range header { 121 | request.Header.Set(key, value) 122 | } 123 | 124 | response, doError := client.Do(request) 125 | if doError != nil { 126 | return "", doError 127 | } 128 | defer func(Body io.ReadCloser) { err = Body.Close() }(response.Body) 129 | 130 | if 200 != response.StatusCode { 131 | return "", fmt.Errorf("server responded with status code '%d'", response.StatusCode) 132 | } 133 | 134 | bodyBytes, readError := io.ReadAll(response.Body) 135 | if readError != nil { 136 | return "", readError 137 | } 138 | 139 | result := string(bodyBytes) 140 | 141 | return result, nil 142 | } 143 | -------------------------------------------------------------------------------- /frz/libjs.go: -------------------------------------------------------------------------------- 1 | package frz 2 | 3 | import ( 4 | "fmt" 5 | "github.com/evanw/esbuild/pkg/api" 6 | "rogchap.com/v8go" 7 | ) 8 | 9 | var javaScriptCache = map[string]*v8go.CompilerCachedData{} 10 | 11 | // JavaScriptInvalidate invalidates compiler cached data. 12 | // 13 | // Whenever you invoke JavaScriptRun, cache data is extracted from the script, 14 | // which is then used to speed up execution the next time a given script is executed. 15 | func JavaScriptInvalidate(id string) { 16 | delete(javaScriptCache, id) 17 | } 18 | 19 | // JavaScriptRun runs a javascript module. 20 | // 21 | // It returns the last expression of the script and a destroyer function. 22 | // 23 | // The destroyer function is never nil. 24 | // 25 | // You should always call the destroyer function as soon as possible to limit memory usage. 26 | // 27 | // Each global function will be injected into the context of the module automatically so that you can invoke them from the script. 28 | func JavaScriptRun(id string, source []byte, functions map[string]v8go.FunctionCallback) ( 29 | result *v8go.Value, 30 | destroy func(), 31 | scriptError error, 32 | ) { 33 | isolate := v8go.NewIsolate() 34 | globals := v8go.NewObjectTemplate(isolate) 35 | 36 | for key, callback := range functions { 37 | setError := globals.Set(key, v8go.NewFunctionTemplate(isolate, callback)) 38 | if setError != nil { 39 | return nil, nil, setError 40 | } 41 | } 42 | 43 | context := v8go.NewContext(isolate, globals) 44 | 45 | var script *v8go.UnboundScript 46 | 47 | codeCache, hasCodeCash := javaScriptCache[id] 48 | if hasCodeCash { 49 | compiledScript, compilationError := isolate.CompileUnboundScript(string(source), id, v8go.CompileOptions{CachedData: codeCache}) 50 | if compilationError != nil { 51 | return nil, nil, compilationError 52 | } 53 | script = compiledScript 54 | } else { 55 | compiledScript, compilationError := isolate.CompileUnboundScript(string(source), id, v8go.CompileOptions{}) 56 | if compilationError != nil { 57 | return nil, nil, compilationError 58 | } 59 | javaScriptCache[id] = compiledScript.CreateCodeCache() 60 | script = compiledScript 61 | } 62 | 63 | scriptResult, scriptError := script.Run(context) 64 | if scriptError != nil { 65 | return nil, func() {}, scriptError 66 | } 67 | 68 | return scriptResult, func() { 69 | context.Close() 70 | isolate.Dispose() 71 | }, nil 72 | } 73 | 74 | // JavaScriptBundle bundles JavaScript source code into a specific format. 75 | func JavaScriptBundle(rootDirectory string, format api.Format, source []byte) (bundle []byte, bundleError error) { 76 | result := api.Build(api.BuildOptions{ 77 | Bundle: true, 78 | Format: format, 79 | Write: false, 80 | Stdin: &api.StdinOptions{ 81 | Contents: string(source), 82 | ResolveDir: rootDirectory, 83 | }, 84 | }) 85 | 86 | for _, err := range result.Errors { 87 | return make([]byte, 0), fmt.Errorf("%s in %s:%d:%d", err.Text, err.Location.File, err.Location.Line, err.Location.Column) 88 | } 89 | 90 | return result.OutputFiles[0].Contents, nil 91 | } 92 | -------------------------------------------------------------------------------- /frz/libjs_test.go: -------------------------------------------------------------------------------- 1 | package frz 2 | 3 | import ( 4 | "github.com/evanw/esbuild/pkg/api" 5 | "rogchap.com/v8go" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestJavaScriptRun(test *testing.T) { 11 | // Simple. 12 | script := "1+1" 13 | actual, destroy, javaScriptError := JavaScriptRun("test", []byte(script), map[string]v8go.FunctionCallback{}) 14 | defer destroy() 15 | if javaScriptError != nil { 16 | test.Fatal(javaScriptError) 17 | } 18 | 19 | if actual.Int32() != 2 { 20 | test.Fatalf("script was expected to return 2, received '%d' instead", actual.Int32()) 21 | } 22 | 23 | // Complex and with JsDoc. 24 | script = ` 25 | /** 26 | * @param {boolean} payload 27 | * @returns 28 | */ 29 | function uuid(short = false) { 30 | let dt = new Date().getTime() 31 | const BLUEPRINT = short ? 'xyxxyxyx' : 'xxxxxxxx-xxxx-yxxx-yxxx-xxxxxxxxxxxx' 32 | const RESULT = BLUEPRINT.replace(/[xy]/g, function run(c) { 33 | const r = (dt + Math.random() * 16) % 16 | 0 34 | dt = Math.floor(dt / 16) 35 | return (c == 'x' ? r : (r & 0x3) | 0x8).toString(16) 36 | }) 37 | return RESULT 38 | } 39 | 40 | const result = { 41 | long: uuid(), 42 | short: uuid(true), 43 | } 44 | 45 | result 46 | ` 47 | actual, destroy, javaScriptError = JavaScriptRun("test", []byte(script), map[string]v8go.FunctionCallback{}) 48 | defer destroy() 49 | if javaScriptError != nil { 50 | test.Fatal(javaScriptError) 51 | } 52 | 53 | obj := actual.Object() 54 | 55 | if !obj.Has("long") { 56 | test.Fatal("actual value was expected to have a 'long' key") 57 | } 58 | 59 | if !obj.Has("short") { 60 | test.Fatal("actual value was expected to have a 'short' key") 61 | } 62 | 63 | long, longError := obj.Get("long") 64 | if longError != nil { 65 | test.Fatal(longError) 66 | } 67 | short, shortError := obj.Get("short") 68 | if shortError != nil { 69 | test.Fatal(shortError) 70 | } 71 | 72 | longPieces := strings.Split(long.String(), "-") 73 | if len(longPieces) != 5 { 74 | test.Fatalf("long string was expected to be composed of 5 part separated by 4 -, received '%s' instead", long.String()) 75 | } 76 | 77 | shortPieces := strings.Split(short.String(), "-") 78 | if len(shortPieces) != 1 { 79 | test.Fatalf("string was expected to be composed of 1 part, received '%s' instead", short.String()) 80 | } 81 | 82 | } 83 | 84 | func TestJavaScriptBundle(test *testing.T) { 85 | script := ` 86 | import { writable } from 'svelte/store' 87 | const test = writable("hello") 88 | test.subscribe(function updated(value){ 89 | signal(value) 90 | }) 91 | ` 92 | 93 | cjs, bundleError := JavaScriptBundle(".", api.FormatCommonJS, []byte(script)) 94 | if bundleError != nil { 95 | test.Fatal(bundleError) 96 | } 97 | actual := "" 98 | expected := "hello" 99 | _, destroy, javaScriptError := JavaScriptRun("test", cjs, map[string]v8go.FunctionCallback{ 100 | "signal": func(info *v8go.FunctionCallbackInfo) *v8go.Value { 101 | args := info.Args() 102 | if len(args) > 0 { 103 | actual = args[0].String() 104 | } 105 | return nil 106 | }, 107 | }) 108 | defer destroy() 109 | if javaScriptError != nil { 110 | test.Fatal(javaScriptError) 111 | } 112 | 113 | if actual != expected { 114 | test.Fatalf("script was expected to update the actual value to '%s', received '%s' instead.", expected, actual) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /frz/libnotifier.go: -------------------------------------------------------------------------------- 1 | package frz 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "runtime" 8 | ) 9 | 10 | type Notifier struct { 11 | messageLogger *log.Logger 12 | errorLogger *log.Logger 13 | } 14 | 15 | // NewNotifier creates a notifier. 16 | func NewNotifier() *Notifier { 17 | return &Notifier{ 18 | messageLogger: log.New(os.Stdout, "[message]: ", log.Ldate|log.Ltime), 19 | errorLogger: log.New(os.Stderr, "[error]: ", log.Ldate|log.Ltime), 20 | } 21 | } 22 | 23 | func (notifier *Notifier) WithMessageLogger(logger *log.Logger) *Notifier { 24 | notifier.messageLogger = logger 25 | return notifier 26 | } 27 | 28 | func (notifier *Notifier) WithErrorLogger(logger *log.Logger) *Notifier { 29 | notifier.messageLogger = logger 30 | return notifier 31 | } 32 | 33 | // SendMessage sends a message to the notifier. 34 | func (notifier *Notifier) SendMessage(message string) *Notifier { 35 | notifier.messageLogger.Println(message) 36 | return notifier 37 | } 38 | 39 | // SendMessageAndTrace sends an error to the notifier and traces the runtime caller. 40 | func (notifier *Notifier) SendMessageAndTrace(message string, skip int) *Notifier { 41 | _, file, line, ok := runtime.Caller(skip + 1) 42 | if ok { 43 | notifier.messageLogger.Println(fmt.Sprintf("%s:%d %s", file, line, message)) 44 | } else { 45 | notifier.messageLogger.Println(message) 46 | } 47 | return notifier 48 | } 49 | 50 | // SendError sends an error to the notifier. 51 | func (notifier *Notifier) SendError(err error) *Notifier { 52 | notifier.errorLogger.Println(err.Error()) 53 | return notifier 54 | } 55 | 56 | // SendErrorAndTrace sends an error to the notifier and traces the runtime caller. 57 | func (notifier *Notifier) SendErrorAndTrace(err error, skip int) *Notifier { 58 | _, file, line, ok := runtime.Caller(skip + 1) 59 | if ok { 60 | notifier.errorLogger.Println(fmt.Sprintf("%s:%d %s", file, line, err.Error())) 61 | } else { 62 | notifier.errorLogger.Println(err.Error()) 63 | } 64 | return notifier 65 | } 66 | -------------------------------------------------------------------------------- /frz/libnumbers.go: -------------------------------------------------------------------------------- 1 | package frz 2 | 3 | var nextNumbers = map[int]int{} 4 | 5 | // NextNumber gets the next number in line starting from base. 6 | // 7 | // Bases are stateful, meaning regardless of when and where you call NextNumber 8 | // it will keep track of the previous number generated for a given base, 9 | // and give you the next one. 10 | func NextNumber(base int) int { 11 | number, ok := nextNumbers[base] 12 | if !ok { 13 | nextNumbers[base] = 0 14 | } 15 | 16 | nextNumbers[base]++ 17 | 18 | return base + number + 1 19 | } 20 | -------------------------------------------------------------------------------- /frz/libnumbers_test.go: -------------------------------------------------------------------------------- 1 | package frz 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestNextNumber(test *testing.T) { 9 | number := NextNumber(7) 10 | if 8 != number { 11 | test.Fatal(fmt.Sprintf("Expected next number to be 8, received %d instead", number)) 12 | } 13 | 14 | number = NextNumber(7) 15 | if 9 != number { 16 | test.Fatal(fmt.Sprintf("Expected next number to be 9, received %d instead", number)) 17 | } 18 | 19 | number = NextNumber(7) 20 | if 10 != number { 21 | test.Fatal(fmt.Sprintf("Expected next number to be 10, received %d instead", number)) 22 | } 23 | 24 | number = NextNumber(8) 25 | if 9 != number { 26 | test.Fatal(fmt.Sprintf("Expected next number to be 9, received %d instead", number)) 27 | } 28 | 29 | number = NextNumber(8) 30 | if 10 != number { 31 | test.Fatal(fmt.Sprintf("Expected next number to be 10, received %d instead", number)) 32 | } 33 | 34 | number = NextNumber(8) 35 | if 11 != number { 36 | test.Fatal(fmt.Sprintf("Expected next number to be 11, received %d instead", number)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frz/libroad.go: -------------------------------------------------------------------------------- 1 | package frz 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | ) 7 | 8 | type Road struct { 9 | lanes map[string]*sync.Mutex 10 | } 11 | 12 | func NewRoad() *Road { 13 | return &Road{lanes: map[string]*sync.Mutex{}} 14 | } 15 | 16 | // WithLane adds a new lane to the road. 17 | func (road *Road) WithLane(key ...string) *sync.Mutex { 18 | path := strings.Join(key, ":") 19 | lane, laneExists := road.lanes[path] 20 | if laneExists { 21 | return lane 22 | } 23 | 24 | var newLane sync.Mutex 25 | road.lanes[path] = &newLane 26 | return &newLane 27 | } 28 | 29 | // WithoutLane unlocks the lane and removes it from the road. 30 | func (road *Road) WithoutLane(key ...string) *Road { 31 | path := strings.Join(key, ":") 32 | delete(road.lanes, path) 33 | return road 34 | } 35 | -------------------------------------------------------------------------------- /frz/libserver.go: -------------------------------------------------------------------------------- 1 | package frz 2 | 3 | import ( 4 | "context" 5 | "embed" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "net" 10 | "net/http" 11 | "os" 12 | "slices" 13 | "strings" 14 | "sync" 15 | "time" 16 | 17 | "github.com/gorilla/websocket" 18 | ) 19 | 20 | type Server struct { 21 | address string 22 | secureAddress string 23 | formMaxMemory int64 24 | server *http.Server 25 | mux *http.ServeMux 26 | connections map[string]*net.Conn 27 | readTimeout time.Duration 28 | writeTimeout time.Duration 29 | headerMaxMemory int 30 | certificate string 31 | key string 32 | notifier *Notifier 33 | dist embed.FS 34 | publicRoot string 35 | viewServer []byte 36 | viewIndex []byte 37 | upgrader *websocket.Upgrader 38 | guards []Guard 39 | } 40 | 41 | func NewServer() *Server { 42 | notifier := NewNotifier() 43 | upgrader := &websocket.Upgrader{ 44 | ReadBufferSize: 1024, 45 | WriteBufferSize: 1024, 46 | } 47 | return &Server{ 48 | address: "127.0.0.1:8080", 49 | secureAddress: "127.0.0.1:8383", 50 | formMaxMemory: 4096, 51 | server: nil, 52 | mux: http.NewServeMux(), 53 | connections: map[string]*net.Conn{}, 54 | readTimeout: 10 * time.Second, 55 | writeTimeout: 10 * time.Second, 56 | headerMaxMemory: 3 * MB, 57 | certificate: "", 58 | key: "", 59 | notifier: notifier, 60 | upgrader: upgrader, 61 | } 62 | } 63 | 64 | // WithWsMaxRedMemory sets the maximum buffer size for each incoming web socket message. 65 | // This will not limit the size of said messages. 66 | func (server *Server) WithWsMaxRedMemory(size int) *Server { 67 | server.upgrader.ReadBufferSize = size 68 | return server 69 | } 70 | 71 | // WithWsMaxWriteMemory sets the maximum buffer size for each outgoing web socket message. 72 | // This will not limit the size of said messages. 73 | func (server *Server) WithWsMaxWriteMemory(size int) *Server { 74 | server.upgrader.WriteBufferSize = size 75 | return server 76 | } 77 | 78 | // WithFormMaxMemory sets the maximum memory for multipart forms before they fall back to disk. 79 | func (server *Server) WithFormMaxMemory(size int64) *Server { 80 | server.formMaxMemory = size 81 | return server 82 | } 83 | 84 | // WithAddress sets the address. 85 | func (server *Server) WithAddress(address string) *Server { 86 | server.address = address 87 | return server 88 | } 89 | 90 | // WithSecureAddress sets the secure address. 91 | func (server *Server) WithSecureAddress(secureAddress string) *Server { 92 | server.secureAddress = secureAddress 93 | return server 94 | } 95 | 96 | // WithReadTimeout sets the read timeout. 97 | func (server *Server) WithReadTimeout(timeout time.Duration) *Server { 98 | server.readTimeout = timeout 99 | return server 100 | } 101 | 102 | // WithWriteTimeout sets the write timeout. 103 | func (server *Server) WithWriteTimeout(timeout time.Duration) *Server { 104 | server.writeTimeout = timeout 105 | return server 106 | } 107 | 108 | // WithHeaderMaxMemory sets the maximum allowed bytes in the header of the request. 109 | func (server *Server) WithHeaderMaxMemory(maxHeaderBytes int) *Server { 110 | server.headerMaxMemory = maxHeaderBytes 111 | return server 112 | } 113 | 114 | // WithCertificate sets certificate and key. 115 | func (server *Server) WithCertificate(certificate string, key string) *Server { 116 | server.certificate = certificate 117 | server.key = key 118 | return server 119 | } 120 | 121 | // WithNotifier sets the notifier. 122 | func (server *Server) WithNotifier(notifier *Notifier) *Server { 123 | server.notifier = notifier 124 | return server 125 | } 126 | 127 | // WithDist sets the dist directory. 128 | func (server *Server) WithDist(dist embed.FS) *Server { 129 | server.dist = dist 130 | return server 131 | } 132 | 133 | func (server *Server) WithPublicRoot(fileName string) *Server { 134 | if "1" == os.Getenv("DEV") { 135 | server.publicRoot = fileName 136 | } else { 137 | server.publicRoot = strings.ReplaceAll(fileName, "\\", "/") 138 | } 139 | return server 140 | } 141 | 142 | func (server *Server) WithViewServer(fileName string) *Server { 143 | if "1" == os.Getenv("DEV") { 144 | data, readError := os.ReadFile(fileName) 145 | if readError != nil { 146 | server.notifier.SendErrorAndTrace(readError, 1) 147 | return server 148 | } 149 | server.viewServer = data 150 | } else { 151 | data, readError := server.dist.ReadFile(strings.ReplaceAll(fileName, "\\", "/")) 152 | if readError != nil { 153 | server.notifier.SendErrorAndTrace(readError, 1) 154 | return server 155 | } 156 | server.viewServer = data 157 | } 158 | 159 | return server 160 | } 161 | 162 | func (server *Server) WithViewIndex(fileName string) *Server { 163 | if "1" == os.Getenv("DEV") { 164 | data, readError := os.ReadFile(fileName) 165 | if readError != nil { 166 | server.notifier.SendErrorAndTrace(readError, 1) 167 | return server 168 | } 169 | server.viewIndex = data 170 | } else { 171 | data, readError := server.dist.ReadFile(strings.ReplaceAll(fileName, "\\", "/")) 172 | if readError != nil { 173 | server.notifier.SendErrorAndTrace(readError, 1) 174 | return server 175 | } 176 | server.viewIndex = data 177 | } 178 | 179 | return server 180 | } 181 | 182 | // Start starts the server. 183 | // 184 | // If the server fails to start, ServerStart crashes the program. 185 | func (server *Server) Start() { 186 | server.server = &http.Server{ 187 | Handler: server.mux, 188 | ReadTimeout: server.readTimeout, 189 | WriteTimeout: server.writeTimeout, 190 | MaxHeaderBytes: server.headerMaxMemory, 191 | ErrorLog: server.notifier.errorLogger, 192 | } 193 | 194 | var group sync.WaitGroup 195 | 196 | group.Add(2) 197 | 198 | go func() { 199 | server.notifier.SendMessage(fmt.Sprintf("listening for requests at http://%s", server.address)) 200 | serverError := http.ListenAndServe(server.address, server.mux) 201 | if nil != serverError { 202 | if errors.Is(serverError, http.ErrServerClosed) { 203 | server.notifier.SendMessage("shutting down server") 204 | return 205 | } 206 | log.Fatal(serverError) 207 | } 208 | }() 209 | 210 | go func() { 211 | if "" != server.certificate && "" != server.key { 212 | server.notifier.SendMessage(fmt.Sprintf("listening for requests at https://%s", server.secureAddress)) 213 | serverError := http.ListenAndServeTLS(server.secureAddress, server.certificate, server.key, server.mux) 214 | if nil != serverError { 215 | if errors.Is(serverError, http.ErrServerClosed) { 216 | server.notifier.SendMessage("shutting down server") 217 | return 218 | } 219 | log.Fatal(serverError) 220 | } 221 | } 222 | }() 223 | 224 | group.Wait() 225 | } 226 | 227 | // Stop attempts to stop the server. 228 | // 229 | // If the shutdown attempt fails, ServerStop crashes the program. 230 | func (server *Server) Stop() { 231 | shutdownError := server.server.Shutdown(context.Background()) 232 | if nil != shutdownError { 233 | log.Fatal(shutdownError) 234 | } 235 | } 236 | 237 | type Guard struct { 238 | Name string 239 | Handler func(c *Connection, allow func()) 240 | Tags []string 241 | } 242 | type Route struct { 243 | Pattern string 244 | Handler func(c *Connection) 245 | Tags []string 246 | } 247 | 248 | // AddGuard adds a guard. 249 | func (server *Server) AddGuard(guard Guard) *Server { 250 | server.guards = append(server.guards, guard) 251 | return server 252 | } 253 | 254 | // AddRoute adds a route. 255 | func (server *Server) AddRoute(route Route) *Server { 256 | server.mux.HandleFunc(route.Pattern, func(writer http.ResponseWriter, request *http.Request) { 257 | connection := &Connection{ 258 | server: server, 259 | request: request, 260 | writer: writer, 261 | locked: false, 262 | status: 200, 263 | header: writer.Header(), 264 | eventName: "", 265 | eventId: 1, 266 | } 267 | 268 | for _, tag := range route.Tags { 269 | for _, guard := range server.guards { 270 | if !slices.Contains(guard.Tags, tag) { 271 | continue 272 | } 273 | allowed := false 274 | guard.Handler(connection, func() { allowed = true }) 275 | if !allowed { 276 | server.notifier.SendMessage( 277 | fmt.Sprintf("route `%s` tagged with `%s` denied the request because guard `%s` did not pass", route.Pattern, tag, guard.Name), 278 | ) 279 | return 280 | } 281 | } 282 | } 283 | 284 | route.Handler(connection) 285 | }) 286 | return server 287 | } 288 | -------------------------------------------------------------------------------- /frz/libserver_test.go: -------------------------------------------------------------------------------- 1 | package frz 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestNewServer(test *testing.T) { 11 | NewServer() 12 | } 13 | 14 | func TestServerWithAddress(test *testing.T) { 15 | server := NewServer() 16 | expected := "127.0.0.1:8080" 17 | server.WithAddress("127.0.0.1:8080") 18 | actual := server.address 19 | if actual != expected { 20 | test.Fatalf("server was expected to have host name '%s', received '%s' instead", expected, actual) 21 | } 22 | } 23 | 24 | func TestServerWithReadTimeout(test *testing.T) { 25 | server := NewServer() 26 | expected := 10 * time.Second 27 | server.WithReadTimeout(expected) 28 | actual := server.readTimeout 29 | if actual != expected { 30 | test.Fatalf("server was expected to have read timeout '%d', received '%d' instead", expected, actual) 31 | } 32 | 33 | } 34 | 35 | func TestServerWithWriteTimeout(test *testing.T) { 36 | server := NewServer() 37 | expected := 10 * time.Second 38 | server.WithWriteTimeout(expected) 39 | actual := server.writeTimeout 40 | if actual != expected { 41 | test.Fatalf("server was expected to have write timeout '%d', received '%d' instead", expected, actual) 42 | } 43 | } 44 | 45 | func TestServerWithMaxHeaderBytes(test *testing.T) { 46 | server := NewServer() 47 | expected := 1 * MB 48 | server.WithHeaderMaxMemory(expected) 49 | actual := server.headerMaxMemory 50 | if actual != expected { 51 | test.Fatalf("server was expected to have max header bytes '%d', received '%d' instead", expected, actual) 52 | } 53 | } 54 | 55 | func TestServerWithCertificateAndKey(test *testing.T) { 56 | server := NewServer() 57 | expectedCertificate := "cert.pem" 58 | expectedCertificateKey := "key.pem" 59 | server.WithCertificate(expectedCertificate, expectedCertificateKey) 60 | actualCertificate := server.certificate 61 | if actualCertificate != expectedCertificate { 62 | test.Fatalf("server was expected to have certificate '%s', received '%s' instead", expectedCertificate, actualCertificate) 63 | } 64 | actualCertificateKey := server.key 65 | if actualCertificateKey != expectedCertificateKey { 66 | test.Fatalf("server was expected to have certificate key '%s', received '%s' instead", expectedCertificateKey, actualCertificateKey) 67 | } 68 | } 69 | 70 | func TestServerWithApi(test *testing.T) { 71 | expected := "hello" 72 | port := NextNumber(8080) 73 | server := NewServer(). 74 | WithDist(dist). 75 | WithAddress(fmt.Sprintf("127.0.0.1:%d", port)). 76 | AddRoute(Route{Pattern: "GET /", Handler: func(c *Connection) { 77 | c.SendMessage(expected) 78 | }}) 79 | 80 | go server.Start() 81 | defer server.Stop() 82 | 83 | time.Sleep(1 * time.Second) 84 | 85 | actual, getError := HttpGet(fmt.Sprintf("http://127.0.0.1:%d/", port), nil) 86 | if getError != nil { 87 | test.Fatal(getError) 88 | } 89 | 90 | if actual != expected { 91 | test.Fatalf("server was expected to respond with '%s', received '%s' instead", expected, actual) 92 | } 93 | } 94 | 95 | func TestSendStatus(test *testing.T) { 96 | expected := 201 97 | port := NextNumber(8080) 98 | server := NewServer(). 99 | WithDist(dist). 100 | WithAddress(fmt.Sprintf("127.0.0.1:%d", port)). 101 | AddRoute(Route{Pattern: "GET /", Handler: func(c *Connection) { 102 | c.SendStatus(expected) 103 | c.SendMessage("ok") 104 | }}) 105 | 106 | go server.Start() 107 | defer server.Stop() 108 | 109 | time.Sleep(1 * time.Second) 110 | 111 | response, getError := http.Get(fmt.Sprintf("http://127.0.0.1:%d/", port)) 112 | if getError != nil { 113 | test.Fatal(getError) 114 | } 115 | defer response.Body.Close() 116 | 117 | actual := response.StatusCode 118 | 119 | if actual != expected { 120 | test.Fatalf("server was expected to respond with status code '%d', received '%d' intead", expected, actual) 121 | } 122 | } 123 | 124 | func TestSendHeader(test *testing.T) { 125 | expected := "application/json" 126 | port := NextNumber(8080) 127 | server := NewServer(). 128 | WithDist(dist). 129 | WithAddress(fmt.Sprintf("127.0.0.1:%d", port)). 130 | AddRoute(Route{Pattern: "GET /", Handler: func(c *Connection) { 131 | c.SendHeader("Content-Type", expected) 132 | c.SendMessage("{}") 133 | }}) 134 | 135 | go server.Start() 136 | defer server.Stop() 137 | 138 | time.Sleep(1 * time.Second) 139 | 140 | response, getError := http.Get(fmt.Sprintf("http://127.0.0.1:%d/", port)) 141 | if getError != nil { 142 | test.Fatal(getError) 143 | } 144 | defer response.Body.Close() 145 | 146 | actual := response.Header.Get("Content-Type") 147 | 148 | if actual != expected { 149 | test.Fatalf("server was expected to respond with header content type '%s', received '%s' intead", expected, actual) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /frz/libsession.go: -------------------------------------------------------------------------------- 1 | package frz 2 | 3 | import ( 4 | "encoding/json" 5 | uuid "github.com/nu7hatch/gouuid" 6 | ) 7 | 8 | type SessionOperator interface { 9 | Id() string 10 | Exists() bool 11 | Load(state any) 12 | Save(state any) 13 | Destroy() 14 | WithArchive(archive Archive) SessionOperator 15 | } 16 | 17 | type SessionHandler[T any] = func(state *T) 18 | 19 | type ConnectedSessionOperator struct { 20 | archive Archive 21 | connection *Connection 22 | } 23 | 24 | // Session creates a new session. 25 | func Session[T any](c *Connection, state T) (*T, SessionOperator) { 26 | archive := NewDiskArchive().WithName("sessions") 27 | manager := &ConnectedSessionOperator{ 28 | archive: archive, 29 | connection: c, 30 | } 31 | 32 | if !manager.Exists() { 33 | manager.Save(&state) 34 | } else { 35 | manager.Load(&state) 36 | } 37 | 38 | return &state, manager 39 | } 40 | 41 | // WithArchive sets the archive. 42 | func (session *ConnectedSessionOperator) WithArchive(archive Archive) SessionOperator { 43 | session.archive = archive 44 | return session 45 | } 46 | 47 | // Id tries to find a session id among the user's cookies. 48 | // If no session id is found, it creates a new one and returns it. 49 | func (session *ConnectedSessionOperator) Id() string { 50 | if "" != session.connection.sessionId { 51 | return session.connection.sessionId 52 | } 53 | 54 | var sessionId string 55 | cookies := session.connection.request.CookiesNamed("session-id") 56 | count := 0 57 | 58 | for _, cookie := range cookies { 59 | sessionId = cookie.Value 60 | count++ 61 | } 62 | 63 | if count > 0 { 64 | session.connection.sessionId = sessionId 65 | return sessionId 66 | } 67 | 68 | // Create new session. 69 | id4, idError := uuid.NewV4() 70 | if idError != nil { 71 | session.connection.server.notifier.SendErrorAndTrace(idError, 1) 72 | session.connection.sessionId = "" 73 | return "" 74 | } 75 | sessionId = id4.String() 76 | session.connection.SendCookie("session-id", sessionId) 77 | session.connection.sessionId = sessionId 78 | return sessionId 79 | } 80 | 81 | // Exists checks if the session exists into the archive. 82 | func (session *ConnectedSessionOperator) Exists() bool { 83 | id := session.Id() 84 | has, hasError := session.archive.Has(id, SessionKey) 85 | if hasError != nil { 86 | session.connection.server.notifier.SendErrorAndTrace(hasError, 1) 87 | } 88 | return has 89 | } 90 | 91 | // Save saves the session into the archive. 92 | func (session *ConnectedSessionOperator) Save(state any) { 93 | id := session.Id() 94 | readBytes, marshalError := json.Marshal(state) 95 | if marshalError != nil { 96 | session.connection.server.notifier.SendErrorAndTrace(marshalError, 1) 97 | return 98 | } 99 | setError := session.archive.Set(id, SessionKey, readBytes) 100 | if setError != nil { 101 | session.connection.server.notifier.SendErrorAndTrace(setError, 1) 102 | } 103 | } 104 | 105 | // Load loads the session from the archive. 106 | // 107 | // If the session is not found in the archive it creates it. 108 | func (session *ConnectedSessionOperator) Load(state any) { 109 | id := session.Id() 110 | 111 | has, hasError := session.archive.Has(id, SessionKey) 112 | if hasError != nil { 113 | session.connection.server.notifier.SendErrorAndTrace(hasError, 1) 114 | return 115 | } 116 | if has { 117 | readBytes, getError := session.archive.Get(id, SessionKey) 118 | if getError != nil { 119 | session.connection.server.notifier.SendErrorAndTrace(getError, 1) 120 | return 121 | } 122 | unmarshalError := json.Unmarshal(readBytes, state) 123 | if unmarshalError != nil { 124 | session.connection.server.notifier.SendErrorAndTrace(unmarshalError, 1) 125 | } 126 | } 127 | return 128 | } 129 | 130 | // Destroy removes the session from the archive. 131 | func (session *ConnectedSessionOperator) Destroy() { 132 | id := session.Id() 133 | destroyError := session.archive.RemoveDomain(id) 134 | if destroyError != nil { 135 | session.connection.server.notifier.SendErrorAndTrace(destroyError, 1) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /frz/libview.go: -------------------------------------------------------------------------------- 1 | package frz 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/evanw/esbuild/pkg/api" 7 | uuid "github.com/nu7hatch/gouuid" 8 | "regexp" 9 | "rogchap.com/v8go" 10 | "strings" 11 | ) 12 | 13 | type RenderMode int 14 | 15 | const ( 16 | RenderModeFull RenderMode = 0 // Renders on both the server and the client. 17 | RenderModeServer RenderMode = 1 // Renders only on the server. 18 | RenderModeClient RenderMode = 2 // Renders only on the client. 19 | RenderModeHeadless RenderMode = 3 // Renders only on the server and omits the base template. 20 | ) 21 | 22 | type View struct { 23 | Name string `json:"name"` 24 | Data any `json:"data"` 25 | Error string `json:"error"` 26 | RenderMode RenderMode `json:"renderMode"` 27 | server []byte 28 | index []byte 29 | } 30 | 31 | func (view *View) WithServer(data []byte) *View { 32 | view.server = data 33 | return view 34 | } 35 | 36 | func (view *View) WithIndex(data []byte) *View { 37 | view.index = data 38 | return view 39 | } 40 | 41 | var noScript = regexp.MustCompile(`.*`) 42 | var bundle []byte 43 | 44 | // Render renders the view. 45 | // 46 | // If the View is using RenderModeServer, then ViewRender returns an HTML document. 47 | // The head of the document will contain *only* content declared with the tag. 48 | // The body of the document will contain the fully rendered content of the view as HTML. 49 | // 50 | // If the View is using RenderModeClient, then ViewRender returns an HTML document. 51 | // The document itself doesn't include any of the view content, instead, custom ", targetId), 87 | 1, 88 | ), 89 | "", 90 | fmt.Sprintf("
", targetId), 91 | 1, 92 | ), 93 | "", 94 | "", 95 | 1, 96 | ), 97 | "", 98 | fmt.Sprintf( 99 | "", 100 | props, 101 | ), 102 | 1, 103 | ), nil 104 | } 105 | 106 | // SSR. 107 | serverCjsBytes, javaScriptBundleError := JavaScriptBundle(".", api.FormatCommonJS, view.server) 108 | if javaScriptBundleError != nil { 109 | return "", javaScriptBundleError 110 | } 111 | 112 | serverIif := fmt.Sprintf( 113 | ` 114 | const module={exports:{}}; const render = (function(){ 115 | %s 116 | return render; 117 | })() 118 | render(JSON.parse(props())).then(function done(rendered){ 119 | head(rendered.head??''); 120 | body(rendered.body??''); 121 | }); 122 | `, 123 | serverCjsBytes, 124 | ) 125 | 126 | bundleLocal, bundleError := JavaScriptBundle(".", api.FormatCommonJS, []byte(serverIif)) 127 | if bundleError != nil { 128 | return "", bundleError 129 | } 130 | bundle = bundleLocal 131 | 132 | var head string 133 | var body string 134 | 135 | globals := map[string]v8go.FunctionCallback{ 136 | "props": func(info *v8go.FunctionCallbackInfo) *v8go.Value { 137 | value, valueError := v8go.NewValue(info.Context().Isolate(), props) 138 | if nil != valueError { 139 | return nil 140 | } 141 | return value 142 | }, 143 | "inspect": func(info *v8go.FunctionCallbackInfo) *v8go.Value { 144 | args := info.Args() 145 | if len(args) > 0 { 146 | message := args[0].String() 147 | println(message) 148 | } 149 | return nil 150 | }, 151 | "head": func(info *v8go.FunctionCallbackInfo) *v8go.Value { 152 | args := info.Args() 153 | if len(args) > 0 { 154 | head = args[0].String() 155 | } 156 | return nil 157 | }, 158 | "body": func(info *v8go.FunctionCallbackInfo) *v8go.Value { 159 | args := info.Args() 160 | if len(args) > 0 { 161 | body = args[0].String() 162 | } 163 | return nil 164 | }, 165 | } 166 | 167 | _, destroy, javaScriptError := JavaScriptRun("server.js", bundle, globals) 168 | defer destroy() 169 | if javaScriptError != nil { 170 | return "", javaScriptError 171 | } 172 | 173 | if RenderModeHeadless == view.RenderMode { 174 | return body, nil 175 | } 176 | 177 | if RenderModeServer == view.RenderMode { 178 | return strings.Replace( 179 | strings.Replace( 180 | strings.Replace( 181 | strings.Replace( 182 | noScript.ReplaceAllString(string(view.index), ""), 183 | "", 184 | "", 185 | 1, 186 | ), 187 | "", 188 | fmt.Sprintf("
%s
", targetId, body), 189 | 1, 190 | ), 191 | "", 192 | head, 193 | 1, 194 | ), 195 | "", 196 | "", 197 | 1, 198 | ), nil 199 | } 200 | 201 | if RenderModeFull == view.RenderMode { 202 | return strings.Replace( 203 | strings.Replace( 204 | strings.Replace( 205 | strings.Replace( 206 | string(view.index), 207 | "", 208 | fmt.Sprintf("", targetId), 209 | 1, 210 | ), 211 | "", 212 | fmt.Sprintf("
%s
", targetId, body), 213 | 1, 214 | ), 215 | "", 216 | head, 217 | 1, 218 | ), 219 | "", 220 | fmt.Sprintf( 221 | "", 222 | props, 223 | ), 224 | 1, 225 | ), nil 226 | } 227 | 228 | return 229 | } 230 | -------------------------------------------------------------------------------- /frz/libview_test.go: -------------------------------------------------------------------------------- 1 | package frz 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | //go:embed dist/* 12 | var dist embed.FS 13 | 14 | type Data struct { 15 | Name string `json:"name"` 16 | } 17 | 18 | func TestRenderServer(test *testing.T) { 19 | port := NextNumber(8080) 20 | server := NewServer(). 21 | WithDist(dist). 22 | WithPublicRoot("dist/client"). 23 | WithViewIndex("dist/client/index.html"). 24 | WithViewServer("dist/server/server.js"). 25 | WithAddress(fmt.Sprintf("127.0.0.1:%d", port)). 26 | AddRoute(Route{Pattern: "GET /welcome", Handler: func(c *Connection) { 27 | c.SendView(View{ 28 | Name: "Welcome", 29 | RenderMode: RenderModeServer, 30 | Data: Data{Name: "world"}, 31 | }) 32 | }}) 33 | 34 | go server.Start() 35 | defer server.Stop() 36 | time.Sleep(1 * time.Second) 37 | 38 | expected := "

Hello world.

" 39 | actual, getError := HttpGet(fmt.Sprintf("http://127.0.0.1:%d/welcome", port), nil) 40 | if getError != nil { 41 | test.Fatal(getError) 42 | } 43 | 44 | ok := strings.Contains(actual, expected) 45 | 46 | if !ok { 47 | test.Fatalf("server was expected to respond with a string that contains '%s', received '%s' instead", expected, actual) 48 | } 49 | } 50 | 51 | func TestRenderClient(test *testing.T) { 52 | port := NextNumber(8080) 53 | server := NewServer(). 54 | WithDist(dist). 55 | WithPublicRoot("dist/client"). 56 | WithViewIndex("dist/client/index.html"). 57 | WithViewServer("dist/server/server.js"). 58 | WithAddress(fmt.Sprintf("127.0.0.1:%d", port)). 59 | AddRoute(Route{Pattern: "GET /welcome", Handler: func(c *Connection) { 60 | c.SendView(View{ 61 | Name: "Welcome", 62 | RenderMode: RenderModeClient, 63 | Data: Data{Name: "world"}, 64 | }) 65 | }}) 66 | 67 | go server.Start() 68 | defer server.Stop() 69 | time.Sleep(1 * time.Second) 70 | 71 | expected := " 11 | 12 | 13 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/razshare/frizzante/cli" 4 | 5 | func main() { cli.Run() } 6 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | test: 2 | make update 3 | make generate 4 | rm frz/dist -fr 5 | bunx vite build --ssr app/frz/scripts/server.ts --outDir frz/dist/server --emptyOutDir 6 | bunx vite build --outDir frz/dist/client --emptyOutDir 7 | CGO_ENABLED=1 cd frz && go test 8 | 9 | generate: 10 | go run main.go -generate -utilities -out="app/frz" 11 | 12 | update: 13 | go mod tidy 14 | bun update 15 | 16 | clean: 17 | go clean 18 | rm bin/app -fr 19 | rm frz/dist -fr 20 | rm app/frz -fr 21 | rm node_modules -fr 22 | 23 | hooks: 24 | printf "#!/usr/bin/env bash\n" > .git/hooks/pre-commit 25 | printf "make test" >> .git/hooks/pre-commit 26 | chmod +x .git/hooks/pre-commit -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-project", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "devDependencies": { 7 | "@eslint/compat": "^1.2.9", 8 | "@sveltejs/vite-plugin-svelte": "^5.1.0", 9 | "@tsconfig/svelte": "^5.0.4", 10 | "@types/node": "^22.15.29", 11 | "eslint": "^9.28.0", 12 | "eslint-config-prettier": "^10.1.5", 13 | "eslint-plugin-svelte": "^3.9.1", 14 | "svelte": "^5.33.14", 15 | "svelte-check": "^4.2.1", 16 | "typescript": "~5.8.3", 17 | "typescript-eslint": "^8.33.1", 18 | "vite": "^6.3.5" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razshare/frizzante/a6c5bbc211b2adddec583b2dde15ef4227bad5e2/public/.gitkeep -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' 2 | 3 | export default { 4 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: vitePreprocess(), 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "allowImportingTsExtensions": true, 6 | "strict": true, 7 | "moduleResolution": "bundler", 8 | "target": "ESNext", 9 | "module": "ESNext", 10 | "verbatimModuleSyntax": true, 11 | "isolatedModules": true, 12 | "resolveJsonModule": true, 13 | "sourceMap": true, 14 | "esModuleInterop": true, 15 | "skipLibCheck": true, 16 | "checkJs": true, 17 | "paths": { 18 | "$frz/*": ["./app/frz/*"], 19 | "$lib/*": ["./app/lib/*"] 20 | }, 21 | "lib": ["esnext", "DOM", "DOM.Iterable"], 22 | "useDefineForClassFields": true, 23 | "moduleDetection": "force" 24 | }, 25 | "include": [ 26 | "./app/**/*.svelte.js", 27 | "./app/**/*.svelte.ts", 28 | "./app/**/*.svelte", 29 | "./app/**/*.d.ts", 30 | "./app/**/*.js", 31 | "./app/**/*.ts" 32 | ], 33 | "exclude": ["./node_modules/**"] 34 | } 35 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { svelte } from "@sveltejs/vite-plugin-svelte"; 3 | 4 | let sourcemap: false | "inline" = false; 5 | 6 | if ("1" === (process.env.DEV ?? "")) { 7 | sourcemap = "inline"; 8 | } 9 | 10 | // https://vite.dev/config/ 11 | export default defineConfig({ 12 | plugins: [ 13 | svelte({ 14 | compilerOptions: { 15 | css: "injected", 16 | }, 17 | }), 18 | ], 19 | resolve: { 20 | alias: { 21 | $frz: "./app/frz", 22 | $lib: "./app/lib", 23 | }, 24 | }, 25 | build: { 26 | sourcemap, 27 | rollupOptions: { 28 | input: { 29 | index: "./index.html", 30 | }, 31 | }, 32 | }, 33 | }); 34 | --------------------------------------------------------------------------------