@devmoods/express-extras 0.30.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cache.js +2 -2
- package/dist/errors.js +1 -1
- package/dist/logger.d.ts +0 -2
- package/dist/logger.js +6 -9
- package/dist/middleware.js +0 -3
- package/dist/migrate.js +8 -9
- package/dist/sentry.d.ts +2 -62
- package/dist/sentry.js +3 -24
- package/dist/validate.js +9 -5
- package/package.json +26 -33
package/dist/cache.js
CHANGED
|
@@ -18,8 +18,8 @@ export function createRedisCacheAdapter(redis, options = {}) {
|
|
|
18
18
|
await redis.del(createKey(key));
|
|
19
19
|
}
|
|
20
20
|
const keys = await redis.keys(`${keyPrefix}*`);
|
|
21
|
-
for (const
|
|
22
|
-
await redis.del(
|
|
21
|
+
for (const existingKey of keys) {
|
|
22
|
+
await redis.del(existingKey);
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
25
|
};
|
package/dist/errors.js
CHANGED
|
@@ -64,7 +64,7 @@ function _errorHandler({ onError }) {
|
|
|
64
64
|
*/
|
|
65
65
|
export function errorHandler({ onError }) {
|
|
66
66
|
return errors([
|
|
67
|
-
Sentry.
|
|
67
|
+
Sentry.expressErrorHandler({
|
|
68
68
|
shouldHandleError(error) {
|
|
69
69
|
if (error instanceof ApiError && error.status < 500) {
|
|
70
70
|
return false;
|
package/dist/logger.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/// <reference types="node" resolution-mode="require"/>
|
|
2
2
|
import { type Duplex } from 'node:stream';
|
|
3
|
-
import { type CreateSentry } from './sentry.js';
|
|
4
3
|
export type LoggerContext = Record<string, any>;
|
|
5
4
|
export interface LoggerOptions {
|
|
6
5
|
/**
|
|
@@ -14,7 +13,6 @@ export interface LoggerOptions {
|
|
|
14
13
|
level?: string;
|
|
15
14
|
output?: Duplex;
|
|
16
15
|
}
|
|
17
|
-
export declare function attachSentryClientToLoggers(sentryClient: CreateSentry['client']): void;
|
|
18
16
|
export declare function setGlobalLoggerContext(getContext: () => LoggerContext): void;
|
|
19
17
|
export type Logger = ReturnType<typeof createLogger>;
|
|
20
18
|
export declare function createLogger({ name, getContext, level, output, }: LoggerOptions): {
|
package/dist/logger.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as os from 'node:os';
|
|
2
2
|
import {} from 'node:stream';
|
|
3
|
+
import * as Sentry from '@sentry/node';
|
|
3
4
|
import { useRequestId } from './context.js';
|
|
4
|
-
import {} from './sentry.js';
|
|
5
5
|
import { readEnv } from './utils.js';
|
|
6
6
|
const LEVELS = {
|
|
7
7
|
TRACE: 10,
|
|
@@ -13,10 +13,6 @@ const LEVELS = {
|
|
|
13
13
|
};
|
|
14
14
|
const LEVELS_TO_NAME = Object.fromEntries(Object.entries(LEVELS).map(([name, level]) => [level, name.toLowerCase()]));
|
|
15
15
|
const _loggers = new Map();
|
|
16
|
-
let _sentryClient;
|
|
17
|
-
export function attachSentryClientToLoggers(sentryClient) {
|
|
18
|
-
_sentryClient = sentryClient;
|
|
19
|
-
}
|
|
20
16
|
let _globalGetContext;
|
|
21
17
|
export function setGlobalLoggerContext(getContext) {
|
|
22
18
|
_globalGetContext = getContext;
|
|
@@ -45,6 +41,7 @@ export function createLogger({ name, getContext, level = readEnv('LOG_LEVEL', 'I
|
|
|
45
41
|
...context,
|
|
46
42
|
};
|
|
47
43
|
}
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
48
45
|
function log(level, message, context) {
|
|
49
46
|
const payload = JSON.stringify({
|
|
50
47
|
level: LEVELS_TO_NAME[level],
|
|
@@ -73,8 +70,8 @@ export function createLogger({ name, getContext, level = readEnv('LOG_LEVEL', 'I
|
|
|
73
70
|
},
|
|
74
71
|
error(error, message, context = {}) {
|
|
75
72
|
const finalContext = getFinalContext(context);
|
|
76
|
-
if (
|
|
77
|
-
|
|
73
|
+
if (Sentry.isInitialized()) {
|
|
74
|
+
Sentry.captureException(error, {
|
|
78
75
|
tags: finalContext,
|
|
79
76
|
});
|
|
80
77
|
}
|
|
@@ -85,8 +82,8 @@ export function createLogger({ name, getContext, level = readEnv('LOG_LEVEL', 'I
|
|
|
85
82
|
},
|
|
86
83
|
fatal(error, message, context = {}) {
|
|
87
84
|
const finalContext = getFinalContext(context);
|
|
88
|
-
if (
|
|
89
|
-
|
|
85
|
+
if (Sentry.isInitialized()) {
|
|
86
|
+
Sentry.captureException(error, {
|
|
90
87
|
tags: finalContext,
|
|
91
88
|
});
|
|
92
89
|
}
|
package/dist/middleware.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as Sentry from '@sentry/node';
|
|
2
1
|
import bodyParser from 'body-parser';
|
|
3
2
|
import { compose } from 'compose-middleware';
|
|
4
3
|
import cookieParser from 'cookie-parser';
|
|
@@ -19,8 +18,6 @@ export function before({ cookies = false, helmet: useHelmet = true, json: useJso
|
|
|
19
18
|
return compose([
|
|
20
19
|
withRequestContext(),
|
|
21
20
|
logRequestDuration(),
|
|
22
|
-
Sentry.Handlers.requestHandler(),
|
|
23
|
-
Sentry.Handlers.tracingHandler(),
|
|
24
21
|
useHelmet &&
|
|
25
22
|
helmet({
|
|
26
23
|
contentSecurityPolicy: {
|
package/dist/migrate.js
CHANGED
|
@@ -4,7 +4,7 @@ import chalk from 'chalk';
|
|
|
4
4
|
import meow from 'meow';
|
|
5
5
|
import { createPostgres } from './postgres.js';
|
|
6
6
|
export async function cli() {
|
|
7
|
-
const
|
|
7
|
+
const app = meow(`
|
|
8
8
|
Usage
|
|
9
9
|
$ migrate init
|
|
10
10
|
$ migrate up
|
|
@@ -16,9 +16,9 @@ Usage
|
|
|
16
16
|
});
|
|
17
17
|
let exitCode = 0;
|
|
18
18
|
try {
|
|
19
|
-
exitCode = await runCli(
|
|
19
|
+
exitCode = await runCli(app.input, app.flags,
|
|
20
20
|
/* c8 ignore next 2 */
|
|
21
|
-
() =>
|
|
21
|
+
() => app.showHelp());
|
|
22
22
|
}
|
|
23
23
|
catch (error) {
|
|
24
24
|
if (error instanceof CliError) {
|
|
@@ -36,13 +36,12 @@ class CliError extends Error {
|
|
|
36
36
|
this.code = code;
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
+
function isValidAction(action) {
|
|
40
|
+
return action != null && ['init', 'up', 'down', 'create'].includes(action);
|
|
41
|
+
}
|
|
39
42
|
export async function runCli(input, flags, showHelp) {
|
|
40
43
|
const verbose = !flags.silent;
|
|
41
|
-
const
|
|
42
|
-
const action = args.shift();
|
|
43
|
-
function isValidAction(action) {
|
|
44
|
-
return action != null && ['init', 'up', 'down', 'create'].includes(action);
|
|
45
|
-
}
|
|
44
|
+
const [inputAction, ...inputArgs] = input.slice();
|
|
46
45
|
const migrator = new Migrator({ verbose });
|
|
47
46
|
async function run(action, ...args) {
|
|
48
47
|
try {
|
|
@@ -78,7 +77,7 @@ export async function runCli(input, flags, showHelp) {
|
|
|
78
77
|
throw new CliError(error.message, 1);
|
|
79
78
|
}
|
|
80
79
|
}
|
|
81
|
-
return run(
|
|
80
|
+
return run(inputAction, ...inputArgs);
|
|
82
81
|
}
|
|
83
82
|
export class Migrator {
|
|
84
83
|
constructor({ postgres = createPostgres(), pathToMigrations = path.resolve(process.cwd(), 'db', 'migrations'), migrationTableName = 'migrations', verbose = true, } = {}) {
|
package/dist/sentry.d.ts
CHANGED
|
@@ -1,65 +1,5 @@
|
|
|
1
|
-
import * as Sentry from '@sentry/node';
|
|
2
|
-
import { type Application } from 'express';
|
|
1
|
+
import type * as Sentry from '@sentry/node';
|
|
3
2
|
export type { Sentry };
|
|
4
3
|
export interface SentryOptions extends Sentry.NodeOptions {
|
|
5
4
|
}
|
|
6
|
-
export
|
|
7
|
-
/**
|
|
8
|
-
* Create a pre-configured Sentry client
|
|
9
|
-
*/
|
|
10
|
-
export declare function createSentry(options?: SentryOptions): {
|
|
11
|
-
client: typeof Sentry;
|
|
12
|
-
init(app: Application | null, getOptions?: (defaultOptions: Sentry.NodeOptions) => Sentry.NodeOptions): void;
|
|
13
|
-
};
|
|
14
|
-
/**
|
|
15
|
-
* Construct sensible Sentry default options for an Express-based app.
|
|
16
|
-
*/
|
|
17
|
-
export declare function createSentryOptions(app: Application | null, options?: SentryOptions): {
|
|
18
|
-
defaultIntegrations?: false | import("@sentry/types").Integration[] | undefined;
|
|
19
|
-
integrations: import("@sentry/types").Integration[] | ((integrations: import("@sentry/types").Integration[]) => import("@sentry/types").Integration[]) | undefined;
|
|
20
|
-
transport?: ((transportOptions: import("@sentry/node/types/transports/http.js").NodeTransportOptions) => import("@sentry/types").Transport) | undefined;
|
|
21
|
-
stackParser?: import("@sentry/types").StackParser | import("@sentry/types").StackLineParser[] | undefined;
|
|
22
|
-
release?: string | undefined;
|
|
23
|
-
debug?: boolean | undefined;
|
|
24
|
-
enabled?: boolean | undefined;
|
|
25
|
-
attachStacktrace?: boolean | undefined;
|
|
26
|
-
autoSessionTracking?: boolean | undefined;
|
|
27
|
-
sendClientReports?: boolean | undefined;
|
|
28
|
-
dsn: string | undefined;
|
|
29
|
-
environment?: string | undefined;
|
|
30
|
-
dist?: string | undefined;
|
|
31
|
-
instrumenter?: import("@sentry/types").Instrumenter | undefined;
|
|
32
|
-
transportOptions?: Partial<import("@sentry/node/types/transports/http.js").NodeTransportOptions> | undefined;
|
|
33
|
-
tracesSampleRate: number;
|
|
34
|
-
enableTracing?: boolean | undefined;
|
|
35
|
-
initialScope?: import("@sentry/types").CaptureContext | undefined;
|
|
36
|
-
maxBreadcrumbs?: number | undefined;
|
|
37
|
-
sampleRate?: number | undefined;
|
|
38
|
-
maxValueLength?: number | undefined;
|
|
39
|
-
normalizeDepth?: number | undefined;
|
|
40
|
-
normalizeMaxBreadth?: number | undefined;
|
|
41
|
-
shutdownTimeout?: number | undefined;
|
|
42
|
-
ignoreErrors?: (string | RegExp)[] | undefined;
|
|
43
|
-
ignoreTransactions?: (string | RegExp)[] | undefined;
|
|
44
|
-
tunnel?: string | undefined;
|
|
45
|
-
sendDefaultPii?: boolean | undefined;
|
|
46
|
-
_metadata?: import("@sentry/types").SdkMetadata | undefined;
|
|
47
|
-
_experiments?: {
|
|
48
|
-
[key: string]: any;
|
|
49
|
-
} | undefined;
|
|
50
|
-
allowUrls?: (string | RegExp)[] | undefined;
|
|
51
|
-
denyUrls?: (string | RegExp)[] | undefined;
|
|
52
|
-
tracePropagationTargets?: import("@sentry/types").TracePropagationTargets | undefined;
|
|
53
|
-
tracesSampler?: ((samplingContext: import("@sentry/types").SamplingContext) => number | boolean) | undefined;
|
|
54
|
-
beforeSend?: ((event: import("@sentry/types").ErrorEvent, hint: Sentry.EventHint) => Sentry.Event | PromiseLike<Sentry.Event | null> | null) | undefined;
|
|
55
|
-
beforeSendTransaction?: ((event: import("@sentry/types").TransactionEvent, hint: Sentry.EventHint) => Sentry.Event | PromiseLike<Sentry.Event | null> | null) | undefined;
|
|
56
|
-
beforeBreadcrumb?: ((breadcrumb: Sentry.Breadcrumb, hint?: Sentry.BreadcrumbHint | undefined) => Sentry.Breadcrumb | null) | undefined;
|
|
57
|
-
profilesSampleRate?: number | undefined;
|
|
58
|
-
profilesSampler?: ((samplingContext: import("@sentry/types").SamplingContext) => number | boolean) | undefined;
|
|
59
|
-
serverName?: string | undefined;
|
|
60
|
-
includeLocalVariables?: boolean | undefined;
|
|
61
|
-
clientClass?: typeof Sentry.NodeClient | undefined;
|
|
62
|
-
spotlight?: string | boolean | undefined;
|
|
63
|
-
shouldCreateSpanForRequest?(this: void, url: string): boolean;
|
|
64
|
-
onFatalError?(this: void, error: Error): void;
|
|
65
|
-
};
|
|
5
|
+
export declare function sentryOptions(options?: SentryOptions): SentryOptions;
|
package/dist/sentry.js
CHANGED
|
@@ -1,31 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {} from 'express';
|
|
3
|
-
import { attachSentryClientToLoggers } from './logger.js';
|
|
1
|
+
import { nodeProfilingIntegration } from '@sentry/profiling-node';
|
|
4
2
|
import { readEnv } from './utils.js';
|
|
5
|
-
|
|
6
|
-
* Create a pre-configured Sentry client
|
|
7
|
-
*/
|
|
8
|
-
export function createSentry(options = {}) {
|
|
9
|
-
const client = Sentry;
|
|
10
|
-
return {
|
|
11
|
-
client,
|
|
12
|
-
init(app, getOptions = (defaultOptions) => defaultOptions) {
|
|
13
|
-
attachSentryClientToLoggers(client);
|
|
14
|
-
Sentry.init(getOptions(createSentryOptions(app, options)));
|
|
15
|
-
},
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Construct sensible Sentry default options for an Express-based app.
|
|
20
|
-
*/
|
|
21
|
-
export function createSentryOptions(app, options = {}) {
|
|
3
|
+
export function sentryOptions(options = {}) {
|
|
22
4
|
return {
|
|
23
5
|
dsn: readEnv('SENTRY_DSN'),
|
|
24
|
-
integrations: [
|
|
25
|
-
new Sentry.Integrations.Http({ tracing: true }),
|
|
26
|
-
app && new Sentry.Integrations.Express({ app }),
|
|
27
|
-
].filter(Boolean),
|
|
28
6
|
tracesSampleRate: 1.0,
|
|
7
|
+
integrations: [nodeProfilingIntegration()],
|
|
29
8
|
...options,
|
|
30
9
|
};
|
|
31
10
|
}
|
package/dist/validate.js
CHANGED
|
@@ -32,11 +32,18 @@ export function validateParams(schema) {
|
|
|
32
32
|
next();
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
function createAjv(options) {
|
|
36
36
|
const ajv = new Ajv({
|
|
37
37
|
allErrors: true,
|
|
38
|
+
validateFormats: false,
|
|
38
39
|
...options,
|
|
39
40
|
});
|
|
41
|
+
ajv.addKeyword('$enumNames');
|
|
42
|
+
ajv.addKeyword('$bitiforms');
|
|
43
|
+
return ajv;
|
|
44
|
+
}
|
|
45
|
+
export function compileValidateSchema(schema, options = {}) {
|
|
46
|
+
const ajv = createAjv(options);
|
|
40
47
|
const validate = ajv.compile(schema);
|
|
41
48
|
return (data) => {
|
|
42
49
|
const valid = validate(data);
|
|
@@ -55,10 +62,7 @@ export function compileValidateSchema(schema, options = {}) {
|
|
|
55
62
|
};
|
|
56
63
|
}
|
|
57
64
|
export function validateSchema(data, schema, options = {}) {
|
|
58
|
-
const ajv =
|
|
59
|
-
allErrors: true,
|
|
60
|
-
...options,
|
|
61
|
-
});
|
|
65
|
+
const ajv = createAjv(options);
|
|
62
66
|
const valid = ajv.validate(schema, data);
|
|
63
67
|
if (valid) {
|
|
64
68
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devmoods/express-extras",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "Kubernetes-ready utilities for rapid Node.js application development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -23,54 +23,55 @@
|
|
|
23
23
|
],
|
|
24
24
|
"license": "ISC",
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@devmoods/eslint-config": "^1.
|
|
26
|
+
"@devmoods/eslint-config": "^1.13.1",
|
|
27
27
|
"@types/koa-compose": "^3.2.8",
|
|
28
|
-
"@types/node": "20.
|
|
28
|
+
"@types/node": "20.14.2",
|
|
29
29
|
"@types/supertest": "^6.0.2",
|
|
30
|
-
"@typescript-eslint/eslint-plugin": "^7.
|
|
31
|
-
"@typescript-eslint/parser": "^7.
|
|
32
|
-
"@vitest/coverage-v8": "^1.
|
|
30
|
+
"@typescript-eslint/eslint-plugin": "^7.12.0",
|
|
31
|
+
"@typescript-eslint/parser": "^7.12.0",
|
|
32
|
+
"@vitest/coverage-v8": "^1.6.0",
|
|
33
33
|
"chalk": "^5.3.0",
|
|
34
34
|
"eslint": "^8.57.0",
|
|
35
35
|
"eslint-plugin-import": "^2.29.1",
|
|
36
36
|
"eslint-plugin-jsx-a11y": "^6.8.0",
|
|
37
37
|
"eslint-plugin-prettier": "^5.1.3",
|
|
38
|
-
"eslint-plugin-react": "^7.
|
|
39
|
-
"eslint-plugin-react-hooks": "^4.6.
|
|
38
|
+
"eslint-plugin-react": "^7.34.2",
|
|
39
|
+
"eslint-plugin-react-hooks": "^4.6.2",
|
|
40
40
|
"faktory-worker": "^4.5.1",
|
|
41
|
-
"prettier": "3.
|
|
41
|
+
"prettier": "3.3.1",
|
|
42
42
|
"supertest": "7.0.0",
|
|
43
43
|
"ts-node": "10.9.2",
|
|
44
44
|
"typescript": "5.4.5",
|
|
45
|
-
"vitest": "^1.
|
|
45
|
+
"vitest": "^1.6.0"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@devmoods/fetch": "^4.0.1",
|
|
49
|
-
"@sentry/node": "^
|
|
49
|
+
"@sentry/node": "^8.8.0",
|
|
50
|
+
"@sentry/profiling-node": "^8.8.0",
|
|
50
51
|
"@types/bcrypt": "^5.0.2",
|
|
51
52
|
"@types/body-parser": "^1.19.5",
|
|
52
53
|
"@types/cookie-parser": "^1.4.7",
|
|
53
54
|
"@types/express": "^4.17.21",
|
|
54
55
|
"@types/jsonwebtoken": "^9.0.6",
|
|
55
|
-
"@types/nodemailer": "^6.4.
|
|
56
|
-
"@types/pg": "^8.11.
|
|
57
|
-
"@types/superagent": "^8.1.
|
|
56
|
+
"@types/nodemailer": "^6.4.15",
|
|
57
|
+
"@types/pg": "^8.11.6",
|
|
58
|
+
"@types/superagent": "^8.1.7",
|
|
58
59
|
"@types/uuid": "^9.0.8",
|
|
59
|
-
"ajv": "^8.
|
|
60
|
+
"ajv": "^8.16.0",
|
|
60
61
|
"bcrypt": "^5.1.1",
|
|
61
62
|
"body-parser": "^1.20.2",
|
|
62
|
-
"commander": "^12.
|
|
63
|
+
"commander": "^12.1.0",
|
|
63
64
|
"compose-middleware": "^5.0.1",
|
|
64
65
|
"cookie-parser": "^1.4.6",
|
|
65
66
|
"dotenv": "^16.4.5",
|
|
66
|
-
"express": "^4.
|
|
67
|
+
"express": "^4.19.2",
|
|
67
68
|
"express-route-parser": "^1.0.5",
|
|
68
69
|
"helmet": "^7.1.0",
|
|
69
70
|
"jsonwebtoken": "^9.0.2",
|
|
70
71
|
"meow": "^13.2.0",
|
|
71
|
-
"nodemailer": "^6.9.
|
|
72
|
-
"pg": "^8.
|
|
73
|
-
"redis": "^4.6.
|
|
72
|
+
"nodemailer": "^6.9.13",
|
|
73
|
+
"pg": "^8.12.0",
|
|
74
|
+
"redis": "^4.6.14",
|
|
74
75
|
"sql-template-strings": "^2.2.2",
|
|
75
76
|
"uuid": "^9.0.1",
|
|
76
77
|
"vitest-fail-on-console": "^0.7.0"
|
|
@@ -79,27 +80,19 @@
|
|
|
79
80
|
"singleQuote": true
|
|
80
81
|
},
|
|
81
82
|
"eslintConfig": {
|
|
82
|
-
"extends": "@devmoods/eslint-config"
|
|
83
|
-
"parserOptions": {
|
|
84
|
-
"project": "./tsconfig.json"
|
|
85
|
-
},
|
|
86
|
-
"rules": {
|
|
87
|
-
"@typescript-eslint/consistent-type-imports": "warn",
|
|
88
|
-
"@typescript-eslint/no-shadow": "off",
|
|
89
|
-
"react-hooks/rules-of-hooks": "off"
|
|
90
|
-
}
|
|
83
|
+
"extends": "@devmoods/eslint-config"
|
|
91
84
|
},
|
|
92
85
|
"peerDependencies": {
|
|
93
86
|
"faktory-worker": "^4.5.1"
|
|
94
87
|
},
|
|
95
88
|
"engines": {
|
|
96
|
-
"node": ">=20.3.0 <
|
|
89
|
+
"node": ">=20.3.0 <23"
|
|
97
90
|
},
|
|
98
91
|
"scripts": {
|
|
99
|
-
"start": "ts-node
|
|
100
|
-
"start:worker": "ts-node
|
|
92
|
+
"start": "node --no-warnings --enable-source-maps --loader ts-node/esm example/index.ts",
|
|
93
|
+
"start:worker": "node --no-warnings --enable-source-maps --loader ts-node/esm example/worker.ts",
|
|
101
94
|
"clean": "rm -rf dist",
|
|
102
|
-
"lint": "eslint src",
|
|
95
|
+
"lint": "eslint src example",
|
|
103
96
|
"test": "./test.sh src",
|
|
104
97
|
"build": "pnpm run clean && tsc --project tsconfig.build.json",
|
|
105
98
|
"format": "prettier --write '**/*.{js,css,md,ts,tsx}'"
|