├── .github
└── workflows
│ └── node.js.yml
├── .gitignore
├── .ncurc.json
├── .prettierrc.json
├── AUTHORS
├── CODE-OF-CONDUCT.md
├── LICENSE
├── README.md
├── STYLE-GUIDE.md
├── TODO.md
├── bin
└── x-server.js
├── package-lock.json
├── package.json
├── src
├── command-line
│ ├── commands
│ │ ├── Default.ts
│ │ ├── ServeSubdomain.ts
│ │ ├── ServeWebsite.ts
│ │ └── index.ts
│ └── index.ts
├── errors
│ ├── AlreadyExists.ts
│ ├── NotFound.ts
│ ├── Processing.ts
│ ├── RevisionMismatch.ts
│ ├── Unauthorized.ts
│ └── Unprocessable.ts
├── server
│ ├── LoggerOptions.ts
│ ├── RequestHandler.ts
│ ├── RequestListener.ts
│ ├── ServerOptions.ts
│ ├── TlsOptions.ts
│ ├── createRequestListener.ts
│ ├── handlePreflight.ts
│ ├── serverListenWithDefault.ts
│ └── serverOptionsFromCommandLineOptions.ts
├── servers
│ ├── subdomain
│ │ ├── Context.ts
│ │ ├── createContext.ts
│ │ ├── findCertificate.ts
│ │ ├── findSubdomain.ts
│ │ ├── handle.ts
│ │ ├── requestFindSubdomain.ts
│ │ └── startServer.ts
│ └── website
│ │ ├── Context.ts
│ │ ├── createContext.ts
│ │ ├── handle.ts
│ │ ├── responseSendContent.ts
│ │ └── startServer.ts
├── utils
│ ├── Json.ts
│ ├── LogOptions.ts
│ ├── URLPattern.ts
│ ├── arrayFromAsyncIterable.ts
│ ├── byteArrayMerge.ts
│ ├── colors.ts
│ ├── contentTypeRecord.ts
│ ├── decrypt.ts
│ ├── encrypt-decrypt.test.ts
│ ├── encrypt.ts
│ ├── formatAuthorizationHeader.ts
│ ├── formatDate.ts
│ ├── generateEncryptionKey.ts
│ ├── globMatch.ts
│ ├── indent.ts
│ ├── leftPad.ts
│ ├── log.ts
│ ├── logJson.ts
│ ├── logPretty.ts
│ ├── logPrettyLine.ts
│ ├── node
│ │ ├── bufferJson.ts
│ │ ├── bufferJsonObject.ts
│ │ ├── compress.ts
│ │ ├── findPort.ts
│ │ ├── isErrnoException.ts
│ │ ├── packageJson.ts
│ │ ├── password.ts
│ │ ├── pathExists.ts
│ │ ├── pathIsDirectory.ts
│ │ ├── pathIsFile.ts
│ │ ├── readJson.ts
│ │ ├── readJsonObject.ts
│ │ ├── requestBasedomain.ts
│ │ ├── requestBuffer.ts
│ │ ├── requestCompressionMethod.ts
│ │ ├── requestFormatRaw.ts
│ │ ├── requestHostname.ts
│ │ ├── requestJson.ts
│ │ ├── requestJsonObject.ts
│ │ ├── requestKind.ts
│ │ ├── requestPath.ts
│ │ ├── requestPathname.ts
│ │ ├── requestQuery.ts
│ │ ├── requestSubdomain.ts
│ │ ├── requestText.ts
│ │ ├── requestTokenName.ts
│ │ ├── requestURLAlwaysWithHttpProtocol.ts
│ │ ├── responseSetHeaders.ts
│ │ ├── responseSetStatus.ts
│ │ ├── serverListen.ts
│ │ └── writeJson.ts
│ ├── objectOmit.ts
│ ├── objectRemoveUndefined.ts
│ ├── pretterLogger.ts
│ ├── randomHexString.ts
│ ├── responseHeaders.ts
│ ├── slug.test.ts
│ ├── slug.ts
│ ├── stringTrimEnd.ts
│ └── wait.ts
└── website
│ ├── Content.ts
│ ├── WebsiteConfig.ts
│ ├── createWebsiteConfig.ts
│ ├── emptyWebsiteConfig.ts
│ ├── mergeWebsiteConfigs.ts
│ ├── readContent.ts
│ ├── readContentWithRewrite.ts
│ ├── readWebsiteConfigFile.ts
│ ├── readWebsiteConfigFileOrDefault.ts
│ ├── responseSetCacheControlHeaders.ts
│ ├── responseSetCorsHeaders.ts
│ └── websiteConfigFromCommandLineOptions.ts
└── tsconfig.json
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean installation of node dependencies,
2 | # cache/restore them, build the source code and run tests across
3 | # different versions of node For more information see:
4 | # https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
5 |
6 | name: Node.js CI
7 |
8 | on:
9 | push:
10 | branches: ["master"]
11 | pull_request:
12 | branches: ["master"]
13 |
14 | jobs:
15 | build:
16 | runs-on: ubuntu-latest
17 |
18 | strategy:
19 | matrix:
20 | node-version: [18.x, 19.x]
21 |
22 | steps:
23 | - uses: actions/checkout@v3
24 | - uses: actions/setup-node@v3
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 | - run: npm install
28 | - run: npm run build
29 | - run: npm test
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # test
2 | tmp
3 |
4 | # for playing with the program
5 | play
6 |
7 | # typescript
8 | lib
9 |
10 | # vitest
11 | coverage
12 |
13 | # npm
14 | node_modules
15 |
16 | # 0x
17 | *.0x/
18 | *.log
19 |
20 | # emacs
21 | *~
22 | *#*
23 | .#*
24 |
25 | # vscode
26 | .vscode
27 |
28 | # idea
29 | .idea
30 | # Local Netlify folder
31 | .netlify
32 | .vercel
33 |
--------------------------------------------------------------------------------
/.ncurc.json:
--------------------------------------------------------------------------------
1 | {
2 | "upgrade": true,
3 | "reject": ["@types/inquirer", "inquirer"]
4 | }
5 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "trailingComma": "all",
4 | "plugins": ["prettier-plugin-organize-imports"]
5 | }
6 |
--------------------------------------------------------------------------------
/AUTHORS:
--------------------------------------------------------------------------------
1 | Xie Yuheng | 谢宇恒 | https://xieyuheng.com | https://github.com/xieyuheng
2 |
--------------------------------------------------------------------------------
/CODE-OF-CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our
7 | project and our community a harassment-free experience for everyone,
8 | regardless of age, body size, disability, ethnicity, gender identity
9 | and expression, level of experience, nationality, personal appearance,
10 | race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive
15 | environment include:
16 |
17 | - Using welcoming and inclusive language
18 | - Being respectful of differing viewpoints and experiences
19 | - Gracefully accepting constructive criticism
20 | - Focusing on what is best for the community
21 | - Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | - The use of sexualized language or imagery and unwelcome sexual
26 | attention or advances
27 | - Trolling, insulting/derogatory comments, and personal or political
28 | attacks
29 | - Public or private harassment
30 | - Publishing others' private information, such as a physical or
31 | electronic address, without explicit permission
32 | - Other conduct which could reasonably be considered inappropriate in
33 | a professional setting
34 |
35 | ## Our Responsibilities
36 |
37 | Project maintainers are responsible for clarifying the standards of
38 | acceptable behavior and are expected to take appropriate and fair
39 | corrective action in response to any instances of unacceptable
40 | behavior.
41 |
42 | Project maintainers have the right and responsibility to remove, edit,
43 | or reject comments, commits, code, wiki edits, issues, and other
44 | contributions that are not aligned to this Code of Conduct, or to ban
45 | temporarily or permanently any contributor for other behaviors that
46 | they deem inappropriate, threatening, offensive, or harmful.
47 |
48 | ## Scope
49 |
50 | This Code of Conduct applies both within project spaces and in public
51 | spaces when an individual is representing the project or its
52 | community. Examples of representing a project or community include
53 | using an official project e-mail address, posting via an official
54 | social media account, or acting as an appointed representative at an
55 | online or offline event. Representation of a project may be further
56 | defined and clarified by project maintainers.
57 |
58 | ## Enforcement
59 |
60 | Instances of abusive, harassing, or otherwise unacceptable behavior
61 | may be reported by contacting the project team at project issue
62 | tracker. The project team will review and investigate all complaints,
63 | and will respond in a way that it deems appropriate to the
64 | circumstances. The project team is obligated to maintain
65 | confidentiality with regard to the reporter of an incident. Further
66 | details of specific enforcement policies may be posted separately.
67 |
68 | Project maintainers who do not follow or enforce the Code of Conduct
69 | in good faith may face temporary or permanent repercussions as
70 | determined by other members of the project's leadership.
71 |
72 | ## Attribution
73 |
74 | This Code of Conduct is adapted from the [Contributor
75 | Covenant][homepage], version 1.4, available at
76 | [http://contributor-covenant.org/version/1/4][version]
77 |
78 | [homepage]: http://contributor-covenant.org
79 | [version]: http://contributor-covenant.org/version/1/4/
80 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise solutionantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has solutionantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or solutionantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # X Server
2 |
3 | A website server
4 | that supports serving many websites
5 | using subdomain-based routing.
6 |
7 | ## Install
8 |
9 | Install it by the following command:
10 |
11 | ```sh
12 | npm install -g @xieyuheng/x-server
13 | ```
14 |
15 | The command line program is called `x-server`.
16 |
17 | ```
18 | Commands:
19 | help [name] Display help for a command
20 | serve:website [path] Serve a website
21 | serve:subdomain [path] Serve many websites using subdomain-based routing
22 | ```
23 |
24 | ## Docs
25 |
26 | - [Serve one website](#serve-one-website)
27 | - [Serve many websites](#serve-many-websites)
28 | - [Config logger](#config-logger)
29 | - [Use custom domain](#use-custom-domain)
30 | - [Get free certificate](#get-free-certificate)
31 | - [Use systemd to start service](#use-systemd-to-start-service)
32 |
33 | ### Serve one website
34 |
35 | Use the `x-server serve:website` command to serve one website:
36 |
37 | ```sh
38 | x-server serve:website
39 | ```
40 |
41 | When serving a [single-page application (SPA)](https://en.wikipedia.org/wiki/Single-page_application),
42 | we need to redirect all requests to `index.html`.
43 |
44 | Example command for serving a SPA:
45 |
46 | ```sh
47 | x-server serve:website \
48 | --cors \
49 | --redirect-not-found-to index.html \
50 | --cache-control-pattern 'assets/**: max-age=31536000'
51 | ```
52 |
53 | To serve a website using HTTPS, we need to provide TLS certificate.
54 |
55 | Example command for serving a SPA using HTTPS:
56 |
57 | ```sh
58 | x-server serve:website \
59 | --cors \
60 | --port 443 \
61 | --redirect-not-found-to index.html \
62 | --cache-control-pattern 'assets/**: max-age=31536000' \
63 | --tls-cert \
64 | --tls-key
65 | ```
66 |
67 | It is unhandy to issue long command,
68 | thus we also support using a `website.json` config file:
69 |
70 | ```sh
71 | x-server serve:website /website.json
72 | ```
73 |
74 | Where `/website.json` is:
75 |
76 | ```json
77 | {
78 | "server": {
79 | "port": 443,
80 | "tls": {
81 | "cert": "",
82 | "key": ""
83 | }
84 | },
85 | "cors": true,
86 | "redirectNotFoundTo": "index.html",
87 | "cacheControlPatterns": {
88 | "assets/**": "max-age=31536000"
89 | }
90 | }
91 | ```
92 |
93 | ### Serve many websites
94 |
95 | The main use case of `x-server` is to
96 | serve many websites in one directory,
97 | using subdomain-based routing.
98 |
99 | For example, I have a VPS machine,
100 | where I put all my websites
101 | in the `/websites` directory.
102 |
103 | ```
104 | /websites/www
105 | /websites/graphwind
106 | /websites/inet
107 | /websites/pomodoro
108 | /websites/readonlylink
109 | ...
110 | ```
111 |
112 | I bought a domain for my server -- say `example.com`,
113 | and configured my DNS to resolve `example.com`
114 | and `*.example.com` to my server.
115 |
116 | I also created certificate files for my domain using `certbot`.
117 |
118 | - About how to use `certbot`, please see
119 | the ["Get free certificate"](#get-free-certificate) section.
120 |
121 | I can use `x-server serve:subdomain` command to serve all of
122 | the websites in `/websites` directory.
123 |
124 | ```sh
125 | x-server serve:subdomain /websites \
126 | --hostname example.com \
127 | --port 443 \
128 | --tls-cert /etc/letsencrypt/live/example.com/fullchain.pem \
129 | --tls-key /etc/letsencrypt/live/example.com/privkey.pem
130 | ```
131 |
132 | Then I can visit all my websites via subdomain of `example.com`.
133 |
134 | ```
135 | https://www.example.com
136 | https://graphwind.example.com
137 | https://inet.example.com
138 | https://pomodoro.example.com
139 | https://readonlylink.example.com
140 | ...
141 | ```
142 |
143 | If no subdomain is given in a request,
144 | `www/` will be used as the default subdomain directory
145 | (while no redirect will be done).
146 |
147 | Thus the following websites have the same contents:
148 |
149 | ```
150 | https://example.com
151 | https://www.example.com
152 | ```
153 |
154 | Instead of issuing long command,
155 | we can also use a root `website.json` config file.
156 |
157 | ```sh
158 | x-server serve:subdomain /websites/website.json
159 | ```
160 |
161 | Where `/websites/website.json` is:
162 |
163 | ```json
164 | {
165 | "server": {
166 | "hostname": "example.com",
167 | "port": 443,
168 | "tls": {
169 | "cert": "/etc/letsencrypt/live/example.com/fullchain.pem",
170 | "key": "/etc/letsencrypt/live/example.com/privkey.pem"
171 | }
172 | }
173 | }
174 | ```
175 |
176 | - When using `x-server serve:subdomain`,
177 | the `server.hostname` option is required.
178 |
179 | - And each website in `/websites` might have
180 | it's own `website.json` config file.
181 |
182 | ### Config logger
183 |
184 | We can config logger in `/websites/website.json`:
185 |
186 | ```json
187 | {
188 | ...,
189 | "logger": {
190 | "name": "pretty-line",
191 | "disableRequestLogging": true
192 | }
193 | }
194 | ```
195 |
196 | The type of logger options are:
197 |
198 | ```ts
199 | export type LoggerOptions = {
200 | name: "json" | "silent" | "pretty" | "pretty-line"
201 | disableRequestLogging?: boolean
202 | }
203 | ```
204 |
205 | The default logger options are:
206 |
207 | ```json
208 | {
209 | "name": "pretty-line",
210 | "disableRequestLogging": false
211 | }
212 | ```
213 |
214 | ### Use custom domain
215 |
216 | When doing subdomain-based routing,
217 | we can also support custom domain for a subdomain,
218 | by adding a file in `.domain-map/` directory.
219 |
220 | ```
221 | /websites/.domain-map//subdomain
222 | ```
223 |
224 | Where the content of the file is the subdomain, for examples:
225 |
226 | ```
227 | /websites/.domain-map/readonly.link/subdomain -- (content: readonlylink)
228 | ...
229 | ```
230 |
231 | Then I can the following DNS ALIAS records to my custom domains:
232 |
233 | - You can also use A record and IP addresses.
234 |
235 | | Domain | Type | Value |
236 | |---------------|-------|------------------------|
237 | | readonly.link | ALIAS | readonlylink.example.com. |
238 |
239 |
240 | Custom domain is only supported when TLS is enabled.
241 | To provide TLS certificate for a custom domain,
242 | add the following files:
243 |
244 | ```
245 | /websites/.domain-map//cert
246 | /websites/.domain-map//key
247 | ```
248 |
249 | For example, the listing of `.domain-map/` is the following:
250 |
251 | ```
252 | /websites/.domain-map/readonly.link/subdomain
253 | /websites/.domain-map/readonly.link/cert
254 | /websites/.domain-map/readonly.link/key
255 | ...
256 | ```
257 |
258 | ### Get free certificate
259 |
260 | You can use `certbot` to get free certificate for your domains.
261 |
262 | - [Certbot website](https://certbot.eff.org/instructions)
263 | - [Certbot on archlinux wiki](https://wiki.archlinux.org/title/certbot)
264 |
265 | After install `certbot`,
266 | I prefer creating certificate via DNS TXT record,
267 | using the following command:
268 |
269 | ```sh
270 | sudo certbot certonly --manual --preferred-challenges dns
271 | ```
272 |
273 | Then you can follow the prompt of `certbot`
274 | to create the certificate files,
275 | during which you will need to add TXT record
276 | to the DNS record of your domain
277 | to accomplish the challenge given by `certbot`.
278 |
279 | After created the certificate files,
280 | I use the follow command to copy them to my `.domain-map`:
281 |
282 | ```sh
283 | sudo cat /etc/letsencrypt/live//fullchain.pem > /websites/.domain-map//cert
284 | sudo cat /etc/letsencrypt/live//privkey.pem > /websites/.domain-map//key
285 | ```
286 |
287 | ### Use systemd to start service
288 |
289 | On a Linux server, we can use `systemd` to start a service,
290 | or enable a service to start whenever the server is booted.
291 |
292 |
293 | Example service file `fidb-app-x-server.service`:
294 |
295 | ```
296 | [Unit]
297 | Description=example.com x-server
298 | After=network.target
299 |
300 | [Service]
301 | ExecStart=/usr/local/bin/x-server serve:subdomain /websites/website.json
302 | Restart=on-failure
303 |
304 | [Install]
305 | WantedBy=multi-user.target
306 | ```
307 |
308 | Install service:
309 |
310 | ```
311 | sudo cp .service /etc/systemd/system/
312 | ```
313 |
314 | Using service:
315 |
316 | ```
317 | sudo systemctl start .service
318 | sudo systemctl enable .service
319 | sudo systemctl status .service
320 | ```
321 |
322 | To view log:
323 |
324 | ```
325 | journalctl -f -u .service
326 | ```
327 |
328 | Reload systemd config files:
329 |
330 | ```
331 | sudo systemctl daemon-reload
332 | ```
333 |
334 | ## Development
335 |
336 | ```sh
337 | npm install # Install dependencies
338 | npm run build # Compile `src/` to `lib/`
339 | npm run build:watch # Watch the compilation
340 | npm run format # Format the code
341 | npm run test # Run test
342 | npm run test:watch # Watch the testing
343 | ```
344 |
345 | ## Contributions
346 |
347 | To make a contribution, fork this project and create a pull request.
348 |
349 | Please read the [STYLE-GUIDE.md](STYLE-GUIDE.md) before you change the code.
350 |
351 | Remember to add yourself to [AUTHORS](AUTHORS).
352 | Your line belongs to you, you can write a little
353 | introduction to yourself but not too long.
354 |
355 | ## License
356 |
357 | [GPLv3](LICENSE)
358 |
--------------------------------------------------------------------------------
/STYLE-GUIDE.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Style Guide
3 | ---
4 |
5 | **Principle 1: The aim of a style guide is for people to write code that looks like one person wrote it.**
6 |
7 | **Principle 2: In general, observe the style of existing code and respect it.**
8 |
9 | # About commits and pull requests
10 |
11 | Do commits and pull requests in small steps.
12 |
13 | We loosely follows: https://rfc.zeromq.org/spec/42
14 |
15 | We do not force any commit message style,
16 | just write clear and easy to understand messages.
17 |
18 | # Use of abbreviations
19 |
20 | In documentation and everyday discussion,
21 | do not use too much abbreviations.
22 |
23 | If you must, explain them before using.
24 |
25 | # Open vs. closed types
26 |
27 | Beware of open vs. closed types.
28 |
29 | We use object-oriented style for open types,
30 | and use functional style for closed types.
31 |
32 | ## About modules
33 |
34 | For open types: one class, one file.
35 |
36 | For closed types: almost one function, one file, except for constructors.
37 |
38 | ## About file name
39 |
40 | File name should be the same as class or function name,
41 | to avoid name casting when editing the code.
42 |
43 | ## About directory name
44 |
45 | Use `lisp-case` for directory name.
46 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xieyuheng/x-server/57216f6fa13ed38318f258167c33f6f86d735e6e/TODO.md
--------------------------------------------------------------------------------
/bin/x-server.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S node --no-warnings --stack-size=10000
2 |
3 | const process = require("process")
4 |
5 | process.on("unhandledRejection", (error) => {
6 | console.error(error)
7 | process.exit(1)
8 | })
9 |
10 | const { createCommandRunner } = require("../lib/command-line")
11 |
12 | createCommandRunner().run()
13 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@xieyuheng/x-server",
3 | "version": "0.0.15",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "@xieyuheng/x-server",
9 | "version": "0.0.15",
10 | "license": "GPL-3.0-or-later",
11 | "dependencies": {
12 | "@xieyuheng/command-line": "^0.0.13",
13 | "@xieyuheng/ty": "^0.1.26",
14 | "bcrypt": "^5.1.1",
15 | "detect-port": "^1.5.1",
16 | "micromatch": "^4.0.5",
17 | "picocolors": "^1.0.0",
18 | "qs": "^6.11.2",
19 | "readdirp": "^3.6.0",
20 | "urlpattern-polyfill": "^9.0.0"
21 | },
22 | "bin": {
23 | "x-server": "bin/x-server.js"
24 | },
25 | "devDependencies": {
26 | "@types/bcrypt": "^5.0.0",
27 | "@types/detect-port": "^1.3.3",
28 | "@types/micromatch": "^4.0.3",
29 | "@types/node": "^20.8.6",
30 | "@types/qs": "^6.9.8",
31 | "prettier": "^3.0.3",
32 | "prettier-plugin-organize-imports": "^3.2.3",
33 | "typescript": "^5.2.2",
34 | "vite": "^4.4.11",
35 | "vitest": "^0.34.6"
36 | }
37 | },
38 | "node_modules/@esbuild/android-arm": {
39 | "version": "0.18.20",
40 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
41 | "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
42 | "cpu": [
43 | "arm"
44 | ],
45 | "dev": true,
46 | "optional": true,
47 | "os": [
48 | "android"
49 | ],
50 | "engines": {
51 | "node": ">=12"
52 | }
53 | },
54 | "node_modules/@esbuild/android-arm64": {
55 | "version": "0.18.20",
56 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
57 | "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
58 | "cpu": [
59 | "arm64"
60 | ],
61 | "dev": true,
62 | "optional": true,
63 | "os": [
64 | "android"
65 | ],
66 | "engines": {
67 | "node": ">=12"
68 | }
69 | },
70 | "node_modules/@esbuild/android-x64": {
71 | "version": "0.18.20",
72 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
73 | "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
74 | "cpu": [
75 | "x64"
76 | ],
77 | "dev": true,
78 | "optional": true,
79 | "os": [
80 | "android"
81 | ],
82 | "engines": {
83 | "node": ">=12"
84 | }
85 | },
86 | "node_modules/@esbuild/darwin-arm64": {
87 | "version": "0.18.20",
88 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
89 | "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
90 | "cpu": [
91 | "arm64"
92 | ],
93 | "dev": true,
94 | "optional": true,
95 | "os": [
96 | "darwin"
97 | ],
98 | "engines": {
99 | "node": ">=12"
100 | }
101 | },
102 | "node_modules/@esbuild/darwin-x64": {
103 | "version": "0.18.20",
104 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
105 | "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
106 | "cpu": [
107 | "x64"
108 | ],
109 | "dev": true,
110 | "optional": true,
111 | "os": [
112 | "darwin"
113 | ],
114 | "engines": {
115 | "node": ">=12"
116 | }
117 | },
118 | "node_modules/@esbuild/freebsd-arm64": {
119 | "version": "0.18.20",
120 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
121 | "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
122 | "cpu": [
123 | "arm64"
124 | ],
125 | "dev": true,
126 | "optional": true,
127 | "os": [
128 | "freebsd"
129 | ],
130 | "engines": {
131 | "node": ">=12"
132 | }
133 | },
134 | "node_modules/@esbuild/freebsd-x64": {
135 | "version": "0.18.20",
136 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
137 | "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
138 | "cpu": [
139 | "x64"
140 | ],
141 | "dev": true,
142 | "optional": true,
143 | "os": [
144 | "freebsd"
145 | ],
146 | "engines": {
147 | "node": ">=12"
148 | }
149 | },
150 | "node_modules/@esbuild/linux-arm": {
151 | "version": "0.18.20",
152 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
153 | "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
154 | "cpu": [
155 | "arm"
156 | ],
157 | "dev": true,
158 | "optional": true,
159 | "os": [
160 | "linux"
161 | ],
162 | "engines": {
163 | "node": ">=12"
164 | }
165 | },
166 | "node_modules/@esbuild/linux-arm64": {
167 | "version": "0.18.20",
168 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
169 | "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
170 | "cpu": [
171 | "arm64"
172 | ],
173 | "dev": true,
174 | "optional": true,
175 | "os": [
176 | "linux"
177 | ],
178 | "engines": {
179 | "node": ">=12"
180 | }
181 | },
182 | "node_modules/@esbuild/linux-ia32": {
183 | "version": "0.18.20",
184 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
185 | "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
186 | "cpu": [
187 | "ia32"
188 | ],
189 | "dev": true,
190 | "optional": true,
191 | "os": [
192 | "linux"
193 | ],
194 | "engines": {
195 | "node": ">=12"
196 | }
197 | },
198 | "node_modules/@esbuild/linux-loong64": {
199 | "version": "0.18.20",
200 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
201 | "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
202 | "cpu": [
203 | "loong64"
204 | ],
205 | "dev": true,
206 | "optional": true,
207 | "os": [
208 | "linux"
209 | ],
210 | "engines": {
211 | "node": ">=12"
212 | }
213 | },
214 | "node_modules/@esbuild/linux-mips64el": {
215 | "version": "0.18.20",
216 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
217 | "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
218 | "cpu": [
219 | "mips64el"
220 | ],
221 | "dev": true,
222 | "optional": true,
223 | "os": [
224 | "linux"
225 | ],
226 | "engines": {
227 | "node": ">=12"
228 | }
229 | },
230 | "node_modules/@esbuild/linux-ppc64": {
231 | "version": "0.18.20",
232 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
233 | "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
234 | "cpu": [
235 | "ppc64"
236 | ],
237 | "dev": true,
238 | "optional": true,
239 | "os": [
240 | "linux"
241 | ],
242 | "engines": {
243 | "node": ">=12"
244 | }
245 | },
246 | "node_modules/@esbuild/linux-riscv64": {
247 | "version": "0.18.20",
248 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
249 | "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
250 | "cpu": [
251 | "riscv64"
252 | ],
253 | "dev": true,
254 | "optional": true,
255 | "os": [
256 | "linux"
257 | ],
258 | "engines": {
259 | "node": ">=12"
260 | }
261 | },
262 | "node_modules/@esbuild/linux-s390x": {
263 | "version": "0.18.20",
264 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
265 | "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
266 | "cpu": [
267 | "s390x"
268 | ],
269 | "dev": true,
270 | "optional": true,
271 | "os": [
272 | "linux"
273 | ],
274 | "engines": {
275 | "node": ">=12"
276 | }
277 | },
278 | "node_modules/@esbuild/linux-x64": {
279 | "version": "0.18.20",
280 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
281 | "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
282 | "cpu": [
283 | "x64"
284 | ],
285 | "dev": true,
286 | "optional": true,
287 | "os": [
288 | "linux"
289 | ],
290 | "engines": {
291 | "node": ">=12"
292 | }
293 | },
294 | "node_modules/@esbuild/netbsd-x64": {
295 | "version": "0.18.20",
296 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
297 | "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
298 | "cpu": [
299 | "x64"
300 | ],
301 | "dev": true,
302 | "optional": true,
303 | "os": [
304 | "netbsd"
305 | ],
306 | "engines": {
307 | "node": ">=12"
308 | }
309 | },
310 | "node_modules/@esbuild/openbsd-x64": {
311 | "version": "0.18.20",
312 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
313 | "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
314 | "cpu": [
315 | "x64"
316 | ],
317 | "dev": true,
318 | "optional": true,
319 | "os": [
320 | "openbsd"
321 | ],
322 | "engines": {
323 | "node": ">=12"
324 | }
325 | },
326 | "node_modules/@esbuild/sunos-x64": {
327 | "version": "0.18.20",
328 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
329 | "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
330 | "cpu": [
331 | "x64"
332 | ],
333 | "dev": true,
334 | "optional": true,
335 | "os": [
336 | "sunos"
337 | ],
338 | "engines": {
339 | "node": ">=12"
340 | }
341 | },
342 | "node_modules/@esbuild/win32-arm64": {
343 | "version": "0.18.20",
344 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
345 | "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
346 | "cpu": [
347 | "arm64"
348 | ],
349 | "dev": true,
350 | "optional": true,
351 | "os": [
352 | "win32"
353 | ],
354 | "engines": {
355 | "node": ">=12"
356 | }
357 | },
358 | "node_modules/@esbuild/win32-ia32": {
359 | "version": "0.18.20",
360 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
361 | "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
362 | "cpu": [
363 | "ia32"
364 | ],
365 | "dev": true,
366 | "optional": true,
367 | "os": [
368 | "win32"
369 | ],
370 | "engines": {
371 | "node": ">=12"
372 | }
373 | },
374 | "node_modules/@esbuild/win32-x64": {
375 | "version": "0.18.20",
376 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
377 | "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
378 | "cpu": [
379 | "x64"
380 | ],
381 | "dev": true,
382 | "optional": true,
383 | "os": [
384 | "win32"
385 | ],
386 | "engines": {
387 | "node": ">=12"
388 | }
389 | },
390 | "node_modules/@jest/schemas": {
391 | "version": "29.6.3",
392 | "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
393 | "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
394 | "dev": true,
395 | "dependencies": {
396 | "@sinclair/typebox": "^0.27.8"
397 | },
398 | "engines": {
399 | "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
400 | }
401 | },
402 | "node_modules/@jridgewell/sourcemap-codec": {
403 | "version": "1.4.15",
404 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
405 | "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
406 | "dev": true
407 | },
408 | "node_modules/@mapbox/node-pre-gyp": {
409 | "version": "1.0.11",
410 | "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
411 | "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
412 | "dependencies": {
413 | "detect-libc": "^2.0.0",
414 | "https-proxy-agent": "^5.0.0",
415 | "make-dir": "^3.1.0",
416 | "node-fetch": "^2.6.7",
417 | "nopt": "^5.0.0",
418 | "npmlog": "^5.0.1",
419 | "rimraf": "^3.0.2",
420 | "semver": "^7.3.5",
421 | "tar": "^6.1.11"
422 | },
423 | "bin": {
424 | "node-pre-gyp": "bin/node-pre-gyp"
425 | }
426 | },
427 | "node_modules/@sinclair/typebox": {
428 | "version": "0.27.8",
429 | "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
430 | "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
431 | "dev": true
432 | },
433 | "node_modules/@types/bcrypt": {
434 | "version": "5.0.0",
435 | "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.0.tgz",
436 | "integrity": "sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw==",
437 | "dev": true,
438 | "dependencies": {
439 | "@types/node": "*"
440 | }
441 | },
442 | "node_modules/@types/braces": {
443 | "version": "3.0.2",
444 | "resolved": "https://registry.npmjs.org/@types/braces/-/braces-3.0.2.tgz",
445 | "integrity": "sha512-U5tlMYa0U/2eFTmJgKcPWQOEICP173sJDa6OjHbj5Tv+NVaYcrq2xmdWpNXOwWYGwJu+jER/pfTLdoQ31q8PzA==",
446 | "dev": true
447 | },
448 | "node_modules/@types/chai": {
449 | "version": "4.3.8",
450 | "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.8.tgz",
451 | "integrity": "sha512-yW/qTM4mRBBcsA9Xw9FbcImYtFPY7sgr+G/O5RDYVmxiy9a+pE5FyoFUi8JYCZY5nicj8atrr1pcfPiYpeNGOA==",
452 | "dev": true
453 | },
454 | "node_modules/@types/chai-subset": {
455 | "version": "1.3.3",
456 | "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz",
457 | "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==",
458 | "dev": true,
459 | "dependencies": {
460 | "@types/chai": "*"
461 | }
462 | },
463 | "node_modules/@types/detect-port": {
464 | "version": "1.3.3",
465 | "resolved": "https://registry.npmjs.org/@types/detect-port/-/detect-port-1.3.3.tgz",
466 | "integrity": "sha512-bV/jQlAJ/nPY3XqSatkGpu+nGzou+uSwrH1cROhn+jBFg47yaNH+blW4C7p9KhopC7QxCv/6M86s37k8dMk0Yg==",
467 | "dev": true
468 | },
469 | "node_modules/@types/micromatch": {
470 | "version": "4.0.3",
471 | "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-4.0.3.tgz",
472 | "integrity": "sha512-QX1czv7QoLU76Asb1NSVSlu5zTMx/TFNswUDtQSbH9hgvCg+JHvIEoVvVSzBf1WNCT8XsK515W+p3wFOCuvhCg==",
473 | "dev": true,
474 | "dependencies": {
475 | "@types/braces": "*"
476 | }
477 | },
478 | "node_modules/@types/node": {
479 | "version": "20.8.6",
480 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.6.tgz",
481 | "integrity": "sha512-eWO4K2Ji70QzKUqRy6oyJWUeB7+g2cRagT3T/nxYibYcT4y2BDL8lqolRXjTHmkZCdJfIPaY73KbJAZmcryxTQ==",
482 | "dev": true,
483 | "dependencies": {
484 | "undici-types": "~5.25.1"
485 | }
486 | },
487 | "node_modules/@types/qs": {
488 | "version": "6.9.8",
489 | "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.8.tgz",
490 | "integrity": "sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==",
491 | "dev": true
492 | },
493 | "node_modules/@vitest/expect": {
494 | "version": "0.34.6",
495 | "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz",
496 | "integrity": "sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==",
497 | "dev": true,
498 | "dependencies": {
499 | "@vitest/spy": "0.34.6",
500 | "@vitest/utils": "0.34.6",
501 | "chai": "^4.3.10"
502 | },
503 | "funding": {
504 | "url": "https://opencollective.com/vitest"
505 | }
506 | },
507 | "node_modules/@vitest/runner": {
508 | "version": "0.34.6",
509 | "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.6.tgz",
510 | "integrity": "sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==",
511 | "dev": true,
512 | "dependencies": {
513 | "@vitest/utils": "0.34.6",
514 | "p-limit": "^4.0.0",
515 | "pathe": "^1.1.1"
516 | },
517 | "funding": {
518 | "url": "https://opencollective.com/vitest"
519 | }
520 | },
521 | "node_modules/@vitest/snapshot": {
522 | "version": "0.34.6",
523 | "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.6.tgz",
524 | "integrity": "sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==",
525 | "dev": true,
526 | "dependencies": {
527 | "magic-string": "^0.30.1",
528 | "pathe": "^1.1.1",
529 | "pretty-format": "^29.5.0"
530 | },
531 | "funding": {
532 | "url": "https://opencollective.com/vitest"
533 | }
534 | },
535 | "node_modules/@vitest/spy": {
536 | "version": "0.34.6",
537 | "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.6.tgz",
538 | "integrity": "sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==",
539 | "dev": true,
540 | "dependencies": {
541 | "tinyspy": "^2.1.1"
542 | },
543 | "funding": {
544 | "url": "https://opencollective.com/vitest"
545 | }
546 | },
547 | "node_modules/@vitest/utils": {
548 | "version": "0.34.6",
549 | "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz",
550 | "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==",
551 | "dev": true,
552 | "dependencies": {
553 | "diff-sequences": "^29.4.3",
554 | "loupe": "^2.3.6",
555 | "pretty-format": "^29.5.0"
556 | },
557 | "funding": {
558 | "url": "https://opencollective.com/vitest"
559 | }
560 | },
561 | "node_modules/@xieyuheng/command-line": {
562 | "version": "0.0.13",
563 | "resolved": "https://registry.npmjs.org/@xieyuheng/command-line/-/command-line-0.0.13.tgz",
564 | "integrity": "sha512-IJtUxtRXAKnFLEdecdGgwrsDuTCki2iSS0/ADEXI4RVuWXjSxLTJ+ZSfgq5uf8cNP5rYXUYD7ZuupIOWn4fX3Q==",
565 | "dependencies": {
566 | "@xieyuheng/ty": "^0.1.26",
567 | "picocolors": "^1.0.0",
568 | "yargs-parser": "^21.1.1"
569 | }
570 | },
571 | "node_modules/@xieyuheng/ty": {
572 | "version": "0.1.26",
573 | "resolved": "https://registry.npmjs.org/@xieyuheng/ty/-/ty-0.1.26.tgz",
574 | "integrity": "sha512-UKB7IQzjsm1lwF5vODXtTqT5e70SX1pWXUf80lVpul5Cw+QVH1gpgUu4tAdQAEhqNvZRFrlkgbSKcZYA0lUwYA=="
575 | },
576 | "node_modules/abbrev": {
577 | "version": "1.1.1",
578 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
579 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
580 | },
581 | "node_modules/acorn": {
582 | "version": "8.10.0",
583 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
584 | "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
585 | "dev": true,
586 | "bin": {
587 | "acorn": "bin/acorn"
588 | },
589 | "engines": {
590 | "node": ">=0.4.0"
591 | }
592 | },
593 | "node_modules/acorn-walk": {
594 | "version": "8.2.0",
595 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
596 | "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
597 | "dev": true,
598 | "engines": {
599 | "node": ">=0.4.0"
600 | }
601 | },
602 | "node_modules/address": {
603 | "version": "1.2.2",
604 | "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz",
605 | "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==",
606 | "engines": {
607 | "node": ">= 10.0.0"
608 | }
609 | },
610 | "node_modules/agent-base": {
611 | "version": "6.0.2",
612 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
613 | "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
614 | "dependencies": {
615 | "debug": "4"
616 | },
617 | "engines": {
618 | "node": ">= 6.0.0"
619 | }
620 | },
621 | "node_modules/ansi-regex": {
622 | "version": "5.0.1",
623 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
624 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
625 | "engines": {
626 | "node": ">=8"
627 | }
628 | },
629 | "node_modules/ansi-styles": {
630 | "version": "5.2.0",
631 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
632 | "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
633 | "dev": true,
634 | "engines": {
635 | "node": ">=10"
636 | },
637 | "funding": {
638 | "url": "https://github.com/chalk/ansi-styles?sponsor=1"
639 | }
640 | },
641 | "node_modules/aproba": {
642 | "version": "2.0.0",
643 | "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
644 | "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ=="
645 | },
646 | "node_modules/are-we-there-yet": {
647 | "version": "2.0.0",
648 | "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
649 | "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
650 | "dependencies": {
651 | "delegates": "^1.0.0",
652 | "readable-stream": "^3.6.0"
653 | },
654 | "engines": {
655 | "node": ">=10"
656 | }
657 | },
658 | "node_modules/assertion-error": {
659 | "version": "1.1.0",
660 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
661 | "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
662 | "dev": true,
663 | "engines": {
664 | "node": "*"
665 | }
666 | },
667 | "node_modules/balanced-match": {
668 | "version": "1.0.2",
669 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
670 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
671 | },
672 | "node_modules/bcrypt": {
673 | "version": "5.1.1",
674 | "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
675 | "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==",
676 | "hasInstallScript": true,
677 | "dependencies": {
678 | "@mapbox/node-pre-gyp": "^1.0.11",
679 | "node-addon-api": "^5.0.0"
680 | },
681 | "engines": {
682 | "node": ">= 10.0.0"
683 | }
684 | },
685 | "node_modules/brace-expansion": {
686 | "version": "1.1.11",
687 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
688 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
689 | "dependencies": {
690 | "balanced-match": "^1.0.0",
691 | "concat-map": "0.0.1"
692 | }
693 | },
694 | "node_modules/braces": {
695 | "version": "3.0.2",
696 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
697 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
698 | "dependencies": {
699 | "fill-range": "^7.0.1"
700 | },
701 | "engines": {
702 | "node": ">=8"
703 | }
704 | },
705 | "node_modules/cac": {
706 | "version": "6.7.14",
707 | "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
708 | "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
709 | "dev": true,
710 | "engines": {
711 | "node": ">=8"
712 | }
713 | },
714 | "node_modules/call-bind": {
715 | "version": "1.0.2",
716 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
717 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
718 | "dependencies": {
719 | "function-bind": "^1.1.1",
720 | "get-intrinsic": "^1.0.2"
721 | },
722 | "funding": {
723 | "url": "https://github.com/sponsors/ljharb"
724 | }
725 | },
726 | "node_modules/chai": {
727 | "version": "4.3.10",
728 | "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz",
729 | "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==",
730 | "dev": true,
731 | "dependencies": {
732 | "assertion-error": "^1.1.0",
733 | "check-error": "^1.0.3",
734 | "deep-eql": "^4.1.3",
735 | "get-func-name": "^2.0.2",
736 | "loupe": "^2.3.6",
737 | "pathval": "^1.1.1",
738 | "type-detect": "^4.0.8"
739 | },
740 | "engines": {
741 | "node": ">=4"
742 | }
743 | },
744 | "node_modules/check-error": {
745 | "version": "1.0.3",
746 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
747 | "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==",
748 | "dev": true,
749 | "dependencies": {
750 | "get-func-name": "^2.0.2"
751 | },
752 | "engines": {
753 | "node": "*"
754 | }
755 | },
756 | "node_modules/chownr": {
757 | "version": "2.0.0",
758 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
759 | "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
760 | "engines": {
761 | "node": ">=10"
762 | }
763 | },
764 | "node_modules/color-support": {
765 | "version": "1.1.3",
766 | "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
767 | "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
768 | "bin": {
769 | "color-support": "bin.js"
770 | }
771 | },
772 | "node_modules/concat-map": {
773 | "version": "0.0.1",
774 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
775 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
776 | },
777 | "node_modules/console-control-strings": {
778 | "version": "1.1.0",
779 | "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
780 | "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="
781 | },
782 | "node_modules/debug": {
783 | "version": "4.3.4",
784 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
785 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
786 | "dependencies": {
787 | "ms": "2.1.2"
788 | },
789 | "engines": {
790 | "node": ">=6.0"
791 | },
792 | "peerDependenciesMeta": {
793 | "supports-color": {
794 | "optional": true
795 | }
796 | }
797 | },
798 | "node_modules/deep-eql": {
799 | "version": "4.1.3",
800 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz",
801 | "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==",
802 | "dev": true,
803 | "dependencies": {
804 | "type-detect": "^4.0.0"
805 | },
806 | "engines": {
807 | "node": ">=6"
808 | }
809 | },
810 | "node_modules/delegates": {
811 | "version": "1.0.0",
812 | "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
813 | "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="
814 | },
815 | "node_modules/detect-libc": {
816 | "version": "2.0.2",
817 | "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
818 | "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==",
819 | "engines": {
820 | "node": ">=8"
821 | }
822 | },
823 | "node_modules/detect-port": {
824 | "version": "1.5.1",
825 | "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.5.1.tgz",
826 | "integrity": "sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ==",
827 | "dependencies": {
828 | "address": "^1.0.1",
829 | "debug": "4"
830 | },
831 | "bin": {
832 | "detect": "bin/detect-port.js",
833 | "detect-port": "bin/detect-port.js"
834 | }
835 | },
836 | "node_modules/diff-sequences": {
837 | "version": "29.6.3",
838 | "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
839 | "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==",
840 | "dev": true,
841 | "engines": {
842 | "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
843 | }
844 | },
845 | "node_modules/emoji-regex": {
846 | "version": "8.0.0",
847 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
848 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
849 | },
850 | "node_modules/esbuild": {
851 | "version": "0.18.20",
852 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
853 | "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
854 | "dev": true,
855 | "hasInstallScript": true,
856 | "bin": {
857 | "esbuild": "bin/esbuild"
858 | },
859 | "engines": {
860 | "node": ">=12"
861 | },
862 | "optionalDependencies": {
863 | "@esbuild/android-arm": "0.18.20",
864 | "@esbuild/android-arm64": "0.18.20",
865 | "@esbuild/android-x64": "0.18.20",
866 | "@esbuild/darwin-arm64": "0.18.20",
867 | "@esbuild/darwin-x64": "0.18.20",
868 | "@esbuild/freebsd-arm64": "0.18.20",
869 | "@esbuild/freebsd-x64": "0.18.20",
870 | "@esbuild/linux-arm": "0.18.20",
871 | "@esbuild/linux-arm64": "0.18.20",
872 | "@esbuild/linux-ia32": "0.18.20",
873 | "@esbuild/linux-loong64": "0.18.20",
874 | "@esbuild/linux-mips64el": "0.18.20",
875 | "@esbuild/linux-ppc64": "0.18.20",
876 | "@esbuild/linux-riscv64": "0.18.20",
877 | "@esbuild/linux-s390x": "0.18.20",
878 | "@esbuild/linux-x64": "0.18.20",
879 | "@esbuild/netbsd-x64": "0.18.20",
880 | "@esbuild/openbsd-x64": "0.18.20",
881 | "@esbuild/sunos-x64": "0.18.20",
882 | "@esbuild/win32-arm64": "0.18.20",
883 | "@esbuild/win32-ia32": "0.18.20",
884 | "@esbuild/win32-x64": "0.18.20"
885 | }
886 | },
887 | "node_modules/fill-range": {
888 | "version": "7.0.1",
889 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
890 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
891 | "dependencies": {
892 | "to-regex-range": "^5.0.1"
893 | },
894 | "engines": {
895 | "node": ">=8"
896 | }
897 | },
898 | "node_modules/fs-minipass": {
899 | "version": "2.1.0",
900 | "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
901 | "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
902 | "dependencies": {
903 | "minipass": "^3.0.0"
904 | },
905 | "engines": {
906 | "node": ">= 8"
907 | }
908 | },
909 | "node_modules/fs-minipass/node_modules/minipass": {
910 | "version": "3.3.6",
911 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
912 | "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
913 | "dependencies": {
914 | "yallist": "^4.0.0"
915 | },
916 | "engines": {
917 | "node": ">=8"
918 | }
919 | },
920 | "node_modules/fs.realpath": {
921 | "version": "1.0.0",
922 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
923 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
924 | },
925 | "node_modules/fsevents": {
926 | "version": "2.3.3",
927 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
928 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
929 | "dev": true,
930 | "hasInstallScript": true,
931 | "optional": true,
932 | "os": [
933 | "darwin"
934 | ],
935 | "engines": {
936 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
937 | }
938 | },
939 | "node_modules/function-bind": {
940 | "version": "1.1.2",
941 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
942 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
943 | "funding": {
944 | "url": "https://github.com/sponsors/ljharb"
945 | }
946 | },
947 | "node_modules/gauge": {
948 | "version": "3.0.2",
949 | "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
950 | "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
951 | "dependencies": {
952 | "aproba": "^1.0.3 || ^2.0.0",
953 | "color-support": "^1.1.2",
954 | "console-control-strings": "^1.0.0",
955 | "has-unicode": "^2.0.1",
956 | "object-assign": "^4.1.1",
957 | "signal-exit": "^3.0.0",
958 | "string-width": "^4.2.3",
959 | "strip-ansi": "^6.0.1",
960 | "wide-align": "^1.1.2"
961 | },
962 | "engines": {
963 | "node": ">=10"
964 | }
965 | },
966 | "node_modules/get-func-name": {
967 | "version": "2.0.2",
968 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
969 | "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
970 | "dev": true,
971 | "engines": {
972 | "node": "*"
973 | }
974 | },
975 | "node_modules/get-intrinsic": {
976 | "version": "1.2.1",
977 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
978 | "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
979 | "dependencies": {
980 | "function-bind": "^1.1.1",
981 | "has": "^1.0.3",
982 | "has-proto": "^1.0.1",
983 | "has-symbols": "^1.0.3"
984 | },
985 | "funding": {
986 | "url": "https://github.com/sponsors/ljharb"
987 | }
988 | },
989 | "node_modules/glob": {
990 | "version": "7.2.3",
991 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
992 | "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
993 | "dependencies": {
994 | "fs.realpath": "^1.0.0",
995 | "inflight": "^1.0.4",
996 | "inherits": "2",
997 | "minimatch": "^3.1.1",
998 | "once": "^1.3.0",
999 | "path-is-absolute": "^1.0.0"
1000 | },
1001 | "engines": {
1002 | "node": "*"
1003 | },
1004 | "funding": {
1005 | "url": "https://github.com/sponsors/isaacs"
1006 | }
1007 | },
1008 | "node_modules/has": {
1009 | "version": "1.0.4",
1010 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz",
1011 | "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==",
1012 | "engines": {
1013 | "node": ">= 0.4.0"
1014 | }
1015 | },
1016 | "node_modules/has-proto": {
1017 | "version": "1.0.1",
1018 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
1019 | "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
1020 | "engines": {
1021 | "node": ">= 0.4"
1022 | },
1023 | "funding": {
1024 | "url": "https://github.com/sponsors/ljharb"
1025 | }
1026 | },
1027 | "node_modules/has-symbols": {
1028 | "version": "1.0.3",
1029 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
1030 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
1031 | "engines": {
1032 | "node": ">= 0.4"
1033 | },
1034 | "funding": {
1035 | "url": "https://github.com/sponsors/ljharb"
1036 | }
1037 | },
1038 | "node_modules/has-unicode": {
1039 | "version": "2.0.1",
1040 | "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
1041 | "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="
1042 | },
1043 | "node_modules/https-proxy-agent": {
1044 | "version": "5.0.1",
1045 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
1046 | "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
1047 | "dependencies": {
1048 | "agent-base": "6",
1049 | "debug": "4"
1050 | },
1051 | "engines": {
1052 | "node": ">= 6"
1053 | }
1054 | },
1055 | "node_modules/inflight": {
1056 | "version": "1.0.6",
1057 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
1058 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
1059 | "dependencies": {
1060 | "once": "^1.3.0",
1061 | "wrappy": "1"
1062 | }
1063 | },
1064 | "node_modules/inherits": {
1065 | "version": "2.0.4",
1066 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
1067 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
1068 | },
1069 | "node_modules/is-fullwidth-code-point": {
1070 | "version": "3.0.0",
1071 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
1072 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
1073 | "engines": {
1074 | "node": ">=8"
1075 | }
1076 | },
1077 | "node_modules/is-number": {
1078 | "version": "7.0.0",
1079 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
1080 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
1081 | "engines": {
1082 | "node": ">=0.12.0"
1083 | }
1084 | },
1085 | "node_modules/jsonc-parser": {
1086 | "version": "3.2.0",
1087 | "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
1088 | "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==",
1089 | "dev": true
1090 | },
1091 | "node_modules/local-pkg": {
1092 | "version": "0.4.3",
1093 | "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz",
1094 | "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==",
1095 | "dev": true,
1096 | "engines": {
1097 | "node": ">=14"
1098 | },
1099 | "funding": {
1100 | "url": "https://github.com/sponsors/antfu"
1101 | }
1102 | },
1103 | "node_modules/loupe": {
1104 | "version": "2.3.7",
1105 | "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz",
1106 | "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==",
1107 | "dev": true,
1108 | "dependencies": {
1109 | "get-func-name": "^2.0.1"
1110 | }
1111 | },
1112 | "node_modules/lru-cache": {
1113 | "version": "6.0.0",
1114 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
1115 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
1116 | "dependencies": {
1117 | "yallist": "^4.0.0"
1118 | },
1119 | "engines": {
1120 | "node": ">=10"
1121 | }
1122 | },
1123 | "node_modules/magic-string": {
1124 | "version": "0.30.5",
1125 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
1126 | "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==",
1127 | "dev": true,
1128 | "dependencies": {
1129 | "@jridgewell/sourcemap-codec": "^1.4.15"
1130 | },
1131 | "engines": {
1132 | "node": ">=12"
1133 | }
1134 | },
1135 | "node_modules/make-dir": {
1136 | "version": "3.1.0",
1137 | "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
1138 | "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
1139 | "dependencies": {
1140 | "semver": "^6.0.0"
1141 | },
1142 | "engines": {
1143 | "node": ">=8"
1144 | },
1145 | "funding": {
1146 | "url": "https://github.com/sponsors/sindresorhus"
1147 | }
1148 | },
1149 | "node_modules/make-dir/node_modules/semver": {
1150 | "version": "6.3.1",
1151 | "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
1152 | "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
1153 | "bin": {
1154 | "semver": "bin/semver.js"
1155 | }
1156 | },
1157 | "node_modules/micromatch": {
1158 | "version": "4.0.5",
1159 | "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
1160 | "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
1161 | "dependencies": {
1162 | "braces": "^3.0.2",
1163 | "picomatch": "^2.3.1"
1164 | },
1165 | "engines": {
1166 | "node": ">=8.6"
1167 | }
1168 | },
1169 | "node_modules/minimatch": {
1170 | "version": "3.1.2",
1171 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
1172 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
1173 | "dependencies": {
1174 | "brace-expansion": "^1.1.7"
1175 | },
1176 | "engines": {
1177 | "node": "*"
1178 | }
1179 | },
1180 | "node_modules/minipass": {
1181 | "version": "5.0.0",
1182 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
1183 | "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
1184 | "engines": {
1185 | "node": ">=8"
1186 | }
1187 | },
1188 | "node_modules/minizlib": {
1189 | "version": "2.1.2",
1190 | "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
1191 | "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
1192 | "dependencies": {
1193 | "minipass": "^3.0.0",
1194 | "yallist": "^4.0.0"
1195 | },
1196 | "engines": {
1197 | "node": ">= 8"
1198 | }
1199 | },
1200 | "node_modules/minizlib/node_modules/minipass": {
1201 | "version": "3.3.6",
1202 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
1203 | "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
1204 | "dependencies": {
1205 | "yallist": "^4.0.0"
1206 | },
1207 | "engines": {
1208 | "node": ">=8"
1209 | }
1210 | },
1211 | "node_modules/mkdirp": {
1212 | "version": "1.0.4",
1213 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
1214 | "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
1215 | "bin": {
1216 | "mkdirp": "bin/cmd.js"
1217 | },
1218 | "engines": {
1219 | "node": ">=10"
1220 | }
1221 | },
1222 | "node_modules/mlly": {
1223 | "version": "1.4.2",
1224 | "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz",
1225 | "integrity": "sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==",
1226 | "dev": true,
1227 | "dependencies": {
1228 | "acorn": "^8.10.0",
1229 | "pathe": "^1.1.1",
1230 | "pkg-types": "^1.0.3",
1231 | "ufo": "^1.3.0"
1232 | }
1233 | },
1234 | "node_modules/ms": {
1235 | "version": "2.1.2",
1236 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
1237 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
1238 | },
1239 | "node_modules/nanoid": {
1240 | "version": "3.3.6",
1241 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
1242 | "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
1243 | "dev": true,
1244 | "funding": [
1245 | {
1246 | "type": "github",
1247 | "url": "https://github.com/sponsors/ai"
1248 | }
1249 | ],
1250 | "bin": {
1251 | "nanoid": "bin/nanoid.cjs"
1252 | },
1253 | "engines": {
1254 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1255 | }
1256 | },
1257 | "node_modules/node-addon-api": {
1258 | "version": "5.1.0",
1259 | "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
1260 | "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
1261 | },
1262 | "node_modules/node-fetch": {
1263 | "version": "2.7.0",
1264 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
1265 | "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
1266 | "dependencies": {
1267 | "whatwg-url": "^5.0.0"
1268 | },
1269 | "engines": {
1270 | "node": "4.x || >=6.0.0"
1271 | },
1272 | "peerDependencies": {
1273 | "encoding": "^0.1.0"
1274 | },
1275 | "peerDependenciesMeta": {
1276 | "encoding": {
1277 | "optional": true
1278 | }
1279 | }
1280 | },
1281 | "node_modules/nopt": {
1282 | "version": "5.0.0",
1283 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
1284 | "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
1285 | "dependencies": {
1286 | "abbrev": "1"
1287 | },
1288 | "bin": {
1289 | "nopt": "bin/nopt.js"
1290 | },
1291 | "engines": {
1292 | "node": ">=6"
1293 | }
1294 | },
1295 | "node_modules/npmlog": {
1296 | "version": "5.0.1",
1297 | "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
1298 | "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
1299 | "dependencies": {
1300 | "are-we-there-yet": "^2.0.0",
1301 | "console-control-strings": "^1.1.0",
1302 | "gauge": "^3.0.0",
1303 | "set-blocking": "^2.0.0"
1304 | }
1305 | },
1306 | "node_modules/object-assign": {
1307 | "version": "4.1.1",
1308 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
1309 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1310 | "engines": {
1311 | "node": ">=0.10.0"
1312 | }
1313 | },
1314 | "node_modules/object-inspect": {
1315 | "version": "1.13.0",
1316 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.0.tgz",
1317 | "integrity": "sha512-HQ4J+ic8hKrgIt3mqk6cVOVrW2ozL4KdvHlqpBv9vDYWx9ysAgENAdvy4FoGF+KFdhR7nQTNm5J0ctAeOwn+3g==",
1318 | "funding": {
1319 | "url": "https://github.com/sponsors/ljharb"
1320 | }
1321 | },
1322 | "node_modules/once": {
1323 | "version": "1.4.0",
1324 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
1325 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
1326 | "dependencies": {
1327 | "wrappy": "1"
1328 | }
1329 | },
1330 | "node_modules/p-limit": {
1331 | "version": "4.0.0",
1332 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz",
1333 | "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==",
1334 | "dev": true,
1335 | "dependencies": {
1336 | "yocto-queue": "^1.0.0"
1337 | },
1338 | "engines": {
1339 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
1340 | },
1341 | "funding": {
1342 | "url": "https://github.com/sponsors/sindresorhus"
1343 | }
1344 | },
1345 | "node_modules/path-is-absolute": {
1346 | "version": "1.0.1",
1347 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
1348 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
1349 | "engines": {
1350 | "node": ">=0.10.0"
1351 | }
1352 | },
1353 | "node_modules/pathe": {
1354 | "version": "1.1.1",
1355 | "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz",
1356 | "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==",
1357 | "dev": true
1358 | },
1359 | "node_modules/pathval": {
1360 | "version": "1.1.1",
1361 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
1362 | "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
1363 | "dev": true,
1364 | "engines": {
1365 | "node": "*"
1366 | }
1367 | },
1368 | "node_modules/picocolors": {
1369 | "version": "1.0.0",
1370 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
1371 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
1372 | },
1373 | "node_modules/picomatch": {
1374 | "version": "2.3.1",
1375 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
1376 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
1377 | "engines": {
1378 | "node": ">=8.6"
1379 | },
1380 | "funding": {
1381 | "url": "https://github.com/sponsors/jonschlinkert"
1382 | }
1383 | },
1384 | "node_modules/pkg-types": {
1385 | "version": "1.0.3",
1386 | "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz",
1387 | "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==",
1388 | "dev": true,
1389 | "dependencies": {
1390 | "jsonc-parser": "^3.2.0",
1391 | "mlly": "^1.2.0",
1392 | "pathe": "^1.1.0"
1393 | }
1394 | },
1395 | "node_modules/postcss": {
1396 | "version": "8.4.31",
1397 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
1398 | "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
1399 | "dev": true,
1400 | "funding": [
1401 | {
1402 | "type": "opencollective",
1403 | "url": "https://opencollective.com/postcss/"
1404 | },
1405 | {
1406 | "type": "tidelift",
1407 | "url": "https://tidelift.com/funding/github/npm/postcss"
1408 | },
1409 | {
1410 | "type": "github",
1411 | "url": "https://github.com/sponsors/ai"
1412 | }
1413 | ],
1414 | "dependencies": {
1415 | "nanoid": "^3.3.6",
1416 | "picocolors": "^1.0.0",
1417 | "source-map-js": "^1.0.2"
1418 | },
1419 | "engines": {
1420 | "node": "^10 || ^12 || >=14"
1421 | }
1422 | },
1423 | "node_modules/prettier": {
1424 | "version": "3.0.3",
1425 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz",
1426 | "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==",
1427 | "dev": true,
1428 | "bin": {
1429 | "prettier": "bin/prettier.cjs"
1430 | },
1431 | "engines": {
1432 | "node": ">=14"
1433 | },
1434 | "funding": {
1435 | "url": "https://github.com/prettier/prettier?sponsor=1"
1436 | }
1437 | },
1438 | "node_modules/prettier-plugin-organize-imports": {
1439 | "version": "3.2.3",
1440 | "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.3.tgz",
1441 | "integrity": "sha512-KFvk8C/zGyvUaE3RvxN2MhCLwzV6OBbFSkwZ2OamCrs9ZY4i5L77jQ/w4UmUr+lqX8qbaqVq6bZZkApn+IgJSg==",
1442 | "dev": true,
1443 | "peerDependencies": {
1444 | "@volar/vue-language-plugin-pug": "^1.0.4",
1445 | "@volar/vue-typescript": "^1.0.4",
1446 | "prettier": ">=2.0",
1447 | "typescript": ">=2.9"
1448 | },
1449 | "peerDependenciesMeta": {
1450 | "@volar/vue-language-plugin-pug": {
1451 | "optional": true
1452 | },
1453 | "@volar/vue-typescript": {
1454 | "optional": true
1455 | }
1456 | }
1457 | },
1458 | "node_modules/pretty-format": {
1459 | "version": "29.7.0",
1460 | "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
1461 | "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
1462 | "dev": true,
1463 | "dependencies": {
1464 | "@jest/schemas": "^29.6.3",
1465 | "ansi-styles": "^5.0.0",
1466 | "react-is": "^18.0.0"
1467 | },
1468 | "engines": {
1469 | "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
1470 | }
1471 | },
1472 | "node_modules/qs": {
1473 | "version": "6.11.2",
1474 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz",
1475 | "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==",
1476 | "dependencies": {
1477 | "side-channel": "^1.0.4"
1478 | },
1479 | "engines": {
1480 | "node": ">=0.6"
1481 | },
1482 | "funding": {
1483 | "url": "https://github.com/sponsors/ljharb"
1484 | }
1485 | },
1486 | "node_modules/react-is": {
1487 | "version": "18.2.0",
1488 | "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
1489 | "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
1490 | "dev": true
1491 | },
1492 | "node_modules/readable-stream": {
1493 | "version": "3.6.2",
1494 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
1495 | "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
1496 | "dependencies": {
1497 | "inherits": "^2.0.3",
1498 | "string_decoder": "^1.1.1",
1499 | "util-deprecate": "^1.0.1"
1500 | },
1501 | "engines": {
1502 | "node": ">= 6"
1503 | }
1504 | },
1505 | "node_modules/readdirp": {
1506 | "version": "3.6.0",
1507 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
1508 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
1509 | "dependencies": {
1510 | "picomatch": "^2.2.1"
1511 | },
1512 | "engines": {
1513 | "node": ">=8.10.0"
1514 | }
1515 | },
1516 | "node_modules/rimraf": {
1517 | "version": "3.0.2",
1518 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
1519 | "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
1520 | "dependencies": {
1521 | "glob": "^7.1.3"
1522 | },
1523 | "bin": {
1524 | "rimraf": "bin.js"
1525 | },
1526 | "funding": {
1527 | "url": "https://github.com/sponsors/isaacs"
1528 | }
1529 | },
1530 | "node_modules/rollup": {
1531 | "version": "3.29.4",
1532 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
1533 | "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
1534 | "dev": true,
1535 | "bin": {
1536 | "rollup": "dist/bin/rollup"
1537 | },
1538 | "engines": {
1539 | "node": ">=14.18.0",
1540 | "npm": ">=8.0.0"
1541 | },
1542 | "optionalDependencies": {
1543 | "fsevents": "~2.3.2"
1544 | }
1545 | },
1546 | "node_modules/safe-buffer": {
1547 | "version": "5.2.1",
1548 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1549 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1550 | "funding": [
1551 | {
1552 | "type": "github",
1553 | "url": "https://github.com/sponsors/feross"
1554 | },
1555 | {
1556 | "type": "patreon",
1557 | "url": "https://www.patreon.com/feross"
1558 | },
1559 | {
1560 | "type": "consulting",
1561 | "url": "https://feross.org/support"
1562 | }
1563 | ]
1564 | },
1565 | "node_modules/semver": {
1566 | "version": "7.5.4",
1567 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
1568 | "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
1569 | "dependencies": {
1570 | "lru-cache": "^6.0.0"
1571 | },
1572 | "bin": {
1573 | "semver": "bin/semver.js"
1574 | },
1575 | "engines": {
1576 | "node": ">=10"
1577 | }
1578 | },
1579 | "node_modules/set-blocking": {
1580 | "version": "2.0.0",
1581 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
1582 | "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
1583 | },
1584 | "node_modules/side-channel": {
1585 | "version": "1.0.4",
1586 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
1587 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
1588 | "dependencies": {
1589 | "call-bind": "^1.0.0",
1590 | "get-intrinsic": "^1.0.2",
1591 | "object-inspect": "^1.9.0"
1592 | },
1593 | "funding": {
1594 | "url": "https://github.com/sponsors/ljharb"
1595 | }
1596 | },
1597 | "node_modules/siginfo": {
1598 | "version": "2.0.0",
1599 | "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
1600 | "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
1601 | "dev": true
1602 | },
1603 | "node_modules/signal-exit": {
1604 | "version": "3.0.7",
1605 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
1606 | "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
1607 | },
1608 | "node_modules/source-map-js": {
1609 | "version": "1.0.2",
1610 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
1611 | "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
1612 | "dev": true,
1613 | "engines": {
1614 | "node": ">=0.10.0"
1615 | }
1616 | },
1617 | "node_modules/stackback": {
1618 | "version": "0.0.2",
1619 | "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
1620 | "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
1621 | "dev": true
1622 | },
1623 | "node_modules/std-env": {
1624 | "version": "3.4.3",
1625 | "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.4.3.tgz",
1626 | "integrity": "sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==",
1627 | "dev": true
1628 | },
1629 | "node_modules/string_decoder": {
1630 | "version": "1.3.0",
1631 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
1632 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
1633 | "dependencies": {
1634 | "safe-buffer": "~5.2.0"
1635 | }
1636 | },
1637 | "node_modules/string-width": {
1638 | "version": "4.2.3",
1639 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
1640 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
1641 | "dependencies": {
1642 | "emoji-regex": "^8.0.0",
1643 | "is-fullwidth-code-point": "^3.0.0",
1644 | "strip-ansi": "^6.0.1"
1645 | },
1646 | "engines": {
1647 | "node": ">=8"
1648 | }
1649 | },
1650 | "node_modules/strip-ansi": {
1651 | "version": "6.0.1",
1652 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
1653 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
1654 | "dependencies": {
1655 | "ansi-regex": "^5.0.1"
1656 | },
1657 | "engines": {
1658 | "node": ">=8"
1659 | }
1660 | },
1661 | "node_modules/strip-literal": {
1662 | "version": "1.3.0",
1663 | "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz",
1664 | "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==",
1665 | "dev": true,
1666 | "dependencies": {
1667 | "acorn": "^8.10.0"
1668 | },
1669 | "funding": {
1670 | "url": "https://github.com/sponsors/antfu"
1671 | }
1672 | },
1673 | "node_modules/tar": {
1674 | "version": "6.2.0",
1675 | "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
1676 | "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==",
1677 | "dependencies": {
1678 | "chownr": "^2.0.0",
1679 | "fs-minipass": "^2.0.0",
1680 | "minipass": "^5.0.0",
1681 | "minizlib": "^2.1.1",
1682 | "mkdirp": "^1.0.3",
1683 | "yallist": "^4.0.0"
1684 | },
1685 | "engines": {
1686 | "node": ">=10"
1687 | }
1688 | },
1689 | "node_modules/tinybench": {
1690 | "version": "2.5.1",
1691 | "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.1.tgz",
1692 | "integrity": "sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==",
1693 | "dev": true
1694 | },
1695 | "node_modules/tinypool": {
1696 | "version": "0.7.0",
1697 | "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz",
1698 | "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==",
1699 | "dev": true,
1700 | "engines": {
1701 | "node": ">=14.0.0"
1702 | }
1703 | },
1704 | "node_modules/tinyspy": {
1705 | "version": "2.2.0",
1706 | "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz",
1707 | "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==",
1708 | "dev": true,
1709 | "engines": {
1710 | "node": ">=14.0.0"
1711 | }
1712 | },
1713 | "node_modules/to-regex-range": {
1714 | "version": "5.0.1",
1715 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
1716 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
1717 | "dependencies": {
1718 | "is-number": "^7.0.0"
1719 | },
1720 | "engines": {
1721 | "node": ">=8.0"
1722 | }
1723 | },
1724 | "node_modules/tr46": {
1725 | "version": "0.0.3",
1726 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
1727 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
1728 | },
1729 | "node_modules/type-detect": {
1730 | "version": "4.0.8",
1731 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
1732 | "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
1733 | "dev": true,
1734 | "engines": {
1735 | "node": ">=4"
1736 | }
1737 | },
1738 | "node_modules/typescript": {
1739 | "version": "5.2.2",
1740 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
1741 | "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
1742 | "dev": true,
1743 | "bin": {
1744 | "tsc": "bin/tsc",
1745 | "tsserver": "bin/tsserver"
1746 | },
1747 | "engines": {
1748 | "node": ">=14.17"
1749 | }
1750 | },
1751 | "node_modules/ufo": {
1752 | "version": "1.3.1",
1753 | "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.1.tgz",
1754 | "integrity": "sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==",
1755 | "dev": true
1756 | },
1757 | "node_modules/undici-types": {
1758 | "version": "5.25.3",
1759 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz",
1760 | "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==",
1761 | "dev": true
1762 | },
1763 | "node_modules/urlpattern-polyfill": {
1764 | "version": "9.0.0",
1765 | "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-9.0.0.tgz",
1766 | "integrity": "sha512-WHN8KDQblxd32odxeIgo83rdVDE2bvdkb86it7bMhYZwWKJz0+O0RK/eZiHYnM+zgt/U7hAHOlCQGfjjvSkw2g=="
1767 | },
1768 | "node_modules/util-deprecate": {
1769 | "version": "1.0.2",
1770 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
1771 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
1772 | },
1773 | "node_modules/vite": {
1774 | "version": "4.4.11",
1775 | "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.11.tgz",
1776 | "integrity": "sha512-ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5A==",
1777 | "dev": true,
1778 | "dependencies": {
1779 | "esbuild": "^0.18.10",
1780 | "postcss": "^8.4.27",
1781 | "rollup": "^3.27.1"
1782 | },
1783 | "bin": {
1784 | "vite": "bin/vite.js"
1785 | },
1786 | "engines": {
1787 | "node": "^14.18.0 || >=16.0.0"
1788 | },
1789 | "funding": {
1790 | "url": "https://github.com/vitejs/vite?sponsor=1"
1791 | },
1792 | "optionalDependencies": {
1793 | "fsevents": "~2.3.2"
1794 | },
1795 | "peerDependencies": {
1796 | "@types/node": ">= 14",
1797 | "less": "*",
1798 | "lightningcss": "^1.21.0",
1799 | "sass": "*",
1800 | "stylus": "*",
1801 | "sugarss": "*",
1802 | "terser": "^5.4.0"
1803 | },
1804 | "peerDependenciesMeta": {
1805 | "@types/node": {
1806 | "optional": true
1807 | },
1808 | "less": {
1809 | "optional": true
1810 | },
1811 | "lightningcss": {
1812 | "optional": true
1813 | },
1814 | "sass": {
1815 | "optional": true
1816 | },
1817 | "stylus": {
1818 | "optional": true
1819 | },
1820 | "sugarss": {
1821 | "optional": true
1822 | },
1823 | "terser": {
1824 | "optional": true
1825 | }
1826 | }
1827 | },
1828 | "node_modules/vite-node": {
1829 | "version": "0.34.6",
1830 | "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.6.tgz",
1831 | "integrity": "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==",
1832 | "dev": true,
1833 | "dependencies": {
1834 | "cac": "^6.7.14",
1835 | "debug": "^4.3.4",
1836 | "mlly": "^1.4.0",
1837 | "pathe": "^1.1.1",
1838 | "picocolors": "^1.0.0",
1839 | "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0"
1840 | },
1841 | "bin": {
1842 | "vite-node": "vite-node.mjs"
1843 | },
1844 | "engines": {
1845 | "node": ">=v14.18.0"
1846 | },
1847 | "funding": {
1848 | "url": "https://opencollective.com/vitest"
1849 | }
1850 | },
1851 | "node_modules/vitest": {
1852 | "version": "0.34.6",
1853 | "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz",
1854 | "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==",
1855 | "dev": true,
1856 | "dependencies": {
1857 | "@types/chai": "^4.3.5",
1858 | "@types/chai-subset": "^1.3.3",
1859 | "@types/node": "*",
1860 | "@vitest/expect": "0.34.6",
1861 | "@vitest/runner": "0.34.6",
1862 | "@vitest/snapshot": "0.34.6",
1863 | "@vitest/spy": "0.34.6",
1864 | "@vitest/utils": "0.34.6",
1865 | "acorn": "^8.9.0",
1866 | "acorn-walk": "^8.2.0",
1867 | "cac": "^6.7.14",
1868 | "chai": "^4.3.10",
1869 | "debug": "^4.3.4",
1870 | "local-pkg": "^0.4.3",
1871 | "magic-string": "^0.30.1",
1872 | "pathe": "^1.1.1",
1873 | "picocolors": "^1.0.0",
1874 | "std-env": "^3.3.3",
1875 | "strip-literal": "^1.0.1",
1876 | "tinybench": "^2.5.0",
1877 | "tinypool": "^0.7.0",
1878 | "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0",
1879 | "vite-node": "0.34.6",
1880 | "why-is-node-running": "^2.2.2"
1881 | },
1882 | "bin": {
1883 | "vitest": "vitest.mjs"
1884 | },
1885 | "engines": {
1886 | "node": ">=v14.18.0"
1887 | },
1888 | "funding": {
1889 | "url": "https://opencollective.com/vitest"
1890 | },
1891 | "peerDependencies": {
1892 | "@edge-runtime/vm": "*",
1893 | "@vitest/browser": "*",
1894 | "@vitest/ui": "*",
1895 | "happy-dom": "*",
1896 | "jsdom": "*",
1897 | "playwright": "*",
1898 | "safaridriver": "*",
1899 | "webdriverio": "*"
1900 | },
1901 | "peerDependenciesMeta": {
1902 | "@edge-runtime/vm": {
1903 | "optional": true
1904 | },
1905 | "@vitest/browser": {
1906 | "optional": true
1907 | },
1908 | "@vitest/ui": {
1909 | "optional": true
1910 | },
1911 | "happy-dom": {
1912 | "optional": true
1913 | },
1914 | "jsdom": {
1915 | "optional": true
1916 | },
1917 | "playwright": {
1918 | "optional": true
1919 | },
1920 | "safaridriver": {
1921 | "optional": true
1922 | },
1923 | "webdriverio": {
1924 | "optional": true
1925 | }
1926 | }
1927 | },
1928 | "node_modules/webidl-conversions": {
1929 | "version": "3.0.1",
1930 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
1931 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
1932 | },
1933 | "node_modules/whatwg-url": {
1934 | "version": "5.0.0",
1935 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
1936 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
1937 | "dependencies": {
1938 | "tr46": "~0.0.3",
1939 | "webidl-conversions": "^3.0.0"
1940 | }
1941 | },
1942 | "node_modules/why-is-node-running": {
1943 | "version": "2.2.2",
1944 | "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz",
1945 | "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==",
1946 | "dev": true,
1947 | "dependencies": {
1948 | "siginfo": "^2.0.0",
1949 | "stackback": "0.0.2"
1950 | },
1951 | "bin": {
1952 | "why-is-node-running": "cli.js"
1953 | },
1954 | "engines": {
1955 | "node": ">=8"
1956 | }
1957 | },
1958 | "node_modules/wide-align": {
1959 | "version": "1.1.5",
1960 | "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
1961 | "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
1962 | "dependencies": {
1963 | "string-width": "^1.0.2 || 2 || 3 || 4"
1964 | }
1965 | },
1966 | "node_modules/wrappy": {
1967 | "version": "1.0.2",
1968 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
1969 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
1970 | },
1971 | "node_modules/yallist": {
1972 | "version": "4.0.0",
1973 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
1974 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
1975 | },
1976 | "node_modules/yargs-parser": {
1977 | "version": "21.1.1",
1978 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
1979 | "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
1980 | "engines": {
1981 | "node": ">=12"
1982 | }
1983 | },
1984 | "node_modules/yocto-queue": {
1985 | "version": "1.0.0",
1986 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz",
1987 | "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==",
1988 | "dev": true,
1989 | "engines": {
1990 | "node": ">=12.20"
1991 | },
1992 | "funding": {
1993 | "url": "https://github.com/sponsors/sindresorhus"
1994 | }
1995 | }
1996 | }
1997 | }
1998 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@xieyuheng/x-server",
3 | "version": "0.0.15",
4 | "repository": "github:xieyuheng/x-server",
5 | "main": "./lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "bin": {
10 | "x-server": "bin/x-server.js"
11 | },
12 | "scripts": {
13 | "build": "rm -rf lib; tsc",
14 | "build:watch": "rm -rf lib; tsc --watch",
15 | "test:ts": "vitest --dir src --threads false --run",
16 | "test:watch": "vitest --dir src --threads false",
17 | "test": "npm run test:ts",
18 | "format": "prettier src --write"
19 | },
20 | "dependencies": {
21 | "@xieyuheng/command-line": "^0.0.13",
22 | "@xieyuheng/ty": "^0.1.26",
23 | "bcrypt": "^5.1.1",
24 | "detect-port": "^1.5.1",
25 | "micromatch": "^4.0.5",
26 | "picocolors": "^1.0.0",
27 | "qs": "^6.11.2",
28 | "readdirp": "^3.6.0",
29 | "urlpattern-polyfill": "^9.0.0"
30 | },
31 | "devDependencies": {
32 | "@types/bcrypt": "^5.0.0",
33 | "@types/detect-port": "^1.3.3",
34 | "@types/micromatch": "^4.0.3",
35 | "@types/node": "^20.8.6",
36 | "@types/qs": "^6.9.8",
37 | "prettier": "^3.0.3",
38 | "prettier-plugin-organize-imports": "^3.2.3",
39 | "typescript": "^5.2.2",
40 | "vite": "^4.4.11",
41 | "vitest": "^0.34.6"
42 | },
43 | "license": "GPL-3.0-or-later"
44 | }
45 |
--------------------------------------------------------------------------------
/src/command-line/commands/Default.ts:
--------------------------------------------------------------------------------
1 | import { Command, CommandRunner } from "@xieyuheng/command-line"
2 | import { ty } from "@xieyuheng/ty"
3 | import * as Commands from "."
4 | import { packageJson } from "../../utils/node/packageJson"
5 |
6 | type Args = {}
7 | type Opts = { version?: boolean }
8 |
9 | export class Default extends Command {
10 | name = "default"
11 |
12 | description = "Print help message"
13 |
14 | args = {}
15 | opts = { version: ty.optional(ty.boolean()) }
16 | alias = { version: ["v"] }
17 |
18 | async execute(argv: Args & Opts, runner: CommandRunner): Promise {
19 | if (argv["version"]) {
20 | const { version } = packageJson()
21 | console.log(version)
22 | return
23 | }
24 |
25 | const command = new Commands.CommonHelp()
26 | await command.execute({}, runner)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/command-line/commands/ServeSubdomain.ts:
--------------------------------------------------------------------------------
1 | import { Command, CommandRunner } from "@xieyuheng/command-line"
2 | import { ty } from "@xieyuheng/ty"
3 | import { dirname } from "node:path"
4 | import { startServer } from "../../servers/subdomain/startServer"
5 | import { LoggerName, LoggerNameSchema, changeLogger } from "../../utils/log"
6 | import { pathIsFile } from "../../utils/node/pathIsFile"
7 | import { mergeWebsiteConfigs } from "../../website/mergeWebsiteConfigs"
8 | import { readWebsiteConfigFile } from "../../website/readWebsiteConfigFile"
9 | import { readWebsiteConfigFileOrDefault } from "../../website/readWebsiteConfigFileOrDefault"
10 | import { websiteConfigFromCommandLineOptions } from "../../website/websiteConfigFromCommandLineOptions"
11 |
12 | type Args = { path: string }
13 | type Opts = {
14 | hostname?: string
15 | port?: number
16 | "tls-cert"?: string
17 | "tls-key"?: string
18 | cors?: boolean
19 | "redirect-not-found-to"?: string
20 | "cache-control-pattern"?: string | Array
21 | "logger-name"?: LoggerName
22 | }
23 |
24 | export class ServeSubdomain extends Command {
25 | name = "serve:subdomain"
26 |
27 | description = "Serve many websites using subdomain-based routing"
28 |
29 | args = { path: ty.string() }
30 | opts = {
31 | hostname: ty.optional(ty.string()),
32 | port: ty.optional(ty.number()),
33 | "tls-cert": ty.optional(ty.string()),
34 | "tls-key": ty.optional(ty.string()),
35 | cors: ty.optional(ty.boolean()),
36 | "redirect-not-found-to": ty.optional(ty.string()),
37 | "cache-control-pattern": ty.optional(
38 | ty.union(ty.string(), ty.array(ty.string())),
39 | ),
40 | "logger-name": ty.optional(LoggerNameSchema),
41 | }
42 |
43 | // prettier-ignore
44 | help(runner: CommandRunner): string {
45 | const { blue } = this.colors
46 |
47 | return [
48 | `The ${blue(this.name)} command takes a website.json config file,`,
49 | `and serve the directory that contains the config file`,
50 | `using subdomain-based routing.`,
51 | ``,
52 | blue(` ${runner.name} ${this.name} /websites/website.json`),
53 | ``,
54 | ].join("\n")
55 | }
56 |
57 | async execute(argv: Args & Opts): Promise {
58 | changeLogger(argv["logger-name"] || "pretty-line")
59 |
60 | if (await pathIsFile(argv.path)) {
61 | const configFile = argv.path
62 | const config = mergeWebsiteConfigs([
63 | await readWebsiteConfigFile(configFile),
64 | websiteConfigFromCommandLineOptions(argv),
65 | ])
66 |
67 | const path = dirname(argv.path)
68 | await startServer(path, config)
69 | } else {
70 | const configFile = `${argv.path}/website.json`
71 | const config = mergeWebsiteConfigs([
72 | await readWebsiteConfigFileOrDefault(configFile),
73 | websiteConfigFromCommandLineOptions(argv),
74 | ])
75 | const { path } = argv
76 | await startServer(path, config)
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/command-line/commands/ServeWebsite.ts:
--------------------------------------------------------------------------------
1 | import { Command, CommandRunner } from "@xieyuheng/command-line"
2 | import { ty } from "@xieyuheng/ty"
3 | import { dirname } from "node:path"
4 | import { startServer } from "../../servers/website/startServer"
5 | import { LoggerName, LoggerNameSchema, changeLogger } from "../../utils/log"
6 | import { pathIsFile } from "../../utils/node/pathIsFile"
7 | import { mergeWebsiteConfigs } from "../../website/mergeWebsiteConfigs"
8 | import { readWebsiteConfigFile } from "../../website/readWebsiteConfigFile"
9 | import { readWebsiteConfigFileOrDefault } from "../../website/readWebsiteConfigFileOrDefault"
10 | import { websiteConfigFromCommandLineOptions } from "../../website/websiteConfigFromCommandLineOptions"
11 |
12 | type Args = { path: string }
13 | type Opts = {
14 | hostname?: string
15 | port?: number
16 | "tls-cert"?: string
17 | "tls-key"?: string
18 | cors?: boolean
19 | "redirect-not-found-to"?: string
20 | "cache-control-pattern"?: string | Array
21 | "logger-name"?: LoggerName
22 | }
23 |
24 | export class ServeWebsite extends Command {
25 | name = "serve:website"
26 |
27 | description = "Serve a website"
28 |
29 | args = { path: ty.string() }
30 | opts = {
31 | hostname: ty.optional(ty.string()),
32 | port: ty.optional(ty.number()),
33 | "tls-cert": ty.optional(ty.string()),
34 | "tls-key": ty.optional(ty.string()),
35 | cors: ty.optional(ty.boolean()),
36 | "redirect-not-found-to": ty.optional(ty.string()),
37 | "cache-control-pattern": ty.optional(
38 | ty.union(ty.string(), ty.array(ty.string())),
39 | ),
40 | "logger-name": ty.optional(LoggerNameSchema),
41 | }
42 |
43 | // prettier-ignore
44 | help(runner: CommandRunner): string {
45 | const { blue } = this.colors
46 |
47 | return [
48 | `The ${blue(this.name)} command takes a path`,
49 | `to a website directory or to a ${blue('website.json')} file,`,
50 | `and serve it as a website.`,
51 | ``,
52 | blue(` ${runner.name} ${this.name} dist`),
53 | ``,
54 | blue(` ${runner.name} ${this.name} dist/website.json`),
55 | ``,
56 | ].join("\n")
57 | }
58 |
59 | async execute(argv: Args & Opts): Promise {
60 | changeLogger(argv["logger-name"] || "pretty-line")
61 |
62 | if (await pathIsFile(argv.path)) {
63 | const configFile = argv.path
64 | const config = mergeWebsiteConfigs([
65 | await readWebsiteConfigFile(configFile),
66 | websiteConfigFromCommandLineOptions(argv),
67 | ])
68 | const path = dirname(argv.path)
69 | await startServer(path, config)
70 | } else {
71 | const configFile = `${argv.path}/website.json`
72 | const config = mergeWebsiteConfigs([
73 | await readWebsiteConfigFileOrDefault(configFile),
74 | websiteConfigFromCommandLineOptions(argv),
75 | ])
76 | const { path } = argv
77 | await startServer(path, config)
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/command-line/commands/index.ts:
--------------------------------------------------------------------------------
1 | export * from "@xieyuheng/command-line/lib/commands"
2 | export * from "./Default"
3 | export * from "./ServeSubdomain"
4 | export * from "./ServeWebsite"
5 |
--------------------------------------------------------------------------------
/src/command-line/index.ts:
--------------------------------------------------------------------------------
1 | import { CommandRunner, CommandRunners } from "@xieyuheng/command-line"
2 | import * as Commands from "./commands"
3 |
4 | export function createCommandRunner(): CommandRunner {
5 | return new CommandRunners.CommonCommandRunner({
6 | defaultCommand: new Commands.Default(),
7 | commands: [
8 | new Commands.CommonHelp(),
9 | new Commands.ServeWebsite(),
10 | new Commands.ServeSubdomain(),
11 | ],
12 | })
13 | }
14 |
--------------------------------------------------------------------------------
/src/errors/AlreadyExists.ts:
--------------------------------------------------------------------------------
1 | export class AlreadyExists extends Error {
2 | constructor(message: string) {
3 | super(message)
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/errors/NotFound.ts:
--------------------------------------------------------------------------------
1 | export class NotFound extends Error {
2 | constructor(message: string) {
3 | super(message)
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/errors/Processing.ts:
--------------------------------------------------------------------------------
1 | export class Processing extends Error {
2 | constructor(message: string) {
3 | super(message)
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/errors/RevisionMismatch.ts:
--------------------------------------------------------------------------------
1 | export class RevisionMismatch extends Error {
2 | constructor(message: string) {
3 | super(message)
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/errors/Unauthorized.ts:
--------------------------------------------------------------------------------
1 | export class Unauthorized extends Error {
2 | constructor(message: string) {
3 | super(message)
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/errors/Unprocessable.ts:
--------------------------------------------------------------------------------
1 | export class Unprocessable extends Error {
2 | constructor(message: string) {
3 | super(message)
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/server/LoggerOptions.ts:
--------------------------------------------------------------------------------
1 | import { Schema, ty } from "@xieyuheng/ty"
2 | import { LoggerName, LoggerNameSchema } from "../utils/log"
3 |
4 | export type LoggerOptions = {
5 | name?: LoggerName
6 | disableRequestLogging?: boolean
7 | }
8 |
9 | export const LoggerOptionsSchema: Schema = ty.object({
10 | name: ty.optional(LoggerNameSchema),
11 | disableRequestLogging: ty.optional(ty.boolean()),
12 | })
13 |
--------------------------------------------------------------------------------
/src/server/RequestHandler.ts:
--------------------------------------------------------------------------------
1 | import type Http from "node:http"
2 | import type { Json } from "../utils/Json"
3 |
4 | export type RequestHandler = (
5 | ctx: Context,
6 | request: Http.IncomingMessage,
7 | response: Http.ServerResponse,
8 | ) => Promise
9 |
--------------------------------------------------------------------------------
/src/server/RequestListener.ts:
--------------------------------------------------------------------------------
1 | import type Http from "node:http"
2 |
3 | export type RequestListener = (
4 | request: Http.IncomingMessage,
5 | response: Http.ServerResponse,
6 | ) => Promise
7 |
--------------------------------------------------------------------------------
/src/server/ServerOptions.ts:
--------------------------------------------------------------------------------
1 | import { Schema, ty } from "@xieyuheng/ty"
2 | import { TlsOptions, TlsOptionsSchema } from "./TlsOptions"
3 |
4 | export type ServerOptions = {
5 | hostname?: string
6 | port?: number
7 | startingPort?: number
8 | tls?: TlsOptions
9 | }
10 |
11 | export const ServerOptionsSchema: Schema = ty.object({
12 | hostname: ty.optional(ty.string()),
13 | port: ty.optional(ty.number()),
14 | startingPort: ty.optional(ty.number()),
15 | tls: ty.optional(TlsOptionsSchema),
16 | })
17 |
--------------------------------------------------------------------------------
/src/server/TlsOptions.ts:
--------------------------------------------------------------------------------
1 | import { Schema, ty } from "@xieyuheng/ty"
2 |
3 | export type TlsOptions = {
4 | cert: string
5 | key: string
6 | }
7 |
8 | export const TlsOptionsSchema: Schema = ty.object({
9 | cert: ty.string(),
10 | key: ty.string(),
11 | })
12 |
--------------------------------------------------------------------------------
/src/server/createRequestListener.ts:
--------------------------------------------------------------------------------
1 | import { isReport } from "@xieyuheng/ty"
2 | import { AlreadyExists } from "../errors/AlreadyExists"
3 | import { NotFound } from "../errors/NotFound"
4 | import { Processing } from "../errors/Processing"
5 | import { RevisionMismatch } from "../errors/RevisionMismatch"
6 | import { Unauthorized } from "../errors/Unauthorized"
7 | import { Unprocessable } from "../errors/Unprocessable"
8 | import { log } from "../utils/log"
9 | import { responseSetHeaders } from "../utils/node/responseSetHeaders"
10 | import { responseSetStatus } from "../utils/node/responseSetStatus"
11 | import { LoggerOptions } from "./LoggerOptions"
12 | import { RequestHandler } from "./RequestHandler"
13 | import { RequestListener } from "./RequestListener"
14 |
15 | export function createRequestListener(options: {
16 | ctx: Context
17 | handle: RequestHandler
18 | logger?: LoggerOptions
19 | }): RequestListener {
20 | const { ctx, handle } = options
21 |
22 | return async (request, response) => {
23 | const withLog = !options.logger?.disableRequestLogging
24 |
25 | try {
26 | const body = await handle(ctx, request, response)
27 |
28 | if (response.writableEnded) {
29 | return
30 | }
31 |
32 | if (body === undefined) {
33 | const code = 204
34 | responseSetStatus(response, { code })
35 | responseSetHeaders(response, {
36 | "content-type": "application/json",
37 | "access-control-allow-origin": "*",
38 | connection: "close",
39 | })
40 | response.end()
41 | } else if (body instanceof Buffer) {
42 | const code = 200
43 | responseSetStatus(response, { code })
44 | responseSetHeaders(response, {
45 | "content-type": "text/plain",
46 | "access-control-allow-origin": "*",
47 | connection: "close",
48 | })
49 | response.write(body)
50 | response.end()
51 | } else {
52 | const code = 200
53 |
54 | responseSetStatus(response, { code })
55 | responseSetHeaders(response, {
56 | "content-type": "application/json",
57 | "access-control-allow-origin": "*",
58 | connection: "close",
59 | })
60 | response.write(JSON.stringify(body))
61 | response.end()
62 | }
63 | } catch (error) {
64 | if (error instanceof Processing) {
65 | return
66 | }
67 |
68 | const headers = {
69 | "content-type": "application/json",
70 | "access-control-allow-origin": "*",
71 | connection: "close",
72 | }
73 |
74 | const message = error instanceof Error ? error.message : "Unknown error"
75 |
76 | if (withLog)
77 | log({
78 | who: "RequestListener",
79 | kind: "Error",
80 | message,
81 | host: request.headers.host,
82 | url: request.url,
83 | })
84 |
85 | if (error instanceof Unauthorized) {
86 | responseSetStatus(response, { code: 401, message })
87 | responseSetHeaders(response, headers)
88 | response.end()
89 | } else if (error instanceof AlreadyExists) {
90 | responseSetStatus(response, { code: 403, message })
91 | responseSetHeaders(response, headers)
92 | response.end()
93 | } else if (error instanceof NotFound) {
94 | responseSetStatus(response, { code: 404, message })
95 | responseSetHeaders(response, headers)
96 | response.end()
97 | } else if (error instanceof RevisionMismatch) {
98 | responseSetStatus(response, { code: 409, message })
99 | responseSetHeaders(response, headers)
100 | response.end()
101 | } else if (error instanceof Unprocessable) {
102 | responseSetStatus(response, { code: 422, message })
103 | responseSetHeaders(response, headers)
104 | response.end()
105 | } else if (isReport(error)) {
106 | responseSetStatus(response, { code: 422, message })
107 | responseSetHeaders(response, headers)
108 | response.end()
109 | } else {
110 | responseSetStatus(response, { code: 500, message })
111 | responseSetHeaders(response, headers)
112 | response.end()
113 | }
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/server/handlePreflight.ts:
--------------------------------------------------------------------------------
1 | import type Http from "node:http"
2 | import { responseSetHeaders } from "../utils/node/responseSetHeaders"
3 |
4 | export function handlePreflight(
5 | request: Http.IncomingMessage,
6 | response: Http.ServerResponse,
7 | ): void {
8 | const headers: Record = {}
9 |
10 | if (request.headers["origin"]) {
11 | headers["access-control-allow-origin"] = request.headers["origin"]
12 | }
13 |
14 | if (request.headers["access-control-request-method"]) {
15 | headers["access-control-allow-methods"] =
16 | request.headers["access-control-request-method"]
17 | }
18 |
19 | if (request.headers["access-control-request-headers"]) {
20 | headers["access-control-allow-headers"] =
21 | request.headers["access-control-request-headers"]
22 | }
23 |
24 | responseSetHeaders(response, headers)
25 | response.end()
26 | }
27 |
--------------------------------------------------------------------------------
/src/server/serverListenWithDefault.ts:
--------------------------------------------------------------------------------
1 | import Http from "node:http"
2 | import Https from "node:https"
3 | import { log } from "../utils/log"
4 | import { findPort } from "../utils/node/findPort"
5 | import { serverListen } from "../utils/node/serverListen"
6 | import { ServerOptions } from "./ServerOptions"
7 |
8 | export async function serverListenWithDefault(
9 | server: Http.Server | Https.Server,
10 | options?: ServerOptions,
11 | ): Promise {
12 | const scheme = options?.tls ? "https" : "http"
13 |
14 | const hostname = options?.hostname || "127.0.0.1"
15 |
16 | const port = Number(
17 | process.env.PORT ||
18 | options?.port ||
19 | (await findPort(options?.startingPort || 8080)),
20 | )
21 |
22 | await serverListen(server, { hostname, port })
23 |
24 | log({
25 | who: "serverListenWithDefault",
26 | url: `${scheme}://${hostname}:${port}`,
27 | })
28 | }
29 |
--------------------------------------------------------------------------------
/src/server/serverOptionsFromCommandLineOptions.ts:
--------------------------------------------------------------------------------
1 | import { ServerOptions } from "./ServerOptions"
2 |
3 | export function serverOptionsFromCommandLineOptions(options: {
4 | hostname?: string
5 | port?: number
6 | "tls-cert"?: string
7 | "tls-key"?: string
8 | }): ServerOptions {
9 | const tls =
10 | options["tls-cert"] && options["tls-key"]
11 | ? {
12 | cert: options["tls-cert"],
13 | key: options["tls-key"],
14 | }
15 | : undefined
16 |
17 | return {
18 | hostname: options["hostname"],
19 | port: options["port"],
20 | tls,
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/servers/subdomain/Context.ts:
--------------------------------------------------------------------------------
1 | import { WebsiteConfig } from "../../website/WebsiteConfig"
2 |
3 | export type Context = {
4 | domain: string
5 | directory: string
6 | config: WebsiteConfig
7 | }
8 |
--------------------------------------------------------------------------------
/src/servers/subdomain/createContext.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from "node:path"
2 | import { pathIsDirectory } from "../../utils/node/pathIsDirectory"
3 | import { WebsiteConfig } from "../../website/WebsiteConfig"
4 | import { Context } from "./Context"
5 |
6 | type ContextOptions = {
7 | path: string
8 | config: WebsiteConfig
9 | }
10 |
11 | export async function createContext(options: ContextOptions): Promise {
12 | const { path, config } = options
13 |
14 | const hostname = config?.server?.hostname
15 | if (hostname === undefined) {
16 | throw new Error(
17 | `[subdomain/createContext] I expect server.hostname to be given.`,
18 | )
19 | }
20 |
21 | if (await pathIsDirectory(path)) {
22 | return {
23 | domain: hostname,
24 | directory: resolve(path),
25 | config,
26 | }
27 | }
28 |
29 | throw new Error(
30 | `[subdomain/createContext] I expect path to be a directory: ${path}`,
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/src/servers/subdomain/findCertificate.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs"
2 |
3 | export async function findCertificate(
4 | directory: string,
5 | hostname: string,
6 | ): Promise<{ cert: string; key: string } | undefined> {
7 | try {
8 | const certFile = `${directory}/.domain-map/${hostname}/cert`
9 | const keyFile = `${directory}/.domain-map/${hostname}/key`
10 | return {
11 | cert: await fs.promises.readFile(certFile, "utf-8"),
12 | key: await fs.promises.readFile(keyFile, "utf-8"),
13 | }
14 | } catch (_error) {
15 | return undefined
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/servers/subdomain/findSubdomain.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs"
2 |
3 | export async function findSubdomain(
4 | directory: string,
5 | hostname: string,
6 | ): Promise {
7 | try {
8 | const file = `${directory}/.domain-map/${hostname}/subdomain`
9 | const text = await fs.promises.readFile(file, "utf-8")
10 | return text.trim()
11 | } catch (_error) {
12 | return undefined
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/servers/subdomain/handle.ts:
--------------------------------------------------------------------------------
1 | import type { Buffer } from "node:buffer"
2 | import type Http from "node:http"
3 | import { normalize, resolve } from "node:path"
4 | import { handlePreflight } from "../../server/handlePreflight"
5 | import type { Json } from "../../utils/Json"
6 | import { log } from "../../utils/log"
7 | import { requestPath } from "../../utils/node/requestPath"
8 | import { requestPathname } from "../../utils/node/requestPathname"
9 | import { responseSetHeaders } from "../../utils/node/responseSetHeaders"
10 | import { responseSetStatus } from "../../utils/node/responseSetStatus"
11 | import { mergeWebsiteConfigs } from "../../website/mergeWebsiteConfigs"
12 | import { readContentWithRewrite } from "../../website/readContentWithRewrite"
13 | import { readWebsiteConfigFileOrDefault } from "../../website/readWebsiteConfigFileOrDefault"
14 | import { responseSetCacheControlHeaders } from "../../website/responseSetCacheControlHeaders"
15 | import { responseSetCorsHeaders } from "../../website/responseSetCorsHeaders"
16 | import { responseSendContent } from "../website/responseSendContent"
17 | import type { Context } from "./Context"
18 | import { requestFindSubdomain } from "./requestFindSubdomain"
19 |
20 | export async function handle(
21 | ctx: Context,
22 | request: Http.IncomingMessage,
23 | response: Http.ServerResponse,
24 | ): Promise {
25 | const who = "subdomain/handle"
26 | const subdomain = await requestFindSubdomain(ctx, request)
27 | const pathname = requestPathname(request)
28 | const path = requestPath(request)
29 | const withLog = !ctx.config.logger?.disableRequestLogging
30 |
31 | if (subdomain === undefined) {
32 | const code = 404
33 | if (withLog) log({ who, hostname: ctx.domain, subdomain, pathname, code })
34 | responseSetStatus(response, { code })
35 | responseSetHeaders(response, { connection: "close" })
36 | response.end()
37 | return
38 | }
39 |
40 | const subdirectory = normalize(resolve(ctx.directory, subdomain))
41 | const websiteConfig = await readWebsiteConfigFileOrDefault(
42 | `${subdirectory}/website.json`,
43 | )
44 | const config = mergeWebsiteConfigs([ctx.config, websiteConfig])
45 |
46 | if (request.method === "OPTIONS" && config.cors)
47 | return handlePreflight(request, response)
48 |
49 | if (withLog) log({ who, subdomain, pathname })
50 |
51 | if (request.method === "GET") {
52 | responseSetCorsHeaders(config, response)
53 | responseSetCacheControlHeaders(config, response, path)
54 | const content = await readContentWithRewrite(subdirectory, config, path)
55 | await responseSendContent(request, response, content, { withLog })
56 | return
57 | }
58 |
59 | throw new Error(
60 | [
61 | `[subdomain/handle] unhandled http request`,
62 | ` method: ${request.method}`,
63 | ].join("\n"),
64 | )
65 | }
66 |
--------------------------------------------------------------------------------
/src/servers/subdomain/requestFindSubdomain.ts:
--------------------------------------------------------------------------------
1 | import type Http from "node:http"
2 | import { requestBasedomain } from "../../utils/node/requestBasedomain"
3 | import { requestHostname } from "../../utils/node/requestHostname"
4 | import { requestSubdomain } from "../../utils/node/requestSubdomain"
5 | import { Context } from "./Context"
6 | import { findSubdomain } from "./findSubdomain"
7 |
8 | export async function requestFindSubdomain(
9 | ctx: Context,
10 | request: Http.IncomingMessage,
11 | ): Promise {
12 | const hostname = requestHostname(request)
13 | const basedomain = requestBasedomain(request)
14 |
15 | const subdomain =
16 | ctx.domain === hostname
17 | ? "www"
18 | : ctx.domain === basedomain
19 | ? requestSubdomain(request, ctx.domain)
20 | : await findSubdomain(ctx.directory, hostname)
21 |
22 | return subdomain
23 | }
24 |
--------------------------------------------------------------------------------
/src/servers/subdomain/startServer.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs"
2 | import Http from "node:http"
3 | import Https from "node:https"
4 | import tls from "node:tls"
5 | import { createRequestListener } from "../../server/createRequestListener"
6 | import { serverListenWithDefault } from "../../server/serverListenWithDefault"
7 | import { log } from "../../utils/log"
8 | import { WebsiteConfig } from "../../website/WebsiteConfig"
9 | import { createContext } from "./createContext"
10 | import { findCertificate } from "./findCertificate"
11 | import { handle } from "./handle"
12 |
13 | export async function startServer(
14 | path: string,
15 | config: WebsiteConfig,
16 | ): Promise {
17 | const who = "subdomain/startServer"
18 |
19 | const ctx = await createContext({ path, config })
20 | log({ who, message: "createContext", ctx })
21 |
22 | const listener = createRequestListener({ ctx, handle, logger: config.logger })
23 |
24 | if (config.server?.tls) {
25 | const server = Https.createServer(
26 | {
27 | cert: await fs.promises.readFile(config.server.tls.cert),
28 | key: await fs.promises.readFile(config.server.tls.key),
29 | SNICallback: async (hostname, changeSecureContext) => {
30 | const certificate = await findCertificate(ctx.directory, hostname)
31 | if (certificate !== undefined) {
32 | const secureContext = tls.createSecureContext({ ...certificate })
33 | changeSecureContext(null, secureContext)
34 | } else {
35 | changeSecureContext(null, undefined)
36 | }
37 | },
38 | },
39 | listener,
40 | )
41 | await serverListenWithDefault(server, config.server)
42 | } else {
43 | const server = Http.createServer({}, listener)
44 | await serverListenWithDefault(server, config.server)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/servers/website/Context.ts:
--------------------------------------------------------------------------------
1 | import { WebsiteConfig } from "../../website/WebsiteConfig"
2 |
3 | export type Context = {
4 | directory: string
5 | config: WebsiteConfig
6 | }
7 |
--------------------------------------------------------------------------------
/src/servers/website/createContext.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from "node:path"
2 | import { pathIsDirectory } from "../../utils/node/pathIsDirectory"
3 | import { WebsiteConfig } from "../../website/WebsiteConfig"
4 | import { Context } from "./Context"
5 |
6 | type ContextOptions = {
7 | path: string
8 | config: WebsiteConfig
9 | }
10 |
11 | export async function createContext(options: ContextOptions): Promise {
12 | const { path, config } = options
13 |
14 | if (await pathIsDirectory(path)) {
15 | return {
16 | directory: resolve(path),
17 | config,
18 | }
19 | }
20 |
21 | throw new Error(
22 | `[website/createContext] I expect path to be a directory: ${path}`,
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/src/servers/website/handle.ts:
--------------------------------------------------------------------------------
1 | import type { Buffer } from "node:buffer"
2 | import type Http from "node:http"
3 | import { handlePreflight } from "../../server/handlePreflight"
4 | import type { Json } from "../../utils/Json"
5 | import { log } from "../../utils/log"
6 | import { requestPath } from "../../utils/node/requestPath"
7 | import { requestPathname } from "../../utils/node/requestPathname"
8 | import { readContentWithRewrite } from "../../website/readContentWithRewrite"
9 | import { responseSetCacheControlHeaders } from "../../website/responseSetCacheControlHeaders"
10 | import { responseSetCorsHeaders } from "../../website/responseSetCorsHeaders"
11 | import type { Context } from "./Context"
12 | import { responseSendContent } from "./responseSendContent"
13 |
14 | export async function handle(
15 | ctx: Context,
16 | request: Http.IncomingMessage,
17 | response: Http.ServerResponse,
18 | ): Promise {
19 | const who = "website/handle"
20 | const withLog = !ctx.config.logger?.disableRequestLogging
21 | const pathname = requestPathname(request)
22 | const path = requestPath(request)
23 |
24 | if (request.method === "OPTIONS" && ctx.config.cors)
25 | return handlePreflight(request, response)
26 |
27 | if (withLog) log({ who, pathname })
28 |
29 | if (request.method === "GET") {
30 | responseSetCorsHeaders(ctx.config, response)
31 | responseSetCacheControlHeaders(ctx.config, response, path)
32 | const { directory, config } = ctx
33 | const content = await readContentWithRewrite(directory, config, path)
34 | await responseSendContent(request, response, content, { withLog })
35 | return
36 | }
37 |
38 | throw new Error(
39 | [
40 | `[website/handle] unhandled http request`,
41 | ` method: ${request.method}`,
42 | ].join("\n"),
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/src/servers/website/responseSendContent.ts:
--------------------------------------------------------------------------------
1 | import type { Buffer } from "node:buffer"
2 | import type Http from "node:http"
3 | import type { Json } from "../../utils/Json"
4 | import { log } from "../../utils/log"
5 | import { compress } from "../../utils/node/compress"
6 | import { requestCompressionMethod } from "../../utils/node/requestCompressionMethod"
7 | import { requestPathname } from "../../utils/node/requestPathname"
8 | import { responseSetHeaders } from "../../utils/node/responseSetHeaders"
9 | import { responseSetStatus } from "../../utils/node/responseSetStatus"
10 | import { Content } from "../../website/Content"
11 |
12 | export async function responseSendContent(
13 | request: Http.IncomingMessage,
14 | response: Http.ServerResponse,
15 | content: Content | undefined,
16 | options: {
17 | withLog: boolean
18 | },
19 | ): Promise {
20 | const who = "responseSendContent"
21 | const { withLog } = options
22 | const pathname = requestPathname(request)
23 |
24 | if (content === undefined) {
25 | const code = 404
26 | if (withLog) log({ who, pathname, code })
27 | responseSetStatus(response, { code })
28 | responseSetHeaders(response, { connection: "close" })
29 | response.end()
30 | } else {
31 | const compressionMethod = requestCompressionMethod(request)
32 | const buffer = await compress(compressionMethod, content.buffer)
33 | const code = 200
34 | if (options.withLog)
35 | log({ who, pathname, code, "content-type": content.type })
36 | responseSetStatus(response, { code })
37 | responseSetHeaders(response, {
38 | "content-type": content.type,
39 | "content-encoding": compressionMethod,
40 | connection: "close",
41 | })
42 | response.end(buffer)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/servers/website/startServer.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs"
2 | import Http from "node:http"
3 | import Https from "node:https"
4 | import { createRequestListener } from "../../server/createRequestListener"
5 | import { serverListenWithDefault } from "../../server/serverListenWithDefault"
6 | import { log } from "../../utils/log"
7 | import { WebsiteConfig } from "../../website/WebsiteConfig"
8 | import { createContext } from "./createContext"
9 | import { handle } from "./handle"
10 |
11 | export async function startServer(
12 | path: string,
13 | config: WebsiteConfig,
14 | ): Promise {
15 | const who = "website/startServer"
16 |
17 | const ctx = await createContext({ path, config })
18 | log({ who, message: "createContext", ctx })
19 |
20 | const listener = createRequestListener({ ctx, handle, logger: config.logger })
21 |
22 | if (config.server?.tls) {
23 | const server = Https.createServer(
24 | {
25 | cert: await fs.promises.readFile(config.server.tls.cert),
26 | key: await fs.promises.readFile(config.server.tls.key),
27 | },
28 | listener,
29 | )
30 | await serverListenWithDefault(server, config.server)
31 | } else {
32 | const server = Http.createServer({}, listener)
33 | await serverListenWithDefault(server, config.server)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/utils/Json.ts:
--------------------------------------------------------------------------------
1 | export type Json = JsonAtom | JsonArray | JsonObject
2 |
3 | export type JsonAtom = string | number | boolean | null
4 |
5 | export type JsonArray = Array
6 |
7 | export type JsonObject = { [x: string]: Json }
8 |
9 | export function isJsonObject(json: Json): json is JsonObject {
10 | return typeof json === "object" && json !== null && !isJsonArray(json)
11 | }
12 |
13 | export function isJsonArray(json: Json): json is JsonArray {
14 | return json instanceof Array
15 | }
16 |
--------------------------------------------------------------------------------
/src/utils/LogOptions.ts:
--------------------------------------------------------------------------------
1 | export type LogOptions = Record & {
2 | kind?: string
3 | who: string
4 | elapse?: number
5 | message?: string
6 | }
7 |
--------------------------------------------------------------------------------
/src/utils/URLPattern.ts:
--------------------------------------------------------------------------------
1 | export { URLPattern } from "urlpattern-polyfill"
2 |
--------------------------------------------------------------------------------
/src/utils/arrayFromAsyncIterable.ts:
--------------------------------------------------------------------------------
1 | export async function arrayFromAsyncIterable(
2 | iter: AsyncIterable,
3 | ): Promise> {
4 | const array: Array = []
5 | for await (const data of iter) {
6 | array.push(data)
7 | }
8 |
9 | return array
10 | }
11 |
--------------------------------------------------------------------------------
/src/utils/byteArrayMerge.ts:
--------------------------------------------------------------------------------
1 | export function byteArrayMerge(parts: Array): Uint8Array {
2 | const length = parts.reduce((sum, part) => sum + part.length, 0)
3 | const result = new Uint8Array(length)
4 |
5 | let index = 0
6 | for (const part of parts) {
7 | result.set(part, index)
8 | index += part.length
9 | }
10 |
11 | return result
12 | }
13 |
--------------------------------------------------------------------------------
/src/utils/colors.ts:
--------------------------------------------------------------------------------
1 | import picocolors from "picocolors"
2 |
3 | export const colors = picocolors
4 |
--------------------------------------------------------------------------------
/src/utils/contentTypeRecord.ts:
--------------------------------------------------------------------------------
1 | export const contentTypeRecord: Record = {
2 | ".html": "text/html",
3 | ".htm": "text/html",
4 | ".css": "text/css",
5 | ".js": "text/javascript",
6 | ".mjs": "text/javascript",
7 | ".markdown": "text/markdown",
8 | ".md": "text/markdown",
9 |
10 | ".txt": "text/plain",
11 | ".text": "text/plain",
12 | ".csv": "text/csv",
13 |
14 | ".json": "application/json",
15 | ".pdf": "application/pdf",
16 | ".zip": "application/zip",
17 | ".rar": "application/x-rar-compressed",
18 |
19 | ".mp2": "audio/mpeg",
20 | ".mp3": "audio/mpeg",
21 | ".m4a": "audio/mp4",
22 | ".wav": "audio/vnd.wav",
23 | ".ogg": "audio/ogg",
24 |
25 | ".mp4": "video/mp4",
26 | ".mpg": "video/mpeg",
27 | ".mpeg": "video/mpeg",
28 | ".mov": "video/quicktime",
29 | ".avi": "video/x-msvideo",
30 | ".mkv": "video/x-matroska",
31 | ".webm": "video/webm",
32 |
33 | ".ico": "image/vnd.microsoft.icon",
34 | ".jpg": "image/jpeg",
35 | ".jpeg": "image/jpeg",
36 | ".png": "image/png",
37 | ".gif": "image/gif",
38 | ".svg": "image/svg+xml",
39 | ".webp": "image/webp",
40 |
41 | ".woff": "font/woff",
42 | ".ttf": "font/ttf",
43 | ".otf": "font/otf",
44 | }
45 |
--------------------------------------------------------------------------------
/src/utils/decrypt.ts:
--------------------------------------------------------------------------------
1 | import crypto from "node:crypto"
2 | import { ivLength } from "./generateEncryptionKey"
3 |
4 | // Learned from:
5 | // - https://github.com/diafygi/webcrypto-examples#aes-gcm
6 |
7 | export async function decrypt(
8 | encryptedData: Uint8Array,
9 | key: Uint8Array,
10 | ): Promise {
11 | const iv = key.subarray(0, ivLength)
12 | const exportedKey = key.subarray(ivLength)
13 |
14 | const importedKey = await crypto.subtle.importKey(
15 | "raw",
16 | exportedKey,
17 | { name: "AES-GCM" },
18 | true,
19 | ["encrypt", "decrypt"],
20 | )
21 |
22 | const decryptedData = new Uint8Array(
23 | await crypto.subtle.decrypt(
24 | {
25 | name: "AES-GCM",
26 | iv,
27 | },
28 | importedKey,
29 | encryptedData,
30 | ),
31 | )
32 |
33 | return decryptedData
34 | }
35 |
--------------------------------------------------------------------------------
/src/utils/encrypt-decrypt.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest"
2 | import { decrypt } from "./decrypt"
3 | import { encrypt } from "./encrypt"
4 | import { generateEncryptionKey } from "./generateEncryptionKey"
5 |
6 | test("encrypt-decrypt", async () => {
7 | const data = new Uint8Array([1, 2, 3])
8 |
9 | const encryptionKey = await generateEncryptionKey()
10 | const encryptedData = await encrypt(data, encryptionKey)
11 |
12 | expect(encryptedData).not.toEqual(data)
13 | expect(await decrypt(encryptedData, encryptionKey)).toEqual(data)
14 | })
15 |
16 | test("encrypt-decrypt -- encrypt empty data", async () => {
17 | const data = new Uint8Array([])
18 |
19 | const encryptionKey = await generateEncryptionKey()
20 | const encryptedData = await encrypt(data, encryptionKey)
21 |
22 | expect(encryptedData).not.toEqual(data)
23 | expect(encryptedData.length).not.toEqual(0)
24 | expect(await decrypt(encryptedData, encryptionKey)).toEqual(data)
25 | })
26 |
--------------------------------------------------------------------------------
/src/utils/encrypt.ts:
--------------------------------------------------------------------------------
1 | import crypto from "node:crypto"
2 | import { ivLength } from "./generateEncryptionKey"
3 |
4 | // Learned from:
5 | // - https://github.com/diafygi/webcrypto-examples#aes-gcm
6 |
7 | export async function encrypt(
8 | data: Uint8Array,
9 | key: Uint8Array,
10 | ): Promise {
11 | const iv = key.subarray(0, ivLength)
12 | const exportedKey = key.subarray(ivLength)
13 |
14 | const importedKey = await crypto.subtle.importKey(
15 | "raw",
16 | exportedKey,
17 | { name: "AES-GCM" },
18 | true,
19 | ["encrypt", "decrypt"],
20 | )
21 |
22 | const encryptedData = new Uint8Array(
23 | await crypto.subtle.encrypt({ name: "AES-GCM", iv }, importedKey, data),
24 | )
25 |
26 | return encryptedData
27 | }
28 |
--------------------------------------------------------------------------------
/src/utils/formatAuthorizationHeader.ts:
--------------------------------------------------------------------------------
1 | export function formatAuthorizationHeader(token: string): string {
2 | return `token ${token}`
3 | }
4 |
--------------------------------------------------------------------------------
/src/utils/formatDate.ts:
--------------------------------------------------------------------------------
1 | import { leftPad } from "./leftPad"
2 |
3 | export function formatDate(t: Date | number | string): string {
4 | if (typeof t === "string") t = new Date(t)
5 | if (typeof t === "number") t = new Date(t)
6 |
7 | const YYYY = t.getFullYear()
8 | const MM = leftPad((t.getMonth() + 1).toString(), 2, "0")
9 | const DD = leftPad(t.getDate().toString(), 2, "0")
10 |
11 | return `${YYYY}-${MM}-${DD}`
12 | }
13 |
14 | export function formatTime(
15 | t: Date | number | string,
16 | opts?: { withMilliseconds?: boolean },
17 | ): string {
18 | if (typeof t === "string") t = new Date(t)
19 | if (typeof t === "number") t = new Date(t)
20 |
21 | const hh = leftPad(t.getHours().toString(), 2, "0")
22 | const mm = leftPad(t.getMinutes().toString(), 2, "0")
23 | const ss = leftPad(t.getSeconds().toString(), 2, "0")
24 |
25 | const mi = leftPad((t.getTime() % 1000).toString(), 3, "0")
26 |
27 | if (opts?.withMilliseconds) {
28 | return `${hh}:${mm}:${ss}.${mi}`
29 | } else {
30 | return `${hh}:${mm}:${ss}`
31 | }
32 | }
33 |
34 | export function formatDateTime(
35 | t: Date | number,
36 | opts?: { withMilliseconds?: boolean },
37 | ): string {
38 | if (typeof t === "number") t = new Date(t)
39 |
40 | const date = formatDate(t)
41 | const time = formatTime(t, opts)
42 |
43 | return `${date} ${time}`
44 | }
45 |
46 | export function formatAgo(
47 | t: Date | number | string,
48 | options: { lang: string },
49 | ): string {
50 | if (typeof t === "string") t = new Date(t)
51 | if (typeof t === "number") t = new Date(t)
52 |
53 | const detla = Date.now() - t.getTime()
54 |
55 | const A_MONTH = 30 * 24 * 60 * 60 * 1000
56 |
57 | if (detla > A_MONTH) {
58 | return formatDate(t)
59 | }
60 |
61 | return formatDetla(detla, options)
62 | }
63 |
64 | export function formatDetla(t: number, options: { lang: string }): string {
65 | const { lang } = options
66 |
67 | const s = Math.floor(t / 1000)
68 | const m = Math.floor(s / 60)
69 | const h = Math.floor(m / 60)
70 | const d = Math.floor(h / 24)
71 |
72 | if (d) return lang.startsWith("zh") ? `${d} 天前` : `${d} days ago`
73 | if (h) return lang.startsWith("zh") ? `${h} 小时前` : `${h} hours ago`
74 | if (m) return lang.startsWith("zh") ? `${m} 分钟前` : `${m} minutes ago`
75 | else return lang.startsWith("zh") ? `刚才` : `just now`
76 | }
77 |
--------------------------------------------------------------------------------
/src/utils/generateEncryptionKey.ts:
--------------------------------------------------------------------------------
1 | import crypto from "node:crypto"
2 | import { byteArrayMerge } from "./byteArrayMerge"
3 |
4 | // Learned from:
5 | // - https://github.com/diafygi/webcrypto-examples#aes-gcm
6 |
7 | export const ivLength = 12
8 |
9 | export async function generateEncryptionKey(): Promise {
10 | const cryptoKey = await crypto.subtle.generateKey(
11 | { name: "AES-GCM", length: 256 },
12 | true,
13 | ["encrypt", "decrypt"],
14 | )
15 |
16 | const exportedKey = await crypto.subtle.exportKey("raw", cryptoKey)
17 |
18 | const iv = crypto.getRandomValues(new Uint8Array(ivLength))
19 |
20 | return byteArrayMerge([iv, new Uint8Array(exportedKey)])
21 | }
22 |
--------------------------------------------------------------------------------
/src/utils/globMatch.ts:
--------------------------------------------------------------------------------
1 | import micromatch from "micromatch"
2 |
3 | export function globMatch(pattern: string, input: string): boolean {
4 | // We add a "/" because:
5 | // micromatch.isMatch("", "**") => false
6 | // micromatch.isMatch("/", "/**") => true
7 | // Note the argument order: `input` first, `pattern` second.
8 | return micromatch.isMatch("/" + input, "/" + pattern)
9 | }
10 |
--------------------------------------------------------------------------------
/src/utils/indent.ts:
--------------------------------------------------------------------------------
1 | export function indent(text: string, indentation: string = " "): string {
2 | return text
3 | .split("\n")
4 | .map((line) => indentation + line)
5 | .join("\n")
6 | }
7 |
--------------------------------------------------------------------------------
/src/utils/leftPad.ts:
--------------------------------------------------------------------------------
1 | export function leftPad(
2 | line: string,
3 | size: number,
4 | char: string = " ",
5 | ): string {
6 | return char.repeat(size - line.length) + line
7 | }
8 |
--------------------------------------------------------------------------------
/src/utils/log.ts:
--------------------------------------------------------------------------------
1 | import { Schema, ty } from "@xieyuheng/ty"
2 | import type { LogOptions } from "./LogOptions"
3 | import { logJson } from "./logJson"
4 | import { logPretty } from "./logPretty"
5 | import { logPrettyLine } from "./logPrettyLine"
6 |
7 | export type LoggerName = "json" | "silent" | "pretty" | "pretty-line"
8 |
9 | export const LoggerNameSchema: Schema = ty.union(
10 | ty.const("json"),
11 | ty.union(
12 | ty.const("silent"),
13 | ty.union(ty.const("pretty"), ty.const("pretty-line")),
14 | ),
15 | )
16 |
17 | let globalLoggerName: LoggerName = "pretty-line"
18 |
19 | export function log(options: LogOptions): void {
20 | if (globalLoggerName === "json") {
21 | logJson(options)
22 | } else if (globalLoggerName === "silent") {
23 | } else if (globalLoggerName === "pretty") {
24 | logPretty(options)
25 | } else if (globalLoggerName === "pretty-line") {
26 | logPrettyLine(options)
27 | }
28 | }
29 |
30 | export function changeLogger(loggerName: LoggerName): void {
31 | globalLoggerName = loggerName
32 | }
33 |
--------------------------------------------------------------------------------
/src/utils/logJson.ts:
--------------------------------------------------------------------------------
1 | import type { LogOptions } from "./LogOptions"
2 |
3 | export function logJson(options: LogOptions): void {
4 | console.dir(options, { depth: null })
5 | }
6 |
--------------------------------------------------------------------------------
/src/utils/logPretty.ts:
--------------------------------------------------------------------------------
1 | import { colors } from "./colors"
2 | import { formatTime } from "./formatDate"
3 | import { indent } from "./indent"
4 | import type { LogOptions } from "./LogOptions"
5 |
6 | export function logPretty(options: LogOptions): void {
7 | const { kind, who, elapse, message } = options
8 |
9 | let s = ""
10 |
11 | s += formatNow() + " "
12 |
13 | if (kind === "Error") {
14 | s += colors.red(formatWho(who)) + " "
15 | } else if (kind === "Warning") {
16 | s += colors.magenta(formatWho(who)) + " "
17 | } else {
18 | s += colors.blue(formatWho(who)) + " "
19 | }
20 |
21 | if (message) s += `${message}`
22 | if (elapse !== undefined) s += " " + formatElapse(elapse)
23 |
24 | s += "\n"
25 |
26 | for (const [key, value] of Object.entries(options)) {
27 | if (!["who", "kind", "message", "elapse"].includes(key)) {
28 | if (value !== undefined) {
29 | s += formatProperty(key, value)
30 | s += "\n"
31 | }
32 | }
33 | }
34 |
35 | console.log(s.trim())
36 | }
37 |
38 | function formatWho(who: string): string {
39 | return colors.bold(`[${who}]`)
40 | }
41 |
42 | function formatNow(): string {
43 | const time = formatTime(new Date(), { withMilliseconds: true })
44 | return colors.yellow(`${time}`)
45 | }
46 |
47 | function formatElapse(elapse: number): string {
48 | return colors.yellow(`<${elapse}ms>`)
49 | }
50 |
51 | function formatProperty(key: string, value: any): string {
52 | const k = colors.italic(colors.yellow(key))
53 | const j = JSON.stringify(value, null, 2)
54 | const v = indent(j, " ").trim()
55 | return ` ${k}: ${v}`
56 | }
57 |
--------------------------------------------------------------------------------
/src/utils/logPrettyLine.ts:
--------------------------------------------------------------------------------
1 | import { colors } from "./colors"
2 | import { formatTime } from "./formatDate"
3 | import type { LogOptions } from "./LogOptions"
4 |
5 | export function logPrettyLine(options: LogOptions): void {
6 | const { kind, who, elapse, message } = options
7 |
8 | let s = ""
9 |
10 | s += formatNow() + " "
11 |
12 | if (kind === "Error") {
13 | s += colors.red(formatWho(who)) + " "
14 | } else if (kind === "Warning") {
15 | s += colors.magenta(formatWho(who)) + " "
16 | } else {
17 | s += colors.blue(formatWho(who)) + " "
18 | }
19 |
20 | if (message) s += colors.bold(`${message} `)
21 | if (elapse !== undefined) s += formatElapse(elapse)
22 |
23 | const properties = Object.fromEntries(
24 | Object.entries(options).filter(
25 | ([key, value]) =>
26 | value !== undefined &&
27 | !["who", "kind", "message", "elapse"].includes(key),
28 | ),
29 | )
30 |
31 | if (Object.keys(properties).length > 0) {
32 | s += "-- "
33 | s += JSON.stringify(properties)
34 | }
35 |
36 | console.log(s.trim())
37 | }
38 |
39 | function formatWho(who: string): string {
40 | return colors.bold(`[${who}]`)
41 | }
42 |
43 | function formatNow(): string {
44 | const time = formatTime(new Date(), { withMilliseconds: true })
45 | return colors.yellow(`${time}`)
46 | }
47 |
48 | function formatElapse(elapse: number): string {
49 | return colors.yellow(`<${elapse}ms>`)
50 | }
51 |
--------------------------------------------------------------------------------
/src/utils/node/bufferJson.ts:
--------------------------------------------------------------------------------
1 | import type { Buffer } from "node:buffer"
2 | import type { Json } from "../Json"
3 |
4 | export async function bufferJson(buffer: Buffer): Promise {
5 | const text = buffer.toString()
6 | const json = JSON.stringify(text)
7 | return json
8 | }
9 |
--------------------------------------------------------------------------------
/src/utils/node/bufferJsonObject.ts:
--------------------------------------------------------------------------------
1 | import type { Buffer } from "node:buffer"
2 | import { isJsonObject, JsonObject } from "../Json"
3 |
4 | export async function bufferJsonObject(buffer: Buffer): Promise {
5 | const text = buffer.toString()
6 | const json = JSON.stringify(text)
7 | if (!isJsonObject(json)) {
8 | throw new Error(
9 | [
10 | `[bufferJsonObject] expect JsonObject`,
11 | ` json: ${JSON.stringify(json)}`,
12 | ].join("\n"),
13 | )
14 | }
15 |
16 | return json
17 | }
18 |
--------------------------------------------------------------------------------
/src/utils/node/compress.ts:
--------------------------------------------------------------------------------
1 | import type { Buffer } from "node:buffer"
2 | import { promisify } from "node:util"
3 | import Zlib from "node:zlib"
4 |
5 | const brotliCompress = promisify(Zlib.brotliCompress)
6 | const gzip = promisify(Zlib.gzip)
7 |
8 | export async function compress(
9 | compressionMethod: string | undefined,
10 | buffer: Buffer,
11 | ): Promise {
12 | if (compressionMethod === "br") {
13 | return await brotliCompress(buffer)
14 | }
15 |
16 | if (compressionMethod === "gzip") {
17 | return await gzip(buffer)
18 | }
19 |
20 | return buffer
21 | }
22 |
--------------------------------------------------------------------------------
/src/utils/node/findPort.ts:
--------------------------------------------------------------------------------
1 | import detect from "detect-port"
2 |
3 | export async function findPort(port: number): Promise {
4 | return await detect(port)
5 | }
6 |
--------------------------------------------------------------------------------
/src/utils/node/isErrnoException.ts:
--------------------------------------------------------------------------------
1 | // Code taken from: https://github.com/TokugawaTakeshi/Yamato-Daiwa-ES-Extensions
2 |
3 | export function isErrnoException(
4 | error: unknown,
5 | ): error is NodeJS.ErrnoException {
6 | return (
7 | isArbitraryObject(error) &&
8 | error instanceof Error &&
9 | (typeof error.errno === "number" || typeof error.errno === "undefined") &&
10 | (typeof error.code === "string" || typeof error.code === "undefined") &&
11 | (typeof error.path === "string" || typeof error.path === "undefined") &&
12 | (typeof error.syscall === "string" || typeof error.syscall === "undefined")
13 | )
14 | }
15 |
16 | type ArbitraryObject = { [key: string]: unknown }
17 |
18 | function isArbitraryObject(
19 | potentialObject: unknown,
20 | ): potentialObject is ArbitraryObject {
21 | return typeof potentialObject === "object" && potentialObject !== null
22 | }
23 |
--------------------------------------------------------------------------------
/src/utils/node/packageJson.ts:
--------------------------------------------------------------------------------
1 | export function packageJson() {
2 | return require("../../../package.json")
3 | }
4 |
--------------------------------------------------------------------------------
/src/utils/node/password.ts:
--------------------------------------------------------------------------------
1 | import bcrypt from "bcrypt"
2 |
3 | export async function passwordHash(password: string): Promise {
4 | const saltRounds = 10
5 | return await bcrypt.hash(password, saltRounds)
6 | }
7 |
8 | export async function passwordCheck(
9 | password: string,
10 | hash: string,
11 | ): Promise {
12 | return await bcrypt.compare(password, hash)
13 | }
14 |
--------------------------------------------------------------------------------
/src/utils/node/pathExists.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs"
2 |
3 | export async function pathExists(path: string): Promise {
4 | try {
5 | await fs.promises.access(path)
6 | return true
7 | } catch (_error) {
8 | return false
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/utils/node/pathIsDirectory.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs"
2 | import { isErrnoException } from "../../utils/node/isErrnoException"
3 |
4 | export async function pathIsDirectory(path: string): Promise {
5 | try {
6 | const stats = await fs.promises.stat(path)
7 | return stats.isDirectory()
8 | } catch (error) {
9 | if (isErrnoException(error) && error.code === "ENOENT") {
10 | return false
11 | }
12 |
13 | throw error
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/utils/node/pathIsFile.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs"
2 | import { isErrnoException } from "../../utils/node/isErrnoException"
3 |
4 | export async function pathIsFile(path: string): Promise {
5 | try {
6 | const stats = await fs.promises.stat(path)
7 | return stats.isFile()
8 | } catch (error) {
9 | if (isErrnoException(error) && error.code === "ENOENT") {
10 | return false
11 | }
12 |
13 | throw error
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/utils/node/readJson.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs"
2 | import type { Json } from "../Json"
3 |
4 | export async function readJson(path: string): Promise {
5 | const who = "readJson"
6 |
7 | const text = await fs.promises.readFile(path, "utf-8")
8 |
9 | try {
10 | return JSON.parse(text)
11 | } catch (error) {
12 | if (error instanceof SyntaxError) {
13 | const message = `[${who}] request path: ${path}, text: ${text}`
14 | error.message += "\n"
15 | error.message += message
16 | }
17 |
18 | throw error
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/utils/node/readJsonObject.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs"
2 | import { isJsonObject, JsonObject } from "../Json"
3 |
4 | export async function readJsonObject(path: string): Promise {
5 | const who = "readJsonObject"
6 |
7 | const text = await fs.promises.readFile(path, "utf-8")
8 |
9 | try {
10 | const json = JSON.parse(text)
11 | if (!isJsonObject(json)) {
12 | throw new Error(`expect JsonObject`)
13 | }
14 |
15 | return json
16 | } catch (error) {
17 | if (error instanceof SyntaxError) {
18 | const message = `[${who}] path: ${path}, text: ${text}`
19 | error.message += "\n"
20 | error.message += message
21 | }
22 |
23 | throw error
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/utils/node/requestBasedomain.ts:
--------------------------------------------------------------------------------
1 | import type Http from "node:http"
2 | import { requestHostname } from "./requestHostname"
3 |
4 | export function requestBasedomain(request: Http.IncomingMessage): string {
5 | const hostname = requestHostname(request)
6 | const [_subdomain, ...rest] = hostname.split(".")
7 | const basedomain = rest.join(".")
8 |
9 | return basedomain
10 | }
11 |
--------------------------------------------------------------------------------
/src/utils/node/requestBuffer.ts:
--------------------------------------------------------------------------------
1 | import { Buffer } from "node:buffer"
2 | import type Http from "node:http"
3 |
4 | export function requestBuffer(request: Http.IncomingMessage): Promise {
5 | return new Promise((resolve, reject) => {
6 | const chunks: Array = []
7 |
8 | request.on("data", (chunk: Buffer) => {
9 | chunks.push(chunk)
10 | })
11 |
12 | request.on("end", () => {
13 | resolve(Buffer.concat(chunks))
14 | })
15 |
16 | request.on("error", (error) => {
17 | reject(error)
18 | })
19 | })
20 | }
21 |
--------------------------------------------------------------------------------
/src/utils/node/requestCompressionMethod.ts:
--------------------------------------------------------------------------------
1 | import type Http from "node:http"
2 |
3 | export function requestCompressionMethod(
4 | request: Http.IncomingMessage,
5 | ): string | undefined {
6 | if (typeof request.headers["accept-encoding"] === "string") {
7 | const encodings = request.headers["accept-encoding"].split(",")
8 |
9 | if (encodings.find((encoding) => encoding.trim().startsWith("gzip"))) {
10 | return "gzip"
11 | }
12 |
13 | // if (encodings.find((encoding) => encoding.trim().startsWith("br"))) {
14 | // return "br"
15 | // }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/utils/node/requestFormatRaw.ts:
--------------------------------------------------------------------------------
1 | import { Buffer } from "node:buffer"
2 | import type Http from "node:http"
3 | import { requestBuffer } from "./requestBuffer"
4 |
5 | export async function requestFormatRaw(
6 | request: Http.IncomingMessage,
7 | ): Promise {
8 | const requestLine = `${request.method} ${request.url} HTTP/${request.httpVersion}`
9 |
10 | const headerLines: Array = []
11 |
12 | for (const [index, value] of request.rawHeaders.entries()) {
13 | if (index % 2 === 0) {
14 | headerLines.push(value)
15 | } else {
16 | const line = headerLines.pop()
17 | headerLines.push(`${line}: ${value}`)
18 | }
19 | }
20 |
21 | const head = [requestLine, ...headerLines].join("\r\n")
22 |
23 | return Buffer.concat([
24 | new TextEncoder().encode(head),
25 | new TextEncoder().encode("\r\n"),
26 | new TextEncoder().encode("\r\n"),
27 | await requestBuffer(request),
28 | ])
29 | }
30 |
--------------------------------------------------------------------------------
/src/utils/node/requestHostname.ts:
--------------------------------------------------------------------------------
1 | import type Http from "node:http"
2 | import { requestURLAlwaysWithHttpProtocol } from "./requestURLAlwaysWithHttpProtocol"
3 |
4 | export function requestHostname(request: Http.IncomingMessage): string {
5 | const url = requestURLAlwaysWithHttpProtocol(request)
6 | return url.hostname
7 | }
8 |
--------------------------------------------------------------------------------
/src/utils/node/requestJson.ts:
--------------------------------------------------------------------------------
1 | import type Http from "node:http"
2 | import type { Json } from "../Json"
3 | import { requestText } from "./requestText"
4 | import { requestURLAlwaysWithHttpProtocol } from "./requestURLAlwaysWithHttpProtocol"
5 |
6 | export async function requestJson(
7 | request: Http.IncomingMessage,
8 | ): Promise {
9 | const who = "requestJson"
10 |
11 | const text = await requestText(request)
12 |
13 | try {
14 | return JSON.parse(text)
15 | } catch (error) {
16 | if (error instanceof SyntaxError) {
17 | const url = requestURLAlwaysWithHttpProtocol(request)
18 | const message = `[${who}] request url: ${url}, text: ${text}`
19 | error.message += "\n"
20 | error.message += message
21 | }
22 |
23 | throw error
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/utils/node/requestJsonObject.ts:
--------------------------------------------------------------------------------
1 | import type Http from "node:http"
2 | import { isJsonObject, JsonObject } from "../Json"
3 | import { requestJson } from "./requestJson"
4 |
5 | export async function requestJsonObject(
6 | request: Http.IncomingMessage,
7 | ): Promise {
8 | const json = await requestJson(request)
9 | if (!isJsonObject(json)) {
10 | throw new Error(
11 | [
12 | `[requestJsonObject] expect JsonObject`,
13 | ` json: ${JSON.stringify(json)}`,
14 | ].join("\n"),
15 | )
16 | }
17 |
18 | return json
19 | }
20 |
--------------------------------------------------------------------------------
/src/utils/node/requestKind.ts:
--------------------------------------------------------------------------------
1 | import type Http from "node:http"
2 | import { requestQuery } from "./requestQuery"
3 |
4 | export function requestKind(request: Http.IncomingMessage): string {
5 | const query = requestQuery(request)
6 | const kind = query.kind ? query.kind.toLowerCase() : ""
7 | return kind
8 | }
9 |
--------------------------------------------------------------------------------
/src/utils/node/requestPath.ts:
--------------------------------------------------------------------------------
1 | import type Http from "node:http"
2 | import { normalize } from "node:path"
3 | import { requestPathname } from "./requestPathname"
4 |
5 | export function requestPath(request: Http.IncomingMessage): string {
6 | const pathname = requestPathname(request)
7 | // NOTE `decodeURIComponent` is necessary for the space characters in url.
8 | const path = normalize(decodeURIComponent(pathname.slice(1)))
9 | return path
10 | }
11 |
--------------------------------------------------------------------------------
/src/utils/node/requestPathname.ts:
--------------------------------------------------------------------------------
1 | import type Http from "node:http"
2 | import { requestURLAlwaysWithHttpProtocol } from "./requestURLAlwaysWithHttpProtocol"
3 |
4 | export function requestPathname(request: Http.IncomingMessage): string {
5 | return requestURLAlwaysWithHttpProtocol(request).pathname
6 | }
7 |
--------------------------------------------------------------------------------
/src/utils/node/requestQuery.ts:
--------------------------------------------------------------------------------
1 | import type Http from "node:http"
2 | import qs from "qs"
3 | import { requestURLAlwaysWithHttpProtocol } from "./requestURLAlwaysWithHttpProtocol"
4 |
5 | export function requestQuery(
6 | request: Http.IncomingMessage,
7 | ): Record {
8 | const url = requestURLAlwaysWithHttpProtocol(request)
9 | return qs.parse(url.search.slice(1))
10 | }
11 |
--------------------------------------------------------------------------------
/src/utils/node/requestSubdomain.ts:
--------------------------------------------------------------------------------
1 | import type Http from "node:http"
2 | import { requestHostname } from "./requestHostname"
3 |
4 | export function requestSubdomain(
5 | request: Http.IncomingMessage,
6 | domain: string,
7 | ): string {
8 | const hostname = requestHostname(request)
9 | const [subdomain, ...rest] = hostname.split(".")
10 | const basedomain = rest.join(".")
11 |
12 | if (basedomain !== domain) {
13 | throw new Error(
14 | [
15 | `[requestSubdomain] I found basedomain mismatch.`,
16 | ``,
17 | ` expected basedomain: ${domain}`,
18 | ` requested basedomain: ${basedomain}`,
19 | ` subdomain: ${subdomain}`,
20 | ].join("\n"),
21 | )
22 | }
23 |
24 | return subdomain
25 | }
26 |
--------------------------------------------------------------------------------
/src/utils/node/requestText.ts:
--------------------------------------------------------------------------------
1 | import type Http from "node:http"
2 | import { requestBuffer } from "./requestBuffer"
3 |
4 | export async function requestText(
5 | request: Http.IncomingMessage,
6 | ): Promise {
7 | const buffer = await requestBuffer(request)
8 | return buffer.toString()
9 | }
10 |
--------------------------------------------------------------------------------
/src/utils/node/requestTokenName.ts:
--------------------------------------------------------------------------------
1 | import type Http from "node:http"
2 |
3 | export function requestTokenName(
4 | request: Http.IncomingMessage,
5 | ): string | undefined {
6 | if (!request.headers["authorization"]) {
7 | return
8 | }
9 |
10 | const authorization = request.headers["authorization"].toLowerCase()
11 |
12 | if (!authorization.startsWith("token")) {
13 | return
14 | }
15 |
16 | return authorization.slice("token".length).trim()
17 | }
18 |
--------------------------------------------------------------------------------
/src/utils/node/requestURLAlwaysWithHttpProtocol.ts:
--------------------------------------------------------------------------------
1 | import type Http from "node:http"
2 |
3 | export function requestURLAlwaysWithHttpProtocol(
4 | request: Http.IncomingMessage,
5 | ): URL {
6 | if (request.url === undefined) {
7 | throw new Error("[requestURLAlwaysWithHttpProtocol] expect request.url")
8 | }
9 |
10 | const protocol = "http"
11 |
12 | // NOTE For https request, the `protocol` might be wrong,
13 | // this function should only be used to parse url,
14 | // we should no rely on the `protocol` of the resulting url.
15 |
16 | return new URL(request.url, `${protocol}://${request.headers.host}`)
17 | }
18 |
--------------------------------------------------------------------------------
/src/utils/node/responseSetHeaders.ts:
--------------------------------------------------------------------------------
1 | import type Http from "node:http"
2 |
3 | export function responseSetHeaders(
4 | response: Http.ServerResponse,
5 | headers: Record,
6 | ) {
7 | for (const [name, value] of Object.entries(headers))
8 | if (value !== undefined) {
9 | response.setHeader(name, value)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/utils/node/responseSetStatus.ts:
--------------------------------------------------------------------------------
1 | import type Http from "node:http"
2 |
3 | export function responseSetStatus(
4 | response: Http.ServerResponse,
5 | status: {
6 | code?: number
7 | message?: string
8 | },
9 | ) {
10 | if (status?.code) {
11 | response.statusCode = status.code
12 | }
13 |
14 | if (status?.message) {
15 | const message = status.message.replaceAll("\n", "; ")
16 | response.statusMessage = message
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/utils/node/serverListen.ts:
--------------------------------------------------------------------------------
1 | import type { Server } from "node:net"
2 |
3 | export type ServerListenOptions = {
4 | hostname?: string
5 | port?: number | string
6 | }
7 |
8 | export function serverListen(
9 | server: Server,
10 | options: ServerListenOptions,
11 | ): Promise {
12 | return new Promise((resolve, reject) => {
13 | server.listen(options, () => {
14 | resolve()
15 | })
16 |
17 | server.on("error", (error) => {
18 | reject(error)
19 | })
20 | })
21 | }
22 |
--------------------------------------------------------------------------------
/src/utils/node/writeJson.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs"
2 | import { dirname } from "node:path"
3 | import type { Json } from "../Json"
4 |
5 | export async function writeJson(path: string, input: Json): Promise {
6 | const text = JSON.stringify(input)
7 | await fs.promises.mkdir(dirname(path), { recursive: true })
8 | await fs.promises.writeFile(path, text)
9 | }
10 |
--------------------------------------------------------------------------------
/src/utils/objectOmit.ts:
--------------------------------------------------------------------------------
1 | export function objectOmit>(
2 | x: A,
3 | key: Key,
4 | ): Omit {
5 | const result = { ...x }
6 | delete result[key]
7 | return result
8 | }
9 |
--------------------------------------------------------------------------------
/src/utils/objectRemoveUndefined.ts:
--------------------------------------------------------------------------------
1 | import { JsonObject } from "./Json"
2 |
3 | export function objectRemoveUndefined(object: JsonObject): JsonObject {
4 | return Object.fromEntries(
5 | Object.entries(object).filter(([key, value]) => value !== undefined),
6 | )
7 | }
8 |
--------------------------------------------------------------------------------
/src/utils/pretterLogger.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xieyuheng/x-server/57216f6fa13ed38318f258167c33f6f86d735e6e/src/utils/pretterLogger.ts
--------------------------------------------------------------------------------
/src/utils/randomHexString.ts:
--------------------------------------------------------------------------------
1 | import crypto from "node:crypto"
2 |
3 | export function randomHexString(size: number): string {
4 | const array = new Uint8Array(size)
5 | crypto.getRandomValues(array)
6 | let hexString = ""
7 | for (const n of array) {
8 | const s = n.toString(16)
9 | if (s.length === 1) {
10 | hexString += "0" + s
11 | } else {
12 | hexString += s
13 | }
14 | }
15 |
16 | return hexString
17 | }
18 |
--------------------------------------------------------------------------------
/src/utils/responseHeaders.ts:
--------------------------------------------------------------------------------
1 | export function responseHeaders(response: Response): Record {
2 | return Object.fromEntries((response.headers as any).entries())
3 | }
4 |
--------------------------------------------------------------------------------
/src/utils/slug.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest"
2 | import { slug } from "./slug"
3 |
4 | // prettier-ignore
5 | test("slug", () => {
6 | const target = "構造-the-constructivization-of-mathematics"
7 |
8 | expect(slug("構造 / The constructivization of mathematics")).toEqual(target)
9 | expect(slug("[構造] / The constructivization of mathematics---")).toEqual(target)
10 | expect(slug("---[構造] / The constructivization of mathematics---")).toEqual(target)
11 | expect(slug("---「構造」 / The constructivization of mathematics---")).toEqual(target)
12 | expect(slug("---「構造」 / The constructivization of mathematics___")).toEqual(target)
13 | expect(slug("---「構造」 / The_constructivization_of_mathematics___")).toEqual(target)
14 | })
15 |
--------------------------------------------------------------------------------
/src/utils/slug.ts:
--------------------------------------------------------------------------------
1 | export function slug(text: string): string {
2 | return text
3 | .trim()
4 | .toLowerCase()
5 | .replace(/[^\p{Letter}\-_ 0-9]+/gu, "")
6 | .replace(/\s/gu, "-")
7 | .replace(/_/g, "-")
8 | .replace(/\-+/g, "-")
9 | .replace(/\-$/g, "")
10 | .replace(/^\-/g, "")
11 | }
12 |
--------------------------------------------------------------------------------
/src/utils/stringTrimEnd.ts:
--------------------------------------------------------------------------------
1 | export function stringTrimEnd(input: string, pattern: string): string {
2 | if (input.endsWith(pattern)) {
3 | const prefix = input.slice(0, input.length - pattern.length)
4 | return stringTrimEnd(prefix, pattern)
5 | }
6 |
7 | return input
8 | }
9 |
--------------------------------------------------------------------------------
/src/utils/wait.ts:
--------------------------------------------------------------------------------
1 | export function wait(ms: number): Promise {
2 | return new Promise((resolve) => setTimeout(resolve, ms))
3 | }
4 |
--------------------------------------------------------------------------------
/src/website/Content.ts:
--------------------------------------------------------------------------------
1 | import type { Buffer } from "node:buffer"
2 |
3 | export type Content = {
4 | type: string
5 | buffer: Buffer
6 | }
7 |
--------------------------------------------------------------------------------
/src/website/WebsiteConfig.ts:
--------------------------------------------------------------------------------
1 | import { Schema, ty } from "@xieyuheng/ty"
2 | import { LoggerOptions, LoggerOptionsSchema } from "../server/LoggerOptions"
3 | import { ServerOptions, ServerOptionsSchema } from "../server/ServerOptions"
4 |
5 | export type WebsiteConfig = {
6 | server?: ServerOptions
7 | logger?: LoggerOptions
8 | cors?: boolean
9 | redirectNotFoundTo?: string
10 | cacheControlPatterns?: Record
11 | }
12 |
13 | export const WebsiteConfigSchema: Schema = ty.object({
14 | server: ty.optional(ServerOptionsSchema),
15 | logger: ty.optional(LoggerOptionsSchema),
16 | cors: ty.optional(ty.boolean()),
17 | redirectNotFoundTo: ty.optional(ty.string()),
18 | cacheControlPatterns: ty.optional(ty.dict(ty.string())),
19 | })
20 |
--------------------------------------------------------------------------------
/src/website/createWebsiteConfig.ts:
--------------------------------------------------------------------------------
1 | import { WebsiteConfig, WebsiteConfigSchema } from "./WebsiteConfig"
2 |
3 | export function createWebsiteConfig(json: any): WebsiteConfig {
4 | return WebsiteConfigSchema.validate(json)
5 | }
6 |
--------------------------------------------------------------------------------
/src/website/emptyWebsiteConfig.ts:
--------------------------------------------------------------------------------
1 | import { WebsiteConfig } from "./WebsiteConfig"
2 |
3 | export function emptyWebsiteConfig(): WebsiteConfig {
4 | return {
5 | cacheControlPatterns: {},
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/website/mergeWebsiteConfigs.ts:
--------------------------------------------------------------------------------
1 | import { objectRemoveUndefined } from "../utils/objectRemoveUndefined"
2 | import { WebsiteConfig } from "./WebsiteConfig"
3 | import { emptyWebsiteConfig } from "./emptyWebsiteConfig"
4 |
5 | export function mergeWebsiteConfigs(
6 | configs: Array,
7 | ): WebsiteConfig {
8 | let result = emptyWebsiteConfig()
9 | for (const config of configs) {
10 | result = mergeTwoWebsiteConfigs(result, config)
11 | }
12 |
13 | return result
14 | }
15 |
16 | function mergeTwoWebsiteConfigs(
17 | left: WebsiteConfig,
18 | right: WebsiteConfig,
19 | ): WebsiteConfig {
20 | const cacheControlPatterns = {
21 | ...left.cacheControlPatterns,
22 | ...objectRemoveUndefined(right.cacheControlPatterns || {}),
23 | } as Record
24 |
25 | const server = {
26 | ...left.server,
27 | ...objectRemoveUndefined(right.server || {}),
28 | }
29 |
30 | const logger = {
31 | ...left.logger,
32 | ...objectRemoveUndefined(right.logger || {}),
33 | }
34 |
35 | return {
36 | ...left,
37 | ...(objectRemoveUndefined(right) as WebsiteConfig),
38 | server,
39 | logger,
40 | cacheControlPatterns,
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/website/readContent.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs"
2 | import { extname, normalize, resolve } from "node:path"
3 | import { contentTypeRecord } from "../utils/contentTypeRecord"
4 | import { pathIsDirectory } from "../utils/node/pathIsDirectory"
5 | import { pathIsFile } from "../utils/node/pathIsFile"
6 | import { Content } from "./Content"
7 |
8 | export async function readContent(
9 | directory: string,
10 | path: string,
11 | ): Promise {
12 | const resolvedPath = normalize(resolve(directory, path))
13 |
14 | // NOTE We should not access path outside of given directory.
15 |
16 | // The URL constructor can already
17 | // rewrite
18 | // "http://example.com/../secret"
19 | // to
20 | // "http://example.com/secret"
21 | // But we want to be absolutely sure about this.
22 |
23 | if (!resolvedPath.startsWith(directory)) {
24 | return undefined
25 | }
26 |
27 | if (
28 | (await pathIsDirectory(resolvedPath)) &&
29 | (await pathIsFile(resolvedPath + "/index.html"))
30 | ) {
31 | return {
32 | type: "text/html",
33 | buffer: await fs.promises.readFile(resolvedPath + "/index.html"),
34 | }
35 | }
36 |
37 | if (await pathIsFile(resolvedPath)) {
38 | return {
39 | type:
40 | contentTypeRecord[extname(resolvedPath)] || "application/octet-stream",
41 | buffer: await fs.promises.readFile(resolvedPath),
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/website/readContentWithRewrite.ts:
--------------------------------------------------------------------------------
1 | import { Content } from "./Content"
2 | import { WebsiteConfig } from "./WebsiteConfig"
3 | import { readContent } from "./readContent"
4 |
5 | export async function readContentWithRewrite(
6 | directory: string,
7 | config: WebsiteConfig,
8 | path: string,
9 | ): Promise {
10 | const content = await readContent(directory, path)
11 | if (content !== undefined) {
12 | return content
13 | }
14 |
15 | if (config.redirectNotFoundTo !== undefined) {
16 | return await readContent(directory, config.redirectNotFoundTo)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/website/readWebsiteConfigFile.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs"
2 | import { WebsiteConfig } from "./WebsiteConfig"
3 | import { createWebsiteConfig } from "./createWebsiteConfig"
4 |
5 | export async function readWebsiteConfigFile(
6 | file: string,
7 | ): Promise {
8 | const text = await fs.promises.readFile(file, "utf-8")
9 | const json = JSON.parse(text)
10 | return createWebsiteConfig(json)
11 | }
12 |
--------------------------------------------------------------------------------
/src/website/readWebsiteConfigFileOrDefault.ts:
--------------------------------------------------------------------------------
1 | import { isErrnoException } from "../utils/node/isErrnoException"
2 | import { WebsiteConfig } from "./WebsiteConfig"
3 | import { emptyWebsiteConfig } from "./emptyWebsiteConfig"
4 | import { readWebsiteConfigFile } from "./readWebsiteConfigFile"
5 |
6 | export async function readWebsiteConfigFileOrDefault(
7 | file: string,
8 | ): Promise {
9 | try {
10 | return await readWebsiteConfigFile(file)
11 | } catch (error) {
12 | if (isErrnoException(error) && error.code === "ENOENT") {
13 | return emptyWebsiteConfig()
14 | }
15 |
16 | throw error
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/website/responseSetCacheControlHeaders.ts:
--------------------------------------------------------------------------------
1 | import type Http from "node:http"
2 | import { globMatch } from "../utils/globMatch"
3 | import { responseSetHeaders } from "../utils/node/responseSetHeaders"
4 | import { WebsiteConfig } from "./WebsiteConfig"
5 |
6 | export function responseSetCacheControlHeaders(
7 | config: WebsiteConfig,
8 | response: Http.ServerResponse,
9 | path: string,
10 | ): void {
11 | const cacheControlHeaders: Record = {}
12 | for (const [pattern, value] of Object.entries(
13 | config.cacheControlPatterns || {},
14 | )) {
15 | if (globMatch(pattern, path)) {
16 | cacheControlHeaders["cache-control"] = value
17 | }
18 | }
19 |
20 | responseSetHeaders(response, cacheControlHeaders)
21 | }
22 |
--------------------------------------------------------------------------------
/src/website/responseSetCorsHeaders.ts:
--------------------------------------------------------------------------------
1 | import type Http from "node:http"
2 | import { responseSetHeaders } from "../utils/node/responseSetHeaders"
3 | import { WebsiteConfig } from "./WebsiteConfig"
4 |
5 | export function responseSetCorsHeaders(
6 | config: WebsiteConfig,
7 | response: Http.ServerResponse,
8 | ): void {
9 | responseSetHeaders(response, {
10 | "access-control-allow-origin": config.cors ? "*" : undefined,
11 | })
12 | }
13 |
--------------------------------------------------------------------------------
/src/website/websiteConfigFromCommandLineOptions.ts:
--------------------------------------------------------------------------------
1 | import { serverOptionsFromCommandLineOptions } from "../server/serverOptionsFromCommandLineOptions"
2 | import { LoggerName } from "../utils/log"
3 | import { WebsiteConfig } from "./WebsiteConfig"
4 |
5 | export function websiteConfigFromCommandLineOptions(options: {
6 | hostname?: string
7 | port?: number
8 | "tls-cert"?: string
9 | "tls-key"?: string
10 | cors?: boolean
11 | "redirect-not-found-to"?: string
12 | "cache-control-pattern"?: string | Array
13 | "logger-name"?: LoggerName
14 | }): WebsiteConfig {
15 | const server = serverOptionsFromCommandLineOptions(options)
16 | const cacheControlPatterns = createCacheControlPatterns(
17 | options["cache-control-pattern"],
18 | )
19 |
20 | return {
21 | server,
22 | cors: options["cors"],
23 | redirectNotFoundTo: options["redirect-not-found-to"],
24 | cacheControlPatterns,
25 | }
26 | }
27 |
28 | function createCacheControlPatterns(
29 | input: undefined | string | Array,
30 | ): Record {
31 | if (input === undefined) {
32 | return {}
33 | }
34 |
35 | if (typeof input === "string") {
36 | const [pattern, value] = input.split(":").map((s) => s.trim())
37 | return Object.fromEntries([[pattern, value]])
38 | }
39 |
40 | return Object.fromEntries(
41 | input.map((entry) => entry.split(":").map((s) => s.trim())),
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["src"],
3 | "compilerOptions": {
4 | "lib": ["esnext", "dom"],
5 | "target": "es6",
6 | "module": "commonjs",
7 | "moduleResolution": "node",
8 | "esModuleInterop": true,
9 | "resolveJsonModule": true,
10 | "declaration": true,
11 | "sourceMap": true,
12 | "strict": true,
13 | "outDir": "lib",
14 | "baseUrl": "."
15 | }
16 | }
17 |
--------------------------------------------------------------------------------