diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 45001da4a..000000000 --- a/.babelrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "presets": [ - ["es2015", { "loose": true }] - ] -} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..60404ce83 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto eol=lf +*.ts linguist-language=JavaScript \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..f433b7e28 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: typicode diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml new file mode 100644 index 000000000..6204e5d05 --- /dev/null +++ b/.github/workflows/node.js.yml @@ -0,0 +1,31 @@ +# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs + +name: Node.js CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 20.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm ci + - run: npm run build --if-present + - run: npm test diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml new file mode 100644 index 000000000..10707c7e3 --- /dev/null +++ b/.github/workflows/npm-publish.yml @@ -0,0 +1,38 @@ +# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created +# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages + +name: Node.js Package + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: npm ci + - run: npm test + + publish-npm: + needs: build + runs-on: ubuntu-latest + permissions: + id-token: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: https://registry.npmjs.org/ + - run: npm ci + - run: npm publish --provenance --access public + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} diff --git a/.gitignore b/.gitignore index 306cc31e5..e0b3ff284 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ **/*.log -node_modules -tmp -lib .DS_Store .idea -db.json \ No newline at end of file +lib +node_modules +public/output.css +tmp diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 000000000..72c4429bc --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npm test diff --git a/.npmignore b/.npmignore deleted file mode 100644 index e8310385c..000000000 --- a/.npmignore +++ /dev/null @@ -1 +0,0 @@ -src \ No newline at end of file diff --git a/.prettier.config.js b/.prettier.config.js new file mode 100644 index 000000000..45713a76e --- /dev/null +++ b/.prettier.config.js @@ -0,0 +1,5 @@ +export default { + semi: false, + singleQuote: true, + trailingComma: 'all', +} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 29d5864eb..000000000 --- a/.travis.yml +++ /dev/null @@ -1,5 +0,0 @@ -sudo: false -language: node_js -node_js: - - "stable" -# - "0.12" diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index b0090b7ab..000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,216 +0,0 @@ -# Change Log - -## Unreleased - -* Display custom routes on homepage - -## [0.9.4][2016-12-08] - -* Improve rewriter [#431](https://github.com/typicode/json-server/issues/431) -* Improve watch mode [#427](https://github.com/typicode/json-server/pull/427) - -## [0.9.3][2016-12-07] - -* Fix [#396](https://github.com/typicode/json-server/issues/396) PUT/PATCH saves the updated item with an id that has been converted to string - -## [0.9.2][2016-11-29] - -* Fix [#221](https://github.com/typicode/json-server/issues/221) `nohup` support -* Fix [#420](https://github.com/typicode/json-server/issues/420) TypeError when watching `db.json` - -## [0.9.1][2016-11-21] - -* Fix - * [#412](https://github.com/typicode/json-server/issues/412) - * [#451](https://github.com/typicode/json-server/issues/411) - -## [0.9.0][2016-11-11] - -* Shorter `uuid` -* No automatic conversion of strings to boolean or integer -* Create a default `db.json` file if it doesn't exist -* Fix - * [#361](https://github.com/typicode/json-server/issues/361) - * [#363](https://github.com/typicode/json-server/issues/363) [#365](https://github.com/typicode/json-server/issues/365) - * [#374](https://github.com/typicode/json-server/issues/374) - * [#383](https://github.com/typicode/json-server/issues/383) -* Updated dependencies and codebase to ES6 - -## [0.8.23][2016-11-03] - -* Fix `Links` header - -## [0.8.22][2016-10-04] - -* Fix `Links` header issue when using `_page` -* Add query params support to the route rewriter - -## [0.8.21][2016-09-13] - -* Fix bodyParser issue when using custom routes - -## [0.8.20][2016-09-12] - -* Fix [#355](https://github.com/typicode/json-server/issues/355) -* Add `_page` support - -## [0.8.19][2016-08-18] - -* Fix [#341](https://github.com/typicode/json-server/issues/341) - -## [0.8.18][2016-08-17] - -* Add CLI option `--middlewares` and support them in `json-server.json` config file - -## [0.8.17][2016-07-25] - -* Fix snapshot creation for JS files (ex: `json-server generator.js`) - -## [0.8.16][2016-07-11] - -* Support `x-www-form-urlencoded` - -## [0.8.15][2016-07-03] - -* Bug fix: `--watch` option on OS X - -## [0.8.14][2016-05-15] - -* Bug fix: data wasn't written to file in `v0.8.13` and `v0.8.12` - -## [0.8.13][2016-05-12] - -* Make `_like` operator case insensitive - -## [0.8.12][2016-05-08] - -* Minor bug fix - -## [0.8.11][2016-05-08] - -* Support sort by nested field (e.g. `_sort=author.name`) -* Fix `graceful-fs` warning - -## [0.8.10][2016-04-18] - -* CLI option `-ng/--no-gzip` to disable `gzip` compression - -## [0.8.9][2016-03-17] - -* CLI can now read options from `json-server.json` if present -* CLI option `-c/--config` to point to a different configuration file - -## [0.8.8][2016-02-13] - -### Fixed - -* Fix #233 - -## [0.8.7][2016-01-22] - -### Added - -* `gzip` compression to improve performances -* CLI option `-nc/--no-cors` to disable CORS - -## [0.8.6][2016-01-07] - -### Added - -* CLI option `-ro/--read-only` to allow only GET requests - -## [0.8.5][2015-12-28] - -### Fixed - -* Fix #177 - -## [0.8.4][2015-12-13] - -### Added - -* Like operator `GET /posts?title_like=json` (accepts RegExp) - -## [0.8.3][2015-11-25] - -### Added - -* CLI option `-q/--quiet` -* Nested route `POST /posts/1/comments` -* Not equal operator `GET /posts?id_ne=1` - -## [0.8.2][2015-10-15] - -### Added - -* CLI option `-S/--snapshots` to set a custom snapshots directory. - -### Fixed - -* Fix plural resources: `DELETE` should return `404` if resource doesn't exist. - -## [0.8.1][2015-10-06] - -### Fixed - -* Fix plural resources: `PUT` should replace resource instead of updating properties. -* Fix singular resources: `POST`, `PUT`, `PATCH` should not convert resource properties. - -## [0.8.0][2015-09-21] - -### Changed - -* `jsonServer.defaults` is now a function and can take an object. -If you're using the project as a module, you need to update your code: - -```js -// Before -jsonServer.defaults -// After -jsonServer.defaults() -jsonServer.defaults({ static: '/some/path'}) -``` - -* Automatically ignore unknown query parameters. - -```bash -# Before -GET /posts?author=typicode&foo=bar # [] -# After -GET /posts?author=typicode&foo=bar # [{...}, {...}] -``` - -### Added - -* CLI option for setting a custom static files directory. - -```bash -json-server --static some/path -``` - -## [0.7.28][2015-09-09] - -```bash -# Support range -GET /products?price_gte=50&price_lte=100 -``` - -## [0.7.27][2015-09-02] - -### Added - -```bash -# Support OR -GET /posts?id=1&id2 -GET /posts?category=javascript&category=html -``` - -## [0.7.26][2015-09-01] - -### Added - -```bash -# Support embed and expand in lists -GET /posts?embed=comments -GET /posts?expand=user -``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..bbb830ad5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,21 @@ +## Agreement + +Thanks for your interest in contributing! + +By contributing to this project, you agree to the following: + +1. **Relicensing:** to support the project's sustainability and ensure it's longevity, your contributions can be relicensed to any license. + +2. **Ownership Rights:** You confirm you own the rights to your contributed code. + +3. **Disagreement:** If you disagree with these terms, please create an issue instead. I'll handle the bug or feature request. + +4. **Benefits for Contributors:** If your contribution is merged, you'll enjoy the same benefits as a sponsor for one year. This includes using the project in a company context, free of charge. + +## Fair Source License + +This project uses the Fair Source License, which is neither purely open-source nor closed-source. It allows visibility of the source code and free usage for a limited number of two users within an organization. Beyond this limit (three or more users), a small licensing fee via [GitHub Sponsors](https://github.com/sponsors/typicode) applies. + +This doesn't apply to individuals, students, teachers, small teams, ... + +Got questions or need support? Feel free to reach out to typicode@gmail.com. diff --git a/LICENSE b/LICENSE index ce53c0583..752515b57 100644 --- a/LICENSE +++ b/LICENSE @@ -1,20 +1,44 @@ -The MIT License (MIT) - -Copyright (c) 2015 typicode - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Fair Source License, version 0.9 + +Copyright (C) 2023-present typicode + +Licensor: typicode + +Software: json-server + +Use Limitation: 2 users + +License Grant. Licensor hereby grants to each recipient of the +Software ("you") a non-exclusive, non-transferable, royalty-free and +fully-paid-up license, under all of the Licensor's copyright and +patent rights, to use, copy, distribute, prepare derivative works of, +publicly perform and display the Software, subject to the Use +Limitation and the conditions set forth below. + +Use Limitation. The license granted above allows use by up to the +number of users per entity set forth above (the "Use Limitation"). For +determining the number of users, "you" includes all affiliates, +meaning legal entities controlling, controlled by, or under common +control with you. If you exceed the Use Limitation, your use is +subject to payment of Licensor's then-current list price for licenses. + +Conditions. Redistribution in source code or other forms must include +a copy of this license document to be provided in a reasonable +manner. Any redistribution of the Software is only allowed subject to +this license. + +Trademarks. This license does not grant you any right in the +trademarks, service marks, brand names or logos of Licensor. + +DISCLAIMER. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OR +CONDITION, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. LICENSORS HEREBY DISCLAIM ALL LIABILITY, WHETHER IN +AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE. + +Termination. If you violate the terms of this license, your rights +will terminate automatically and will not be reinstated without the +prior written consent of Licensor. Any such termination will not +affect the right of others who may have received copies of the +Software from you. diff --git a/README.md b/README.md index 1916c1aa2..91ea528f2 100644 --- a/README.md +++ b/README.md @@ -1,537 +1,204 @@ -# JSON Server [![](https://travis-ci.org/typicode/json-server.svg?branch=master)](https://travis-ci.org/typicode/json-server) [![](https://badge.fury.io/js/json-server.svg)](http://badge.fury.io/js/json-server) [![](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/typicode/json-server?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +# json-server -Get a full fake REST API with __zero coding__ in __less than 30 seconds__ (seriously) +[![Node.js CI](https://github.com/typicode/json-server/actions/workflows/node.js.yml/badge.svg)](https://github.com/typicode/json-server/actions/workflows/node.js.yml) -Created with <3 for front-end developers who need a quick back-end for prototyping and mocking. +> [!IMPORTANT] +> Viewing beta v1 documentation – usable but expect breaking changes. For stable version, see [here](https://github.com/typicode/json-server/tree/v0) -* [Egghead.io free video tutorial - Creating demo APIs with json-server](https://egghead.io/lessons/nodejs-creating-demo-apis-with-json-server) -* [JSONPlaceholder - Live running version](http://jsonplaceholder.typicode.com) +πŸ‘‹ _Hey! Using React, Vue or Astro? Check my new project [MistCSS](https://github.com/typicode/mistcss) to write 50% less code._ -See also: -* :hotel: [hotel - Start apps from your browser and get local dev domains in seconds](https://github.com/typicode/hotel) -* :dog: [husky - Git hooks made easy](https://github.com/typicode/husky) - -## Table of contents - -
- - - -- [Example](#example) -- [Install](#install) -- [Routes](#routes) - * [Plural routes](#plural-routes) - * [Singular routes](#singular-routes) - * [Filter](#filter) - * [Paginate](#paginate) - * [Sort](#sort) - * [Slice](#slice) - * [Operators](#operators) - * [Full-text search](#full-text-search) - * [Relationships](#relationships) - * [Database](#database) - * [Homepage](#homepage) -- [Extras](#extras) - * [Static file server](#static-file-server) - * [Alternative port](#alternative-port) - * [Access from anywhere](#access-from-anywhere) - * [Remote schema](#remote-schema) - * [Generate random data](#generate-random-data) - * [HTTPS](#https) - * [Add custom routes](#add-custom-routes) - * [Add middlewares](#add-middlewares) - * [CLI usage](#cli-usage) - * [Module](#module) - + [Simple example](#simple-example) - + [Custom routes example](#custom-routes-example) - + [Access control example](#access-control-example) - + [Custom output example](#custom-output-example) - + [Rewriter example](#rewriter-example) - + [Mounting JSON Server on another endpoint example](#mounting-json-server-on-another-endpoint-example) - * [Deployment](#deployment) -- [Links](#links) - * [Video](#video) - * [Articles](#articles) - * [Third-party tools](#third-party-tools) -- [License](#license) - - +## Install -
+```shell +npm install json-server +``` -## Example +## Usage -Create a `db.json` file +Create a `db.json` or `db.json5` file ```json { "posts": [ - { "id": 1, "title": "json-server", "author": "typicode" } + { "id": "1", "title": "a title", "views": 100 }, + { "id": "2", "title": "another title", "views": 200 } ], "comments": [ - { "id": 1, "body": "some comment", "postId": 1 } + { "id": "1", "text": "a comment about post 1", "postId": "1" }, + { "id": "2", "text": "another comment about post 1", "postId": "1" } ], - "profile": { "name": "typicode" } + "profile": { + "name": "typicode" + } } ``` -Start JSON Server - -```bash -$ json-server --watch db.json -``` - -Now if you go to [http://localhost:3000/posts/1](), you'll get - -```json -{ "id": 1, "title": "json-server", "author": "typicode" } -``` - -Also when doing requests, it's good to know that: - -- If you make POST, PUT, PATCH or DELETE requests, changes will be automatically and safely saved to `db.json` using [lowdb](https://github.com/typicode/lowdb). -- Your request body JSON should be object enclosed, just like the GET output. (for example `{"name": "Foobar"}`) -- Id values are not mutable. Any `id` value in the body of your PUT or PATCH request wil be ignored. Only a value set in a POST request wil be respected, but only if not already taken. -- A POST, PUT or PATCH request should include a `Content-Type: application/json` header to use the JSON in the request body. Otherwise it will result in a 200 OK but without changes being made to the data. - -## Install - -```bash -$ npm install -g json-server -``` - -## Routes - -Based on the previous `db.json` file, here are all the default routes. You can also add [other routes](#add-custom-routes) using `--routes`. - -### Plural routes - -``` -GET /posts -GET /posts/1 -POST /posts -PUT /posts/1 -PATCH /posts/1 -DELETE /posts/1 -``` - -### Singular routes - -``` -GET /profile -POST /profile -PUT /profile -PATCH /profile -``` - -### Filter - -Use `.` to access deep properties - -``` -GET /posts?title=json-server&author=typicode -GET /posts?id=1&id=2 -GET /comments?author.name=typicode -``` - -### Paginate - -Use `_page` and optionally `_limit` to paginate returned data. - -In the `Link` header you'll get `first`, `prev`, `next` and `last` links. +
+View db.json5 example -``` -GET /posts?_page=7 -GET /posts?_page=7&_limit=20 +```json5 +{ + posts: [ + { id: '1', title: 'a title', views: 100 }, + { id: '2', title: 'another title', views: 200 }, + ], + comments: [ + { id: '1', text: 'a comment about post 1', postId: '1' }, + { id: '2', text: 'another comment about post 1', postId: '1' }, + ], + profile: { + name: 'typicode', + }, +} ``` -_10 items are returned by default_ +You can read more about JSON5 format [here](https://github.com/json5/json5). -### Sort +
-Add `_sort` and `_order` (ascending order by default) +Pass it to JSON Server CLI -``` -GET /posts?_sort=views&_order=DESC -GET /posts/1/comments?_sort=votes&_order=ASC +```shell +$ npx json-server db.json ``` -### Slice +Get a REST API -Add `_start` and `_end` or `_limit` (an `X-Total-Count` header is included in the response) - -``` -GET /posts?_start=20&_end=30 -GET /posts/1/comments?_start=20&_end=30 -GET /posts/1/comments?_start=20&_limit=10 +```shell +$ curl http://localhost:3000/posts/1 +{ + "id": "1", + "title": "a title", + "views": 100 +} ``` -### Operators +Run `json-server --help` for a list of options -Add `_gte` or `_lte` for getting a range +## Sponsors ✨ -``` -GET /posts?views_gte=10&views_lte=20 -``` +| Sponsors | +| :---: | +| | +| | +| | -Add `_ne` to exclude a value +| Sponsors | | +| :---: | :---: | +| | | -``` -GET /posts?id_ne=1 -``` +[Become a sponsor and have your company logo here](https://github.com/users/typicode/sponsorship) -Add `_like` to filter (RegExp supported) +## Sponsorware -``` -GET /posts?title_like=server -``` +> [!NOTE] +> This project uses the [Fair Source License](https://fair.io/). Only organizations with 3+ users are kindly asked to contribute a small amount through sponsorship [sponsor](https://github.com/sponsors/typicode) for usage. __This license helps keep the project sustainable and healthy, benefiting everyone.__ +> +> For more information, FAQs, and the rationale behind this, visit [https://fair.io/](https://fair.io/). -### Full-text search +## Routes -Add `q` +Based on the example `db.json`, you'll get the following routes: ``` -GET /posts?q=internet -``` - -### Relationships - -To include children resources, add `_embed` +GET /posts +GET /posts/:id +POST /posts +PUT /posts/:id +PATCH /posts/:id +DELETE /posts/:id +# Same for comments ``` -GET /posts?_embed=comments -GET /posts/1?_embed=comments -``` - -To include parent resource, add `_expand` ``` -GET /comments?_expand=post -GET /comments/1?_expand=post +GET /profile +PUT /profile +PATCH /profile ``` -To get or create nested resources (by default one level, [add custom routes](#add-custom-routes) for more) +## Params -``` -GET /posts/1/comments -POST /posts/1/comments -``` +### Conditions -### Database +- ` ` β†’ `==` +- `lt` β†’ `<` +- `lte` β†’ `<=` +- `gt` β†’ `>` +- `gte` β†’ `>=` +- `ne` β†’ `!=` ``` -GET /db +GET /posts?views_gt=9000 ``` -### Homepage +### Range -Returns default index file or serves `./public` directory +- `start` +- `end` +- `limit` ``` -GET / +GET /posts?_start=10&_end=20 +GET /posts?_start=10&_limit=10 ``` -## Extras - -### Static file server - -You can use JSON Server to serve your HTML, JS and CSS, simply create a `./public` directory -or use `--static` to set a different static files directory. - -```bash -mkdir public -echo 'hello world' > public/index.html -json-server db.json -``` - -```bash -json-server db.json --static ./some-other-dir -``` - -### Alternative port +### Paginate -You can start JSON Server on other ports with the `--port` flag: +- `page` +- `per_page` (default = 10) -```bash -$ json-server --watch db.json --port 3004 ``` - -### Access from anywhere - -You can access your fake API from anywhere using CORS and JSONP. - -### Remote schema - -You can load remote schemas. - -```bash -$ json-server http://example.com/file.json -$ json-server http://jsonplaceholder.typicode.com/db +GET /posts?_page=1&_per_page=25 ``` -### Generate random data +### Sort -Using JS instead of a JSON file, you can create data programmatically. +- `_sort=f1,f2` -```javascript -// index.js -module.exports = function() { - var data = { users: [] } - // Create 1000 users - for (var i = 0; i < 1000; i++) { - data.users.push({ id: i, name: 'user' + i }) - } - return data -} ``` - -```bash -$ json-server index.js +GET /posts?_sort=id,-views ``` -__Tip__ use modules like [Faker](https://github.com/Marak/faker.js), [Casual](https://github.com/boo1ean/casual), [Chance](https://github.com/victorquinn/chancejs) or [JSON Schema Faker](https://github.com/json-schema-faker/json-schema-faker). - -### HTTPS - -There's many way to set up SSL in development. One simple way though is to use [hotel](https://github.com/typicode/hotel). - -### Add custom routes - -Create a `routes.json` file. Pay attention to start every route with `/`. - -```json -{ - "/api/": "/", - "/blog/:resource/:id/show": "/:resource/:id", - "/blog/:category": "/posts/:id?category=:category" -} -``` +### Nested and array fields -Start JSON Server with `--routes` option. +- `x.y.z...` +- `x.y.z[i]...` -```bash -json-server db.json --routes routes.json ``` - -Now you can access resources using additional routes. - -```sh -/api/posts # β†’ /posts -/api/posts/1 # β†’ /posts/1 -/blog/posts/1/show # β†’ /posts/1 -/blog/javascript # β†’ /posts?category=javascript +GET /foo?a.b=bar +GET /foo?x.y_lt=100 +GET /foo?arr[0]=bar ``` -### Add middlewares - -You can add your middlewares from the CLI using `--middlewares` option: +### Embed -```js -// hello.js -module.exports = function (req, res, next) { - res.header('X-Hello', 'World') - next() -} ``` - -```bash -json-server db.json --middlewares ./hello.js -json-server db.json --middlewares ./first.js ./second.js +GET /posts?_embed=comments +GET /comments?_embed=post ``` -### CLI usage +## Delete ``` -json-server [options] - -Options: - --config, -c Path to config file [default: "json-server.json"] - --port, -p Set port [default: 3000] - --host, -H Set host [default: "0.0.0.0"] - --watch, -w Watch file(s) [boolean] - --routes, -r Path to routes file - --middlewares, -m Paths to middleware files [array] - --static, -s Set static files directory - --read-only, --ro Allow only GET requests [boolean] - --no-cors, --nc Disable Cross-Origin Resource Sharing [boolean] - --no-gzip, --ng Disable GZIP Content-Encoding [boolean] - --snapshots, -S Set snapshots directory [default: "."] - --delay, -d Add delay to responses (ms) - --id, -i Set database id property (e.g. _id) [default: "id"] - --quiet, -q Suppress log messages from output [boolean] - --help, -h Show help [boolean] - --version, -v Show version number [boolean] - -Examples: - json-server db.json - json-server file.js - json-server http://example.com/db.json - -https://github.com/typicode/json-server -``` - -You can also set options in a `json-server.json` configuration file. - -```json -{ - "port": 3000 -} +DELETE /posts/1 +DELETE /posts/1?_dependent=comments ``` -### Module - -If you need to add authentication, validation, or __any behavior__, you can use the project as a module in combination with other Express middlewares. - -#### Simple example +## Serving static files -```js -// server.js -var jsonServer = require('json-server') -var server = jsonServer.create() -var router = jsonServer.router('db.json') -var middlewares = jsonServer.defaults() +If you create a `./public` directory, JSON Server will serve its content in addition to the REST API. -server.use(middlewares) -server.use(router) -server.listen(3000, function () { - console.log('JSON Server is running') -}) -``` +You can also add custom directories using `-s/--static` option. ```sh -$ node server.js -``` - -The path you provide to the `jsonServer.router` function is relative to the directory from where you launch your node process. If you run the above code from another directory, it’s better to use an absolute path: - -```js -var path = require('path') -var router = jsonServer.router(path.join(__dirname, 'db.json')) -``` - -For an in-memory database, simply pass an object to `jsonServer.router()`. - -Please note also that `jsonServer.router()` can be used in existing Express projects. - -#### Custom routes example - -Let's say you want a route that echoes query parameters and another one that set a timestamp on every resource created. - -```js -var jsonServer = require('json-server') -var server = jsonServer.create() -var router = jsonServer.router('db.json') -var middlewares = jsonServer.defaults() - -// Set default middlewares (logger, static, cors and no-cache) -server.use(middlewares) - -// Add custom routes before JSON Server router -server.get('/echo', function (req, res) { - res.jsonp(req.query) -}) - -// To handle POST, PUT and PATCH you need to use a body-parser -// You can use the one used by JSON Server -server.use(jsonServer.bodyParser) -server.use(function (req, res, next) { - if (req.method === 'POST') { - req.body.createdAt = Date.now() - } - // Continue to JSON Server router - next() -}) - -// Use default router -server.use(router) -server.listen(3000, function () { - console.log('JSON Server is running') -}) -``` - -#### Access control example - -```js -var jsonServer = require('json-server') -var server = jsonServer.create() -var router = jsonServer.router('db.json') -var middlewares = jsonServer.defaults() - -server.use(middlewares) -server.use(function (req, res, next) { - if (isAuthorized(req)) { // add your authorization logic here - next() // continue to JSON Server router - } else { - res.sendStatus(401) - } -}) -server.use(router) -server.listen(3000, function () { - console.log('JSON Server is running') -}) -``` - -#### Custom output example - -To modify responses, overwrite `router.render` method: - -```javascript -// In this example, returned resources will be wrapped in a body property -router.render = function (req, res) { - res.jsonp({ - body: res.locals.data - }) -} -``` - -#### Rewriter example - -To add rewrite rules, use `jsonServer.rewriter()`: - -```javascript -// Add this before server.use(router) -server.use(jsonServer.rewriter({ - '/api/': '/', - '/blog/:resource/:id/show': '/:resource/:id' -})) -``` - -#### Mounting JSON Server on another endpoint example - -Alternatively, you can also mount the router on `/api`. - -```javascript -server.use('/api', router) +json-server -s ./static +json-server -s ./static -s ./node_modules ``` -### Deployment - -You can deploy JSON Server. For example, [JSONPlaceholder](http://jsonplaceholder.typicode.com) is an online fake API powered by JSON Server and running on Heroku. - -## Links - -### Video - -* [Creating Demo APIs with json-server on egghead.io](https://egghead.io/lessons/nodejs-creating-demo-apis-with-json-server) - -### Articles - -* [Node Module Of The Week - json-server](http://nmotw.in/json-server/) -* [Mock up your REST API with JSON Server](http://www.betterpixels.co.uk/projects/2015/05/09/mock-up-your-rest-api-with-json-server/) -* [ng-admin: Add an AngularJS admin GUI to any RESTful API](http://marmelab.com/blog/2014/09/15/easy-backend-for-your-restful-api.html) -* [Fast prototyping using Restangular and Json-server](http://glebbahmutov.com/blog/fast-prototyping-using-restangular-and-json-server/) -* [Create a Mock REST API in Seconds for Prototyping your Frontend](https://coligo.io/create-mock-rest-api-with-json-server/) -* [No API? No Problem! Rapid Development via Mock APIs](https://medium.com/@housecor/rapid-development-via-mock-apis-e559087be066#.93d7w8oro) - -### Third-party tools - -* [Grunt JSON Server](https://github.com/tfiwm/grunt-json-server) -* [Docker JSON Server](https://github.com/clue/docker-json-server) -* [JSON Server GUI](https://github.com/naholyr/json-server-gui) -* [JSON file generator](https://github.com/dfsq/json-server-init) -* [JSON Server extension](https://github.com/maty21/json-server-extension) - -## License +## Notable differences with v0.17 -MIT - [Typicode](https://github.com/typicode) +- `id` is always a string and will be generated for you if missing +- use `_per_page` with `_page` instead of `_limit`for pagination +- use Chrome's `Network tab > throtling` to delay requests instead of `--delay` CLI option diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 5fef32bbf..000000000 --- a/appveyor.yml +++ /dev/null @@ -1,21 +0,0 @@ -# Test against this version of Node.js -environment: - nodejs_version: "7" - -# Install scripts. (runs after repo cloning) -install: - # Get the latest stable version of Node.js - - ps: Install-Product node $env:nodejs_version - # install modules - - npm install - -# Post-install test scripts. -test_script: - # Output useful info for debugging. - - node --version - - npm --version - # run tests - - npm test - -# Don't actually build. -build: off diff --git a/bin/index.js b/bin/index.js deleted file mode 100755 index af3551aa9..000000000 --- a/bin/index.js +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env node -require('../lib/cli/bin') diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 000000000..e8b7f785c --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,11 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; + + +export default [ + {files: ["**/*.{js,mjs,cjs,ts}"]}, + {languageOptions: { globals: globals.node }}, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, +]; \ No newline at end of file diff --git a/fixtures/db.json b/fixtures/db.json new file mode 100644 index 000000000..4e736d112 --- /dev/null +++ b/fixtures/db.json @@ -0,0 +1,13 @@ +{ + "posts": [ + { "id": "1", "title": "a title" }, + { "id": "2", "title": "another title" } + ], + "comments": [ + { "id": "1", "text": "a comment about post 1", "postId": "1" }, + { "id": "2", "text": "another comment about post 1", "postId": "1" } + ], + "profile": { + "name": "typicode" + } +} \ No newline at end of file diff --git a/fixtures/db.json5 b/fixtures/db.json5 new file mode 100644 index 000000000..3b06138e7 --- /dev/null +++ b/fixtures/db.json5 @@ -0,0 +1,27 @@ +{ + posts: [ + { + id: '1', + title: 'a title', + }, + { + id: '2', + title: 'another title', + }, + ], + comments: [ + { + id: '1', + text: 'a comment about post 1', + postId: '1', + }, + { + id: '2', + text: 'another comment about post 1', + postId: '1', + }, + ], + profile: { + name: 'typicode', + }, +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..60d0c463e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3908 @@ +{ + "name": "json-server", + "version": "1.0.0-beta.3", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "json-server", + "version": "1.0.0-beta.3", + "license": "SEE LICENSE IN ./LICENSE", + "dependencies": { + "@tinyhttp/app": "^2.4.0", + "@tinyhttp/cors": "^2.0.1", + "@tinyhttp/logger": "^2.0.0", + "chalk": "^5.3.0", + "chokidar": "^4.0.1", + "dot-prop": "^9.0.0", + "eta": "^3.5.0", + "inflection": "^3.0.0", + "json5": "^2.2.3", + "lowdb": "^7.0.1", + "milliparsec": "^4.0.0", + "sirv": "^2.0.4", + "sort-on": "^6.1.0" + }, + "bin": { + "json-server": "lib/bin.js" + }, + "devDependencies": { + "@eslint/js": "^9.11.0", + "@sindresorhus/tsconfig": "^6.0.0", + "@tailwindcss/typography": "^0.5.15", + "@types/node": "^22.5.5", + "concurrently": "^9.0.1", + "eslint": "^9.11.0", + "get-port": "^7.1.0", + "globals": "^15.9.0", + "husky": "^9.1.6", + "tempy": "^3.1.0", + "tsx": "^4.19.1", + "type-fest": "^4.26.1", + "typescript": "^5.6.2", + "typescript-eslint": "^8.6.0" + }, + "engines": { + "node": ">=18.3" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.11.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.0.tgz", + "integrity": "sha512-LPkkenkDqyzTFauZLLAPhIb48fj6drrfMvRGSL9tS3AcZBSVTllemLSNyCvHNNL2t797S/6DJNSIwRwXgMO/eQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz", + "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true, + "peer": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.24", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.24.tgz", + "integrity": "sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==" + }, + "node_modules/@sindresorhus/tsconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/tsconfig/-/tsconfig-6.0.0.tgz", + "integrity": "sha512-+fUdfuDd/7O2OZ9/UvJy76IEWn2Tpvm2l+rwUoS2Yz4jCUTSNOQQv2PLWrwekt8cPLwHmpHaBpay34bkBmVl2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.15.tgz", + "integrity": "sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20" + } + }, + "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@tinyhttp/accepts": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@tinyhttp/accepts/-/accepts-2.2.3.tgz", + "integrity": "sha512-9pQN6pJAJOU3McmdJWTcyq7LLFW8Lj5q+DadyKcvp+sxMkEpktKX5sbfJgJuOvjk6+1xWl7pe0YL1US1vaO/1w==", + "license": "MIT", + "dependencies": { + "mime": "4.0.4", + "negotiator": "^0.6.3" + }, + "engines": { + "node": ">=12.20.0" + }, + "funding": { + "type": "individual", + "url": "https://github.com/tinyhttp/tinyhttp?sponsor=1" + } + }, + "node_modules/@tinyhttp/app": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@tinyhttp/app/-/app-2.4.0.tgz", + "integrity": "sha512-vOPiCemQRJq5twnl06dde6XnWiNbVMdVRFJWW/yC/9G0qgvV2TvzNNTxrdlz6YmyB7vIC7Fg3qS6m6gx8RbBNQ==", + "license": "MIT", + "dependencies": { + "@tinyhttp/cookie": "2.1.1", + "@tinyhttp/proxy-addr": "2.2.0", + "@tinyhttp/req": "2.2.4", + "@tinyhttp/res": "2.2.4", + "@tinyhttp/router": "2.2.3", + "header-range-parser": "1.1.3", + "regexparam": "^2.0.2" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "individual", + "url": "https://github.com/tinyhttp/tinyhttp?sponsor=1" + } + }, + "node_modules/@tinyhttp/content-disposition": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@tinyhttp/content-disposition/-/content-disposition-2.2.2.tgz", + "integrity": "sha512-crXw1txzrS36huQOyQGYFvhTeLeG0Si1xu+/l6kXUVYpE0TjFjEZRqTbuadQLfKGZ0jaI+jJoRyqaWwxOSHW2g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "funding": { + "type": "individual", + "url": "https://github.com/tinyhttp/tinyhttp?sponsor=1" + } + }, + "node_modules/@tinyhttp/content-type": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@tinyhttp/content-type/-/content-type-0.1.4.tgz", + "integrity": "sha512-dl6f3SHIJPYbhsW1oXdrqOmLSQF/Ctlv3JnNfXAE22kIP7FosqJHxkz/qj2gv465prG8ODKH5KEyhBkvwrueKQ==", + "license": "MIT", + "engines": { + "node": ">=12.4" + } + }, + "node_modules/@tinyhttp/cookie": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@tinyhttp/cookie/-/cookie-2.1.1.tgz", + "integrity": "sha512-h/kL9jY0e0Dvad+/QU3efKZww0aTvZJslaHj3JTPmIPC9Oan9+kYqmh3M6L5JUQRuTJYFK2nzgL2iJtH2S+6dA==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "funding": { + "type": "individual", + "url": "https://github.com/tinyhttp/tinyhttp?sponsor=1" + } + }, + "node_modules/@tinyhttp/cookie-signature": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@tinyhttp/cookie-signature/-/cookie-signature-2.1.1.tgz", + "integrity": "sha512-VDsSMY5OJfQJIAtUgeQYhqMPSZptehFSfvEEtxr+4nldPA8IImlp3QVcOVuK985g4AFR4Hl1sCbWCXoqBnVWnw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@tinyhttp/cors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tinyhttp/cors/-/cors-2.0.1.tgz", + "integrity": "sha512-qrmo6WJuaiCzKWagv2yA/kw6hIISfF/hOqPWwmI6w0o8apeTMmRN3DoCFvQ/wNVuWVdU5J4KU7OX8aaSOEq51A==", + "license": "MIT", + "dependencies": { + "@tinyhttp/vary": "^0.1.3" + }, + "engines": { + "node": ">=12.20 || 14.x || >=16" + } + }, + "node_modules/@tinyhttp/encode-url": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@tinyhttp/encode-url/-/encode-url-2.1.1.tgz", + "integrity": "sha512-AhY+JqdZ56qV77tzrBm0qThXORbsVjs/IOPgGCS7x/wWnsa/Bx30zDUU/jPAUcSzNOzt860x9fhdGpzdqbUeUw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@tinyhttp/etag": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@tinyhttp/etag/-/etag-2.1.2.tgz", + "integrity": "sha512-j80fPKimGqdmMh6962y+BtQsnYPVCzZfJw0HXjyH70VaJBHLKGF+iYhcKqzI3yef6QBNa8DKIPsbEYpuwApXTw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@tinyhttp/forwarded": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@tinyhttp/forwarded/-/forwarded-2.1.1.tgz", + "integrity": "sha512-nO3kq0R1LRl2+CAMlnggm22zE6sT8gfvGbNvSitV6F9eaUSurHP0A8YZFMihSkugHxK+uIegh1TKrqgD8+lyGQ==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@tinyhttp/logger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tinyhttp/logger/-/logger-2.0.0.tgz", + "integrity": "sha512-8DfLQjGDIaIJeivYamVrrpmwmsGwS8wt2DGvzlcY5HEBagdiI4QJy/veAFcUHuaJqufn4wLwmn4q5VUkW8BCpQ==", + "license": "MIT", + "dependencies": { + "colorette": "^2.0.20", + "dayjs": "^1.11.10", + "http-status-emojis": "^2.2.0" + }, + "engines": { + "node": ">=14.18 || >=16.20" + } + }, + "node_modules/@tinyhttp/proxy-addr": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@tinyhttp/proxy-addr/-/proxy-addr-2.2.0.tgz", + "integrity": "sha512-WM/PPL9xNvrs7/8Om5nhKbke5FHrP3EfjOOR+wBnjgESfibqn0K7wdUTnzSLp1lBmemr88os1XvzwymSgaibyA==", + "license": "MIT", + "dependencies": { + "@tinyhttp/forwarded": "2.1.1", + "ipaddr.js": "^2.2.0" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@tinyhttp/req": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tinyhttp/req/-/req-2.2.4.tgz", + "integrity": "sha512-lQAZIAo0NOeghxFOZS57tQzxpHSPPLs9T68Krq2BncEBImKwqaDKUt7M9Y5Kb+rvC/GwIL3LeErhkg7f5iG4IQ==", + "license": "MIT", + "dependencies": { + "@tinyhttp/accepts": "2.2.3", + "@tinyhttp/type-is": "2.2.4", + "@tinyhttp/url": "2.1.1", + "header-range-parser": "^1.1.3" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@tinyhttp/res": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tinyhttp/res/-/res-2.2.4.tgz", + "integrity": "sha512-ETBRShnO19oJyIg2XQHQoofXPWeTXPAuwnIVYkU8WaftvXd/Vz4y5+WFQDHUzKlmdGOw5fAFnrEU7pIVMeFeVA==", + "license": "MIT", + "dependencies": { + "@tinyhttp/content-disposition": "2.2.2", + "@tinyhttp/cookie": "2.1.1", + "@tinyhttp/cookie-signature": "2.1.1", + "@tinyhttp/encode-url": "2.1.1", + "@tinyhttp/req": "2.2.4", + "@tinyhttp/send": "2.2.3", + "@tinyhttp/vary": "^0.1.3", + "es-escape-html": "^0.1.1", + "mime": "4.0.4" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@tinyhttp/router": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@tinyhttp/router/-/router-2.2.3.tgz", + "integrity": "sha512-O0MQqWV3Vpg/uXsMYg19XsIgOhwjyhTYWh51Qng7bxqXixxx2PEvZWnFjP7c84K7kU/nUX41KpkEBTLnznk9/Q==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@tinyhttp/send": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@tinyhttp/send/-/send-2.2.3.tgz", + "integrity": "sha512-o4cVHHGQ8WjVBS8UT0EE/2WnjoybrfXikHwsRoNlG1pfrC/Sd01u1N4Te8cOd/9aNGLr4mGxWb5qTm2RRtEi7g==", + "license": "MIT", + "dependencies": { + "@tinyhttp/content-type": "^0.1.4", + "@tinyhttp/etag": "2.1.2", + "mime": "4.0.4" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@tinyhttp/type-is": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tinyhttp/type-is/-/type-is-2.2.4.tgz", + "integrity": "sha512-7F328NheridwjIfefBB2j1PEcKKABpADgv7aCJaE8x8EON77ZFrAkI3Rir7pGjopV7V9MBmW88xUQigBEX2rmQ==", + "license": "MIT", + "dependencies": { + "@tinyhttp/content-type": "^0.1.4", + "mime": "4.0.4" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@tinyhttp/url": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@tinyhttp/url/-/url-2.1.1.tgz", + "integrity": "sha512-POJeq2GQ5jI7Zrdmj22JqOijB5/GeX+LEX7DUdml1hUnGbJOTWDx7zf2b5cCERj7RoXL67zTgyzVblBJC+NJWg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@tinyhttp/vary": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@tinyhttp/vary/-/vary-0.1.3.tgz", + "integrity": "sha512-SoL83sQXAGiHN1jm2VwLUWQSQeDAAl1ywOm6T0b0Cg1CZhVsjoiZadmjhxF6FHCCY7OHHVaLnTgSMxTPIDLxMg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@types/node": { + "version": "22.5.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz", + "integrity": "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.6.0.tgz", + "integrity": "sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.6.0", + "@typescript-eslint/type-utils": "8.6.0", + "@typescript-eslint/utils": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.6.0.tgz", + "integrity": "sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "8.6.0", + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/typescript-estree": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz", + "integrity": "sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.6.0.tgz", + "integrity": "sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.6.0", + "@typescript-eslint/utils": "8.6.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.6.0.tgz", + "integrity": "sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.6.0.tgz", + "integrity": "sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.6.0.tgz", + "integrity": "sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.6.0", + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/typescript-estree": "8.6.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.6.0.tgz", + "integrity": "sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.6.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "peer": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/concurrently": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.0.1.tgz", + "integrity": "sha512-wYKvCd/f54sTXJMSfV6Ln/B8UrfLBKOYa+lzc6CHay3Qek+LorVSBdMVfyewFhRbH0Rbabsk4D+3PL/VjQ5gzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", + "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", + "dev": true, + "dependencies": { + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/crypto-random-string/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "peer": true + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "peer": true + }, + "node_modules/dot-prop": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", + "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==", + "dependencies": { + "type-fest": "^4.18.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/es-escape-html": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/es-escape-html/-/es-escape-html-0.1.1.tgz", + "integrity": "sha512-yUx1o+8RsG7UlszmYPtks+dm6Lho2m8lgHMOsLJQsFI0R8XwUJwiMhM1M4E/S8QLeGyf6MkDV/pWgjQ0tdTSyQ==", + "license": "MIT", + "engines": { + "node": ">=12.x" + } + }, + "node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.11.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.11.0.tgz", + "integrity": "sha512-yVS6XODx+tMFMDFcG4+Hlh+qG7RM6cCJXtQhCKLSsr3XkLvWggHjCqjfh0XsPPnt1c56oaT6PMgW9XWQQjdHXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.11.0", + "@eslint/plugin-kit": "^0.2.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.0.2", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", + "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/espree": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eta": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-3.5.0.tgz", + "integrity": "sha512-e3x3FBvGzeCIHhF+zhK8FZA2vC5uFn6b4HJjegUbIWrDb4mJ7JjTGMJY9VGIbRVpmSwHopNiaJibhjIr+HfLug==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "url": "https://github.com/eta-dev/eta?sponsor=1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "peer": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-port": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz", + "integrity": "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.5.tgz", + "integrity": "sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz", + "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "peer": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/header-range-parser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/header-range-parser/-/header-range-parser-1.1.3.tgz", + "integrity": "sha512-B9zCFt3jH8g09LR1vHL4pcAn8yMEtlSlOUdQemzHMRKMImNIhhszdeosYFfNW0WXKQtXIlWB+O4owHJKvEJYaA==", + "license": "MIT", + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/http-status-emojis": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/http-status-emojis/-/http-status-emojis-2.2.0.tgz", + "integrity": "sha512-ompKtgwpx8ff0hsbpIB7oE4ax1LXoHmftsHHStMELX56ivG3GhofTX8ZHWlUaFKfGjcGjw6G3rPk7dJRXMmbbg==", + "license": "MIT" + }, + "node_modules/husky": { + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.6.tgz", + "integrity": "sha512-sqbjZKK7kf44hfdE94EoX8MZNk0n7HeW37O4YrVGCF4wzgQjp+akPAkfUK5LZ6KuR/6sqeAVuXHji+RzQgOn5A==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-3.0.0.tgz", + "integrity": "sha512-1zEJU1l19SgJlmwqsEyFTbScw/tkMHFenUo//Y0i+XEP83gDFdMvPizAD/WGcE+l1ku12PcTVHQhO6g5E0UCMw==", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "peer": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "peer": true + }, + "node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", + "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", + "dev": true, + "peer": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "dev": true, + "peer": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "peer": true + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", + "dev": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lowdb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-7.0.1.tgz", + "integrity": "sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==", + "dependencies": { + "steno": "^4.0.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/milliparsec": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/milliparsec/-/milliparsec-4.0.0.tgz", + "integrity": "sha512-/wk9d4Z6/9ZvoEH/6BI4TrTCgmkpZPuSRN/6fI9aUHOfXdNTuj/VhLS7d+NqG26bi6L9YmGXutVYvWC8zQ0qtA==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/mime": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.4.tgz", + "integrity": "sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ==", + "funding": [ + "https://github.com/sponsors/broofa" + ], + "license": "MIT", + "bin": { + "mime": "bin/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "peer": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "peer": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "peer": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true, + "peer": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", + "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.4.33", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", + "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true, + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "peer": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "peer": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", + "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", + "dev": true, + "peer": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^2.1.1" + }, + "engines": { + "node": ">= 14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dev": true, + "peer": true, + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", + "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", + "dev": true, + "peer": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "peer": true + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "peer": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.1.tgz", + "integrity": "sha512-GkMg9uOTpIWWKbSsgwb5fA4EavTR+SG/PMPoAY8hkhHfEEY0/vqljY+XHqtDf2cr2IJtoNRDbrrEpZUiZCkYRw==", + "license": "MIT", + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/regexparam": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-2.0.2.tgz", + "integrity": "sha512-A1PeDEYMrkLrfyOwv2jwihXbo9qxdGD3atBYQA9JJgreAx8/7rC6IUkWOw2NQlOxLp2wL0ifQbh1HuidDfYA6w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, + "peer": true, + "dependencies": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sort-on": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sort-on/-/sort-on-6.1.0.tgz", + "integrity": "sha512-WTECP0nYNWO1n2g5bpsV0yZN9cBmZsF8ThHFbOqVN0HBFRoaQZLLEMvMmJlKHNPYQeVngeI5+jJzIfFqOIo1OA==", + "license": "MIT", + "dependencies": { + "dot-prop": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/steno": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/steno/-/steno-4.0.2.tgz", + "integrity": "sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.32.0.tgz", + "integrity": "sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "7.1.6", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", + "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", + "dev": true, + "peer": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.19.1", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "peer": true + }, + "node_modules/tailwindcss/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tailwindcss/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/temp-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", + "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==", + "dev": true, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/tempy": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.1.0.tgz", + "integrity": "sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==", + "dev": true, + "dependencies": { + "is-stream": "^3.0.0", + "temp-dir": "^3.0.0", + "type-fest": "^2.12.2", + "unique-string": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "peer": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "peer": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "peer": true + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/tsx": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.1.tgz", + "integrity": "sha512-0flMz1lh74BR4wOvBjuh9olbnwqCPc35OOlfyzHba0Dc+QNUeWX/Gq2YTbnwcWPO3BMd8fkzRVrHcsR+a7z7rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.23.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", + "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.6.0.tgz", + "integrity": "sha512-eEhhlxCEpCd4helh3AO1hk0UP2MvbRi9CtIAJTVPQjuSXOOO2jsEacNi4UdcJzZJbeuVg1gMhtZ8UYb+NFYPrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.6.0", + "@typescript-eslint/parser": "8.6.0", + "@typescript-eslint/utils": "8.6.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unique-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", + "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", + "dev": true, + "dependencies": { + "crypto-random-string": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "peer": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", + "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index 428442416..a0cfb2993 100644 --- a/package.json +++ b/package.json @@ -1,93 +1,63 @@ { "name": "json-server", - "version": "0.9.4", - "description": "Serves JSON files through REST routes.", - "main": "./lib/server/index.js", - "bin": "./bin/index.js", - "directories": { - "test": "test" + "version": "1.0.0-beta.3", + "description": "", + "type": "module", + "bin": { + "json-server": "lib/bin.js" }, - "dependencies": { - "body-parser": "^1.15.2", - "chalk": "^1.1.3", - "compression": "^1.6.0", - "connect-pause": "^0.1.0", - "cors": "^2.3.0", - "errorhandler": "^1.2.0", - "express": "^4.9.5", - "json-parse-helpfulerror": "^1.0.3", - "lodash": "^4.11.2", - "lowdb": "^0.14.0", - "method-override": "^2.1.2", - "morgan": "^1.3.1", - "object-assign": "^4.0.1", - "pluralize": "^3.0.0", - "request": "^2.72.0", - "server-destroy": "^1.0.1", - "shortid": "^2.2.6", - "underscore-db": "^0.12.2", - "update-notifier": "^1.0.2", - "yargs": "^6.0.0" - }, - "devDependencies": { - "babel-cli": "^6.10.1", - "babel-preset-es2015": "^6.16.0", - "babel-register": "^6.16.3", - "cross-env": "^2.0.1", - "husky": "^0.11.4", - "markdown-toc": "^0.13.0", - "mkdirp": "^0.5.1", - "mocha": "^3.1.2", - "os-tmpdir": "^1.0.1", - "rimraf": "^2.5.2", - "server-ready": "^0.3.1", - "standard": "^8.3.0", - "supertest": "^2.0.0", - "temp-write": "^2.1.0" + "types": "lib", + "files": [ + "lib", + "views" + ], + "engines": { + "node": ">=18.3" }, "scripts": { - "test": "npm run test:cli && npm run test:server && standard", - "test:cli": "npm run build && cross-env NODE_ENV=test mocha test/cli/*.js", - "test:server": "cross-env NODE_ENV=test mocha test/server/*.js", - "start": "babel-node src/cli/bin", - "prepush": "npm t", - "build": "babel src -d lib --copy-files", - "toc": "markdown-toc -i README.md", - "prepublish": "npm run build" + "dev": "tsx watch src/bin.ts fixtures/db.json", + "build": "rm -rf lib && tsc", + "test": "node --import tsx/esm --test src/*.test.ts", + "lint": "eslint src", + "prepare": "husky", + "prepublishOnly": "npm run build" }, + "keywords": [], + "author": "typicode ", + "license": "SEE LICENSE IN ./LICENSE", "repository": { "type": "git", - "url": "git://github.com/typicode/json-server.git" + "url": "git+https://github.com/typicode/json-server.git" }, - "keywords": [ - "JSON", - "server", - "fake", - "REST", - "API", - "prototyping", - "mock", - "mocking", - "test", - "testing", - "rest", - "data", - "dummy", - "sandbox" - ], - "author": "Typicode ", - "license": "MIT", - "bugs": { - "url": "https://github.com/typicode/json-server/issues" - }, - "homepage": "https://github.com/typicode/json-server", - "standard": { - "fix": true, - "env": { - "mocha": true - } + "devDependencies": { + "@eslint/js": "^9.11.0", + "@sindresorhus/tsconfig": "^6.0.0", + "@tailwindcss/typography": "^0.5.15", + "@types/node": "^22.5.5", + "concurrently": "^9.0.1", + "eslint": "^9.11.0", + "get-port": "^7.1.0", + "globals": "^15.9.0", + "husky": "^9.1.6", + "tempy": "^3.1.0", + "tsx": "^4.19.1", + "type-fest": "^4.26.1", + "typescript": "^5.6.2", + "typescript-eslint": "^8.6.0" }, - "engines": { - "node": ">= 0.12" + "dependencies": { + "@tinyhttp/app": "^2.4.0", + "@tinyhttp/cors": "^2.0.1", + "@tinyhttp/logger": "^2.0.0", + "chalk": "^5.3.0", + "chokidar": "^4.0.1", + "dot-prop": "^9.0.0", + "eta": "^3.5.0", + "inflection": "^3.0.0", + "json5": "^2.2.3", + "lowdb": "^7.0.1", + "milliparsec": "^4.0.0", + "sirv": "^2.0.4", + "sort-on": "^6.1.0" } } diff --git a/public/test.html b/public/test.html new file mode 100644 index 000000000..a2b4bbc78 --- /dev/null +++ b/public/test.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app.test.ts b/src/app.test.ts new file mode 100644 index 000000000..7faf530a6 --- /dev/null +++ b/src/app.test.ts @@ -0,0 +1,129 @@ +import assert from 'node:assert/strict' +import { writeFileSync } from 'node:fs' +import { join } from 'node:path' +import test from 'node:test' + +import getPort from 'get-port' +import { Low, Memory } from 'lowdb' +import { temporaryDirectory } from 'tempy' + +import { createApp } from './app.js' +import { Data } from './service.js' + +type Test = { + + method: HTTPMethods + url: string + statusCode: number +} + +type HTTPMethods = + | 'DELETE' + | 'GET' + | 'HEAD' + | 'PATCH' + | 'POST' + | 'PUT' + | 'OPTIONS' + +const port = await getPort() + +// Create custom static dir with an html file +const tmpDir = temporaryDirectory() +const file = 'file.html' +writeFileSync(join(tmpDir, file), 'utf-8') + +// Create app +const db = new Low(new Memory(), {}) +db.data = { + posts: [{ id: '1', title: 'foo' }], + comments: [{ id: '1', postId: '1' }], + object: { f1: 'foo' }, +} +const app = createApp(db, { static: [tmpDir] }) + +await new Promise((resolve, reject) => { + try { + const server = app.listen(port, () => resolve()) + test.after(() => server.close()) + } catch (err) { + reject(err) + } +}) + +await test('createApp', async (t) => { + // URLs + const POSTS = '/posts' + const POSTS_WITH_COMMENTS = '/posts?_embed=comments' + const POST_1 = '/posts/1' + const POST_NOT_FOUND = '/posts/-1' + const POST_WITH_COMMENTS = '/posts/1?_embed=comments' + const COMMENTS = '/comments' + const POST_COMMENTS = '/comments?postId=1' + const NOT_FOUND = '/not-found' + const OBJECT = '/object' + const OBJECT_1 = '/object/1' + + const arr: Test[] = [ + // Static + { method: 'GET', url: '/', statusCode: 200 }, + { method: 'GET', url: '/test.html', statusCode: 200 }, + { method: 'GET', url: `/${file}`, statusCode: 200 }, + + // CORS + { method: 'OPTIONS', url: POSTS, statusCode: 204 }, + + // API + { method: 'GET', url: POSTS, statusCode: 200 }, + { method: 'GET', url: POSTS_WITH_COMMENTS, statusCode: 200 }, + { method: 'GET', url: POST_1, statusCode: 200 }, + { method: 'GET', url: POST_NOT_FOUND, statusCode: 404 }, + { method: 'GET', url: POST_WITH_COMMENTS, statusCode: 200 }, + { method: 'GET', url: COMMENTS, statusCode: 200 }, + { method: 'GET', url: POST_COMMENTS, statusCode: 200 }, + { method: 'GET', url: OBJECT, statusCode: 200 }, + { method: 'GET', url: OBJECT_1, statusCode: 404 }, + { method: 'GET', url: NOT_FOUND, statusCode: 404 }, + + { method: 'POST', url: POSTS, statusCode: 201 }, + { method: 'POST', url: POST_1, statusCode: 404 }, + { method: 'POST', url: POST_NOT_FOUND, statusCode: 404 }, + { method: 'POST', url: OBJECT, statusCode: 404 }, + { method: 'POST', url: OBJECT_1, statusCode: 404 }, + { method: 'POST', url: NOT_FOUND, statusCode: 404 }, + + { method: 'PUT', url: POSTS, statusCode: 404 }, + { method: 'PUT', url: POST_1, statusCode: 200 }, + { method: 'PUT', url: OBJECT, statusCode: 200 }, + { method: 'PUT', url: OBJECT_1, statusCode: 404 }, + { method: 'PUT', url: POST_NOT_FOUND, statusCode: 404 }, + { method: 'PUT', url: NOT_FOUND, statusCode: 404 }, + + { method: 'PATCH', url: POSTS, statusCode: 404 }, + { method: 'PATCH', url: POST_1, statusCode: 200 }, + { method: 'PATCH', url: OBJECT, statusCode: 200 }, + { method: 'PATCH', url: OBJECT_1, statusCode: 404 }, + { method: 'PATCH', url: POST_NOT_FOUND, statusCode: 404 }, + { method: 'PATCH', url: NOT_FOUND, statusCode: 404 }, + + { method: 'DELETE', url: POSTS, statusCode: 404 }, + { method: 'DELETE', url: POST_1, statusCode: 200 }, + { method: 'DELETE', url: OBJECT, statusCode: 404 }, + { method: 'DELETE', url: OBJECT_1, statusCode: 404 }, + { method: 'DELETE', url: POST_NOT_FOUND, statusCode: 404 }, + { method: 'DELETE', url: NOT_FOUND, statusCode: 404 }, + ] + + for (const tc of arr) { + await t.test(`${tc.method} ${tc.url}`, async () => { + const response = await fetch(`http://localhost:${port}${tc.url}`, { + method: tc.method, + }) + assert.equal( + response.status, + tc.statusCode, + `${response.status} !== ${tc.statusCode} ${tc.method} ${tc.url} failed`, + ) + }) + } +}) diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 000000000..c8f6ff849 --- /dev/null +++ b/src/app.ts @@ -0,0 +1,150 @@ +import { dirname, isAbsolute, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +import { App, type Request } from '@tinyhttp/app' +import { cors } from '@tinyhttp/cors' +import { Eta } from 'eta' +import { Low } from 'lowdb' +import { json } from 'milliparsec' +import sirv from 'sirv' + +import { Data, isItem, Service } from './service.js' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const isProduction = process.env['NODE_ENV'] === 'production' + +type QueryValue = Request['query'][string] | number +type Query = Record + +export type AppOptions = { + logger?: boolean + static?: string[] +} + +const eta = new Eta({ + views: join(__dirname, '../views'), + cache: isProduction, +}) + +export function createApp(db: Low, options: AppOptions = {}) { + // Create service + const service = new Service(db) + + // Create app + const app = new App() + + // Static files + app.use(sirv('public', { dev: !isProduction })) + options.static + ?.map((path) => (isAbsolute(path) ? path : join(process.cwd(), path))) + .forEach((dir) => app.use(sirv(dir, { dev: !isProduction }))) + + // CORS + app + .use((req, res, next) => { + return cors({ + allowedHeaders: req.headers['access-control-request-headers'] + ?.split(',') + .map((h) => h.trim()), + })(req, res, next) + }) + .options('*', cors()) + + // Body parser + // @ts-expect-error expected + app.use(json()) + + app.get('/', (_req, res) => + res.send(eta.render('index.html', { data: db.data })), + ) + + app.get('/:name', (req, res, next) => { + const { name = '' } = req.params + const query: Query = {} + + Object.keys(req.query).forEach((key) => { + let value: QueryValue = req.query[key] + + if ( + ['_start', '_end', '_limit', '_page', '_per_page'].includes(key) && + typeof value === 'string' + ) { + value = parseInt(value); + } + + if (!Number.isNaN(value)) { + query[key] = value; + } + }) + res.locals['data'] = service.find(name, query) + next?.() + }) + + app.get('/:name/:id', (req, res, next) => { + const { name = '', id = '' } = req.params + res.locals['data'] = service.findById(name, id, req.query) + next?.() + }) + + app.post('/:name', async (req, res, next) => { + const { name = '' } = req.params + if (isItem(req.body)) { + res.locals['data'] = await service.create(name, req.body) + } + next?.() + }) + + app.put('/:name', async (req, res, next) => { + const { name = '' } = req.params + if (isItem(req.body)) { + res.locals['data'] = await service.update(name, req.body) + } + next?.() + }) + + app.put('/:name/:id', async (req, res, next) => { + const { name = '', id = '' } = req.params + if (isItem(req.body)) { + res.locals['data'] = await service.updateById(name, id, req.body) + } + next?.() + }) + + app.patch('/:name', async (req, res, next) => { + const { name = '' } = req.params + if (isItem(req.body)) { + res.locals['data'] = await service.patch(name, req.body) + } + next?.() + }) + + app.patch('/:name/:id', async (req, res, next) => { + const { name = '', id = '' } = req.params + if (isItem(req.body)) { + res.locals['data'] = await service.patchById(name, id, req.body) + } + next?.() + }) + + app.delete('/:name/:id', async (req, res, next) => { + const { name = '', id = '' } = req.params + res.locals['data'] = await service.destroyById( + name, + id, + req.query['_dependent'], + ) + next?.() + }) + + app.use('/:name', (req, res) => { + const { data } = res.locals + if (data === undefined) { + res.sendStatus(404) + } else { + if (req.method === 'POST') res.status(201) + res.json(data) + } + }) + + return app +} diff --git a/src/bin.ts b/src/bin.ts new file mode 100644 index 000000000..4633e5e43 --- /dev/null +++ b/src/bin.ts @@ -0,0 +1,227 @@ +#!/usr/bin/env node +import { existsSync, readFileSync, writeFileSync } from 'node:fs' +import { extname } from 'node:path' +import { parseArgs } from 'node:util' + +import chalk from 'chalk' +import { watch } from 'chokidar' +import JSON5 from 'json5' +import { Adapter, Low } from 'lowdb' +import { DataFile, JSONFile } from 'lowdb/node' +import { PackageJson } from 'type-fest' + +import { fileURLToPath } from 'node:url' +import { createApp } from './app.js' +import { Observer } from './observer.js' +import { Data } from './service.js' + +function help() { + console.log(`Usage: json-server [options] + +Options: + -p, --port Port (default: 3000) + -h, --host Host (default: localhost) + -s, --static Static files directory (multiple allowed) + --help Show this message + --version Show version number +`) +} + +// Parse args +function args(): { + file: string + port: number + host: string + static: string[] +} { + try { + const { values, positionals } = parseArgs({ + options: { + port: { + type: 'string', + short: 'p', + default: process.env['PORT'] ?? '3000', + }, + host: { + type: 'string', + short: 'h', + default: process.env['HOST'] ?? 'localhost', + }, + static: { + type: 'string', + short: 's', + multiple: true, + default: [], + }, + help: { + type: 'boolean', + }, + version: { + type: 'boolean', + }, + // Deprecated + watch: { + type: 'boolean', + short: 'w', + }, + }, + allowPositionals: true, + }) + + // --version + if (values.version) { + const pkg = JSON.parse( + readFileSync( + fileURLToPath(new URL('../package.json', import.meta.url)), + 'utf-8', + ), + ) as PackageJson + console.log(pkg.version) + process.exit() + } + + // Handle --watch + if (values.watch) { + console.log( + chalk.yellow( + '--watch/-w can be omitted, JSON Server 1+ watches for file changes by default', + ), + ) + } + + if (values.help || positionals.length === 0) { + help() + process.exit() + } + + // App args and options + return { + file: positionals[0] ?? '', + port: parseInt(values.port as string), + host: values.host as string, + static: values.static as string[], + } + } catch (e) { + if ((e as NodeJS.ErrnoException).code === 'ERR_PARSE_ARGS_UNKNOWN_OPTION') { + console.log(chalk.red((e as NodeJS.ErrnoException).message.split('.')[0])) + help() + process.exit(1) + } else { + throw e + } + } +} + +const { file, port, host, static: staticArr } = args() + +if (!existsSync(file)) { + console.log(chalk.red(`File ${file} not found`)) + process.exit(1) +} + +// Handle empty string JSON file +if (readFileSync(file, 'utf-8').trim() === '') { + writeFileSync(file, '{}') +} + +// Set up database +let adapter: Adapter +if (extname(file) === '.json5') { + adapter = new DataFile(file, { + parse: JSON5.parse, + stringify: JSON5.stringify, + }) +} else { + adapter = new JSONFile(file) +} +const observer = new Observer(adapter) + +const db = new Low(observer, {}) +await db.read() + +// Create app +const app = createApp(db, { logger: false, static: staticArr }) + +function logRoutes(data: Data) { + console.log(chalk.bold('Endpoints:')) + if (Object.keys(data).length === 0) { + console.log( + chalk.gray(`No endpoints found, try adding some data to ${file}`), + ) + return + } + console.log( + Object.keys(data) + .map( + (key) => `${chalk.gray(`http://${host}:${port}/`)}${chalk.blue(key)}`, + ) + .join('\n'), + ) +} + +const kaomojis = ['β™‘βΈœ(ΛΆΛƒ α΅• Λ‚ΛΆ)⸝♑', 'β™‘( β—‘β€Ώβ—‘ )', '( ΛΆΛ† α—œ Λ†Λ΅ )', '(ΛΆα΅” α΅• α΅”ΛΆ)'] + +function randomItem(items: string[]): string { + const index = Math.floor(Math.random() * items.length) + return items.at(index) ?? '' +} + +app.listen(port, () => { + console.log( + [ + chalk.bold(`JSON Server started on PORT :${port}`), + chalk.gray('Press CTRL-C to stop'), + chalk.gray(`Watching ${file}...`), + '', + chalk.magenta(randomItem(kaomojis)), + '', + chalk.bold('Index:'), + chalk.gray(`http://localhost:${port}/`), + '', + chalk.bold('Static files:'), + chalk.gray('Serving ./public directory if it exists'), + '', + ].join('\n'), + ) + logRoutes(db.data) +}) + +// Watch file for changes +if (process.env['NODE_ENV'] !== 'production') { + let writing = false // true if the file is being written to by the app + let prevEndpoints = '' + + observer.onWriteStart = () => { + writing = true + } + observer.onWriteEnd = () => { + writing = false + } + observer.onReadStart = () => { + prevEndpoints = JSON.stringify(Object.keys(db.data).sort()) + } + observer.onReadEnd = (data) => { + if (data === null) { + return + } + + const nextEndpoints = JSON.stringify(Object.keys(data).sort()) + if (prevEndpoints !== nextEndpoints) { + console.log() + logRoutes(data) + } + } + watch(file).on('change', () => { + // Do no reload if the file is being written to by the app + if (!writing) { + db.read().catch((e) => { + if (e instanceof SyntaxError) { + return console.log( + chalk.red(['', `Error parsing ${file}`, e.message].join('\n')), + ) + } + console.log(e) + }) + } + }) +} diff --git a/src/cli/bin.js b/src/cli/bin.js deleted file mode 100644 index 2b8efefce..000000000 --- a/src/cli/bin.js +++ /dev/null @@ -1 +0,0 @@ -require('./')() diff --git a/src/cli/example.json b/src/cli/example.json deleted file mode 100644 index 4d3941aea..000000000 --- a/src/cli/example.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "posts": [ - { "id": 1, "title": "json-server", "author": "typicode" } - ], - "comments": [ - { "id": 1, "body": "some comment", "postId": 1 } - ], - "profile": { "name": "typicode" } -} \ No newline at end of file diff --git a/src/cli/index.js b/src/cli/index.js deleted file mode 100644 index 8f62da1b9..000000000 --- a/src/cli/index.js +++ /dev/null @@ -1,91 +0,0 @@ -const updateNotifier = require('update-notifier') -const yargs = require('yargs') -const run = require('./run') -const pkg = require('../../package.json') - -module.exports = function () { - updateNotifier({ pkg }).notify() - - const argv = yargs - .config('config') - .usage('$0 [options] ') - .options({ - port: { - alias: 'p', - description: 'Set port', - default: 3000 - }, - host: { - alias: 'H', - description: 'Set host', - default: '0.0.0.0' - }, - watch: { - alias: 'w', - description: 'Watch file(s)' - }, - routes: { - alias: 'r', - description: 'Path to routes file' - }, - middlewares: { - alias: 'm', - array: true, - description: 'Paths to middleware files' - }, - static: { - alias: 's', - description: 'Set static files directory' - }, - 'read-only': { - alias: 'ro', - description: 'Allow only GET requests' - }, - 'no-cors': { - alias: 'nc', - description: 'Disable Cross-Origin Resource Sharing' - }, - 'no-gzip': { - alias: 'ng', - description: 'Disable GZIP Content-Encoding' - }, - snapshots: { - alias: 'S', - description: 'Set snapshots directory', - default: '.' - }, - delay: { - alias: 'd', - description: 'Add delay to responses (ms)' - }, - id: { - alias: 'i', - description: 'Set database id property (e.g. _id)', - default: 'id' - }, - quiet: { - alias: 'q', - description: 'Suppress log messages from output' - }, - config: { - alias: 'c', - description: 'Path to config file', - default: 'json-server.json' - } - }) - .boolean('watch') - .boolean('read-only') - .boolean('quiet') - .boolean('no-cors') - .boolean('no-gzip') - .help('help').alias('help', 'h') - .version(pkg.version).alias('version', 'v') - .example('$0 db.json', '') - .example('$0 file.js', '') - .example('$0 http://example.com/db.json', '') - .epilog('https://github.com/typicode/json-server') - .require(1, 'Missing argument') - .argv - - run(argv) -} diff --git a/src/cli/run.js b/src/cli/run.js deleted file mode 100644 index a67d7303b..000000000 --- a/src/cli/run.js +++ /dev/null @@ -1,243 +0,0 @@ -const fs = require('fs') -const path = require('path') -const jph = require('json-parse-helpfulerror') -const _ = require('lodash') -const chalk = require('chalk') -const enableDestroy = require('server-destroy') -const pause = require('connect-pause') -const is = require('./utils/is') -const load = require('./utils/load') -const example = require('./example.json') -const jsonServer = require('../server') - -function prettyPrint (argv, object, rules) { - const host = argv.host === '0.0.0.0' ? 'localhost' : argv.host - const port = argv.port - const root = `http://${host}:${port}` - - console.log() - console.log(chalk.bold(' Resources')) - for (let prop in object) { - console.log(' ' + root + '/' + prop) - } - - if (rules) { - console.log() - console.log(chalk.bold(' Other routes')) - for (var rule in rules) { - console.log(' ' + rule + ' -> ' + rules[rule]) - } - } - - console.log() - console.log(chalk.bold(' Home')) - console.log(' ' + root) - console.log() -} - -function createApp (source, object, routes, middlewares, argv) { - const app = jsonServer.create() - - let router - - try { - router = jsonServer.router( - is.JSON(source) - ? source - : object - ) - } catch (e) { - console.log() - console.error(chalk.red(e.message.replace(/^/gm, ' '))) - process.exit(1) - } - - const defaultsOpts = { - logger: !argv.quiet, - readOnly: argv.readOnly, - noCors: argv.noCors, - noGzip: argv.noGzip - } - - if (argv.static) { - defaultsOpts.static = path.join(process.cwd(), argv.static) - } - - const defaults = jsonServer.defaults(defaultsOpts) - app.use(defaults) - - if (routes) { - const rewriter = jsonServer.rewriter(routes) - app.use(rewriter) - } - - if (middlewares) { - app.use(middlewares) - } - - if (argv.delay) { - app.use(pause(argv.delay)) - } - - router.db._.id = argv.id - app.db = router.db - app.use(router) - - return app -} - -module.exports = function (argv) { - const source = argv._[0] - let app - let server - - if (!fs.existsSync(argv.snapshots)) { - console.log(`Error: snapshots directory ${argv.snapshots} doesn't exist`) - process.exit(1) - } - - // noop log fn - if (argv.quiet) { - console.log = () => {} - } - - console.log() - console.log(chalk.cyan(' \\{^_^}/ hi!')) - - function start (cb) { - console.log() - - // Be nice and create a default db.json if it doesn't exist - if (is.JSON(source) && !fs.existsSync(source)) { - console.log(chalk.yellow(` Oops, ${source} doesn't seem to exist`)) - console.log(chalk.yellow(` Creating ${source} with some default data`)) - console.log() - fs.writeFileSync(source, JSON.stringify(example, null, 2)) - } - - console.log(chalk.gray(' Loading', source)) - - // Load JSON, JS or HTTP database - load(source, (err, data) => { - if (err) throw err - - // Load additional routes - let routes - if (argv.routes) { - console.log(chalk.gray(' Loading', argv.routes)) - routes = JSON.parse(fs.readFileSync(argv.routes)) - } - - // Load middlewares - let middlewares - if (argv.middlewares) { - middlewares = argv.middlewares.map(function (m) { - console.log(chalk.gray(' Loading', m)) - return require(path.resolve(m)) - }) - } - - // Done - console.log(chalk.gray(' Done')) - - // Create app and server - app = createApp(source, data, routes, middlewares, argv) - server = app.listen(argv.port, argv.host) - - // Enhance with a destroy function - enableDestroy(server) - - // Display server informations - prettyPrint(argv, data, routes) - - cb && cb() - }) - } - - // Start server - start(() => { - // Snapshot - console.log( - chalk.gray(' Type s + enter at any time to create a snapshot of the database') - ) - - // Support nohup - // https://github.com/typicode/json-server/issues/221 - process.stdin.on('error', () => { - console.log(` Error, can't read from stdin`) - console.log(` Creating a snapshot from the CLI won't be possible`) - }) - process.stdin.setEncoding('utf8') - process.stdin.on('data', (chunk) => { - if (chunk.trim().toLowerCase() === 's') { - const filename = 'db-' + Date.now() + '.json' - const file = path.join(argv.snapshots, filename) - const state = app.db.getState() - fs.writeFileSync(file, JSON.stringify(state, null, 2), 'utf-8') - console.log(` Saved snapshot to ${path.relative(process.cwd(), file)}\n`) - } - }) - - // Watch files - if (argv.watch) { - console.log(chalk.gray(' Watching...')) - console.log() - const source = argv._[0] - - // Can't watch URL - if (is.URL(source)) throw new Error('Can\'t watch URL') - - // Watch .js or .json file - // Since lowdb uses atomic writing, directory is watched instead of file - const watchedDir = path.dirname(source) - let readError = false - fs.watch(watchedDir, (event, file) => { - // https://github.com/typicode/json-server/issues/420 - // file can be null - if (file) { - const watchedFile = path.resolve(watchedDir, file) - if (watchedFile === path.resolve(source)) { - if (is.JSON(watchedFile)) { - let obj - try { - obj = jph.parse(fs.readFileSync(watchedFile)) - if (readError) { - console.log(chalk.green(` Read error has been fixed :)`)) - readError = false - } - } catch (e) { - readError = true - console.log(chalk.red(` Error reading ${watchedFile}`)) - console.error(e.message) - return - } - - // Compare .json file content with in memory database - const isDatabaseDifferent = !_.isEqual(obj, app.db.getState()) - if (isDatabaseDifferent) { - console.log(chalk.gray(` ${source} has changed, reloading...`)) - server && server.destroy() - start() - } - } - } - } - }) - - // Watch routes - if (argv.routes) { - const watchedDir = path.dirname(argv.routes) - fs.watch(watchedDir, (event, file) => { - if (file) { - const watchedFile = path.resolve(watchedDir, file) - if (watchedFile === path.resolve(argv.routes)) { - console.log(chalk.gray(` ${argv.routes} has changed, reloading...`)) - server && server.destroy() - start() - } - } - }) - } - } - }) -} diff --git a/src/cli/utils/is.js b/src/cli/utils/is.js deleted file mode 100644 index 2f5b8dfd2..000000000 --- a/src/cli/utils/is.js +++ /dev/null @@ -1,17 +0,0 @@ -module.exports = { - JSON, - JS, - URL -} - -function JSON (s) { - return !URL(s) && /\.json$/.test(s) -} - -function JS (s) { - return !URL(s) && /\.js$/.test(s) -} - -function URL (s) { - return /^(http|https):/.test(s) -} diff --git a/src/cli/utils/load.js b/src/cli/utils/load.js deleted file mode 100644 index e14a2a547..000000000 --- a/src/cli/utils/load.js +++ /dev/null @@ -1,39 +0,0 @@ -const path = require('path') -const request = require('request') -const low = require('lowdb') -const fileAsync = require('lowdb/lib/file-async') -const is = require('./is') - -module.exports = function (source, cb) { - if (is.URL(source)) { - // Load remote data - const opts = { - url: source, - json: true - } - - request(opts, (err, response) => { - if (err) return cb(err) - cb(null, response.body) - }) - } else if (is.JS(source)) { - // Clear cache - const filename = path.resolve(source) - delete require.cache[filename] - const dataFn = require(filename) - - if (typeof dataFn !== 'function') { - throw new Error('The database is a JavaScript file but the export is not a function.') - } - - // Run dataFn to generate data - const data = dataFn() - cb(null, data) - } else if (is.JSON(source)) { - // Load JSON using lowdb - const data = low(source, { storage: fileAsync }).getState() - cb(null, data) - } else { - throw new Error('Unsupported source ' + source) - } -} diff --git a/src/observer.ts b/src/observer.ts new file mode 100644 index 000000000..f2c890f92 --- /dev/null +++ b/src/observer.ts @@ -0,0 +1,36 @@ +import { Adapter } from 'lowdb' + +// Lowdb adapter to observe read/write events +export class Observer { + #adapter + + onReadStart = function () { + return + } + onReadEnd: (data: T | null) => void = function () { + return + } + onWriteStart = function () { + return + } + onWriteEnd = function () { + return + } + + constructor(adapter: Adapter) { + this.#adapter = adapter + } + + async read() { + this.onReadStart() + const data = await this.#adapter.read() + this.onReadEnd(data) + return data + } + + async write(arg: T) { + this.onWriteStart() + await this.#adapter.write(arg) + this.onWriteEnd() + } +} diff --git a/src/server/body-parser.js b/src/server/body-parser.js deleted file mode 100644 index eeeda211e..000000000 --- a/src/server/body-parser.js +++ /dev/null @@ -1,6 +0,0 @@ -const bodyParser = require('body-parser') - -module.exports = [ - bodyParser.json({limit: '10mb', extended: false}), - bodyParser.urlencoded({extended: false}) -] diff --git a/src/server/defaults.js b/src/server/defaults.js deleted file mode 100644 index 44265a389..000000000 --- a/src/server/defaults.js +++ /dev/null @@ -1,72 +0,0 @@ -const fs = require('fs') -const path = require('path') -const express = require('express') -const logger = require('morgan') -const cors = require('cors') -const compression = require('compression') -const errorhandler = require('errorhandler') -const objectAssign = require('object-assign') - -module.exports = function (opts) { - const userDir = path.join(process.cwd(), 'public') - const defaultDir = path.join(__dirname, 'public') - const staticDir = fs.existsSync(userDir) - ? userDir - : defaultDir - - opts = objectAssign({ logger: true, static: staticDir }, opts) - - const arr = [] - - // Compress all requests - if (!opts.noGzip) { - arr.push(compression()) - } - - // Logger - if (opts.logger) { - arr.push( - logger('dev', { - skip: (req) => ( - process.env.NODE_ENV === 'test' || - req.path === '/favicon.ico' - ) - }) - ) - } - - // Enable CORS for all the requests, including static files - if (!opts.noCors) { - arr.push(cors({ origin: true, credentials: true })) - } - - if (process.env.NODE_ENV === 'development') { - // only use in development - arr.push(errorhandler()) - } - - // Serve static files - arr.push(express.static(opts.static)) - - // No cache for IE - // https://support.microsoft.com/en-us/kb/234067 - arr.push((req, res, next) => { - res.header('Cache-Control', 'no-cache') - res.header('Pragma', 'no-cache') - res.header('Expires', '-1') - next() - }) - - // Read-only - if (opts.readOnly) { - arr.push((req, res, next) => { - if (req.method === 'GET') { - next() // Continue - } else { - res.sendStatus(403) // Forbidden - } - }) - } - - return arr -} diff --git a/src/server/index.js b/src/server/index.js deleted file mode 100644 index dd98979be..000000000 --- a/src/server/index.js +++ /dev/null @@ -1,9 +0,0 @@ -const express = require('express') - -module.exports = { - create: () => express().set('json spaces', 2), - defaults: require('./defaults'), - router: require('./router'), - rewriter: require('./rewriter'), - bodyParser: require('./body-parser') -} diff --git a/src/server/mixins.js b/src/server/mixins.js deleted file mode 100644 index 1f980df73..000000000 --- a/src/server/mixins.js +++ /dev/null @@ -1,72 +0,0 @@ -const shortid = require('shortid') -const pluralize = require('pluralize') - -module.exports = { - getRemovable, - createId, - deepQuery -} - -// Returns document ids that have unsatisfied relations -// Example: a comment that references a post that doesn't exist -function getRemovable (db) { - const _ = this - const removable = [] - _.each(db, (coll, collName) => { - _.each(coll, (doc) => { - _.each(doc, (value, key) => { - if (/Id$/.test(key)) { - const refName = pluralize.plural(key.slice(0, -2)) - // Test if table exists - if (db[refName]) { - // Test if references is defined in table - const ref = _.getById(db[refName], value) - if (_.isUndefined(ref)) { - removable.push({ name: collName, id: doc.id }) - } - } - } - }) - }) - }) - - return removable -} - -// Return incremented id or uuid -// Used to override underscore-db's createId with utils.createId -function createId (coll) { - const _ = this - const idProperty = _.__id() - if (_.isEmpty(coll)) { - return 1 - } else { - let id = _(coll).maxBy(idProperty)[idProperty] - - // Increment integer id or generate string id - return _.isFinite(id) - ? ++id - : shortid.generate() - } -} - -function deepQuery (value, q) { - const _ = this - if (value && q) { - if (_.isArray(value)) { - for (let i = 0; i < value.length; i++) { - if (_.deepQuery(value[i], q)) { - return true - } - } - } else if (_.isObject(value) && !_.isArray(value)) { - for (let k in value) { - if (_.deepQuery(value[k], q)) { - return true - } - } - } else if (value.toString().toLowerCase().indexOf(q) !== -1) { - return true - } - } -} diff --git a/src/server/public/favicon.ico b/src/server/public/favicon.ico deleted file mode 100644 index bfdd40dc7..000000000 Binary files a/src/server/public/favicon.ico and /dev/null differ diff --git a/src/server/public/images/json.png b/src/server/public/images/json.png deleted file mode 100644 index 57176594d..000000000 Binary files a/src/server/public/images/json.png and /dev/null differ diff --git a/src/server/public/index.html b/src/server/public/index.html deleted file mode 100644 index 83a644b90..000000000 --- a/src/server/public/index.html +++ /dev/null @@ -1,95 +0,0 @@ - - - JSON Server - - - - -
-

- -

- -
- -

- - Congrats! You're successfully running JSON Server. - -

- -
- -

Routes

-

- Here are the resources that JSON Server has loaded: -

-
    loading, please wait...
- -
- -

- You can view database current state at any time: -

-
    -
  • - db -
  • -
- -

- You can use any HTTP verbs (GET, POST, PUT, PATCH and DELETE) and access your resources from anywhere - using CORS or JSONP. -

- -

Documentation

-

- View - README - on GitHub. -

- -

Issues

-

Please go - here. -

- -
- -

- To replace this page, create an index.html file in ./public, JSON Server will load it. -

-
- - - - - diff --git a/src/server/public/stylesheets/style.css b/src/server/public/stylesheets/style.css deleted file mode 100644 index 098c6a4c2..000000000 --- a/src/server/public/stylesheets/style.css +++ /dev/null @@ -1,16 +0,0 @@ -a { - color: #1882BC !important; -} - -img { - padding-top: 50px; - padding-bottom: 20px; -} - -li { - list-style-type: square; -} - -h4 { - padding-top: 20px; -} \ No newline at end of file diff --git a/src/server/rewriter.js b/src/server/rewriter.js deleted file mode 100644 index d7f3c7471..000000000 --- a/src/server/rewriter.js +++ /dev/null @@ -1,37 +0,0 @@ -const express = require('express') -const url = require('url') -const _ = require('lodash') -function updateQueryString (target, sourceUrl) { - return ~sourceUrl.indexOf('?') ? _.assign(target, url.parse(sourceUrl, true).query) : {} -} -module.exports = (routes) => { - const router = express.Router() - - router.get('/__rules', (req, res) => { - res.json(routes) - }) - - Object.keys(routes).forEach((route) => { - if (route.indexOf(':') !== -1) { - router.all(route, (req, res, next) => { - // Rewrite target url using params - let target = routes[route] - for (let param in req.params) { - target = target.replace(':' + param, req.params[param]) - } - req.url = target - req.query = updateQueryString(req.query, req.url) - next() - }) - } else { - router.all(route + '*', (req, res, next) => { - // Rewrite url by replacing prefix - req.url = req.url.replace(route, routes[route]) - req.query = updateQueryString(req.query, req.url) - next() - }) - } - }) - - return router -} diff --git a/src/server/router/index.js b/src/server/router/index.js deleted file mode 100644 index 7a7ad6eaf..000000000 --- a/src/server/router/index.js +++ /dev/null @@ -1,90 +0,0 @@ -const express = require('express') -const methodOverride = require('method-override') -const _ = require('lodash') -const _db = require('underscore-db') -const low = require('lowdb') -const fileAsync = require('lowdb/lib/file-async') -const bodyParser = require('../body-parser') -const validateData = require('./validate-data') -const plural = require('./plural') -const nested = require('./nested') -const singular = require('./singular') -const mixins = require('../mixins') - -module.exports = (source) => { - // Create router - const router = express.Router() - - // Add middlewares - router.use(methodOverride()) - router.use(bodyParser) - - // Create database - let db - if (_.isObject(source)) { - db = low() - db.setState(source) - } else { - db = low(source, { storage: fileAsync }) - } - - validateData(db.getState()) - - // Add underscore-db methods to db - db._.mixin(_db) - - // Add specific mixins - db._.mixin(mixins) - - // Expose database - router.db = db - - // Expose render - router.render = (req, res) => { - res.jsonp(res.locals.data) - } - - // GET /db - router.get('/db', (req, res) => { - res.jsonp(db.getState()) - }) - - // Handle /:parent/:parentId/:resource - router.use(nested()) - - // Create routes - db.forEach((value, key) => { - if (_.isPlainObject(value)) { - router.use(`/${key}`, singular(db, key)) - return - } - - if (_.isArray(value)) { - router.use(`/${key}`, plural(db, key)) - return - } - - const msg = - `Type of "${key}" (${typeof value}) ` + - (_.isObject(source) ? '' : `in ${source}`) + ' is not supported. ' + - 'Use objects or arrays of objects.' - - throw new Error(msg) - }).value() - - router.use((req, res) => { - if (!res.locals.data) { - res.status(404) - res.locals.data = {} - } - - router.render(req, res) - }) - - router.use((err, req, res, next) => { - console.error(err.stack) - res.status(500).send(err.stack) - }) - - return router -} diff --git a/src/server/router/nested.js b/src/server/router/nested.js deleted file mode 100644 index 61a1a5926..000000000 --- a/src/server/router/nested.js +++ /dev/null @@ -1,26 +0,0 @@ -const express = require('express') -const pluralize = require('pluralize') - -module.exports = () => { - const router = express.Router() - - // Rewrite URL (/:resource/:id/:nested -> /:nested) and request query - function get (req, res, next) { - const prop = pluralize.singular(req.params.resource) - req.query[`${prop}Id`] = req.params.id - req.url = `/${req.params.nested}` - next() - } - - // Rewrite URL (/:resource/:id/:nested -> /:nested) and request body - function post (req, res, next) { - const prop = pluralize.singular(req.params.resource) - req.body[`${prop}Id`] = req.params.id - req.url = `/${req.params.nested}` - next() - } - - return router - .get('/:resource/:id/:nested', get) - .post('/:resource/:id/:nested', post) -} diff --git a/src/server/router/plural.js b/src/server/router/plural.js deleted file mode 100644 index 0c8b1173b..000000000 --- a/src/server/router/plural.js +++ /dev/null @@ -1,303 +0,0 @@ -const url = require('url') -const express = require('express') -const _ = require('lodash') -const pluralize = require('pluralize') -const utils = require('../utils') - -module.exports = (db, name) => { - // Create router - const router = express.Router() - - // Embed function used in GET /name and GET /name/id - function embed (resource, e) { - e && [].concat(e) - .forEach((externalResource) => { - if (db.get(externalResource).value) { - const query = {} - const singularResource = pluralize.singular(name) - query[`${singularResource}Id`] = resource.id - resource[externalResource] = db.get(externalResource).filter(query).value() - } - }) - } - - // Expand function used in GET /name and GET /name/id - function expand (resource, e) { - e && [].concat(e) - .forEach((innerResource) => { - const plural = pluralize(innerResource) - if (db.get(plural).value()) { - const prop = `${innerResource}Id` - resource[innerResource] = db.get(plural).getById(resource[prop]).value() - } - }) - } - - function getFullURL (req) { - const root = url.format({ - protocol: req.protocol, - host: req.get('host') - }) - - return `${root}${req.originalUrl}` - } - - // GET /name - // GET /name?q= - // GET /name?attr=&attr= - // GET /name?_end=& - // GET /name?_start=&_end=& - // GET /name?_embed=&_expand= - function list (req, res, next) { - // Resource chain - let chain = db.get(name) - - // Remove q, _start, _end, ... from req.query to avoid filtering using those - // parameters - let q = req.query.q - let _start = req.query._start - let _end = req.query._end - let _page = req.query._page - let _sort = req.query._sort - let _order = req.query._order - let _limit = req.query._limit - let _embed = req.query._embed - let _expand = req.query._expand - delete req.query.q - delete req.query._start - delete req.query._end - delete req.query._sort - delete req.query._order - delete req.query._limit - delete req.query._embed - delete req.query._expand - - // Automatically delete query parameters that can't be found - // in the database - Object.keys(req.query).forEach((query) => { - const arr = db.get(name).value() - for (let i in arr) { - if ( - _.has(arr[i], query) || - query === 'callback' || - query === '_' || - /_lte$/.test(query) || - /_gte$/.test(query) || - /_ne$/.test(query) || - /_like$/.test(query) - ) return - } - delete req.query[query] - }) - - if (q) { - // Full-text search - q = q.toLowerCase() - - chain = chain.filter((obj) => { - for (let key in obj) { - const value = obj[key] - if (db._.deepQuery(value, q)) { - return true - } - } - }) - } - - Object.keys(req.query).forEach((key) => { - // Don't take into account JSONP query parameters - // jQuery adds a '_' query parameter too - if (key !== 'callback' && key !== '_') { - // Always use an array, in case req.query is an array - const arr = [].concat(req.query[key]) - - chain = chain.filter((element) => { - return arr - .map(function (value) { - const isDifferent = /_ne$/.test(key) - const isRange = /_lte$/.test(key) || /_gte$/.test(key) - const isLike = /_like$/.test(key) - const path = key.replace(/(_lte|_gte|_ne|_like)$/, '') - const elementValue = _.get(element, path) - - if (elementValue === undefined) { - return - } - - if (isRange) { - const isLowerThan = /_gte$/.test(key) - - return isLowerThan - ? value <= elementValue - : value >= elementValue - } else if (isDifferent) { - return value !== elementValue.toString() - } else if (isLike) { - return new RegExp(value, 'i').test(elementValue.toString()) - } else { - return value === elementValue.toString() - } - }) - .reduce((a, b) => a || b) - }) - } - }) - - // Sort - if (_sort) { - _order = _order || 'ASC' - - chain = chain.sortBy(function (element) { - return _.get(element, _sort) - }) - - if (_order === 'DESC') { - chain = chain.reverse() - } - } - - // Slice result - if (_end || _limit || _page) { - res.setHeader('X-Total-Count', chain.size()) - res.setHeader('Access-Control-Expose-Headers', 'X-Total-Count' + (_page ? ', Link' : '')) - } - - if (_page) { - _page = parseInt(_page, 10) - _page = _page >= 1 ? _page : 1 - _limit = parseInt(_limit, 10) || 10 - const page = utils.getPage(chain.value(), _page, _limit) - const links = {} - const fullURL = getFullURL(req) - - if (page.first) { - links.first = fullURL.replace('page=' + page.current, 'page=' + page.first) - } - - if (page.prev) { - links.prev = fullURL.replace('page=' + page.current, 'page=' + page.prev) - } - - if (page.next) { - links.next = fullURL.replace('page=' + page.current, 'page=' + page.next) - } - - if (page.last) { - links.last = fullURL.replace('page=' + page.current, 'page=' + page.last) - } - - res.links(links) - chain = _.chain(page.items) - } else if (_end) { - _start = parseInt(_start, 10) || 0 - _end = parseInt(_end, 10) - chain = chain.slice(_start, _end) - } else if (_limit) { - _start = parseInt(_start, 10) || 0 - _limit = parseInt(_limit, 10) - chain = chain.slice(_start, _start + _limit) - } - - // embed and expand - chain = chain - .cloneDeep() - .forEach(function (element) { - embed(element, _embed) - expand(element, _expand) - }) - - res.locals.data = chain.value() - next() - } - - // GET /name/:id - // GET /name/:id?_embed=&_expand - function show (req, res, next) { - const _embed = req.query._embed - const _expand = req.query._expand - const resource = db.get(name) - .getById(req.params.id) - .value() - - if (resource) { - // Clone resource to avoid making changes to the underlying object - const clone = _.cloneDeep(resource) - - // Embed other resources based on resource id - // /posts/1?_embed=comments - embed(clone, _embed) - - // Expand inner resources based on id - // /posts/1?_expand=user - expand(clone, _expand) - - res.locals.data = clone - } - - next() - } - - // POST /name - function create (req, res, next) { - const resource = db.get(name) - .insert(req.body) - .value() - - res.status(201) - res.locals.data = resource - next() - } - - // PUT /name/:id - // PATCH /name/:id - function update (req, res, next) { - const id = req.params.id - let chain = db.get(name) - - chain = req.method === 'PATCH' - ? chain.updateById(id, req.body) - : chain.replaceById(id, req.body) - - const resource = chain.value() - - if (resource) { - res.locals.data = resource - } - - next() - } - - // DELETE /name/:id - function destroy (req, res, next) { - const resource = db.get(name) - .removeById(req.params.id) - .value() - - // Remove dependents documents - const removable = db._.getRemovable(db.getState()) - - removable.forEach((item) => { - db.get(item.name) - .removeById(item.id) - .value() - }) - - if (resource) { - res.locals.data = {} - } - - next() - } - - router.route('/') - .get(list) - .post(create) - - router.route('/:id') - .get(show) - .put(update) - .patch(update) - .delete(destroy) - - return router -} diff --git a/src/server/router/singular.js b/src/server/router/singular.js deleted file mode 100644 index f33bbda09..000000000 --- a/src/server/router/singular.js +++ /dev/null @@ -1,39 +0,0 @@ -const express = require('express') - -module.exports = (db, name) => { - const router = express.Router() - - function show (req, res, next) { - res.locals.data = db.get(name).value() - next() - } - - function create (req, res, next) { - db.set(name, req.body).value() - res.locals.data = db.get(name).value() - res.status(201) - next() - } - - function update (req, res, next) { - if (req.method === 'PUT') { - db.set(name, req.body) - .value() - } else { - db.get(name) - .assign(req.body) - .value() - } - - res.locals.data = db.get(name).value() - next() - } - - router.route('/') - .get(show) - .post(create) - .put(update) - .patch(update) - - return router -} diff --git a/src/server/router/validate-data.js b/src/server/router/validate-data.js deleted file mode 100644 index 5bb5dbc1e..000000000 --- a/src/server/router/validate-data.js +++ /dev/null @@ -1,26 +0,0 @@ -const _ = require('lodash') - -function validateKey (key) { - if (key.indexOf('/') !== -1) { - const msg = [ - `Oops, found / character in database property '${key}'.`, - '', - '/ aren\'t supported, if you want to tweak default routes, see', - 'https://github.com/typicode/json-server/tree/next#add-custom-routes' - ].join('\n') - throw new Error(msg) - } -} - -module.exports = (obj) => { - if (_.isPlainObject(obj)) { - Object - .keys(obj) - .forEach(validateKey) - } else { - throw new Error( - `Data must be an object. Found ${typeof obj}.` + - 'See https://github.com/typicode/json-server for example.' - ) - } -} diff --git a/src/server/utils.js b/src/server/utils.js deleted file mode 100644 index f7a5c9a50..000000000 --- a/src/server/utils.js +++ /dev/null @@ -1,30 +0,0 @@ -module.exports = { - getPage -} - -function getPage (array, page, perPage) { - var obj = {} - var start = (page - 1) * perPage - var end = page * perPage - - obj.items = array.slice(start, end) - if (obj.items.length === 0) { - return obj - } - - if (page > 1) { - obj.prev = page - 1 - } - - if (end < array.length) { - obj.next = page + 1 - } - - if (obj.items.length !== array.length) { - obj.current = page - obj.first = 1 - obj.last = Math.ceil(array.length / perPage) - } - - return obj -} diff --git a/src/service.test.ts b/src/service.test.ts new file mode 100644 index 000000000..818a2457b --- /dev/null +++ b/src/service.test.ts @@ -0,0 +1,392 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { Low, Memory } from 'lowdb' + +import { Data, Item, PaginatedItems, Service } from './service.js' + +const defaultData = { posts: [], comments: [], object: {} } +const adapter = new Memory() +const db = new Low(adapter, defaultData) +const service = new Service(db) + +const POSTS = 'posts' +const COMMENTS = 'comments' +const OBJECT = 'object' + +const UNKNOWN_RESOURCE = 'xxx' +const UNKNOWN_ID = 'xxx' + +const post1 = { + id: '1', + title: 'a', + views: 100, + published: true, + author: { name: 'foo' }, + tags: ['foo', 'bar'], +} +const post2 = { + id: '2', + title: 'b', + views: 200, + published: false, + author: { name: 'bar' }, + tags: ['bar'], +} +const post3 = { + id: '3', + title: 'c', + views: 300, + published: false, + author: { name: 'baz' }, + tags: ['foo'], +} +const comment1 = { id: '1', title: 'a', postId: '1' } +const items = 3 + +const obj = { + f1: 'foo', +} + +function reset() { + db.data = structuredClone({ + posts: [post1, post2, post3], + comments: [comment1], + object: obj, + }) +} + +await test('constructor', () => { + const defaultData = { posts: [{ id: '1' }, {}], object: {} } satisfies Data + const db = new Low(adapter, defaultData) + new Service(db) + if (Array.isArray(db.data['posts'])) { + const id0 = db.data['posts']?.at(0)?.['id'] + const id1 = db.data['posts']?.at(1)?.['id'] + assert.ok( + typeof id1 === 'string' && id1.length > 0, + `id should be a non empty string but was: ${String(id1)}`, + ) + assert.ok( + typeof id0 === 'string' && id0 === '1', + `id should not change if already set but was: ${String(id0)}`, + ) + } +}) + +await test('findById', () => { + reset() + if (!Array.isArray(db.data?.[POSTS])) + throw new Error('posts should be an array') + assert.deepEqual(service.findById(POSTS, '1', {}), db.data?.[POSTS]?.[0]) + assert.equal(service.findById(POSTS, UNKNOWN_ID, {}), undefined) + assert.deepEqual(service.findById(POSTS, '1', { _embed: ['comments'] }), { + ...post1, + comments: [comment1], + }) + assert.deepEqual(service.findById(COMMENTS, '1', { _embed: ['post'] }), { + ...comment1, + post: post1, + }) + assert.equal(service.findById(UNKNOWN_RESOURCE, '1', {}), undefined) +}) + +await test('find', async (t) => { + const arr: { + data?: Data + name: string + params?: Parameters[1] + res: Item | Item[] | PaginatedItems | undefined + error?: Error + }[] = [ + { + name: POSTS, + res: [post1, post2, post3], + }, + { + name: POSTS, + params: { id: post1.id }, + res: [post1], + }, + { + name: POSTS, + params: { id: UNKNOWN_ID }, + res: [], + }, + { + name: POSTS, + params: { views: post1.views.toString() }, + res: [post1], + }, + { + name: POSTS, + params: { 'author.name': post1.author.name }, + res: [post1], + }, + { + name: POSTS, + params: { 'tags[0]': 'foo' }, + res: [post1, post3], + }, + { + name: POSTS, + params: { id: UNKNOWN_ID, views: post1.views.toString() }, + res: [], + }, + { + name: POSTS, + params: { views_ne: post1.views.toString() }, + res: [post2, post3], + }, + { + name: POSTS, + params: { views_lt: (post1.views + 1).toString() }, + res: [post1], + }, + { + name: POSTS, + params: { views_lt: post1.views.toString() }, + res: [], + }, + { + name: POSTS, + params: { views_lte: post1.views.toString() }, + res: [post1], + }, + { + name: POSTS, + params: { views_gt: post1.views.toString() }, + res: [post2, post3], + }, + { + name: POSTS, + params: { views_gt: (post1.views - 1).toString() }, + res: [post1, post2, post3], + }, + { + name: POSTS, + params: { views_gte: post1.views.toString() }, + res: [post1, post2, post3], + }, + { + name: POSTS, + params: { + views_gt: post1.views.toString(), + views_lt: post3.views.toString(), + }, + res: [post2], + }, + { + data: { posts: [post3, post1, post2] }, + name: POSTS, + params: { _sort: 'views' }, + res: [post1, post2, post3], + }, + { + data: { posts: [post3, post1, post2] }, + name: POSTS, + params: { _sort: '-views' }, + res: [post3, post2, post1], + }, + { + data: { posts: [post3, post1, post2] }, + name: POSTS, + params: { _sort: '-views,id' }, + res: [post3, post2, post1], + }, + { + name: POSTS, + params: { published: 'true' }, + res: [post1], + }, + { + name: POSTS, + params: { published: 'false' }, + res: [post2, post3], + }, + { + name: POSTS, + params: { views_lt: post3.views.toString(), published: 'false' }, + res: [post2], + }, + { + name: POSTS, + params: { _start: 0, _end: 2 }, + res: [post1, post2], + }, + { + name: POSTS, + params: { _start: 1, _end: 3 }, + res: [post2, post3], + }, + { + name: POSTS, + params: { _start: 0, _limit: 2 }, + res: [post1, post2], + }, + { + name: POSTS, + params: { _start: 1, _limit: 2 }, + res: [post2, post3], + }, + { + name: POSTS, + params: { _page: 1, _per_page: 2 }, + res: { + first: 1, + last: 2, + prev: null, + next: 2, + pages: 2, + items, + data: [post1, post2], + }, + }, + { + name: POSTS, + params: { _page: 2, _per_page: 2 }, + res: { + first: 1, + last: 2, + prev: 1, + next: null, + pages: 2, + items, + data: [post3], + }, + }, + { + name: POSTS, + params: { _page: 3, _per_page: 2 }, + res: { + first: 1, + last: 2, + prev: 1, + next: null, + pages: 2, + items, + data: [post3], + }, + }, + { + name: POSTS, + params: { _page: 2, _per_page: 1 }, + res: { + first: 1, + last: 3, + prev: 1, + next: 3, + pages: 3, + items, + data: [post2], + }, + }, + { + name: POSTS, + params: { _embed: ['comments'] }, + res: [ + { ...post1, comments: [comment1] }, + { ...post2, comments: [] }, + { ...post3, comments: [] }, + ], + }, + { + name: COMMENTS, + params: { _embed: ['post'] }, + res: [{ ...comment1, post: post1 }], + }, + { + name: UNKNOWN_RESOURCE, + res: undefined, + }, + { + name: OBJECT, + res: obj, + }, + ] + for (const tc of arr) { + await t.test(`${tc.name} ${JSON.stringify(tc.params)}`, () => { + if (tc.data) { + db.data = tc.data + } else { + reset() + } + + assert.deepEqual(service.find(tc.name, tc.params), tc.res) + }) + } +}) + +await test('create', async () => { + reset() + const post = { title: 'new post' } + const res = await service.create(POSTS, post) + assert.equal(res?.['title'], post.title) + assert.equal(typeof res?.['id'], 'string', 'id should be a string') + + assert.equal(await service.create(UNKNOWN_RESOURCE, post), undefined) +}) + +await test('update', async () => { + reset() + const obj = { f1: 'bar' } + const res = await service.update(OBJECT, obj) + assert.equal(res, obj) + + assert.equal( + await service.update(UNKNOWN_RESOURCE, obj), + undefined, + 'should ignore unknown resources', + ) + assert.equal( + await service.update(POSTS, {}), + undefined, + 'should ignore arrays', + ) +}) + +await test('updateById', async () => { + reset() + const post = { id: 'xxx', title: 'updated post' } + const res = await service.updateById(POSTS, post1.id, post) + assert.equal(res?.['id'], post1.id, 'id should not change') + assert.equal(res?.['title'], post.title) + + assert.equal( + await service.updateById(UNKNOWN_RESOURCE, post1.id, post), + undefined, + ) + assert.equal(await service.updateById(POSTS, UNKNOWN_ID, post), undefined) +}) + +await test('patchById', async () => { + reset() + const post = { id: 'xxx', title: 'updated post' } + const res = await service.patchById(POSTS, post1.id, post) + assert.notEqual(res, undefined) + assert.equal(res?.['id'], post1.id) + assert.equal(res?.['title'], post.title) + + assert.equal( + await service.patchById(UNKNOWN_RESOURCE, post1.id, post), + undefined, + ) + assert.equal(await service.patchById(POSTS, UNKNOWN_ID, post), undefined) +}) + +await test('destroy', async () => { + reset() + let prevLength = Number(db.data?.[POSTS]?.length) || 0 + await service.destroyById(POSTS, post1.id) + assert.equal(db.data?.[POSTS]?.length, prevLength - 1) + assert.deepEqual(db.data?.[COMMENTS], [{ ...comment1, postId: null }]) + + reset() + prevLength = db.data?.[POSTS]?.length || 0 + await service.destroyById(POSTS, post1.id, [COMMENTS]) + assert.equal(db.data[POSTS].length, prevLength - 1) + assert.equal(db.data[COMMENTS].length, 0) + + assert.equal(await service.destroyById(UNKNOWN_RESOURCE, post1.id), undefined) + assert.equal(await service.destroyById(POSTS, UNKNOWN_ID), undefined) +}) diff --git a/src/service.ts b/src/service.ts new file mode 100644 index 000000000..bb7a63f3b --- /dev/null +++ b/src/service.ts @@ -0,0 +1,461 @@ +import { randomBytes } from 'node:crypto' + +import { getProperty } from 'dot-prop' +import inflection from 'inflection' +import { Low } from 'lowdb' +import sortOn from 'sort-on' + +export type Item = Record + +export type Data = Record + +export function isItem(obj: unknown): obj is Item { + return typeof obj === 'object' && obj !== null +} + +export function isData(obj: unknown): obj is Record { + if (typeof obj !== 'object' || obj === null) { + return false + } + + const data = obj as Record + return Object.values(data).every( + (value) => Array.isArray(value) && value.every(isItem), + ) +} + +enum Condition { + lt = 'lt', + lte = 'lte', + gt = 'gt', + gte = 'gte', + ne = 'ne', + default = '', +} + +function isCondition(value: string): value is Condition { + return Object.values(Condition).includes(value) +} + +export type PaginatedItems = { + first: number + prev: number | null + next: number | null + last: number + pages: number + items: number + data: Item[] +} + +function ensureArray(arg: string | string[] = []): string[] { + return Array.isArray(arg) ? arg : [arg] +} + +function embed(db: Low, name: string, item: Item, related: string): Item { + if (inflection.singularize(related) === related) { + const relatedData = db.data[inflection.pluralize(related)] as Item[] + if (!relatedData) { + return item + } + const foreignKey = `${related}Id` + const relatedItem = relatedData.find((relatedItem: Item) => { + return relatedItem['id'] === item[foreignKey] + }) + return { ...item, [related]: relatedItem } + } + const relatedData: Item[] = db.data[related] as Item[] + + if (!relatedData) { + return item + } + + const foreignKey = `${inflection.singularize(name)}Id` + const relatedItems = relatedData.filter( + (relatedItem: Item) => relatedItem[foreignKey] === item['id'], + ) + + return { ...item, [related]: relatedItems } +} + +function nullifyForeignKey(db: Low, name: string, id: string) { + const foreignKey = `${inflection.singularize(name)}Id` + + Object.entries(db.data).forEach(([key, items]) => { + // Skip + if (key === name) return + + // Nullify + if (Array.isArray(items)) { + items.forEach((item) => { + if (item[foreignKey] === id) { + item[foreignKey] = null + } + }) + } + }) +} + +function deleteDependents(db: Low, name: string, dependents: string[]) { + const foreignKey = `${inflection.singularize(name)}Id` + + Object.entries(db.data).forEach(([key, items]) => { + // Skip + if (key === name || !dependents.includes(key)) return + + // Delete if foreign key is null + if (Array.isArray(items)) { + db.data[key] = items.filter((item) => item[foreignKey] !== null) + } + }) +} + +function randomId(): string { + return randomBytes(2).toString('hex') +} + +function fixItemsIds(items: Item[]) { + items.forEach((item) => { + if (typeof item['id'] === 'number') { + item['id'] = item['id'].toString() + } + if (item['id'] === undefined) { + item['id'] = randomId() + } + }) +} + +// Ensure all items have an id +function fixAllItemsIds(data: Data) { + Object.values(data).forEach((value) => { + if (Array.isArray(value)) { + fixItemsIds(value) + } + }) +} + +export class Service { + #db: Low + + constructor(db: Low) { + fixAllItemsIds(db.data) + this.#db = db + } + + #get(name: string): Item[] | Item | undefined { + return this.#db.data[name] + } + + has(name: string): boolean { + return Object.prototype.hasOwnProperty.call(this.#db?.data, name) + } + + findById( + name: string, + id: string, + query: { _embed?: string[] | string }, + ): Item | undefined { + const value = this.#get(name) + + if (Array.isArray(value)) { + let item = value.find((item) => item['id'] === id) + ensureArray(query._embed).forEach((related) => { + if (item !== undefined) item = embed(this.#db, name, item, related) + }) + return item + } + + return + } + + find( + name: string, + query: { + [key: string]: unknown + _embed?: string | string[] + _sort?: string + _start?: number + _end?: number + _limit?: number + _page?: number + _per_page?: number + } = {}, + ): Item[] | PaginatedItems | Item | undefined { + let items = this.#get(name) + + if (!Array.isArray(items)) { + return items + } + + // Include + ensureArray(query._embed).forEach((related) => { + if (items !== undefined && Array.isArray(items)) { + items = items.map((item) => embed(this.#db, name, item, related)) + } + }) + + // Return list if no query params + if (Object.keys(query).length === 0) { + return items + } + + // Convert query params to conditions + const conds: [string, Condition, string | string[]][] = [] + for (const [key, value] of Object.entries(query)) { + if (value === undefined || typeof value !== 'string') { + continue + } + const re = /_(lt|lte|gt|gte|ne)$/ + const reArr = re.exec(key) + const op = reArr?.at(1) + if (op && isCondition(op)) { + const field = key.replace(re, '') + conds.push([field, op, value]) + continue + } + if ( + [ + '_embed', + '_sort', + '_start', + '_end', + '_limit', + '_page', + '_per_page', + ].includes(key) + ) { + continue + } + conds.push([key, Condition.default, value]) + } + + // Loop through conditions and filter items + let filtered = items + for (const [key, op, paramValue] of conds) { + filtered = filtered.filter((item: Item) => { + if (paramValue && !Array.isArray(paramValue)) { + // https://github.com/sindresorhus/dot-prop/issues/95 + const itemValue: unknown = getProperty(item, key) + switch (op) { + // item_gt=value + case Condition.gt: { + if ( + !( + typeof itemValue === 'number' && + itemValue > parseInt(paramValue) + ) + ) { + return false + } + break + } + // item_gte=value + case Condition.gte: { + if ( + !( + typeof itemValue === 'number' && + itemValue >= parseInt(paramValue) + ) + ) { + return false + } + break + } + // item_lt=value + case Condition.lt: { + if ( + !( + typeof itemValue === 'number' && + itemValue < parseInt(paramValue) + ) + ) { + return false + } + break + } + // item_lte=value + case Condition.lte: { + if ( + !( + typeof itemValue === 'number' && + itemValue <= parseInt(paramValue) + ) + ) { + return false + } + break + } + // item_ne=value + case Condition.ne: { + switch (typeof itemValue) { + case 'number': + return itemValue !== parseInt(paramValue) + case 'string': + return itemValue !== paramValue + case 'boolean': + return itemValue !== (paramValue === 'true') + } + break + } + // item=value + case Condition.default: { + switch (typeof itemValue) { + case 'number': + return itemValue === parseInt(paramValue) + case 'string': + return itemValue === paramValue + case 'boolean': + return itemValue === (paramValue === 'true') + case 'undefined': + return false + } + } + } + } + return true + }) + } + + // Sort + const sort = query._sort || '' + const sorted = sortOn(filtered, sort.split(',')) + + // Slice + const start = query._start + const end = query._end + const limit = query._limit + if (start !== undefined) { + if (end !== undefined) { + return sorted.slice(start, end) + } + return sorted.slice(start, start + (limit || 0)) + } + if (limit !== undefined) { + return sorted.slice(0, limit) + } + + // Paginate + let page = query._page + const perPage = query._per_page || 10 + if (page) { + const items = sorted.length + const pages = Math.ceil(items / perPage) + + // Ensure page is within the valid range + page = Math.max(1, Math.min(page, pages)) + + const first = 1 + const prev = page > 1 ? page - 1 : null + const next = page < pages ? page + 1 : null + const last = pages + + const start = (page - 1) * perPage + const end = start + perPage + const data = sorted.slice(start, end) + + return { + first, + prev, + next, + last, + pages, + items, + data, + } + } + + return sorted.slice(start, end) + } + + async create( + name: string, + data: Omit = {}, + ): Promise { + const items = this.#get(name) + if (items === undefined || !Array.isArray(items)) return + + const item = { id: randomId(), ...data } + items.push(item) + + await this.#db.write() + return item + } + + async #updateOrPatch( + name: string, + body: Item = {}, + isPatch: boolean, + ): Promise { + const item = this.#get(name) + if (item === undefined || Array.isArray(item)) return + + const nextItem = (this.#db.data[name] = isPatch ? { item, ...body } : body) + + await this.#db.write() + return nextItem + } + + async #updateOrPatchById( + name: string, + id: string, + body: Item = {}, + isPatch: boolean, + ): Promise { + const items = this.#get(name) + if (items === undefined || !Array.isArray(items)) return + + const item = items.find((item) => item['id'] === id) + if (!item) return + + const nextItem = isPatch ? { ...item, ...body, id } : { ...body, id } + const index = items.indexOf(item) + items.splice(index, 1, nextItem) + + await this.#db.write() + return nextItem + } + + async update(name: string, body: Item = {}): Promise { + return this.#updateOrPatch(name, body, false) + } + + async patch(name: string, body: Item = {}): Promise { + return this.#updateOrPatch(name, body, true) + } + + async updateById( + name: string, + id: string, + body: Item = {}, + ): Promise { + return this.#updateOrPatchById(name, id, body, false) + } + + async patchById( + name: string, + id: string, + body: Item = {}, + ): Promise { + return this.#updateOrPatchById(name, id, body, true) + } + + async destroyById( + name: string, + id: string, + dependent?: string | string[], + ): Promise { + const items = this.#get(name) + if (items === undefined || !Array.isArray(items)) return + + const item = items.find((item) => item['id'] === id) + if (item === undefined) return + const index = items.indexOf(item) + items.splice(index, 1) + + nullifyForeignKey(this.#db, name, id) + const dependents = ensureArray(dependent) + deleteDependents(this.#db, name, dependents) + + await this.#db.write() + return item + } +} diff --git a/test/cli/fixtures/config.json b/test/cli/fixtures/config.json deleted file mode 100644 index 1b87ef8a4..000000000 --- a/test/cli/fixtures/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "middlewares": [ - "./fixtures/middlewares/en", - "./fixtures/middlewares/jp" - ] -} diff --git a/test/cli/fixtures/middlewares/en.js b/test/cli/fixtures/middlewares/en.js deleted file mode 100644 index 8799d0072..000000000 --- a/test/cli/fixtures/middlewares/en.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = function (req, res, next) { - res.header('X-Hello', 'World') - next() -} diff --git a/test/cli/fixtures/middlewares/jp.js b/test/cli/fixtures/middlewares/jp.js deleted file mode 100644 index 4df4d1686..000000000 --- a/test/cli/fixtures/middlewares/jp.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = function (req, res, next) { - res.header('X-Konnichiwa', 'Sekai') - next() -} diff --git a/test/cli/fixtures/public/index.html b/test/cli/fixtures/public/index.html deleted file mode 100644 index e965047ad..000000000 --- a/test/cli/fixtures/public/index.html +++ /dev/null @@ -1 +0,0 @@ -Hello diff --git a/test/cli/fixtures/seed.js b/test/cli/fixtures/seed.js deleted file mode 100644 index 93b90f3a1..000000000 --- a/test/cli/fixtures/seed.js +++ /dev/null @@ -1,29 +0,0 @@ -// Need some fake data for the gzip test to work -module.exports = function () { - return { posts: [ - { - id: 1, - content: "Oh! The garbage chute was a really wonderful idea. What an incredible smell you've discovered! Let's get out of here! Get away from there... No! wait! Will you forget it? I already tried it. It's magnetically sealed! Put that thing away! You're going to get us all killed. Absolutely, Your Worship. Look, I had everything under control until you led us down here. You know, it's not going to take them long to figure out what happened to us. It could be worst... It's worst. There's something alive in here! That's your imagination. Something just moves past my leg! Look! Did you see that? What? Help!" - }, - { - id: 2, - content: "Oh! The garbage chute was a really wonderful idea. What an incredible smell you've discovered! Let's get out of here! Get away from there... No! wait! Will you forget it? I already tried it. It's magnetically sealed! Put that thing away! You're going to get us all killed. Absolutely, Your Worship. Look, I had everything under control until you led us down here. You know, it's not going to take them long to figure out what happened to us. It could be worst... It's worst. There's something alive in here! That's your imagination. Something just moves past my leg! Look! Did you see that? What? Help!" - }, - { - id: 3, - content: "Oh! The garbage chute was a really wonderful idea. What an incredible smell you've discovered! Let's get out of here! Get away from there... No! wait! Will you forget it? I already tried it. It's magnetically sealed! Put that thing away! You're going to get us all killed. Absolutely, Your Worship. Look, I had everything under control until you led us down here. You know, it's not going to take them long to figure out what happened to us. It could be worst... It's worst. There's something alive in here! That's your imagination. Something just moves past my leg! Look! Did you see that? What? Help!" - }, - { - id: 4, - content: "Oh! The garbage chute was a really wonderful idea. What an incredible smell you've discovered! Let's get out of here! Get away from there... No! wait! Will you forget it? I already tried it. It's magnetically sealed! Put that thing away! You're going to get us all killed. Absolutely, Your Worship. Look, I had everything under control until you led us down here. You know, it's not going to take them long to figure out what happened to us. It could be worst... It's worst. There's something alive in here! That's your imagination. Something just moves past my leg! Look! Did you see that? What? Help!" - }, - { - id: 5, - content: "Oh! The garbage chute was a really wonderful idea. What an incredible smell you've discovered! Let's get out of here! Get away from there... No! wait! Will you forget it? I already tried it. It's magnetically sealed! Put that thing away! You're going to get us all killed. Absolutely, Your Worship. Look, I had everything under control until you led us down here. You know, it's not going to take them long to figure out what happened to us. It could be worst... It's worst. There's something alive in here! That's your imagination. Something just moves past my leg! Look! Did you see that? What? Help!" - }, - { - id: 6, - content: "Oh! The garbage chute was a really wonderful idea. What an incredible smell you've discovered! Let's get out of here! Get away from there... No! wait! Will you forget it? I already tried it. It's magnetically sealed! Put that thing away! You're going to get us all killed. Absolutely, Your Worship. Look, I had everything under control until you led us down here. You know, it's not going to take them long to figure out what happened to us. It could be worst... It's worst. There's something alive in here! That's your imagination. Something just moves past my leg! Look! Did you see that? What? Help!" - } - ]} -} diff --git a/test/cli/index.js b/test/cli/index.js deleted file mode 100644 index f822f6d61..000000000 --- a/test/cli/index.js +++ /dev/null @@ -1,290 +0,0 @@ -const fs = require('fs') -const path = require('path') -const cp = require('child_process') -const assert = require('assert') -const supertest = require('supertest') -const osTmpdir = require('os-tmpdir') -const tempWrite = require('temp-write') -const mkdirp = require('mkdirp') -const rimraf = require('rimraf') -const express = require('express') -const serverReady = require('server-ready') - -let PORT = 3100 - -const middlewareFiles = { - en: './fixtures/middlewares/en.js', - jp: './fixtures/middlewares/jp.js' -} - -const bin = path.join(__dirname, '../../lib/cli/bin') - -function cli (args) { - return cp.spawn('node', ['--', bin, '-p', PORT].concat(args), { - cwd: __dirname, - stdio: ['pipe', process.stdout, process.stderr] - }) -} - -describe('cli', () => { - let child - let request - let dbFile - let routesFile - - beforeEach(() => { - dbFile = tempWrite.sync( - JSON.stringify({ - posts: [ - { id: 1 }, - { _id: 2 } - ] - }), - 'db.json' - ) - - routesFile = tempWrite.sync( - JSON.stringify({ '/blog/': '/' }), - 'routes.json' - ) - - ++PORT - request = supertest(`http://localhost:${PORT}`) - }) - - afterEach(() => { - child.kill('SIGKILL') - }) - - describe('db.json', () => { - beforeEach((done) => { - child = cli([ dbFile ]) - serverReady(PORT, done) - }) - - it('should support JSON file', (done) => { - request.get('/posts').expect(200, done) - }) - - it('should send CORS headers', (done) => { - const origin = 'http://example.com/' - - request.get('/posts') - .set('Origin', origin) - .expect('access-control-allow-origin', origin) - .expect(200, done) - }) - - it('should update JSON file', (done) => { - request.post('/posts') - .send({ title: 'hello' }) - .end(() => { - setTimeout(() => { - const str = fs.readFileSync(dbFile, 'utf8') - assert(str.indexOf('hello') !== -1) - done() - }, 1000) - }) - }) - }) - - describe('seed.js', () => { - beforeEach((done) => { - child = cli([ 'fixtures/seed.js' ]) - serverReady(PORT, done) - }) - - it('should support JS file', (done) => { - request.get('/posts').expect(200, done) - }) - }) - - describe('http://localhost:8080/db', () => { - beforeEach((done) => { - const fakeServer = express() - fakeServer.get('/db', (req, res) => { - res.jsonp({ posts: [] }) - }) - fakeServer.listen(8080, () => { - child = cli([ 'http://localhost:8080/db' ]) - serverReady(PORT, done) - }) - }) - - it('should support URL file', (done) => { - request.get('/posts').expect(200, done) - }) - }) - - describe('db.json -r routes.json -m middleware.js -i _id --read-only', () => { - beforeEach((done) => { - child = cli([ dbFile, '-r', routesFile, '-m', middlewareFiles.en, '-i', '_id', '--read-only' ]) - serverReady(PORT, done) - }) - - it('should use routes.json and _id as the identifier', (done) => { - request.get('/blog/posts/2').expect(200, done) - }) - - it('should apply middlewares', (done) => { - request.get('/blog/posts/2').expect('X-Hello', 'World', done) - }) - - it('should allow only GET requests', (done) => { - request.post('/blog/posts').expect(403, done) - }) - }) - - describe('db.json -m first-middleware.js second-middleware.js', () => { - beforeEach((done) => { - child = cli([ dbFile, '-m', middlewareFiles.en, middlewareFiles.jp ]) - serverReady(PORT, done) - }) - - it('should apply all middlewares', (done) => { - request.get('/posts') - .expect('X-Hello', 'World') - .expect('X-Konnichiwa', 'Sekai', done) - }) - }) - - describe('db.json -d 1000', () => { - beforeEach((done) => { - child = cli([ dbFile, '-d', 1000 ]) - serverReady(PORT, done) - }) - - it('should delay response', (done) => { - const start = new Date() - request.get('/posts').expect(200, function (err) { - const end = new Date() - done( - end - start > 1000 - ? err - : new Error('Request wasn\'t delayed') - ) - }) - }) - }) - - describe('db.json -s fixtures/public -S /some/path/snapshots', () => { - const snapshotsDir = path.join(osTmpdir(), 'snapshots') - const publicDir = 'fixtures/public' - - beforeEach((done) => { - rimraf.sync(snapshotsDir) - mkdirp.sync(snapshotsDir) - - child = cli([ dbFile, '-s', publicDir, '-S', snapshotsDir ]) - serverReady(PORT, () => { - child.stdin.write('s\n') - setTimeout(done, 100) - }) - }) - - it('should serve fixtures/public', (done) => { - request.get('/').expect(/Hello/, done) - }) - - it('should save a snapshot in snapshots dir', () => { - assert.equal(fs.readdirSync(snapshotsDir).length, 1) - }) - }) - - describe('fixtures/seed.json --no-cors=true', () => { - beforeEach((done) => { - child = cli([ 'fixtures/seed.js', '--no-cors=true' ]) - serverReady(PORT, done) - }) - - it('should not send Access-Control-Allow-Origin headers', (done) => { - const origin = 'http://example.com/' - - request.get('/posts') - .set('Origin', origin) - .expect(200) - .end((err, res) => { - if (err) { - done(err) - } if ('access-control-allow-origin' in res.headers) { - done(new Error('CORS headers were not excluded from response')) - } else { - done() - } - }) - }) - }) - - describe('fixtures/seed.json --no-gzip=true', () => { - beforeEach((done) => { - child = cli([ 'fixtures/seed.js', '--no-gzip=true' ]) - serverReady(PORT, done) - }) - - it('should not set Content-Encoding to gzip', (done) => { - request.get('/posts') - .expect(200) - .end(function (err, res) { - if (err) { - done(err) - } else if ('content-encoding' in res.headers) { - done(new Error('Content-Encoding is set to gzip')) - } else { - done() - } - }) - }) - }) - - describe('--watch db.json -r routes.json', () => { - beforeEach((done) => { - child = cli([ dbFile, '-r', routesFile, '--watch' ]) - serverReady(PORT, done) - }) - - it('should watch db file', (done) => { - fs.writeFileSync(dbFile, JSON.stringify({ foo: [] })) - setTimeout(() => { - request.get('/foo').expect(200, done) - }, 1000) - }) - - it('should watch routes file', (done) => { - fs.writeFileSync(routesFile, JSON.stringify({ '/api/': '/' })) - setTimeout(() => { - request.get('/api/posts').expect(200, done) - }, 1000) - }) - }) - - describe('non existent db.json', () => { - beforeEach((done) => { - fs.unlinkSync(dbFile) - child = cli([ dbFile ]) - serverReady(PORT, done) - }) - - it('should create JSON file if it doesn\'t exist', (done) => { - request.get('/posts').expect(200, done) - }) - }) - - describe('db.json with error', () => { - beforeEach(() => { - dbFile = tempWrite.sync( - JSON.stringify({ 'a/b': [] }), - 'db-error.json' - ) - }) - - it('should exit with an error', (done) => { - child = cli([ dbFile ]) - child.on('exit', (code) => { - if (code === 1) { - return done() - } - return done(new Error('should exit with error code')) - }) - }) - }) -}) diff --git a/test/mocha.opts b/test/mocha.opts deleted file mode 100644 index 82a4ddb26..000000000 --- a/test/mocha.opts +++ /dev/null @@ -1,3 +0,0 @@ ---compilers js:babel-register ---reporter spec ---timeout 5000 \ No newline at end of file diff --git a/test/server/mixins.js b/test/server/mixins.js deleted file mode 100644 index 8707fd0c0..000000000 --- a/test/server/mixins.js +++ /dev/null @@ -1,52 +0,0 @@ -const assert = require('assert') -const _ = require('lodash') -const _db = require('underscore-db') -const mixins = require('../../src/server/mixins') - -describe('mixins', () => { - let db - - before(() => { - _.mixin(_db) - _.mixin(mixins) - }) - - beforeEach(() => { - db = { - posts: [ - { id: 1, comment: 1 } - ], - comments: [ - { id: 1, postId: 1 }, - // Comments below references a post that doesn't exist - { id: 2, postId: 2 }, - { id: 3, postId: 2 } - ], - photos: [ - { id: '1' }, - { id: '2' } - ] - } - }) - - describe('getRemovable', () => { - it('should return removable documents', () => { - const expected = [ - { name: 'comments', id: 2 }, - { name: 'comments', id: 3 } - ] - - assert.deepEqual(_.getRemovable(db), expected) - }) - }) - - describe('createId', () => { - it('should return a new id', () => { - assert.equal(_.createId(db.comments), 4) - }) - - it('should return a new uuid', () => { - assert.notEqual(_.createId(db.photos), 3) - }) - }) -}) diff --git a/test/server/plural.js b/test/server/plural.js deleted file mode 100644 index 8e1c55f32..000000000 --- a/test/server/plural.js +++ /dev/null @@ -1,770 +0,0 @@ -const assert = require('assert') -const _ = require('lodash') -const request = require('supertest') -const jsonServer = require('../../src/server') - -describe('Server', () => { - let server - let router - let db - const rewriterRules = { - '/api/': '/', - '/blog/posts/:id/show': '/posts/:id', - '/comments/special/:userId-:body': '/comments/?userId=:userId&body=:body', - '/firstpostwithcomments': '/posts/1?_embed=comments', - '/articles?_id=:id': '/posts/:id' - } - - beforeEach(() => { - db = {} - - db.posts = [ - { id: 1, body: 'foo' }, - { id: 2, body: 'bar' } - ] - - db.tags = [ - { id: 1, body: 'Technology' }, - { id: 2, body: 'Photography' }, - { id: 3, body: 'photo' } - ] - - db.users = [ - { id: 1, username: 'Jim', tel: '0123' }, - { id: 2, username: 'George', tel: '123' } - ] - - db.comments = [ - { id: 1, body: 'foo', published: true, postId: 1, userId: 1 }, - { id: 2, body: 'bar', published: false, postId: 1, userId: 2 }, - { id: 3, body: 'baz', published: false, postId: 2, userId: 1 }, - { id: 4, body: 'qux', published: true, postId: 2, userId: 2 }, - { id: 5, body: 'quux', published: false, postId: 2, userId: 1 } - ] - - db.refs = [ - { id: 'abcd-1234', url: 'http://example.com/', postId: 1, userId: 1 } - ] - - db.stringIds = [ - { id: '1234' } - ] - - db.deep = [ - { a: { b: 1 } }, - { a: 1 } - ] - - db.nested = [ - { resource: {name: 'dewey'} }, - { resource: {name: 'cheatem'} }, - { resource: {name: 'howe'} } - ] - - db.list = [ - { id: 1 }, - { id: 2 }, - { id: 3 }, - { id: 4 }, - { id: 5 }, - { id: 6 }, - { id: 7 }, - { id: 8 }, - { id: 9 }, - { id: 10 }, - { id: 11 }, - { id: 12 }, - { id: 13 }, - { id: 14 }, - { id: 15 } - ] - - server = jsonServer.create() - router = jsonServer.router(db) - server.use(jsonServer.defaults()) - server.use(jsonServer.rewriter(rewriterRules)) - server.use(router) - }) - - describe('GET /db', () => { - it('should respond with json and full database', (done) => { - request(server) - .get('/db') - .expect('Content-Type', /json/) - .expect(db) - .expect(200, done) - }) - }) - - describe('GET /:resource', () => { - it('should respond with json and corresponding resources', (done) => { - request(server) - .get('/posts') - .set('Origin', 'http://example.com/') - .expect('Content-Type', /json/) - .expect('Access-Control-Allow-Credentials', 'true') - .expect('Access-Control-Allow-Origin', 'http://example.com/') - .expect(db.posts) - .expect(200, done) - }) - - it('should respond with 404 if resource is not found', (done) => { - request(server) - .get('/undefined') - .expect(404, done) - }) - }) - - describe('GET /:resource?attr=&attr=', () => { - it('should respond with json and filter resources', (done) => { - request(server) - .get('/comments?postId=1&published=true') - .expect('Content-Type', /json/) - .expect([ db.comments[0] ]) - .expect(200, done) - }) - - it('should be strict', (done) => { - request(server) - .get('/users?tel=123') - .expect('Content-Type', /json/) - .expect([ db.users[1] ]) - .expect(200, done) - }) - - it('should support multiple filters', (done) => { - request(server) - .get('/comments?id=1&id=2') - .expect('Content-Type', /json/) - .expect([ db.comments[0], db.comments[1] ]) - .expect(200, done) - }) - - it('should support deep filter', (done) => { - request(server) - .get('/deep?a.b=1') - .expect('Content-Type', /json/) - .expect([ db.deep[0] ]) - .expect(200, done) - }) - - it('should ignore JSONP query parameters callback and _ ', (done) => { - request(server) - .get('/comments?callback=1&_=1') - .expect('Content-Type', /text/) - .expect(new RegExp(db.comments[0].body)) // JSONP returns text - .expect(200, done) - }) - - it('should ignore unknown query parameters', (done) => { - request(server) - .get('/comments?foo=1&bar=2') - .expect('Content-Type', /json/) - .expect(db.comments) - .expect(200, done) - }) - }) - - describe('GET /:resource?q=', () => { - it('should respond with json and make a full-text search', (done) => { - request(server) - .get('/tags?q=pho') - .expect('Content-Type', /json/) - .expect([ db.tags[1], db.tags[2] ]) - .expect(200, done) - }) - - it('should respond with json and make a deep full-text search', (done) => { - request(server) - .get('/deep?q=1') - .expect('Content-Type', /json/) - .expect(db.deep) - .expect(200, done) - }) - - it('should return an empty array when nothing is matched', (done) => { - request(server) - .get('/tags?q=nope') - .expect('Content-Type', /json/) - .expect([ ]) - .expect(200, done) - }) - - it('should support other query parameters', (done) => { - request(server) - .get('/comments?q=qu&published=true') - .expect('Content-Type', /json/) - .expect([ db.comments[3] ]) - .expect(200, done) - }) - - it('should support filtering by boolean value false', (done) => { - request(server) - .get('/comments?published=false') - .expect('Content-Type', /json/) - .expect([ db.comments[1], db.comments[2], db.comments[4] ]) - .expect(200, done) - }) - }) - - describe('GET /:resource?_end=', () => { - it('should respond with a sliced array', (done) => { - request(server) - .get('/comments?_end=2') - .expect('Content-Type', /json/) - .expect('x-total-count', db.comments.length.toString()) - .expect('Access-Control-Expose-Headers', 'X-Total-Count') - .expect(db.comments.slice(0, 2)) - .expect(200, done) - }) - }) - - describe('GET /:resource?_sort=', () => { - it('should respond with json and sort on a field', (done) => { - request(server) - .get('/tags?_sort=body') - .expect('Content-Type', /json/) - .expect([ db.tags[1], db.tags[0], db.tags[2] ]) - .expect(200, done) - }) - - it('should reverse sorting with _order=DESC', (done) => { - request(server) - .get('/tags?_sort=body&_order=DESC') - .expect('Content-Type', /json/) - .expect([ db.tags[2], db.tags[0], db.tags[1] ]) - .expect(200, done) - }) - - it('should sort on numerical field', (done) => { - request(server) - .get('/posts?_sort=id&_order=DESC') - .expect('Content-Type', /json/) - .expect(db.posts.reverse()) - .expect(200, done) - }) - - it('should sort on nested field', (done) => { - request(server) - .get('/nested?_sort=resource.name') - .expect('Content-Type', /json/) - .expect([ db.nested[1], db.nested[0], db.nested[2] ]) - .expect(200, done) - }) - }) - - describe('GET /:resource?_start=&_end=', () => { - it('should respond with a sliced array', (done) => { - request(server) - .get('/comments?_start=1&_end=2') - .expect('Content-Type', /json/) - .expect('X-Total-Count', db.comments.length.toString()) - .expect('Access-Control-Expose-Headers', 'X-Total-Count') - .expect(db.comments.slice(1, 2)) - .expect(200, done) - }) - }) - - describe('GET /:resource?_start=&_limit=', () => { - it('should respond with a limited array', (done) => { - request(server) - .get('/comments?_start=1&_limit=1') - .expect('Content-Type', /json/) - .expect('X-Total-Count', db.comments.length.toString()) - .expect('Access-Control-Expose-Headers', 'X-Total-Count') - .expect(db.comments.slice(1, 2)) - .expect(200, done) - }) - }) - - describe('GET /:resource?_page=', () => { - it('should paginate', (done) => { - request(server) - .get('/list?_page=2') - .expect('Content-Type', /json/) - .expect('x-total-count', db.list.length.toString()) - .expect('Access-Control-Expose-Headers', 'X-Total-Count, Link') - .expect(db.list.slice(10, 20)) - .expect(200, done) - }) - }) - - describe('GET /:resource?_page=&_limit=', () => { - it('should paginate with a custom limit', (done) => { - const link = [ - '; rel="first"', - '; rel="prev"', - '; rel="next"', - '; rel="last"' - ].join(', ') - request(server) - .get('/list?_page=2&_limit=1') - .set('host', 'localhost') - .expect('Content-Type', /json/) - .expect('x-total-count', db.list.length.toString()) - .expect('link', link) - .expect('Access-Control-Expose-Headers', 'X-Total-Count, Link') - .expect(db.list.slice(1, 2)) - .expect(200, done) - }) - }) - - describe('GET /:resource?attr_gte=&attr_lte=', () => { - it('should respond with a limited array', (done) => { - request(server) - .get('/comments?id_gte=2&id_lte=3') - .expect('Content-Type', /json/) - .expect(db.comments.slice(1, 3)) - .expect(200, done) - }) - }) - - describe('GET /:resource?attr_ne=', () => { - it('should respond with a limited array', (done) => { - request(server) - .get('/comments?id_ne=1') - .expect('Content-Type', /json/) - .expect(db.comments.slice(1)) - .expect(200, done) - }) - }) - - describe('GET /:resource?attr_like=', () => { - it('should respond with an array that matches the like operator (case insensitive)', (done) => { - request(server) - .get('/tags?body_like=photo') - .expect('Content-Type', /json/) - .expect([ - db.tags[1], - db.tags[2] - ]) - .expect(200, done) - }) - }) - - describe('GET /:parent/:parentId/:resource', () => { - it('should respond with json and corresponding nested resources', (done) => { - request(server) - .get('/posts/1/comments') - .expect('Content-Type', /json/) - .expect([ - db.comments[0], - db.comments[1] - ]) - .expect(200, done) - }) - }) - - describe('GET /:resource/:id', () => { - it('should respond with json and corresponding resource', (done) => { - request(server) - .get('/posts/1') - .expect('Content-Type', /json/) - .expect(db.posts[0]) - .expect(200, done) - }) - - it('should support string id, respond with json and corresponding resource', (done) => { - request(server) - .get('/refs/abcd-1234') - .expect('Content-Type', /json/) - .expect(db.refs[0]) - .expect(200, done) - }) - - it('should support integer id as string', (done) => { - request(server) - .get('/stringIds/1234') - .expect('Content-Type', /json/) - .expect(db.stringIds[0]) - .expect(200, done) - }) - - it('should respond with 404 if resource is not found', (done) => { - request(server) - .get('/posts/9001') - .expect('Content-Type', /json/) - .expect({}) - .expect(404, done) - }) - }) - - describe('GET /:resource?_embed=', () => { - it('should respond with corresponding resources and embedded resources', (done) => { - const posts = _.cloneDeep(db.posts) - posts[0].comments = [ db.comments[0], db.comments[1] ] - posts[1].comments = [ db.comments[2], db.comments[3], db.comments[4] ] - request(server) - .get('/posts?_embed=comments') - .expect('Content-Type', /json/) - .expect(posts) - .expect(200, done) - }) - }) - - describe('GET /:resource?_embed&_embed=', () => { - it('should respond with corresponding resources and embedded resources', (done) => { - const posts = _.cloneDeep(db.posts) - posts[0].comments = [ db.comments[0], db.comments[1] ] - posts[0].refs = [ db.refs[0] ] - posts[1].comments = [ db.comments[2], db.comments[3], db.comments[4] ] - posts[1].refs = [] - request(server) - .get('/posts?_embed=comments&_embed=refs') - .expect('Content-Type', /json/) - .expect(posts) - .expect(200, done) - }) - }) - - describe('GET /:resource/:id?_embed=', () => { - it('should respond with corresponding resources and embedded resources', (done) => { - const post = _.cloneDeep(db.posts[0]) - post.comments = [ db.comments[0], db.comments[1] ] - request(server) - .get('/posts/1?_embed=comments') - .expect('Content-Type', /json/) - .expect(post) - .expect(200, done) - }) - }) - - describe('GET /:resource/:id?_embed=&_embed=', () => { - it('should respond with corresponding resource and embedded resources', (done) => { - const post = _.cloneDeep(db.posts[0]) - post.comments = [ db.comments[0], db.comments[1] ] - post.refs = [db.refs[0]] - request(server) - .get('/posts/1?_embed=comments&_embed=refs') - .expect('Content-Type', /json/) - .expect(post) - .expect(200, done) - }) - }) - - describe('GET /:resource?_expand=', () => { - it('should respond with corresponding resource and expanded inner resources', (done) => { - const refs = _.cloneDeep(db.refs) - refs[0].post = db.posts[0] - request(server) - .get('/refs?_expand=post') - .expect('Content-Type', /json/) - .expect(refs) - .expect(200, done) - }) - }) - - describe('GET /:resource/:id?_expand=', () => { - it('should respond with corresponding resource and expanded inner resources', (done) => { - const comment = _.cloneDeep(db.comments[0]) - comment.post = db.posts[0] - request(server) - .get('/comments/1?_expand=post') - .expect('Content-Type', /json/) - .expect(comment) - .expect(200, done) - }) - }) - - describe('GET /:resource?_expand=&_expand', () => { - it('should respond with corresponding resource and expanded inner resources', (done) => { - const refs = _.cloneDeep(db.refs) - refs[0].post = db.posts[0] - refs[0].user = db.users[0] - request(server) - .get('/refs?_expand=post&_expand=user') - .expect('Content-Type', /json/) - .expect(refs) - .expect(200, done) - }) - }) - - describe('GET /:resource/:id?_expand=&_expand=', () => { - it('should respond with corresponding resource and expanded inner resources', (done) => { - const comments = db.comments[0] - comments.post = db.posts[0] - comments.user = db.users[0] - request(server) - .get('/comments/1?_expand=post&_expand=user') - .expect('Content-Type', /json/) - .expect(comments) - .expect(200, done) - }) - }) - - describe('POST /:resource', () => { - it('should respond with json, create a resource and increment id', - (done) => { - request(server) - .post('/posts') - .send({body: 'foo', booleanValue: true, integerValue: 1}) - .expect('Content-Type', /json/) - .expect({id: 3, body: 'foo', booleanValue: true, integerValue: 1}) - .expect(201) - .end((err, res) => { - if (err) return done(err) - assert.equal(db.posts.length, 3) - done() - }) - } - ) - - it('should support x-www-form-urlencoded', - (done) => { - request(server) - .post('/posts') - .type('form') - .send({body: 'foo', booleanValue: true, integerValue: 1}) - .expect('Content-Type', /json/) - // x-www-form-urlencoded will convert to string - .expect({id: 3, body: 'foo', booleanValue: 'true', integerValue: '1'}) - .expect(201) - .end((err, res) => { - if (err) return done(err) - assert.equal(db.posts.length, 3) - done() - }) - } - ) - - it('should respond with json, create a resource and generate string id', - (done) => { - request(server) - .post('/refs') - .send({url: 'http://foo.com/', postId: '1'}) - .expect('Content-Type', /json/) - .expect(201) - .end((err, res) => { - if (err) return done(err) - assert.equal(db.refs.length, 2) - done() - }) - }) - }) - - describe('POST /:parent/:parentId/:resource', () => { - it('should respond with json and set parentId', (done) => { - request(server) - .post('/posts/1/comments') - .send({body: 'foo'}) - .expect('Content-Type', /json/) - .expect({id: 6, postId: 1, body: 'foo'}) - .expect(201, done) - }) - }) - - describe('PUT /:resource/:id', () => { - it('should respond with json and replace resource', (done) => { - var post = {id: 1, booleanValue: true, integerValue: 1} - request(server) - .put('/posts/1') - .set('Accept', 'application/json') - // body property omitted to test that the resource is replaced - .send(post) - .expect('Content-Type', /json/) - .expect(post) - .expect(200) - .end((err, res) => { - if (err) return done(err) - // TODO find a "supertest" way to test this - // https://github.com/typicode/json-server/issues/396 - assert.deepStrictEqual(res.body, post) - // assert it was created in database too - assert.deepStrictEqual(db.posts[0], post) - done() - }) - }) - - it('should respond with 404 if resource is not found', (done) => { - request(server) - .put('/posts/9001') - .send({id: 1, body: 'bar'}) - .expect('Content-Type', /json/) - .expect({}) - .expect(404, done) - }) - }) - - describe('PATCH /:resource/:id', () => { - it('should respond with json and update resource', (done) => { - var partial = {body: 'bar'} - var post = {id: 1, body: 'bar'} - request(server) - .patch('/posts/1') - .send(partial) - .expect('Content-Type', /json/) - .expect(post) - .expect(200) - .end((err, res) => { - if (err) return done(err) - assert.deepStrictEqual(res.body, post) - // assert it was created in database too - assert.deepStrictEqual(db.posts[0], post) - done() - }) - }) - - it('should respond with 404 if resource is not found', (done) => { - request(server) - .patch('/posts/9001') - .send({body: 'bar'}) - .expect('Content-Type', /json/) - .expect({}) - .expect(404, done) - }) - }) - - describe('DELETE /:resource/:id', () => { - it('should respond with empty data, destroy resource and dependent resources', (done) => { - request(server) - .del('/posts/1') - .expect({}) - .expect(200) - .end((err, res) => { - if (err) return done(err) - assert.equal(db.posts.length, 1) - assert.equal(db.comments.length, 3) - done() - }) - }) - - it('should respond with 404 if resource is not found', (done) => { - request(server) - .del('/posts/9001') - .expect('Content-Type', /json/) - .expect({}) - .expect(404, done) - }) - }) - - describe('Static routes', () => { - describe('GET /', () => { - it('should respond with html', (done) => { - request(server) - .get('/') - .expect(/You're successfully running JSON Server/) - .expect(200, done) - }) - }) - - describe('GET /stylesheets/style.css', () => { - it('should respond with css', (done) => { - request(server) - .get('/stylesheets/style.css') - .expect('Content-Type', /css/) - .expect(200, done) - }) - }) - }) - - describe('Database state', () => { - it('should be accessible', () => { - assert(router.db.getState()) - }) - }) - - describe('Responses', () => { - it('should have no cache headers (for IE)', (done) => { - request(server) - .get('/db') - .expect('Cache-Control', 'no-cache') - .expect('Pragma', 'no-cache') - .expect('Expires', '-1') - .end(done) - }) - }) - - describe('Rewriter', () => { - it('should rewrite using prefix', (done) => { - request(server) - .get('/api/posts/1') - .expect(db.posts[0]) - .end(done) - }) - - it('should rewrite using params', (done) => { - request(server) - .get('/blog/posts/1/show') - .expect(db.posts[0]) - .end(done) - }) - - it('should rewrite using query without params', (done) => { - const expectedPost = _.cloneDeep(db.posts[0]) - expectedPost.comments = [ db.comments[0], db.comments[1] ] - request(server) - .get('/firstpostwithcomments') - .expect(expectedPost) - .end(done) - }) - - it('should rewrite using params and query', (done) => { - request(server) - .get('/comments/special/1-quux') - .expect([db.comments[4]]) - .end(done) - }) - - // TODO - // it('should rewrite query params', (done) => { - // request(server) - // .get('/articles?_id=1') - // .expect(db.posts[0]) - // .end(done) - // }) - - it('should expose routes', (done) => { - request(server) - .get('/__rules') - .expect(rewriterRules) - .end(done) - }) - }) - - describe('router.render', (done) => { - beforeEach(() => { - router.render = (req, res) => { - res.jsonp({ - data: res.locals.data - }) - } - }) - - it('should be possible to wrap response', (done) => { - request(server) - .get('/posts/1') - .expect('Content-Type', /json/) - .expect({ data: db.posts[0] }) - .expect(200, done) - }) - }) - - describe('router.db._.id', (done) => { - beforeEach(() => { - router.db.setState({ - posts: [ - { _id: 1 } - ] - }) - - router.db._.id = '_id' - }) - - it('should be possible to GET using a different id property', (done) => { - request(server) - .get('/posts/1') - .expect('Content-Type', /json/) - .expect(router.db.getState().posts[0]) - .expect(200, done) - }) - - it('should be possible to POST using a different id property', (done) => { - request(server) - .post('/posts') - .send({ body: 'hello' }) - .expect('Content-Type', /json/) - .expect({ _id: 2, body: 'hello' }) - .expect(201, done) - }) - }) -}) diff --git a/test/server/singular.js b/test/server/singular.js deleted file mode 100644 index 509a4785e..000000000 --- a/test/server/singular.js +++ /dev/null @@ -1,63 +0,0 @@ -const request = require('supertest') -const jsonServer = require('../../src/server') - -describe('Server', function () { - let server - let router - let db - - beforeEach(function () { - db = {} - - db.user = { - name: 'foo', - email: 'foo@example.com' - } - - server = jsonServer.create() - router = jsonServer.router(db) - server.use(jsonServer.defaults()) - server.use(router) - }) - - describe('GET /:resource', function () { - it('should respond with corresponding resource', function (done) { - request(server) - .get('/user') - .expect(db.user) - .expect(200, done) - }) - }) - - describe('POST /:resource', function () { - it('should create resource', function (done) { - const user = { name: 'bar' } - request(server) - .post('/user') - .send(user) - .expect(user) - .expect(201, done) - }) - }) - - describe('PUT /:resource', function () { - it('should update resource', function (done) { - const user = { name: 'bar' } - request(server) - .put('/user') - .send(user) - .expect(user) - .expect(200, done) - }) - }) - - describe('PATCH /:resource', function () { - it('should update resource', function (done) { - request(server) - .patch('/user') - .send({ name: 'bar' }) - .expect({ name: 'bar', email: 'foo@example.com' }) - .expect(200, done) - }) - }) -}) diff --git a/test/server/utils.js b/test/server/utils.js deleted file mode 100644 index a611db22e..000000000 --- a/test/server/utils.js +++ /dev/null @@ -1,76 +0,0 @@ -const assert = require('assert') -const utils = require('../../src/server/utils') - -describe('utils', function () { - describe('getPage', function () { - const array = [1, 2, 3, 4, 5] - const perPage = 2 - - it('should return first page', function () { - assert.deepEqual( - utils.getPage(array, 1, perPage), - { - items: [1, 2], - current: 1, - first: 1, - next: 2, - last: 3 - } - ) - }) - - it('should return second page', function () { - assert.deepEqual( - utils.getPage(array, 2, perPage), - { - items: [3, 4], - current: 2, - first: 1, - prev: 1, - next: 3, - last: 3 - } - ) - }) - - it('should return third page (last)', function () { - assert.deepEqual( - utils.getPage(array, 3, perPage), - { - items: [5], - current: 3, - first: 1, - prev: 2, - last: 3 - } - ) - }) - - it('should return an empty array if page is greater than the last page', function () { - assert.deepEqual( - utils.getPage(array, 99, perPage), - { - items: [] - } - ) - }) - - it('should return the array if perPage is greater than the array size', function () { - assert.deepEqual( - utils.getPage(array, 1, 99), - { - items: array - } - ) - }) - - it('should return an empty array if the array is empty', function () { - assert.deepEqual( - utils.getPage([], 1, 1), - { - items: [] - } - ) - }) - }) -}) diff --git a/test/server/validate-data.js b/test/server/validate-data.js deleted file mode 100644 index 808721576..000000000 --- a/test/server/validate-data.js +++ /dev/null @@ -1,24 +0,0 @@ -const assert = require('assert') -const validateData = require('../../src/server/router/validate-data') - -describe('validateData', () => { - it('should throw an error if data contains /', () => { - assert.throws( - () => validateData({ 'a/b': [] }), - /found \// - ) - }) - - it('should throw an error if data is an array', () => { - assert.throws( - () => validateData([]), - /must be an object/ - ) - }) - - it('shouldn\'t throw an error', () => { - assert.doesNotThrow( - () => validateData({ a: [] }) - ) - }) -}) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..0e8a67f34 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@sindresorhus/tsconfig", + "exclude": ["src/**/*.test.ts"], + "compilerOptions": { + "outDir": "./lib" + } +} diff --git a/views/index.html b/views/index.html new file mode 100644 index 000000000..96f63c8eb --- /dev/null +++ b/views/index.html @@ -0,0 +1,97 @@ + + + + + + + + + + +
+ +
+
+

✧*q٩(ΛŠα—œΛ‹*)و✧*q

+ <% if (Object.keys(it.data).length===0) { %> +

No resources found in JSON file

+ <% } %> + <% Object.entries(it.data).forEach(function([name]) { %> +
    +
  • + /<%= name %> + + <% if (Array.isArray(it.data[name])) { %> + - <%= it.data[name].length %> + <%= it.data[name].length> 1 ? 'items' : 'item' %> + + <% } %> +
  • +
+ <% }) %> +
+ + + \ No newline at end of file