feat: partially implement user endpoints with fastify (part1) (#2301)

Partially implemented (wip)

- requires follow up for proper token validation
-  no test included (still experimental)
-  types refactoring required, still need alignment with other modules
This commit is contained in:
Juan Picado 2021-10-26 22:03:06 +02:00 committed by GitHub
parent 3dc5b5045f
commit 30875acc70
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 370 additions and 42 deletions

View File

@ -134,6 +134,18 @@ More details in the debug section
We use [`debug`](https://www.npmjs.com/package/debug) to add helpful debugging
output to the code. Each package has it owns namespace.
#### Useful Scripts
To run the application from the source code, ensure the project has been built with `pnpm build`, once this is done, there are few commands that helps to run server:
- `pnpm start`: Run the server and the UI with `concurrently`, the
server runs in the port `8000` and the UI on the port `4873`. This command
is useful if you want to contribute mostly on the UI.
- `pnpm debug`: Run the server in debug mode `--inspect`, the UI is included but does not have hot reload. For automatic break use `pnpm debug:break`.
- `pnpm debug:fastify`: To contribute on the [fastify migration](https://github.com/verdaccio/verdaccio/discussions/2155) this is a temporary command for such purpose.
- `pnpm website`: Build the website, for more commands to run the _website_, run `cd website` and then `pnpm serve`, website will run on port `3000`.
- `pnpm docker`: Build the docker image. Requires `docker` command available in your system.
#### Debugging compiled code
Currently you can only run pre-compiled packages in debug mode. To enable debug

View File

@ -127,6 +127,7 @@
"_debug:reload": "nodemon -d 3 packages/verdaccio/debug/bootstrap.js",
"start:ts": "ts-node packages/verdaccio/src/start.ts -- --listen 8000",
"debug": "node --trace-warnings --trace-uncaught --inspect packages/verdaccio/debug/bootstrap.js",
"debug:fastify": "node --trace-warnings --trace-uncaught --inspect packages/verdaccio/debug/bootstrap.js -- fastify-server",
"debug:break": "node --trace-warnings --trace-uncaught --inspect-brk packages/verdaccio/debug/bootstrap.js",
"changeset": "changeset",
"changeset:check": "changeset status --since-master",

View File

@ -52,6 +52,7 @@ const debug = buildDebug('verdaccio:auth');
export interface IBasicAuth<T> {
config: T & Config;
authenticate(user: string, password: string, cb: Callback): void;
invalidateToken?(token: string): Promise<void>;
changePassword(user: string, password: string, newPassword: string, cb: Callback): void;
allow_access(pkg: AuthPluginPackage, user: RemoteUser, callback: Callback): void;
add_user(user: string, password: string, cb: Callback): any;
@ -83,6 +84,7 @@ export interface IAuth extends IBasicAuth<Config>, IAuthMiddleware, TokenEncrypt
// eslint-disable-next-line @typescript-eslint/no-explicit-any
plugins: any[];
allow_unpublish(pkg: AuthPluginPackage, user: RemoteUser, callback: Callback): void;
invalidateToken(token: string): Promise<void>;
}
class Auth implements IAuth {
@ -177,6 +179,12 @@ class Auth implements IAuth {
}
}
public async invalidateToken(token: string) {
// eslint-disable-next-line no-console
console.log('invalidate token pending to implement', token);
return Promise.resolve();
}
public authenticate(username: string, password: string, cb: Callback): void {
const plugins = this.plugins.slice(0);
(function next(): void {
@ -413,7 +421,9 @@ class Auth implements IAuth {
}
// in case auth header does not exist we return anonymous function
req.remote_user = createAnonymousRemoteUser();
const remoteUser = createAnonymousRemoteUser();
req.remote_user = remoteUser;
res.locals.remote_user = remoteUser;
const { authorization } = req.headers;
if (_.isNil(authorization)) {

View File

@ -3,7 +3,7 @@ import { Cli } from 'clipanion';
import { InfoCommand } from './commands/info';
import { InitCommand } from './commands/init';
import { VersionCommand } from './commands/version';
import { NewServer } from './commands/newServer';
import { FastifyServer } from './commands/FastifyServer';
import { isVersionValid, MIN_NODE_VERSION } from './utils';
if (process.getuid && process.getuid() === 0) {
@ -28,7 +28,7 @@ const cli = new Cli({
cli.register(InfoCommand);
cli.register(InitCommand);
cli.register(VersionCommand);
cli.register(NewServer);
cli.register(FastifyServer);
cli.runExit(args, Cli.defaultContext);
process.on('uncaughtException', function (err) {

View File

@ -9,9 +9,12 @@ export const DEFAULT_PROCESS_NAME: string = 'verdaccio';
/**
* This command is intended to run the server with Fastify
* as a migration step.
* More info: https://github.com/verdaccio/verdaccio/discussions/2155
* To try out.
* pnpm debug:fastify
*/
export class NewServer extends Command {
public static paths = [['new']];
export class FastifyServer extends Command {
public static paths = [['fastify-server']];
private port = Option.String('-l,-p,--listen,--port', {
description: 'host:port number to listen on (default: localhost:4873)',
@ -41,9 +44,11 @@ export class NewServer extends Command {
this.initLogger(configParsed);
process.title = web?.title || DEFAULT_PROCESS_NAME;
// FIXME: need a way to get version of the package.
// const { version, name } = require('../../package.json');
const ser = await server({ logger, config: configParsed });
await ser.listen(4873);
// FIXME: harcoded, this would need to come from the configuration and the --listen flag.
await ser.listen(process.env.PORT || 4873);
} catch (err: any) {
console.error(err);
process.exit(1);

View File

@ -34,15 +34,19 @@
"access": "public"
},
"dependencies": {
"@verdaccio/core": "workspace:6.0.0-6-next.2",
"@verdaccio/config": "workspace:6.0.0-6-next.9",
"@verdaccio/auth": "workspace:6.0.0-6-next.13",
"@verdaccio/logger": "workspace:6.0.0-6-next.6",
"@verdaccio/store": "workspace:6.0.0-6-next.14",
"@verdaccio/tarball": "workspace:11.0.0-6-next.8",
"@verdaccio/utils": "workspace:6.0.0-6-next.7",
"abortcontroller-polyfill": "1.7.3",
"core-js": "3.17.2",
"debug": "4.3.2",
"fastify": "3.22.1",
"fastify-plugin": "3.0.0",
"lodash": "4.17.21",
"semver": "7.3.5"
},
"devDependencies": {

View File

@ -1,13 +1,13 @@
/* eslint-disable no-console */
/* eslint-disable no-invalid-this */
import { logger } from '@verdaccio/logger';
import { FastifyInstance } from 'fastify';
async function pingRoute(fastify) {
async function pingRoute(fastify: FastifyInstance) {
fastify.get('/-/ping', async () => {
logger.http('ping endpoint');
// @ts-ignore
console.log('-storage->', fastify.storage);
console.log('-config->', fastify.config);
console.log('-config->', fastify.configInstance);
return {};
});
}

View File

@ -1,8 +1,9 @@
/* eslint-disable no-console */
/* eslint-disable no-invalid-this */
import { logger } from '@verdaccio/logger';
import { FastifyInstance } from 'fastify';
async function searchRoute(fastify) {
async function searchRoute(fastify: FastifyInstance) {
fastify.get('/-/v1/search', async (request, reply) => {
// TODO: apply security layer here like in
// packages/api/src/v1/search.ts
@ -10,11 +11,11 @@ async function searchRoute(fastify) {
// TODO: review which query fields are mandatory
const abort = new AbortController();
request.on('aborted', () => {
request.socket.on('aborted', () => {
abort.abort();
});
const { url, query } = request;
// @ts-ignore
const { url, query } = request.query;
const storage = fastify.storage;
const data = await storage.searchManager?.search({

View File

@ -0,0 +1,130 @@
/* eslint-disable no-console */
/* eslint-disable no-invalid-this */
import _ from 'lodash';
import { getAuthenticatedMessage, validatePassword } from '@verdaccio/utils';
import { RemoteUser } from '@verdaccio/types';
import { logger } from '@verdaccio/logger';
import { createRemoteUser } from '@verdaccio/config';
import { getApiToken } from '@verdaccio/auth';
import buildDebug from 'debug';
import { FastifyInstance, FastifyRequest } from 'fastify';
const debug = buildDebug('verdaccio:api:user');
async function userRoute(fastify: FastifyInstance) {
fastify.get('/:org_couchdb_user', async (request, reply) => {
// @ts-expect-error
const message = getAuthenticatedMessage(request.userRemote.name);
logger.info('user authenticated message %o', message);
reply.code(fastify.httpStatuscode.OK);
return { ok: message };
});
fastify.delete('/token/:token', async (request: FastifyRequest, reply) => {
debug('loging out');
// FIXME: type params correctly
// @ts-ignore
const { token } = request.params;
const userRemote: RemoteUser = request.userRemote;
await fastify.auth.invalidateToken(token);
console.log('userRoute', userRemote);
reply.code(fastify.httpStatuscode.OK);
return { ok: fastify.apiMessage.LOGGED_OUT };
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
fastify.put<{
Body: { name: string; password: string };
}>('/:username', async (request, reply) => {
const { name, password } = request.body;
const remoteName = request.userRemote.name;
if (_.isNil(remoteName) === false && _.isNil(name) === false && remoteName === name) {
// debug('login: no remote user detected');
fastify.auth.authenticate(
name,
password,
async function callbackAuthenticate(err, user): Promise<void> {
if (err) {
logger.trace(
{ name, err },
'authenticating for user @{username} failed. Error: @{err.message}'
);
reply
.code(fastify.httpStatuscode.UNAUTHORIZED)
.send(
fastify.errorUtils.getCode(
fastify.httpStatuscode.UNAUTHORIZED,
fastify.apiError.BAD_USERNAME_PASSWORD
)
);
}
const restoredRemoteUser: RemoteUser = createRemoteUser(name, user.groups || []);
const token = await getApiToken(
fastify.auth,
fastify.configInstance,
restoredRemoteUser,
password
);
debug('login: new token');
if (!token) {
return reply.send(fastify.errorUtils.getUnauthorized());
} else {
reply.code(fastify.httpStatuscode.CREATED);
const message = getAuthenticatedMessage(remoteName);
debug('login: created user message %o', message);
reply.send({
ok: message,
token,
});
}
}
);
} else {
if (validatePassword(password) === false) {
debug('adduser: invalid password');
reply.code(fastify.httpStatuscode.BAD_REQUEST).send(
fastify.errorUtils.getCode(
fastify.httpStatuscode.BAD_REQUEST,
// eslint-disable-next-line new-cap
fastify.apiError.PASSWORD_SHORT()
)
);
return;
}
fastify.auth.add_user(name, password, async function (err, user): Promise<void> {
if (err) {
if (
err.status >= fastify.httpStatuscode.BAD_REQUEST &&
err.status < fastify.httpStatuscode.INTERNAL_ERROR
) {
debug('adduser: error on create user');
// With npm registering is the same as logging in,
// and npm accepts only an 409 error.
// So, changing status code here.
const addUserError =
fastify.errorUtils.getCode(err.status, err.message) ||
fastify.errorUtils.getConflict(err.message);
reply.send(addUserError);
return;
}
}
const token =
name && password
? await getApiToken(fastify.auth, fastify.configInstance, user, password)
: undefined;
debug('adduser: new token %o', token);
if (!token) {
return reply.send(fastify.errorUtils.getUnauthorized());
}
debug('adduser: user has been created');
reply.code(fastify.httpStatuscode.CREATED).send({
ok: `user '${name}' created`,
token,
});
});
}
});
}
export default userRoute;

View File

@ -1,5 +1,6 @@
import semver from 'semver';
// FUTURE: remove when v15 is the minimum requirement
if (semver.lte(process.version, 'v15.0.0')) {
global.AbortController = require('abortcontroller-polyfill/dist/cjs-ponyfill').AbortController;
}

View File

@ -0,0 +1,21 @@
import fp from 'fastify-plugin';
import { Config as IConfig } from '@verdaccio/types';
import { Auth, IAuth } from '@verdaccio/auth';
import { FastifyInstance } from 'fastify';
export default fp(
async function (fastify: FastifyInstance, opts: { config: IConfig; filters?: unknown }) {
const { config } = opts;
const auth: IAuth = new Auth(config);
fastify.decorate('auth', auth);
},
{
fastify: '>=3.x',
}
);
declare module 'fastify' {
interface FastifyInstance {
auth: IAuth;
}
}

View File

@ -0,0 +1,22 @@
import fp from 'fastify-plugin';
import { Config as IConfig, ConfigRuntime } from '@verdaccio/types';
import { Config as AppConfig } from '@verdaccio/config';
import { FastifyInstance } from 'fastify';
export default fp(
async function (fastify: FastifyInstance, opts: { config: ConfigRuntime }) {
const { config } = opts;
const configInstance: IConfig = new AppConfig(Object.assign({}, config));
fastify.decorate('configInstance', configInstance);
},
{
fastify: '>=3.x',
}
);
declare module 'fastify' {
interface FastifyInstance {
configInstance: IConfig;
}
}

View File

@ -0,0 +1,25 @@
import fp from 'fastify-plugin';
import { errorUtils, validatioUtils, API_ERROR, API_MESSAGE, HTTP_STATUS } from '@verdaccio/core';
export default fp(
async function (fastify) {
fastify.decorate('errorUtils', errorUtils);
fastify.decorate('apiError', API_ERROR);
fastify.decorate('apiMessage', API_MESSAGE);
fastify.decorate('validatioUtils', validatioUtils);
fastify.decorate('statusCode', HTTP_STATUS);
},
{
fastify: '>=3.x',
}
);
declare module 'fastify' {
interface FastifyInstance {
apiError: typeof API_ERROR;
apiMessage: typeof API_MESSAGE;
httpStatuscode: typeof HTTP_STATUS;
errorUtils: typeof errorUtils;
}
}

View File

@ -1,10 +1,23 @@
import fp from 'fastify-plugin';
import { Config as IConfig } from '@verdaccio/types';
import { Storage } from '@verdaccio/store';
import { FastifyInstance } from 'fastify';
export async function storageService(fastify, opts, done) {
const { config, filters } = opts;
// @ts-ignore
const storage: Storage = new Storage(config);
await storage.init(config, filters ?? {});
fastify.decorate('storage', storage);
done();
export default fp(
async function (fastify: FastifyInstance, opts: { config: IConfig; filters?: unknown }) {
const { config } = opts;
const storage: Storage = new Storage(config);
// @ts-ignore
await storage.init(config, {});
fastify.decorate('storage', storage);
},
{
fastify: '>=3.x',
}
);
declare module 'fastify' {
interface FastifyInstance {
storage: Storage;
}
}

View File

@ -1,39 +1,51 @@
import { Config as IConfig } from '@verdaccio/types';
import { Config as AppConfig } from '@verdaccio/config';
import { Config as IConfig, RemoteUser } from '@verdaccio/types';
import { Config as AppConfig, createAnonymousRemoteUser } from '@verdaccio/config';
import fastify from 'fastify';
import buildDebug from 'debug';
import fp from 'fastify-plugin';
import ping from './endpoints/ping';
import search from './endpoints/search';
import { storageService } from './plugins/storage';
import storagePlugin from './plugins/storage';
import authPlugin from './plugins/auth';
import coreUtils from './plugins/coreUtils';
import configPlugin from './plugins/config';
import ping from './endpoints/ping';
import user from './endpoints/user';
const debug = buildDebug('verdaccio:fastify');
async function startServer({ logger, config }) {
// eslint-disable-next-line prettier/prettier
const configInstance: IConfig = new AppConfig(Object.assign({}, config));
debug('start server');
const app = fastify({ logger });
app.decorate('config', configInstance);
app.register(fp(storageService), { config: configInstance });
const fastifyInstance = fastify({ logger });
fastifyInstance.decorateRequest<RemoteUser>('userRemote', createAnonymousRemoteUser());
fastifyInstance.register(configPlugin, { config });
fastifyInstance.register(coreUtils);
fastifyInstance.register(authPlugin, { config: configInstance });
fastifyInstance.register(storagePlugin, { config: configInstance });
// api
app.register((instance, opts, done) => {
instance.decorate('utility', new Map());
fastifyInstance.register((instance, opts, done) => {
instance.register(ping);
instance.register(user, { prefix: '/-/user' });
instance.register(search);
done();
});
// web
app.register((instance, opts, done) => {
fastifyInstance.register((instance, opts, done) => {
instance.register(ping, { prefix: '/web' });
done();
});
return app;
return fastifyInstance;
}
declare module 'fastify' {
interface FastifyRequest {
userRemote: RemoteUser;
}
}
export default startServer;

View File

@ -8,16 +8,22 @@
"exclude": ["src/**/*.test.ts"],
"references": [
{
"path": "../store"
"path": "../../store"
},
{
"path": "../config"
"path": "../../config"
},
{
"path": "../auth"
"path": "../../auth"
},
{
"path": "../logger"
"path": "../../logger"
},
{
"path": "../../utils"
},
{
"path": "../../core/core"
}
]
}

View File

@ -38,7 +38,8 @@
"build": "exit 0"
},
"devDependencies": {
"@types/node": "14.6.0"
"@types/node": "14.6.0",
"tsd": "0.18.0"
},
"funding": {
"type": "opencollective",

View File

@ -7,7 +7,7 @@ import RateLimit from 'express-rate-limit';
import { HttpError } from 'http-errors';
import { loadPlugin } from '@verdaccio/loaders';
import { Auth } from '@verdaccio/auth';
import { Auth, IBasicAuth } from '@verdaccio/auth';
import apiEndpoint from '@verdaccio/api';
import { API_ERROR, HTTP_STATUS, errorUtils } from '@verdaccio/core';
import { Config as AppConfig } from '@verdaccio/config';
@ -15,7 +15,6 @@ import { Config as AppConfig } from '@verdaccio/config';
import webMiddleware from '@verdaccio/web';
import { ConfigRuntime } from '@verdaccio/types';
import { IAuth, IBasicAuth } from '@verdaccio/auth';
import { Storage } from '@verdaccio/store';
import { logger } from '@verdaccio/logger';
import { log, final, errorReportingMiddleware } from '@verdaccio/middleware';
@ -33,7 +32,7 @@ export interface IPluginMiddleware<T> extends IPlugin<T> {
const debug = buildDebug('verdaccio:server');
const defineAPI = function (config: IConfig, storage: Storage): any {
const auth: IAuth = new Auth(config);
const auth: Auth = new Auth(config);
const app: Application = express();
const limiter = new RateLimit(config.serverSettings.rateLimit);
// run in production mode by default, just in case

View File

@ -1,5 +1,5 @@
export function isNil(value: any): boolean {
return value == null;
return value === null || typeof value === 'undefined';
}
export function isFunction(value): boolean {

View File

@ -348,26 +348,34 @@ importers:
'@types/node': 16.9.1
'@verdaccio/auth': workspace:6.0.0-6-next.13
'@verdaccio/config': workspace:6.0.0-6-next.9
'@verdaccio/core': workspace:6.0.0-6-next.2
'@verdaccio/logger': workspace:6.0.0-6-next.6
'@verdaccio/store': workspace:6.0.0-6-next.14
'@verdaccio/tarball': workspace:11.0.0-6-next.8
'@verdaccio/types': workspace:11.0.0-6-next.9
'@verdaccio/utils': workspace:6.0.0-6-next.7
abortcontroller-polyfill: 1.7.3
core-js: 3.17.2
debug: 4.3.2
fastify: 3.22.1
fastify-plugin: 3.0.0
lodash: 4.17.21
semver: 7.3.5
ts-node: 10.2.1
dependencies:
'@verdaccio/auth': link:../../auth
'@verdaccio/config': link:../../config
'@verdaccio/core': link:../core
'@verdaccio/logger': link:../../logger
'@verdaccio/store': link:../../store
'@verdaccio/tarball': link:../tarball
'@verdaccio/utils': link:../../utils
abortcontroller-polyfill: 1.7.3
core-js: 3.17.2
debug: 4.3.2
fastify: 3.22.1
fastify-plugin: 3.0.0
lodash: 4.17.21
semver: 7.3.5
devDependencies:
'@types/node': 16.9.1
@ -402,8 +410,10 @@ importers:
packages/core/types:
specifiers:
'@types/node': 14.6.0
tsd: 0.18.0
devDependencies:
'@types/node': 14.6.0
tsd: 0.18.0
packages/core/url:
specifiers:
@ -6085,6 +6095,11 @@ packages:
resolution: {integrity: sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==}
dev: true
/@tsd/typescript/4.4.4:
resolution: {integrity: sha512-XNaotnbhU6sKSXYg9rVz4L9i9g+j+x1IIgMPztK8KumtMEsrLXcqPBKp/qzmUKwAZEqgHs4+TTz90dUu5/aIqQ==}
hasBin: true
dev: true
/@types/activedirectory2/1.2.3:
resolution: {integrity: sha512-yZERTOJFrOAax2HbDyBBhAKyUEa1PC/GXMe9UGBGyeOF0ZRRBKnIMNXVAYfveJMyrhUBhdRoObwe3CBPoekyjQ==}
dependencies:
@ -6179,6 +6194,13 @@ packages:
'@types/estree': 0.0.50
'@types/json-schema': 7.0.8
/@types/eslint/7.28.2:
resolution: {integrity: sha512-KubbADPkfoU75KgKeKLsFHXnU4ipH7wYg0TRT33NK3N3yiu7jlFAAoygIWBV+KbuHx/G+AvuGX6DllnK35gfJA==}
dependencies:
'@types/estree': 0.0.50
'@types/json-schema': 7.0.8
dev: true
/@types/estree/0.0.47:
resolution: {integrity: sha512-c5ciR06jK8u9BstrmJyO97m+klJrrhCf9u3rLu3DEAJBirxRqSCvDQoYKmxuYwQI5SZChAWu+tq9oVlGRuzPAg==}
dev: false
@ -10886,6 +10908,20 @@ packages:
eslint: 7.32.0
dev: false
/eslint-formatter-pretty/4.1.0:
resolution: {integrity: sha512-IsUTtGxF1hrH6lMWiSl1WbGaiP01eT6kzywdY1U+zLc0MP+nwEnUiS9UI8IaOTUhTeQJLlCEWIbXINBH4YJbBQ==}
engines: {node: '>=10'}
dependencies:
'@types/eslint': 7.28.2
ansi-escapes: 4.3.2
chalk: 4.1.2
eslint-rule-docs: 1.1.231
log-symbols: 4.1.0
plur: 4.0.0
string-width: 4.2.2
supports-hyperlinks: 2.2.0
dev: true
/eslint-import-resolver-node/0.3.4:
resolution: {integrity: sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==}
dependencies:
@ -11048,6 +11084,10 @@ packages:
engines: {node: '>=4.0.0'}
dev: false
/eslint-rule-docs/1.1.231:
resolution: {integrity: sha512-egHz9A1WG7b8CS0x1P6P/Rj5FqZOjray/VjpJa14tMZalfRKvpE2ONJ3plCM7+PcinmU4tcmbPLv0VtwzSdLVA==}
dev: true
/eslint-scope/5.1.1:
resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==}
engines: {node: '>=8.0.0'}
@ -13129,6 +13169,11 @@ packages:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
/irregular-plurals/3.3.0:
resolution: {integrity: sha512-MVBLKUTangM3EfRPFROhmWQQKRDsrgI83J8GS3jXy+OwYqiR2/aoWndYQ5416jLE3uaGgLH7ncme3X9y09gZ3g==}
engines: {node: '>=8'}
dev: true
/is-absolute-url/3.0.3:
resolution: {integrity: sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==}
engines: {node: '>=8'}
@ -16512,6 +16557,13 @@ packages:
semver-compare: 1.0.0
dev: true
/plur/4.0.0:
resolution: {integrity: sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg==}
engines: {node: '>=10'}
dependencies:
irregular-plurals: 3.3.0
dev: true
/pn/1.1.0:
resolution: {integrity: sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==}
dev: true
@ -20478,6 +20530,19 @@ packages:
strip-bom: 3.0.0
dev: false
/tsd/0.18.0:
resolution: {integrity: sha512-UIkxm2CLmSjXlQs4zqxgVV9UmzK8VgJ63eBpgkH/ZsMkiUdzxxHvdCCg8F314HDxzfQl2muJEy/TEcXHIFIPXg==}
engines: {node: '>=12'}
hasBin: true
dependencies:
'@tsd/typescript': 4.4.4
eslint-formatter-pretty: 4.1.0
globby: 11.0.4
meow: 9.0.0
path-exists: 4.0.0
read-pkg-up: 7.0.1
dev: true
/tslib/1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}