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