mirror of
https://github.com/typicode/json-server.git
synced 2026-03-13 09:35:37 +08:00
fix: patch error (#1676)
This commit is contained in:
4
.github/workflows/node.js.yml
vendored
4
.github/workflows/node.js.yml
vendored
@@ -10,8 +10,8 @@ jobs:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.x'
|
||||
cache: 'pnpm'
|
||||
node-version: "22.x"
|
||||
cache: "pnpm"
|
||||
- run: pnpm install
|
||||
- run: pnpm run typecheck
|
||||
- run: pnpm test
|
||||
|
||||
4
.github/workflows/npm-publish.yml
vendored
4
.github/workflows/npm-publish.yml
vendored
@@ -15,8 +15,8 @@ jobs:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
node-version: "22.x"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- run: pnpm install
|
||||
- run: pnpm publish --provenance --access public
|
||||
env:
|
||||
|
||||
4
.oxfmtrc.json
Normal file
4
.oxfmtrc.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export default {
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
}
|
||||
28
README.md
28
README.md
@@ -41,15 +41,15 @@ Create a `db.json` or `db.json5` file
|
||||
```json5
|
||||
{
|
||||
posts: [
|
||||
{ id: '1', title: 'a title', views: 100 },
|
||||
{ id: '2', title: 'another title', views: 200 },
|
||||
{ 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' },
|
||||
{ id: "1", text: "a comment about post 1", postId: "1" },
|
||||
{ id: "2", text: "another comment about post 1", postId: "1" },
|
||||
],
|
||||
profile: {
|
||||
name: 'typicode',
|
||||
name: "typicode",
|
||||
},
|
||||
}
|
||||
```
|
||||
@@ -81,22 +81,22 @@ Run `json-server --help` for a list of options
|
||||
|
||||
### Gold
|
||||
|
||||
||
|
||||
| :---: |
|
||||
| <a href="https://mockend.com/" target="_blank"><img src="https://jsonplaceholder.typicode.com/mockend.svg" height="100px"></a> |
|
||||
| |
|
||||
| :--------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://mockend.com/" target="_blank"><img src="https://jsonplaceholder.typicode.com/mockend.svg" height="100px"></a> |
|
||||
| <a href="https://zuplo.link/json-server-gh"><img src="https://github.com/user-attachments/assets/adfee31f-a8b6-4684-9a9b-af4f03ac5b75" height="100px"></a> |
|
||||
| <a href="https://www.mintlify.com/"><img src="https://github.com/user-attachments/assets/bcc8cc48-b2d9-4577-8939-1eb4196b7cc5" height="100px"></a> |
|
||||
| <a href="https://www.mintlify.com/"><img src="https://github.com/user-attachments/assets/bcc8cc48-b2d9-4577-8939-1eb4196b7cc5" height="100px"></a> |
|
||||
|
||||
### Silver
|
||||
|
||||
||
|
||||
| :---: |
|
||||
| |
|
||||
| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://requestly.com?utm_source=githubsponsor&utm_medium=jsonserver&utm_campaign=jsonserver"><img src="https://github.com/user-attachments/assets/f7e7b3cf-97e2-46b8-81c8-cb3992662a1c" style="height:70px; width:auto;"></a> |
|
||||
|
||||
### Bronze
|
||||
|
||||
|||
|
||||
| :---: | :---: |
|
||||
| | |
|
||||
| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.storyblok.com/" target="_blank"><img src="https://github.com/typicode/json-server/assets/5502029/c6b10674-4ada-4616-91b8-59d30046b45a" height="35px"></a> | <a href="https://betterstack.com/" target="_blank"><img src="https://github.com/typicode/json-server/assets/5502029/44679f8f-9671-470d-b77e-26d90b90cbdc" height="35px"></a> |
|
||||
|
||||
[Become a sponsor and have your company logo here](https://github.com/users/typicode/sponsorship)
|
||||
@@ -104,7 +104,7 @@ Run `json-server --help` for a list of options
|
||||
## Sponsorware
|
||||
|
||||
> [!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.__
|
||||
> 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/).
|
||||
|
||||
|
||||
@@ -10,4 +10,4 @@
|
||||
"profile": {
|
||||
"name": "typicode"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
{
|
||||
posts: [
|
||||
{
|
||||
id: '1',
|
||||
title: 'a title',
|
||||
id: "1",
|
||||
title: "a title",
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'another title',
|
||||
id: "2",
|
||||
title: "another title",
|
||||
},
|
||||
],
|
||||
comments: [
|
||||
{
|
||||
id: '1',
|
||||
text: 'a comment about post 1',
|
||||
postId: '1',
|
||||
id: "1",
|
||||
text: "a comment about post 1",
|
||||
postId: "1",
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
text: 'another comment about post 1',
|
||||
postId: '1',
|
||||
id: "2",
|
||||
text: "another comment about post 1",
|
||||
postId: "1",
|
||||
},
|
||||
],
|
||||
profile: {
|
||||
name: 'typicode',
|
||||
name: "typicode",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
45
package.json
45
package.json
@@ -2,7 +2,13 @@
|
||||
"name": "json-server",
|
||||
"version": "1.0.0-beta.3",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"keywords": [],
|
||||
"license": "SEE LICENSE IN ./LICENSE",
|
||||
"author": "typicode <typicode@gmail.com>",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/typicode/json-server.git"
|
||||
},
|
||||
"bin": {
|
||||
"json-server": "lib/bin.js"
|
||||
},
|
||||
@@ -10,9 +16,7 @@
|
||||
"lib",
|
||||
"views"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node --watch --experimental-strip-types src/bin.ts fixtures/db.json",
|
||||
"build": "rm -rf lib && tsc",
|
||||
@@ -20,25 +24,10 @@
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "node --experimental-strip-types --test src/*.test.ts",
|
||||
"lint": "oxlint src",
|
||||
"fmt": "oxfmt",
|
||||
"fmt:check": "oxfmt --check",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "typicode <typicode@gmail.com>",
|
||||
"license": "SEE LICENSE IN ./LICENSE",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/typicode/json-server.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.0.8",
|
||||
"concurrently": "^9.2.1",
|
||||
"get-port": "^7.1.0",
|
||||
"husky": "^9.1.7",
|
||||
"oxlint": "^1.39.0",
|
||||
"tempy": "^3.1.0",
|
||||
"type-fest": "^5.4.0",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tinyhttp/app": "^3.0.1",
|
||||
"@tinyhttp/cors": "^2.0.1",
|
||||
@@ -53,5 +42,19 @@
|
||||
"milliparsec": "^5.1.0",
|
||||
"sirv": "^3.0.2",
|
||||
"sort-on": "^7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.0.8",
|
||||
"concurrently": "^9.2.1",
|
||||
"get-port": "^7.1.0",
|
||||
"husky": "^9.1.7",
|
||||
"oxfmt": "^0.24.0",
|
||||
"oxlint": "^1.39.0",
|
||||
"tempy": "^3.1.0",
|
||||
"type-fest": "^5.4.0",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
}
|
||||
}
|
||||
|
||||
95
pnpm-lock.yaml
generated
95
pnpm-lock.yaml
generated
@@ -60,6 +60,9 @@ importers:
|
||||
husky:
|
||||
specifier: ^9.1.7
|
||||
version: 9.1.7
|
||||
oxfmt:
|
||||
specifier: ^0.24.0
|
||||
version: 0.24.0
|
||||
oxlint:
|
||||
specifier: ^1.39.0
|
||||
version: 1.39.0
|
||||
@@ -75,6 +78,50 @@ importers:
|
||||
|
||||
packages:
|
||||
|
||||
'@oxfmt/darwin-arm64@0.24.0':
|
||||
resolution: {integrity: sha512-aYXuGf/yq8nsyEcHindGhiz9I+GEqLkVq8CfPbd+6VE259CpPEH+CaGHEO1j6vIOmNr8KHRq+IAjeRO2uJpb8A==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxfmt/darwin-x64@0.24.0':
|
||||
resolution: {integrity: sha512-vs3b8Bs53hbiNvcNeBilzE/+IhDTWKjSBB3v/ztr664nZk65j0xr+5IHMBNz3CFppmX7o/aBta2PxY+t+4KoPg==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxfmt/linux-arm64-gnu@0.24.0':
|
||||
resolution: {integrity: sha512-ItPDOPoQ0wLj/s8osc5ch57uUcA1Wk8r0YdO8vLRpXA3UNg7KPOm1vdbkIZRRiSUphZcuX5ioOEetEK8H7RlTw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/linux-arm64-musl@0.24.0':
|
||||
resolution: {integrity: sha512-JkQO3WnQjQTJONx8nxdgVBfl6BBFfpp9bKhChYhWeakwJdr7QPOAWJ/v3FGZfr0TbqINwnNR74aVZayDDRyXEA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/linux-x64-gnu@0.24.0':
|
||||
resolution: {integrity: sha512-N/SXlFO+2kak5gMt0oxApi0WXQDhwA0PShR0UbkY0PwtHjfSiDqJSOumyNqgQVoroKr1GNnoRmUqjZIz6DKIcw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/linux-x64-musl@0.24.0':
|
||||
resolution: {integrity: sha512-WM0pek5YDCQf50XQ7GLCE9sMBCMPW/NPAEPH/Hx6Qyir37lEsP4rUmSECo/QFNTU6KBc9NnsviAyJruWPpCMXw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/win32-arm64@0.24.0':
|
||||
resolution: {integrity: sha512-vFCseli1KWtwdHrVlT/jWfZ8jP8oYpnPPEjI23mPLW8K/6GEJmmvy0PZP5NpWUFNTzX0lqie58XnrATJYAe9Xw==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@oxfmt/win32-x64@0.24.0':
|
||||
resolution: {integrity: sha512-0tmlNzcyewAnauNeBCq0xmAkmiKzl+H09p0IdHy+QKrTQdtixtf+AOjDAADbRfihkS+heF15Pjc4IyJMdAAJjw==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@oxlint/darwin-arm64@1.39.0':
|
||||
resolution: {integrity: sha512-lT3hNhIa02xCujI6YGgjmYGg3Ht/X9ag5ipUVETaMpx5Rd4BbTNWUPif1WN1YZHxt3KLCIqaAe7zVhatv83HOQ==}
|
||||
cpu: [arm64]
|
||||
@@ -332,6 +379,11 @@ packages:
|
||||
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
oxfmt@0.24.0:
|
||||
resolution: {integrity: sha512-UjeM3Peez8Tl7IJ9s5UwAoZSiDRMww7BEc21gDYxLq3S3/KqJnM3mjNxsoSHgmBvSeX6RBhoVc2MfC/+96RdSw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
|
||||
oxlint@1.39.0:
|
||||
resolution: {integrity: sha512-wSiLr0wjG+KTU6c1LpVoQk7JZ7l8HCKlAkVDVTJKWmCGazsNxexxnOXl7dsar92mQcRnzko5g077ggP3RINSjA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -401,6 +453,10 @@ packages:
|
||||
resolution: {integrity: sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==}
|
||||
engines: {node: '>=14.16'}
|
||||
|
||||
tinypool@2.0.0:
|
||||
resolution: {integrity: sha512-/RX9RzeH2xU5ADE7n2Ykvmi9ED3FBGPAjw9u3zucrNNaEBIO0HPSYgL0NT7+3p147ojeSdaVu08F6hjpv31HJg==}
|
||||
engines: {node: ^20.0.0 || >=22.0.0}
|
||||
|
||||
totalist@3.0.1:
|
||||
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -454,6 +510,30 @@ packages:
|
||||
|
||||
snapshots:
|
||||
|
||||
'@oxfmt/darwin-arm64@0.24.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/darwin-x64@0.24.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/linux-arm64-gnu@0.24.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/linux-arm64-musl@0.24.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/linux-x64-gnu@0.24.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/linux-x64-musl@0.24.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/win32-arm64@0.24.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/win32-x64@0.24.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/darwin-arm64@1.39.0':
|
||||
optional: true
|
||||
|
||||
@@ -656,6 +736,19 @@ snapshots:
|
||||
|
||||
negotiator@1.0.0: {}
|
||||
|
||||
oxfmt@0.24.0:
|
||||
dependencies:
|
||||
tinypool: 2.0.0
|
||||
optionalDependencies:
|
||||
'@oxfmt/darwin-arm64': 0.24.0
|
||||
'@oxfmt/darwin-x64': 0.24.0
|
||||
'@oxfmt/linux-arm64-gnu': 0.24.0
|
||||
'@oxfmt/linux-arm64-musl': 0.24.0
|
||||
'@oxfmt/linux-x64-gnu': 0.24.0
|
||||
'@oxfmt/linux-x64-musl': 0.24.0
|
||||
'@oxfmt/win32-arm64': 0.24.0
|
||||
'@oxfmt/win32-x64': 0.24.0
|
||||
|
||||
oxlint@1.39.0:
|
||||
optionalDependencies:
|
||||
'@oxlint/darwin-arm64': 1.39.0
|
||||
@@ -720,6 +813,8 @@ snapshots:
|
||||
type-fest: 2.19.0
|
||||
unique-string: 3.0.0
|
||||
|
||||
tinypool@2.0.0: {}
|
||||
|
||||
totalist@3.0.1: {}
|
||||
|
||||
tree-kill@1.2.2: {}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<!-- Testing automatic serving of files from the 'public/' directory -->
|
||||
<!-- Testing automatic serving of files from the 'public/' directory -->
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import assert from 'node:assert/strict'
|
||||
import { writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import test from 'node:test'
|
||||
@@ -11,20 +11,12 @@ import { createApp } from './app.ts'
|
||||
import type { Data } from './service.ts'
|
||||
|
||||
type Test = {
|
||||
|
||||
method: HTTPMethods
|
||||
url: string
|
||||
statusCode: number
|
||||
}
|
||||
|
||||
type HTTPMethods =
|
||||
| 'DELETE'
|
||||
| 'GET'
|
||||
| 'HEAD'
|
||||
| 'PATCH'
|
||||
| 'POST'
|
||||
| 'PUT'
|
||||
| 'OPTIONS'
|
||||
type HTTPMethods = 'DELETE' | 'GET' | 'HEAD' | 'PATCH' | 'POST' | 'PUT' | 'OPTIONS'
|
||||
|
||||
const port = await getPort()
|
||||
|
||||
|
||||
16
src/app.ts
16
src/app.ts
@@ -54,9 +54,7 @@ export function createApp(db: Low<Data>, options: AppOptions = {}) {
|
||||
// Body parser
|
||||
app.use(json())
|
||||
|
||||
app.get('/', (_req, res) =>
|
||||
res.send(eta.render('index.html', { data: db.data })),
|
||||
)
|
||||
app.get('/', (_req, res) => res.send(eta.render('index.html', { data: db.data })))
|
||||
|
||||
app.get('/:name', (req, res, next) => {
|
||||
const { name = '' } = req.params
|
||||
@@ -69,11 +67,11 @@ export function createApp(db: Low<Data>, options: AppOptions = {}) {
|
||||
['_start', '_end', '_limit', '_page', '_per_page'].includes(key) &&
|
||||
typeof value === 'string'
|
||||
) {
|
||||
value = parseInt(value);
|
||||
value = parseInt(value)
|
||||
}
|
||||
|
||||
|
||||
if (!Number.isNaN(value)) {
|
||||
query[key] = value;
|
||||
query[key] = value
|
||||
}
|
||||
})
|
||||
res.locals['data'] = service.find(name, query)
|
||||
@@ -128,11 +126,7 @@ export function createApp(db: Low<Data>, options: AppOptions = {}) {
|
||||
|
||||
app.delete('/:name/:id', async (req, res, next) => {
|
||||
const { name = '', id = '' } = req.params
|
||||
res.locals['data'] = await service.destroyById(
|
||||
name,
|
||||
id,
|
||||
req.query['_dependent'],
|
||||
)
|
||||
res.locals['data'] = await service.destroyById(name, id, req.query['_dependent'])
|
||||
next?.()
|
||||
})
|
||||
|
||||
|
||||
209
src/bin.ts
209
src/bin.ts
@@ -1,20 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { extname } from 'node:path'
|
||||
import { parseArgs } from 'node:util'
|
||||
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 { Low } from 'lowdb'
|
||||
import type { Adapter } from 'lowdb'
|
||||
import { DataFile, JSONFile } from 'lowdb/node'
|
||||
import type { PackageJson } from 'type-fest'
|
||||
import chalk from "chalk";
|
||||
import { watch } from "chokidar";
|
||||
import JSON5 from "json5";
|
||||
import { Low } from "lowdb";
|
||||
import type { Adapter } from "lowdb";
|
||||
import { DataFile, JSONFile } from "lowdb/node";
|
||||
import type { PackageJson } from "type-fest";
|
||||
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { createApp } from './app.ts'
|
||||
import { Observer } from './observer.ts'
|
||||
import type { Data } from './service.ts'
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createApp } from "./app.ts";
|
||||
import { Observer } from "./observer.ts";
|
||||
import type { Data } from "./service.ts";
|
||||
|
||||
function help() {
|
||||
console.log(`Usage: json-server [options] <file>
|
||||
@@ -25,204 +25,195 @@ Options:
|
||||
-s, --static <dir> 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[]
|
||||
file: string;
|
||||
port: number;
|
||||
host: string;
|
||||
static: string[];
|
||||
} {
|
||||
try {
|
||||
const { values, positionals } = parseArgs({
|
||||
options: {
|
||||
port: {
|
||||
type: 'string',
|
||||
short: 'p',
|
||||
default: process.env['PORT'] ?? '3000',
|
||||
type: "string",
|
||||
short: "p",
|
||||
default: process.env["PORT"] ?? "3000",
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
short: 'h',
|
||||
default: process.env['HOST'] ?? 'localhost',
|
||||
type: "string",
|
||||
short: "h",
|
||||
default: process.env["HOST"] ?? "localhost",
|
||||
},
|
||||
static: {
|
||||
type: 'string',
|
||||
short: 's',
|
||||
type: "string",
|
||||
short: "s",
|
||||
multiple: true,
|
||||
default: [],
|
||||
},
|
||||
help: {
|
||||
type: 'boolean',
|
||||
type: "boolean",
|
||||
},
|
||||
version: {
|
||||
type: 'boolean',
|
||||
type: "boolean",
|
||||
},
|
||||
// Deprecated
|
||||
watch: {
|
||||
type: 'boolean',
|
||||
short: 'w',
|
||||
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()
|
||||
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',
|
||||
"--watch/-w can be omitted, JSON Server 1+ watches for file changes by default",
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (values.help || positionals.length === 0) {
|
||||
help()
|
||||
process.exit()
|
||||
help();
|
||||
process.exit();
|
||||
}
|
||||
|
||||
// App args and options
|
||||
return {
|
||||
file: positionals[0] ?? '',
|
||||
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)
|
||||
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
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { file, port, host, static: staticArr } = args()
|
||||
const { file, port, host, static: staticArr } = args();
|
||||
|
||||
if (!existsSync(file)) {
|
||||
console.log(chalk.red(`File ${file} not found`))
|
||||
process.exit(1)
|
||||
console.log(chalk.red(`File ${file} not found`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Handle empty string JSON file
|
||||
if (readFileSync(file, 'utf-8').trim() === '') {
|
||||
writeFileSync(file, '{}')
|
||||
if (readFileSync(file, "utf-8").trim() === "") {
|
||||
writeFileSync(file, "{}");
|
||||
}
|
||||
|
||||
// Set up database
|
||||
let adapter: Adapter<Data>
|
||||
if (extname(file) === '.json5') {
|
||||
let adapter: Adapter<Data>;
|
||||
if (extname(file) === ".json5") {
|
||||
adapter = new DataFile<Data>(file, {
|
||||
parse: JSON5.parse,
|
||||
stringify: JSON5.stringify,
|
||||
})
|
||||
});
|
||||
} else {
|
||||
adapter = new JSONFile<Data>(file)
|
||||
adapter = new JSONFile<Data>(file);
|
||||
}
|
||||
const observer = new Observer(adapter)
|
||||
const observer = new Observer(adapter);
|
||||
|
||||
const db = new Low<Data>(observer, {})
|
||||
await db.read()
|
||||
const db = new Low<Data>(observer, {});
|
||||
await db.read();
|
||||
|
||||
// Create app
|
||||
const app = createApp(db, { logger: false, static: staticArr })
|
||||
const app = createApp(db, { logger: false, static: staticArr });
|
||||
|
||||
function logRoutes(data: Data) {
|
||||
console.log(chalk.bold('Endpoints:'))
|
||||
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(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'),
|
||||
)
|
||||
.map((key) => `${chalk.gray(`http://${host}:${port}/`)}${chalk.blue(key)}`)
|
||||
.join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
const kaomojis = ['♡⸜(˶˃ ᵕ ˂˶)⸝♡', '♡( ◡‿◡ )', '( ˶ˆ ᗜ ˆ˵ )', '(˶ᵔ ᵕ ᵔ˶)']
|
||||
const kaomojis = ["♡⸜(˶˃ ᵕ ˂˶)⸝♡", "♡( ◡‿◡ )", "( ˶ˆ ᗜ ˆ˵ )", "(˶ᵔ ᵕ ᵔ˶)"];
|
||||
|
||||
function randomItem(items: string[]): string {
|
||||
const index = Math.floor(Math.random() * items.length)
|
||||
return items.at(index) ?? ''
|
||||
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("Press CTRL-C to stop"),
|
||||
chalk.gray(`Watching ${file}...`),
|
||||
'',
|
||||
"",
|
||||
chalk.magenta(randomItem(kaomojis)),
|
||||
'',
|
||||
chalk.bold('Index:'),
|
||||
"",
|
||||
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)
|
||||
})
|
||||
"",
|
||||
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 = ''
|
||||
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
|
||||
}
|
||||
writing = true;
|
||||
};
|
||||
observer.onWriteEnd = () => {
|
||||
writing = false
|
||||
}
|
||||
writing = false;
|
||||
};
|
||||
observer.onReadStart = () => {
|
||||
prevEndpoints = JSON.stringify(Object.keys(db.data).sort())
|
||||
}
|
||||
prevEndpoints = JSON.stringify(Object.keys(db.data).sort());
|
||||
};
|
||||
observer.onReadEnd = (data) => {
|
||||
if (data === null) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const nextEndpoints = JSON.stringify(Object.keys(data).sort())
|
||||
const nextEndpoints = JSON.stringify(Object.keys(data).sort());
|
||||
if (prevEndpoints !== nextEndpoints) {
|
||||
console.log()
|
||||
logRoutes(data)
|
||||
console.log();
|
||||
logRoutes(data);
|
||||
}
|
||||
}
|
||||
watch(file).on('change', () => {
|
||||
};
|
||||
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')),
|
||||
)
|
||||
return console.log(chalk.red(["", `Error parsing ${file}`, e.message].join("\n")));
|
||||
}
|
||||
console.log(e)
|
||||
})
|
||||
console.log(e);
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
import type { Adapter } from 'lowdb'
|
||||
import type { Adapter } from "lowdb";
|
||||
|
||||
// Lowdb adapter to observe read/write events
|
||||
export class Observer<T> {
|
||||
#adapter
|
||||
#adapter;
|
||||
|
||||
onReadStart = function () {
|
||||
return
|
||||
}
|
||||
return;
|
||||
};
|
||||
onReadEnd: (data: T | null) => void = function () {
|
||||
return
|
||||
}
|
||||
return;
|
||||
};
|
||||
onWriteStart = function () {
|
||||
return
|
||||
}
|
||||
return;
|
||||
};
|
||||
onWriteEnd = function () {
|
||||
return
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
constructor(adapter: Adapter<T>) {
|
||||
this.#adapter = adapter
|
||||
this.#adapter = adapter;
|
||||
}
|
||||
|
||||
async read() {
|
||||
this.onReadStart()
|
||||
const data = await this.#adapter.read()
|
||||
this.onReadEnd(data)
|
||||
return data
|
||||
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()
|
||||
this.onWriteStart();
|
||||
await this.#adapter.write(arg);
|
||||
this.onWriteEnd();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,8 +77,7 @@ await test('constructor', () => {
|
||||
|
||||
await test('findById', () => {
|
||||
reset()
|
||||
if (!Array.isArray(db.data?.[POSTS]))
|
||||
throw new Error('posts should be an array')
|
||||
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'] }), {
|
||||
@@ -339,11 +338,21 @@ await test('update', async () => {
|
||||
undefined,
|
||||
'should ignore unknown resources',
|
||||
)
|
||||
assert.equal(await service.update(POSTS, {}), undefined, 'should ignore arrays')
|
||||
})
|
||||
|
||||
await test('patch', async () => {
|
||||
reset()
|
||||
const obj = { f2: 'bar' }
|
||||
const res = await service.patch(OBJECT, obj)
|
||||
assert.deepEqual(res, { f1: 'foo', ...obj })
|
||||
|
||||
assert.equal(
|
||||
await service.update(POSTS, {}),
|
||||
await service.patch(UNKNOWN_RESOURCE, obj),
|
||||
undefined,
|
||||
'should ignore arrays',
|
||||
'should ignore unknown resources',
|
||||
)
|
||||
assert.equal(await service.patch(POSTS, {}), undefined, 'should ignore arrays')
|
||||
})
|
||||
|
||||
await test('updateById', async () => {
|
||||
@@ -353,10 +362,7 @@ await test('updateById', async () => {
|
||||
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(UNKNOWN_RESOURCE, post1.id, post), undefined)
|
||||
assert.equal(await service.updateById(POSTS, UNKNOWN_ID, post), undefined)
|
||||
})
|
||||
|
||||
@@ -368,10 +374,7 @@ await test('patchById', async () => {
|
||||
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(UNKNOWN_RESOURCE, post1.id, post), undefined)
|
||||
assert.equal(await service.patchById(POSTS, UNKNOWN_ID, post), undefined)
|
||||
})
|
||||
|
||||
|
||||
@@ -19,9 +19,7 @@ export function isData(obj: unknown): obj is Record<string, Item[]> {
|
||||
}
|
||||
|
||||
const data = obj as Record<string, unknown>
|
||||
return Object.values(data).every(
|
||||
(value) => Array.isArray(value) && value.every(isItem),
|
||||
)
|
||||
return Object.values(data).every((value) => Array.isArray(value) && value.every(isItem))
|
||||
}
|
||||
|
||||
const Condition = {
|
||||
@@ -33,7 +31,7 @@ const Condition = {
|
||||
default: '',
|
||||
} as const
|
||||
|
||||
type Condition = typeof Condition[keyof typeof Condition]
|
||||
type Condition = (typeof Condition)[keyof typeof Condition]
|
||||
|
||||
function isCondition(value: string): value is Condition {
|
||||
return Object.values<string>(Condition).includes(value)
|
||||
@@ -151,11 +149,7 @@ export class Service {
|
||||
return Object.prototype.hasOwnProperty.call(this.#db?.data, name)
|
||||
}
|
||||
|
||||
findById(
|
||||
name: string,
|
||||
id: string,
|
||||
query: { _embed?: string[] | string },
|
||||
): Item | undefined {
|
||||
findById(name: string, id: string, query: { _embed?: string[] | string }): Item | undefined {
|
||||
const value = this.#get(name)
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
@@ -214,17 +208,7 @@ export class Service {
|
||||
conds.push([field, op, value])
|
||||
continue
|
||||
}
|
||||
if (
|
||||
[
|
||||
'_embed',
|
||||
'_sort',
|
||||
'_start',
|
||||
'_end',
|
||||
'_limit',
|
||||
'_page',
|
||||
'_per_page',
|
||||
].includes(key)
|
||||
) {
|
||||
if (['_embed', '_sort', '_start', '_end', '_limit', '_page', '_per_page'].includes(key)) {
|
||||
continue
|
||||
}
|
||||
conds.push([key, Condition.default, value])
|
||||
@@ -240,48 +224,28 @@ export class Service {
|
||||
switch (op) {
|
||||
// item_gt=value
|
||||
case Condition.gt: {
|
||||
if (
|
||||
!(
|
||||
typeof itemValue === 'number' &&
|
||||
itemValue > parseInt(paramValue)
|
||||
)
|
||||
) {
|
||||
if (!(typeof itemValue === 'number' && itemValue > parseInt(paramValue))) {
|
||||
return false
|
||||
}
|
||||
break
|
||||
}
|
||||
// item_gte=value
|
||||
case Condition.gte: {
|
||||
if (
|
||||
!(
|
||||
typeof itemValue === 'number' &&
|
||||
itemValue >= parseInt(paramValue)
|
||||
)
|
||||
) {
|
||||
if (!(typeof itemValue === 'number' && itemValue >= parseInt(paramValue))) {
|
||||
return false
|
||||
}
|
||||
break
|
||||
}
|
||||
// item_lt=value
|
||||
case Condition.lt: {
|
||||
if (
|
||||
!(
|
||||
typeof itemValue === 'number' &&
|
||||
itemValue < parseInt(paramValue)
|
||||
)
|
||||
) {
|
||||
if (!(typeof itemValue === 'number' && itemValue < parseInt(paramValue))) {
|
||||
return false
|
||||
}
|
||||
break
|
||||
}
|
||||
// item_lte=value
|
||||
case Condition.lte: {
|
||||
if (
|
||||
!(
|
||||
typeof itemValue === 'number' &&
|
||||
itemValue <= parseInt(paramValue)
|
||||
)
|
||||
) {
|
||||
if (!(typeof itemValue === 'number' && itemValue <= parseInt(paramValue))) {
|
||||
return false
|
||||
}
|
||||
break
|
||||
@@ -368,10 +332,7 @@ export class Service {
|
||||
return sorted.slice(start, end)
|
||||
}
|
||||
|
||||
async create(
|
||||
name: string,
|
||||
data: Omit<Item, 'id'> = {},
|
||||
): Promise<Item | undefined> {
|
||||
async create(name: string, data: Omit<Item, 'id'> = {}): Promise<Item | undefined> {
|
||||
const items = this.#get(name)
|
||||
if (items === undefined || !Array.isArray(items)) return
|
||||
|
||||
@@ -382,15 +343,11 @@ export class Service {
|
||||
return item
|
||||
}
|
||||
|
||||
async #updateOrPatch(
|
||||
name: string,
|
||||
body: Item = {},
|
||||
isPatch: boolean,
|
||||
): Promise<Item | undefined> {
|
||||
async #updateOrPatch(name: string, body: Item = {}, isPatch: boolean): Promise<Item | undefined> {
|
||||
const item = this.#get(name)
|
||||
if (item === undefined || Array.isArray(item)) return
|
||||
|
||||
const nextItem = (this.#db.data[name] = isPatch ? { item, ...body } : body)
|
||||
const nextItem = (this.#db.data[name] = isPatch ? { ...item, ...body } : body)
|
||||
|
||||
await this.#db.write()
|
||||
return nextItem
|
||||
@@ -424,19 +381,11 @@ export class Service {
|
||||
return this.#updateOrPatch(name, body, true)
|
||||
}
|
||||
|
||||
async updateById(
|
||||
name: string,
|
||||
id: string,
|
||||
body: Item = {},
|
||||
): Promise<Item | undefined> {
|
||||
async updateById(name: string, id: string, body: Item = {}): Promise<Item | undefined> {
|
||||
return this.#updateOrPatchById(name, id, body, false)
|
||||
}
|
||||
|
||||
async patchById(
|
||||
name: string,
|
||||
id: string,
|
||||
body: Item = {},
|
||||
): Promise<Item | undefined> {
|
||||
async patchById(name: string, id: string, body: Item = {}): Promise<Item | undefined> {
|
||||
return this.#updateOrPatchById(name, id, body, true)
|
||||
}
|
||||
|
||||
|
||||
158
views/index.html
158
views/index.html
@@ -1,97 +1,95 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<style>
|
||||
html {
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0 auto;
|
||||
max-width: 720px;
|
||||
padding: 0 16px;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #db2777;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
header {
|
||||
margin-bottom: 32px;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
nav div a {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Dark mode styles */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<style>
|
||||
html {
|
||||
background-color: #1e293b;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0 auto;
|
||||
max-width: 720px;
|
||||
padding: 0 16px;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
|
||||
color: #db2777;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
header {
|
||||
margin-bottom: 32px;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<nav>
|
||||
<strong>JSON Server</strong>
|
||||
<div>
|
||||
<a href="https://github.com/typicode/json-server">Docs</a>
|
||||
<a href="https://github.com/sponsors/typicode">♡ Sponsor</a>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<main class="my-12">
|
||||
<p class="bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 text-transparent bg-clip-text">✧*。٩(ˊᗜˋ*)و✧*。</p>
|
||||
<% if (Object.keys(it.data).length===0) { %>
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
nav div a {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Dark mode styles */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background-color: #1e293b;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
a {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<nav>
|
||||
<strong>JSON Server</strong>
|
||||
<div>
|
||||
<a href="https://github.com/typicode/json-server">Docs</a>
|
||||
<a href="https://github.com/sponsors/typicode">♡ Sponsor</a>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<main class="my-12">
|
||||
<p
|
||||
class="bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 text-transparent bg-clip-text"
|
||||
>
|
||||
✧*。٩(ˊᗜˋ*)و✧*。
|
||||
</p>
|
||||
<% if (Object.keys(it.data).length===0) { %>
|
||||
<p>No resources found in JSON file</p>
|
||||
<% } %>
|
||||
<% Object.entries(it.data).forEach(function([name]) { %>
|
||||
<% } %> <% Object.entries(it.data).forEach(function([name]) { %>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="<%= name %>">/<%= name %></a>
|
||||
<span>
|
||||
<% if (Array.isArray(it.data[name])) { %>
|
||||
- <%= it.data[name].length %>
|
||||
<%= it.data[name].length> 1 ? 'items' : 'item' %>
|
||||
<% if (Array.isArray(it.data[name])) { %> - <%= it.data[name].length %> <%=
|
||||
it.data[name].length> 1 ? 'items' : 'item' %>
|
||||
</span>
|
||||
<% } %>
|
||||
</li>
|
||||
</ul>
|
||||
<% }) %>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<% }) %>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user