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