@@ -1,16 +0,0 @@ | |||||
import {DataSource} from '../../src/backend/data-source'; | |||||
export const autoIncrement = async (dataSource: DataSource) => { | |||||
const data = await dataSource.getMultiple() as Record<string, string>[]; | |||||
const highestId = data.reduce<number>( | |||||
(highestId, d) => (Number(d.id) > highestId ? Number(d.id) : highestId), | |||||
-Infinity | |||||
); | |||||
if (Number.isFinite(highestId)) { | |||||
return (highestId + 1); | |||||
} | |||||
return 1; | |||||
}; |
@@ -1,29 +0,0 @@ | |||||
export const TEXT_SERIALIZER_PAIR = { | |||||
name: 'text/plain', | |||||
serialize(obj: unknown, level = 0): string { | |||||
if (Array.isArray(obj)) { | |||||
return obj.map((o) => this.serialize(o)).join('\n\n'); | |||||
} | |||||
if (typeof obj === 'object') { | |||||
if (obj !== null) { | |||||
return Object.entries(obj) | |||||
.map(([key, value]) => `${Array(level * 2).fill(' ').join('')}${key}: ${this.serialize(value, level + 1)}`) | |||||
.join('\n'); | |||||
} | |||||
return ''; | |||||
} | |||||
if (typeof obj === 'number' && Number.isFinite(obj)) { | |||||
return obj.toString(); | |||||
} | |||||
if (typeof obj === 'string') { | |||||
return obj; | |||||
} | |||||
return ''; | |||||
}, | |||||
deserialize: <T>(str: string) => str as T | |||||
}; |
@@ -1,75 +0,0 @@ | |||||
import { | |||||
application, | |||||
resource, | |||||
validation as v, | |||||
} from '../../src/common'; | |||||
import { dataSources } from '../../src/backend'; | |||||
import {TEXT_SERIALIZER_PAIR} from './serializers'; | |||||
import {autoIncrement} from './data-source'; | |||||
const Piano = resource(v.object( | |||||
{ | |||||
brand: v.string() | |||||
}, | |||||
v.never() | |||||
)) | |||||
.name('Piano') | |||||
.route('pianos') | |||||
.canFetchItem() | |||||
.canFetchCollection() | |||||
.canCreate() | |||||
.canEmplace() | |||||
.canPatch() | |||||
.canDelete() | |||||
.id('id', { | |||||
generationStrategy: autoIncrement, | |||||
serialize: (id) => id?.toString() ?? '0', | |||||
deserialize: (id) => Number.isFinite(Number(id)) ? Number(id) : 0, | |||||
schema: v.number(), | |||||
}); | |||||
const User = resource(v.object( | |||||
{ | |||||
firstName: v.string(), | |||||
middleName: v.string(), | |||||
lastName: v.string(), | |||||
bio: v.string(), | |||||
birthday: v.datelike() | |||||
}, | |||||
v.never() | |||||
)) | |||||
.name('User') | |||||
.route('users') | |||||
.fullText('bio') | |||||
.id('id' as const, { | |||||
generationStrategy: autoIncrement, | |||||
serialize: (id) => id?.toString() ?? '0', | |||||
deserialize: (id) => Number.isFinite(Number(id)) ? Number(id) : 0, | |||||
schema: v.number(), | |||||
}); | |||||
const app = application({ | |||||
name: 'piano-service', | |||||
}) | |||||
.mediaType(TEXT_SERIALIZER_PAIR) | |||||
.resource(Piano) | |||||
.resource(User); | |||||
const backend = app.createBackend({ | |||||
dataSource: new dataSources.jsonlFile.DataSource('examples/basic'), | |||||
}); | |||||
const server = backend.createHttpServer({ | |||||
basePath: '/api' | |||||
}); | |||||
server.listen(3000); | |||||
setTimeout(() => { | |||||
// Allow user operations after 5 seconds from startup | |||||
User | |||||
.canFetchItem() | |||||
.canFetchCollection() | |||||
.canCreate() | |||||
.canPatch(); | |||||
}, 5000); |
@@ -1 +0,0 @@ | |||||
{"firstName":"John","middleName":"Smith","lastName":"Doe","bio":"This is my updated bio!","birthday":"1986-04-20T00:00:00.000Z","id":1} |
@@ -261,7 +261,9 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr | |||||
const theBodyStr = encodingPair.decode(theBodyBuffer); | const theBodyStr = encodingPair.decode(theBodyBuffer); | ||||
const theBody = deserializerPair.deserialize(theBodyStr); | const theBody = deserializerPair.deserialize(theBodyStr); | ||||
try { | try { | ||||
// for validation, I wonder why an empty object is returned for PATCH when both methods are enabled | |||||
req.body = await v.parseAsync(bodySchema, theBody, {abortEarly: false, abortPipeEarly: false}); | req.body = await v.parseAsync(bodySchema, theBody, {abortEarly: false, abortPipeEarly: false}); | ||||
req.body = theBody; | |||||
} catch (errRaw) { | } catch (errRaw) { | ||||
const err = errRaw as v.ValiError; | const err = errRaw as v.ValiError; | ||||
// todo use error message key for each method | // todo use error message key for each method | ||||
@@ -432,11 +434,12 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr | |||||
const handleRequest = async (reqRaw: RequestContext, res: http.ServerResponse<RequestContext>) => { | const handleRequest = async (reqRaw: RequestContext, res: http.ServerResponse<RequestContext>) => { | ||||
const plainReq = await decorateRequest(reqRaw); // TODO add type safety here, put handleGetRoot as its own middleware as it does not concern over any resource | const plainReq = await decorateRequest(reqRaw); // TODO add type safety here, put handleGetRoot as its own middleware as it does not concern over any resource | ||||
const language = plainReq.cn.language ?? plainReq.backend.cn.language; | |||||
const mediaType = plainReq.cn.mediaType ?? plainReq.backend.cn.mediaType; | |||||
const charset = plainReq.cn.charset ?? plainReq.backend.cn.charset; | |||||
if (typeof plainReq.resource !== 'undefined') { | if (typeof plainReq.resource !== 'undefined') { | ||||
const resourceReq = plainReq as ResourceRequestContext; | const resourceReq = plainReq as ResourceRequestContext; | ||||
const language = resourceReq.cn.language ?? resourceReq.backend.cn.language; | |||||
const mediaType = resourceReq.cn.mediaType ?? resourceReq.backend.cn.mediaType; | |||||
const charset = resourceReq.cn.charset ?? resourceReq.backend.cn.charset; | |||||
// TODO custom middlewares | // TODO custom middlewares | ||||
const effectiveMiddlewares = ( | const effectiveMiddlewares = ( | ||||
@@ -451,6 +454,7 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr | |||||
try { | try { | ||||
middlewareState = await processRequestFn(resourceReq) as any; // TODO fix this | middlewareState = await processRequestFn(resourceReq) as any; // TODO fix this | ||||
} catch (processRequestErrRaw) { | } catch (processRequestErrRaw) { | ||||
// TODO add error handlers | |||||
handleMiddlewareError(processRequestErrRaw as Error)(resourceReq, res); | handleMiddlewareError(processRequestErrRaw as Error)(resourceReq, res); | ||||
return; | return; | ||||
} | } | ||||
@@ -528,28 +532,28 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr | |||||
} | } | ||||
if (middlewares.length > 0) { | if (middlewares.length > 0) { | ||||
res.statusMessage = resourceReq.backend.cn.language.statusMessages.methodNotAllowed.replace(/\$RESOURCE/g, | |||||
res.statusMessage = language.statusMessages.methodNotAllowed.replace(/\$RESOURCE/g, | |||||
resourceReq.resource!.state.itemName) ?? ''; | resourceReq.resource!.state.itemName) ?? ''; | ||||
res.writeHead(constants.HTTP_STATUS_METHOD_NOT_ALLOWED, { | res.writeHead(constants.HTTP_STATUS_METHOD_NOT_ALLOWED, { | ||||
Allow: middlewares.map((m) => m.method).join(', '), | Allow: middlewares.map((m) => m.method).join(', '), | ||||
'Content-Language': resourceReq.backend.cn.language.name, | |||||
'Content-Language': language.name, | |||||
}); | }); | ||||
res.end(); | res.end(); | ||||
return; | return; | ||||
} | } | ||||
res.statusMessage = resourceReq.backend.cn.language.statusMessages.urlNotFound.replace(/\$RESOURCE/g, | |||||
res.statusMessage = language.statusMessages.urlNotFound.replace(/\$RESOURCE/g, | |||||
resourceReq.resource!.state.itemName) ?? ''; | resourceReq.resource!.state.itemName) ?? ''; | ||||
res.writeHead(constants.HTTP_STATUS_NOT_FOUND, { | res.writeHead(constants.HTTP_STATUS_NOT_FOUND, { | ||||
'Content-Language': resourceReq.backend.cn.language.name, | |||||
'Content-Language': language.name, | |||||
}); | }); | ||||
res.end(); | res.end(); | ||||
return; | return; | ||||
} | } | ||||
res.statusMessage = reqRaw.backend.cn.language.statusMessages.notImplemented.replace(/\$RESOURCE/g, | |||||
res.statusMessage = language.statusMessages.notImplemented.replace(/\$RESOURCE/g, | |||||
reqRaw.resource!.state.itemName) ?? ''; | reqRaw.resource!.state.itemName) ?? ''; | ||||
res.writeHead(constants.HTTP_STATUS_NOT_IMPLEMENTED, { | res.writeHead(constants.HTTP_STATUS_NOT_IMPLEMENTED, { | ||||
'Content-Language': reqRaw.backend.cn.language.name, | |||||
'Content-Language': language.name, | |||||
}); | }); | ||||
res.end(); | res.end(); | ||||
}; | }; | ||||
@@ -176,7 +176,7 @@ export const applyDelta = async <T extends v.BaseSchema = v.BaseSchema>( | |||||
} | } | ||||
mutateObject(mutablePreviousObject, deltaItem, pathSchema); | mutateObject(mutablePreviousObject, deltaItem, pathSchema); | ||||
if (!v.is(resourceObjectSchema, mutablePreviousObject)) { | |||||
if (!v.is(resourceObjectSchema, mutablePreviousObject, { skipPipe: true })) { | |||||
throw new InvalidOperationError(); | throw new InvalidOperationError(); | ||||
} | } | ||||
@@ -27,6 +27,7 @@ export const createTestClient = (options: Omit<RequestOptions, 'method' | 'path' | |||||
...etcAdditionalHeaders | ...etcAdditionalHeaders | ||||
} = additionalHeaders; | } = additionalHeaders; | ||||
// odd that request() uses OutgoingHttpHeaders instead of IncomingHttpHeaders... | |||||
const headers: OutgoingHttpHeaders = { | const headers: OutgoingHttpHeaders = { | ||||
...(options.headers ?? {}), | ...(options.headers ?? {}), | ||||
...etcAdditionalHeaders, | ...etcAdditionalHeaders, | ||||
@@ -1,5 +1,5 @@ | |||||
{ | { | ||||
"name": "data-source-file-jsonl", | |||||
"name": "@modal-sh/yasumi-data-source-file-jsonl", | |||||
"version": "0.0.0", | "version": "0.0.0", | ||||
"files": [ | "files": [ | ||||
"dist", | "dist", | ||||
@@ -45,5 +45,22 @@ | |||||
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>", | "author": "TheoryOfNekomata <allan.crisostomo@outlook.com>", | ||||
"publishConfig": { | "publishConfig": { | ||||
"access": "public" | "access": "public" | ||||
}, | |||||
"types": "./dist/types/index.d.ts", | |||||
"main": "./dist/cjs/production/index.js", | |||||
"module": "./dist/esm/production/index.js", | |||||
"exports": { | |||||
".": { | |||||
"development": { | |||||
"require": "./dist/cjs/development/index.js", | |||||
"import": "./dist/esm/development/index.js" | |||||
}, | |||||
"require": "./dist/cjs/production/index.js", | |||||
"import": "./dist/esm/production/index.js", | |||||
"types": "./dist/types/index.d.ts" | |||||
} | |||||
}, | |||||
"typesVersions": { | |||||
"*": {} | |||||
} | } | ||||
} | } |
@@ -0,0 +1,108 @@ | |||||
# Logs | |||||
logs | |||||
*.log | |||||
npm-debug.log* | |||||
yarn-debug.log* | |||||
yarn-error.log* | |||||
lerna-debug.log* | |||||
# Diagnostic reports (https://nodejs.org/api/report.html) | |||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json | |||||
# Runtime data | |||||
pids | |||||
*.pid | |||||
*.seed | |||||
*.pid.lock | |||||
# Directory for instrumented libs generated by jscoverage/JSCover | |||||
lib-cov | |||||
# Coverage directory used by tools like istanbul | |||||
coverage | |||||
*.lcov | |||||
# nyc test coverage | |||||
.nyc_output | |||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) | |||||
.grunt | |||||
# Bower dependency directory (https://bower.io/) | |||||
bower_components | |||||
# node-waf configuration | |||||
.lock-wscript | |||||
# Compiled binary addons (https://nodejs.org/api/addons.html) | |||||
build/Release | |||||
# Dependency directories | |||||
node_modules/ | |||||
jspm_packages/ | |||||
# TypeScript v1 declaration files | |||||
typings/ | |||||
# TypeScript cache | |||||
*.tsbuildinfo | |||||
# Optional npm cache directory | |||||
.npm | |||||
# Optional eslint cache | |||||
.eslintcache | |||||
# Microbundle cache | |||||
.rpt2_cache/ | |||||
.rts2_cache_cjs/ | |||||
.rts2_cache_es/ | |||||
.rts2_cache_umd/ | |||||
# Optional REPL history | |||||
.node_repl_history | |||||
# Output of 'npm pack' | |||||
*.tgz | |||||
# Yarn Integrity file | |||||
.yarn-integrity | |||||
# dotenv environment variables file | |||||
.env | |||||
.env.production | |||||
.env.development | |||||
# parcel-bundler cache (https://parceljs.org/) | |||||
.cache | |||||
# Next.js build output | |||||
.next | |||||
# Nuxt.js build / generate output | |||||
.nuxt | |||||
dist | |||||
# Gatsby files | |||||
.cache/ | |||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js | |||||
# https://nextjs.org/blog/next-9-1#public-directory-support | |||||
# public | |||||
# vuepress build output | |||||
.vuepress/dist | |||||
# Serverless directories | |||||
.serverless/ | |||||
# FuseBox cache | |||||
.fusebox/ | |||||
# DynamoDB Local files | |||||
.dynamodb/ | |||||
# TernJS port file | |||||
.tern-port | |||||
.npmrc | |||||
*.jsonl |
@@ -0,0 +1,18 @@ | |||||
meta { | |||||
name: Create Post | |||||
type: http | |||||
seq: 2 | |||||
} | |||||
post { | |||||
url: http://localhost:6969/api/posts | |||||
body: json | |||||
auth: none | |||||
} | |||||
body:json { | |||||
{ | |||||
"title": "New Post", | |||||
"content": "Hello there" | |||||
} | |||||
} |
@@ -0,0 +1,11 @@ | |||||
meta { | |||||
name: Delete Post | |||||
type: http | |||||
seq: 8 | |||||
} | |||||
delete { | |||||
url: http://localhost:6969/api/posts/5fac64d6-d261-42bb-a67b-bc7e1955a7e2 | |||||
body: none | |||||
auth: none | |||||
} |
@@ -0,0 +1,11 @@ | |||||
meta { | |||||
name: Get Single Post | |||||
type: http | |||||
seq: 3 | |||||
} | |||||
get { | |||||
url: http://localhost:6969/api/posts/5fac64d6-d261-42bb-a67b-bc7e1955a7e2 | |||||
body: none | |||||
auth: none | |||||
} |
@@ -0,0 +1,25 @@ | |||||
meta { | |||||
name: Modify Post (Delta) | |||||
type: http | |||||
seq: 5 | |||||
} | |||||
patch { | |||||
url: http://localhost:6969/api/posts/5fac64d6-d261-42bb-a67b-bc7e1955a7e2 | |||||
body: json | |||||
auth: none | |||||
} | |||||
headers { | |||||
Content-Type: application/json-patch+json | |||||
} | |||||
body:json { | |||||
[ | |||||
{ | |||||
"op": "replace", | |||||
"path": "content", | |||||
"value": "I changed the value via delta." | |||||
} | |||||
] | |||||
} |
@@ -0,0 +1,23 @@ | |||||
meta { | |||||
name: Modify Post (Merge) | |||||
type: http | |||||
seq: 4 | |||||
} | |||||
patch { | |||||
url: http://localhost:6969/api/posts/5fac64d6-d261-42bb-a67b-bc7e1955a7e2 | |||||
body: json | |||||
auth: none | |||||
} | |||||
headers { | |||||
Content-Type: application/merge-patch+json | |||||
} | |||||
body:json { | |||||
{ | |||||
"title": "Modified Post", | |||||
"content": "I changed the content via merge." | |||||
} | |||||
} |
@@ -0,0 +1,11 @@ | |||||
meta { | |||||
name: Query Posts | |||||
type: http | |||||
seq: 1 | |||||
} | |||||
get { | |||||
url: http://localhost:6969/api/posts | |||||
body: none | |||||
auth: none | |||||
} |
@@ -0,0 +1,19 @@ | |||||
meta { | |||||
name: Replace Post | |||||
type: http | |||||
seq: 7 | |||||
} | |||||
put { | |||||
url: http://localhost:6969/api/posts/5fac64d6-d261-42bb-a67b-bc7e1955a7e2 | |||||
body: json | |||||
auth: none | |||||
} | |||||
body:json { | |||||
{ | |||||
"title": "Replaced Post", | |||||
"content": "The old content is gone.", | |||||
"id": "5fac64d6-d261-42bb-a67b-bc7e1955a7e2" | |||||
} | |||||
} |
@@ -0,0 +1,13 @@ | |||||
{ | |||||
"version": "1", | |||||
"name": "cms-web-api", | |||||
"type": "collection", | |||||
"ignore": [ | |||||
"node_modules", | |||||
".git" | |||||
], | |||||
"presets": { | |||||
"requestType": "http", | |||||
"requestUrl": "http://localhost:6969/api/" | |||||
} | |||||
} |
@@ -0,0 +1,50 @@ | |||||
{ | |||||
"name": "@modal-sh/yasumi-example-cms-web-api", | |||||
"version": "0.0.0", | |||||
"files": [ | |||||
"dist", | |||||
"src" | |||||
], | |||||
"engines": { | |||||
"node": ">=16" | |||||
}, | |||||
"keywords": [ | |||||
"pridepack" | |||||
], | |||||
"devDependencies": { | |||||
"@types/node": "^20.11.0", | |||||
"pridepack": "2.6.0", | |||||
"tslib": "^2.6.2", | |||||
"typescript": "^5.3.3", | |||||
"vitest": "^1.2.0" | |||||
}, | |||||
"dependencies": { | |||||
"@modal-sh/yasumi": "*", | |||||
"@modal-sh/yasumi-data-source-file-jsonl": "*", | |||||
"tsx": "^4.7.1" | |||||
}, | |||||
"scripts": { | |||||
"prepublishOnly": "pridepack clean && pridepack build", | |||||
"build": "pridepack build", | |||||
"type-check": "pridepack check", | |||||
"clean": "pridepack clean", | |||||
"watch": "pridepack watch", | |||||
"start": "tsx src/index.ts", | |||||
"dev": "tsx watch src/index.ts", | |||||
"test": "vitest" | |||||
}, | |||||
"private": true, | |||||
"description": "CMS example service for Yasumi", | |||||
"repository": { | |||||
"url": "", | |||||
"type": "git" | |||||
}, | |||||
"homepage": "", | |||||
"bugs": { | |||||
"url": "" | |||||
}, | |||||
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>", | |||||
"publishConfig": { | |||||
"access": "restricted" | |||||
} | |||||
} |
@@ -0,0 +1,3 @@ | |||||
{ | |||||
"target": "es2018" | |||||
} |
@@ -0,0 +1,67 @@ | |||||
import { application, resource, validation as v } from '@modal-sh/yasumi'; | |||||
import { randomUUID } from 'crypto'; | |||||
import {JsonLinesDataSource} from '@modal-sh/yasumi-data-source-file-jsonl'; | |||||
const User = resource( | |||||
v.object({ | |||||
email: v.string([v.email()]), | |||||
password: v.string(), | |||||
createdAt: v.datelike(), | |||||
updatedAt: v.datelike(), | |||||
}) | |||||
) | |||||
.name('User') | |||||
.route('users') | |||||
.id('id', { | |||||
generationStrategy: () => Promise.resolve(randomUUID()), | |||||
schema: v.string(), | |||||
serialize: (v: unknown) => v?.toString() ?? '', | |||||
deserialize: (v: string) => v, | |||||
}) | |||||
.canFetchItem() | |||||
.canFetchCollection() | |||||
.canCreate() | |||||
.canEmplace() | |||||
.canPatch() | |||||
.canDelete(); | |||||
const now = () => new Date(); | |||||
const Post = resource( | |||||
v.object({ | |||||
title: v.string(), | |||||
content: v.string(), | |||||
createdAt: v.optional(v.datelike(), now), | |||||
updatedAt: v.optional(v.datelike(), now), | |||||
}) | |||||
) | |||||
.name('Post') | |||||
.route('posts') | |||||
.id('id', { | |||||
generationStrategy: () => Promise.resolve(randomUUID()), | |||||
schema: v.string(), | |||||
serialize: (v: unknown) => v?.toString() ?? '', | |||||
deserialize: (v: string) => v, | |||||
}) | |||||
.canFetchItem() | |||||
.canFetchCollection() | |||||
.canCreate() | |||||
.canEmplace() | |||||
.canPatch() | |||||
.canDelete(); | |||||
const app = application({ | |||||
name: 'cms' | |||||
}) | |||||
.resource(User) | |||||
.resource(Post); | |||||
const backend = app.createBackend({ | |||||
dataSource: new JsonLinesDataSource(), | |||||
}); | |||||
const server = backend.createHttpServer({ | |||||
basePath: '/api', | |||||
}); | |||||
server.listen(6969); |
@@ -0,0 +1,8 @@ | |||||
import { describe, it, expect } from 'vitest'; | |||||
import add from '../src'; | |||||
describe('blah', () => { | |||||
it('works', () => { | |||||
expect(add(1, 1)).toEqual(2); | |||||
}); | |||||
}); |
@@ -0,0 +1,23 @@ | |||||
{ | |||||
"exclude": ["node_modules"], | |||||
"include": ["src", "types"], | |||||
"compilerOptions": { | |||||
"module": "ESNext", | |||||
"lib": ["ESNext"], | |||||
"importHelpers": true, | |||||
"declaration": true, | |||||
"sourceMap": true, | |||||
"rootDir": "./src", | |||||
"strict": true, | |||||
"noUnusedLocals": true, | |||||
"noUnusedParameters": true, | |||||
"noImplicitReturns": true, | |||||
"noFallthroughCasesInSwitch": true, | |||||
"moduleResolution": "bundler", | |||||
"jsx": "react", | |||||
"esModuleInterop": true, | |||||
"target": "es2018", | |||||
"useDefineForClassFields": false, | |||||
"declarationMap": true | |||||
} | |||||
} |
@@ -59,6 +59,34 @@ importers: | |||||
specifier: ^1.2.0 | specifier: ^1.2.0 | ||||
version: 1.5.0(@types/node@20.12.7) | version: 1.5.0(@types/node@20.12.7) | ||||
packages/examples/cms-web-api: | |||||
dependencies: | |||||
'@modal-sh/yasumi': | |||||
specifier: '*' | |||||
version: link:../../core | |||||
'@modal-sh/yasumi-data-source-file-jsonl': | |||||
specifier: '*' | |||||
version: link:../../data-sources/file-jsonl | |||||
tsx: | |||||
specifier: ^4.7.1 | |||||
version: 4.7.2 | |||||
devDependencies: | |||||
'@types/node': | |||||
specifier: ^20.11.0 | |||||
version: 20.12.7 | |||||
pridepack: | |||||
specifier: 2.6.0 | |||||
version: 2.6.0(tslib@2.6.2)(typescript@5.4.5) | |||||
tslib: | |||||
specifier: ^2.6.2 | |||||
version: 2.6.2 | |||||
typescript: | |||||
specifier: ^5.3.3 | |||||
version: 5.4.5 | |||||
vitest: | |||||
specifier: ^1.2.0 | |||||
version: 1.5.0(@types/node@20.12.7) | |||||
packages: | packages: | ||||
/@esbuild/aix-ppc64@0.19.12: | /@esbuild/aix-ppc64@0.19.12: | ||||