@devmoods/express-extras 0.64.0 → 0.65.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/README.md +0 -111
- package/dist/auth/emailAndPassword.js +2 -1
- package/dist/auth/google.d.ts +4 -4
- package/dist/auth/google.js +14 -11
- package/dist/auth/passwordless.js +2 -1
- package/dist/auth/token.js +6 -6
- package/dist/auth/types.d.ts +1 -1
- package/dist/auth.d.ts +1 -1
- package/dist/auth.js +17 -10
- package/dist/cache.d.ts +3 -0
- package/dist/cache.js +3 -2
- package/dist/channels.d.ts +6 -5
- package/dist/channels.js +9 -4
- package/dist/cli.js +1 -1
- package/dist/config.d.ts +66 -66
- package/dist/config.js +35 -36
- package/dist/context.d.ts +5 -2
- package/dist/context.js +10 -9
- package/dist/csrf.js +6 -6
- package/dist/email.d.ts +1 -1
- package/dist/email.js +3 -3
- package/dist/errors.js +1 -1
- package/dist/faktory.d.ts +1 -1
- package/dist/faktory.js +5 -15
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -3
- package/dist/listen.d.ts +4 -4
- package/dist/listen.js +41 -43
- package/dist/logger.d.ts +1 -0
- package/dist/logger.js +11 -5
- package/dist/manage.d.ts +4 -6
- package/dist/manage.js +41 -37
- package/dist/middleware.d.ts +3 -0
- package/dist/middleware.js +24 -7
- package/dist/rate-limit.d.ts +1 -0
- package/dist/rate-limit.js +25 -3
- package/dist/redis.d.ts +1 -1
- package/dist/redis.js +4 -4
- package/dist/sentry.js +7 -3
- package/dist/setupTests.global.d.ts +2 -0
- package/dist/setupTests.global.js +4 -0
- package/dist/setupTests.js +3 -0
- package/dist/slack.js +2 -2
- package/dist/utils.d.ts +0 -13
- package/dist/utils.js +0 -21
- package/dist/validate.js +7 -4
- package/dist/vite/__tests__/fixtures/src/entry-server.d.ts +5 -0
- package/dist/vite/__tests__/fixtures/src/entry-server.js +15 -0
- package/dist/vite/defineViteConfig.js +11 -1
- package/dist/vite/index.d.ts +1 -0
- package/dist/vite/index.js +3 -0
- package/dist/vite/ssr.d.ts +1 -0
- package/dist/vite/ssr.js +9 -4
- package/dist/vitest/index.d.ts +5 -0
- package/dist/vitest/index.js +24 -0
- package/dist/vitest/matchers.d.ts +1 -1
- package/dist/vitest/matchers.js +6 -2
- package/dist/vitest/testClient.d.ts +30 -0
- package/dist/vitest/testClient.js +98 -0
- package/dist/webapi.js +4 -0
- package/package.json +31 -42
- package/dist/__tests__/__migrations__/1_a.d.ts +0 -3
- package/dist/__tests__/__migrations__/1_a.js +0 -9
- package/dist/migrate.d.ts +0 -47
- package/dist/migrate.js +0 -255
- package/dist/postgres-ts-generator.d.ts +0 -11
- package/dist/postgres-ts-generator.js +0 -114
- package/dist/postgres.d.ts +0 -71
- package/dist/postgres.js +0 -261
- package/dist/sql.d.ts +0 -33
- package/dist/sql.js +0 -135
- package/dist/vite.d.ts +0 -1
- package/dist/vite.js +0 -1
package/dist/manage.js
CHANGED
|
@@ -1,66 +1,71 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
|
+
import { config } from '@devmoods/config';
|
|
3
|
+
import { generateDatabaseTypes, asCli } from '@devmoods/postgres';
|
|
2
4
|
import { createClient } from '@hey-api/openapi-ts';
|
|
3
|
-
import {
|
|
5
|
+
import { Command } from 'commander';
|
|
4
6
|
import {} from 'express';
|
|
5
7
|
import { hashPassword } from './auth.js';
|
|
6
8
|
import { CLI } from './cli.js';
|
|
7
|
-
import { config } from './config.js';
|
|
8
|
-
import { asCli } from './migrate.js';
|
|
9
9
|
import { parseRoutes, toOpenAPISpec } from './routes.js';
|
|
10
|
-
|
|
10
|
+
const defaultProgram = new Command();
|
|
11
|
+
export function registerManagementCommand(register, program = defaultProgram) {
|
|
11
12
|
const cli = new CLI({ printStackTraces: process.argv.includes('--verbose') });
|
|
12
13
|
register(program.command.bind(program), cli);
|
|
13
14
|
}
|
|
14
|
-
export async function runManagementCommands() {
|
|
15
|
-
program.
|
|
15
|
+
export async function runManagementCommands(program = defaultProgram) {
|
|
16
|
+
if (!program.options.some((o) => o.name() === 'verbose')) {
|
|
17
|
+
program.option('--verbose', 'Verbose mode');
|
|
18
|
+
}
|
|
16
19
|
await program.parseAsync(process.argv);
|
|
17
20
|
}
|
|
18
|
-
export function registerDefaultCommands(app) {
|
|
21
|
+
export function registerDefaultCommands(app, program = defaultProgram) {
|
|
19
22
|
registerManagementCommand((command, cli) => {
|
|
20
23
|
const migrate = command('migrate');
|
|
21
24
|
asCli(migrate, cli);
|
|
22
|
-
});
|
|
23
|
-
registerManagementCommand((command, cli) =>
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
25
|
+
}, program);
|
|
26
|
+
registerManagementCommand((command, cli) => {
|
|
27
|
+
command('routes')
|
|
28
|
+
.option('--openapi', 'Output OpenAPI schema')
|
|
29
|
+
.option('--json', 'Output JSON')
|
|
30
|
+
.action(cli.wrap(async (options) => {
|
|
31
|
+
if (!app) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (options.openapi) {
|
|
35
|
+
const routes = toOpenAPISpec(app);
|
|
36
|
+
console.log(JSON.stringify(routes, null, 2));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const routes = parseRoutes(app);
|
|
40
|
+
if (options.json) {
|
|
41
|
+
console.log(JSON.stringify(routes, null, 2));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
for (const route of routes) {
|
|
45
|
+
cli.writeLine(`${route.method.padEnd(7)} ${route.path}\n`);
|
|
46
|
+
}
|
|
47
|
+
}));
|
|
48
|
+
}, program);
|
|
44
49
|
registerManagementCommand((command, cli) => {
|
|
45
50
|
command('password-hash').action(cli.wrap(async () => {
|
|
46
51
|
const password = await cli.readInput('Type a password: ');
|
|
47
52
|
const hash = await hashPassword(password);
|
|
48
53
|
cli.writeLine(`${hash}\n`);
|
|
49
54
|
}));
|
|
50
|
-
});
|
|
55
|
+
}, program);
|
|
51
56
|
registerManagementCommand((command, cli) => {
|
|
52
57
|
command('show-config')
|
|
53
58
|
.option('--json', 'Output JSON')
|
|
54
59
|
.action(cli.wrap(async (options) => {
|
|
55
60
|
if (options.json) {
|
|
56
|
-
console.log(JSON.stringify(config
|
|
61
|
+
console.log(JSON.stringify(config, null, 2));
|
|
57
62
|
return;
|
|
58
63
|
}
|
|
59
|
-
for (const [key, value] of Object.entries(config
|
|
64
|
+
for (const [key, value] of Object.entries(config)) {
|
|
60
65
|
cli.writeLine(`${key}=${value}\n`);
|
|
61
66
|
}
|
|
62
67
|
}));
|
|
63
|
-
});
|
|
68
|
+
}, program);
|
|
64
69
|
registerManagementCommand((command, cli) => {
|
|
65
70
|
command('generate-api')
|
|
66
71
|
.option('--output <string>', 'Output directory', 'src/types/api-gen')
|
|
@@ -90,17 +95,16 @@ export function registerDefaultCommands(app) {
|
|
|
90
95
|
],
|
|
91
96
|
});
|
|
92
97
|
}));
|
|
93
|
-
});
|
|
98
|
+
}, program);
|
|
94
99
|
registerManagementCommand((command, cli) => {
|
|
95
100
|
command('generate-db')
|
|
96
101
|
.option('--output <string>', 'Output file path', 'src/types/db.gen.ts')
|
|
97
102
|
.option('--connection <string>', 'Database connection string')
|
|
98
103
|
.action(cli.wrap(async (options) => {
|
|
99
|
-
const { generateDatabaseTypes } = await import('./postgres-ts-generator.js');
|
|
100
104
|
try {
|
|
101
105
|
await generateDatabaseTypes({
|
|
102
106
|
outputPath: options.output,
|
|
103
|
-
connectionString: options.connection || config.
|
|
107
|
+
connectionString: options.connection || config.POSTGRES_URL,
|
|
104
108
|
});
|
|
105
109
|
cli.writeLine(`✅ Database types generated successfully at: ${options.output}\n`);
|
|
106
110
|
}
|
|
@@ -108,5 +112,5 @@ export function registerDefaultCommands(app) {
|
|
|
108
112
|
cli.writeLine(`❌ Failed to generate database types: ${error}\n`);
|
|
109
113
|
}
|
|
110
114
|
}));
|
|
111
|
-
});
|
|
115
|
+
}, program);
|
|
112
116
|
}
|
package/dist/middleware.d.ts
CHANGED
|
@@ -77,3 +77,6 @@ type ResponseWrapper<TResponseBody extends AnyResponseBody> = TResponseBody | TR
|
|
|
77
77
|
* Alias of `route()`
|
|
78
78
|
*/
|
|
79
79
|
export declare function asyncMiddleware<RequestBody, RequestQuery extends Record<string, any> = Record<string, any>, ResponseBody extends AnyResponseBody = Record<string, any>, ParamKeys extends string = string>(fn: (req: Request<Record<ParamKeys, string>, ResponseBody, RequestBody, RequestQuery, Record<string, any>>, res: Response<ResponseBody, object>, next: NextFunction) => ResponseWrapper<ResponseBody> | Promise<ResponseWrapper<ResponseBody>>): RequestHandler<Record<ParamKeys, string>, ResponseBody, RequestBody, RequestQuery>;
|
|
80
|
+
export declare function withTransaction(wrapper: {
|
|
81
|
+
transaction: (fn: () => Promise<void>) => Promise<void>;
|
|
82
|
+
}): RequestHandler;
|
package/dist/middleware.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import * as crypto from 'node:crypto';
|
|
2
2
|
import {} from 'node:http';
|
|
3
|
+
import { config } from '@devmoods/config';
|
|
4
|
+
import { startTimer } from '@devmoods/observability';
|
|
3
5
|
import { compose } from 'compose-middleware';
|
|
4
6
|
import cookieParser from 'cookie-parser';
|
|
5
7
|
import { json as jsonParser, urlencoded as urlencodedParser, } from 'express';
|
|
6
8
|
import helmet from 'helmet';
|
|
7
9
|
import {} from './auth.js';
|
|
8
|
-
import {
|
|
9
|
-
import { ANONYMOUS_SESSION_KEY, setContext, useContext, withRequestContext, } from './context.js';
|
|
10
|
+
import { ANONYMOUS_SESSION_KEY, setRequestContext, useRequestContext, withRequestContext, } from './context.js';
|
|
10
11
|
import { csrf, doubleCsrfProtection, } from './csrf.js';
|
|
11
12
|
import { NotFound } from './errors.js';
|
|
12
13
|
import { getLogger } from './logger.js';
|
|
13
|
-
import { startTimer } from './utils.js';
|
|
14
14
|
export { urlencodedParser, jsonParser };
|
|
15
15
|
const logger = getLogger('http');
|
|
16
16
|
export { asyncMiddleware as route };
|
|
17
17
|
export { compose as composeMiddleware };
|
|
18
18
|
const CSP_NONCE_KEY = 'csp-nonce';
|
|
19
19
|
export function useCspNonce() {
|
|
20
|
-
return
|
|
20
|
+
return useRequestContext(CSP_NONCE_KEY) ?? '__CSP_NOT_SET__';
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
23
|
* Register middleware that must come before the app routes.
|
|
@@ -131,7 +131,7 @@ export function notFound() {
|
|
|
131
131
|
*/
|
|
132
132
|
function setCspNonce() {
|
|
133
133
|
return function setCspNonceMiddleware(req, res, next) {
|
|
134
|
-
|
|
134
|
+
setRequestContext(CSP_NONCE_KEY, crypto.randomBytes(16).toString('base64'));
|
|
135
135
|
next();
|
|
136
136
|
};
|
|
137
137
|
}
|
|
@@ -150,11 +150,11 @@ export function setAnonymousCookie() {
|
|
|
150
150
|
maxAge: 365 * 24 * 60 * 60 * 1000,
|
|
151
151
|
httpOnly: true,
|
|
152
152
|
secure: process.env.NODE_ENV === 'production',
|
|
153
|
-
sameSite: config.
|
|
153
|
+
sameSite: config.AUTH_COOKIE_SAME_SITE,
|
|
154
154
|
});
|
|
155
155
|
}
|
|
156
156
|
// Store sessionId in request context
|
|
157
|
-
|
|
157
|
+
setRequestContext(ANONYMOUS_SESSION_KEY, sessionId);
|
|
158
158
|
next();
|
|
159
159
|
};
|
|
160
160
|
}
|
|
@@ -212,3 +212,20 @@ export function asyncMiddleware(fn) {
|
|
|
212
212
|
Object.defineProperty(handler, 'name', { value: fn.name });
|
|
213
213
|
return handler;
|
|
214
214
|
}
|
|
215
|
+
export function withTransaction(wrapper) {
|
|
216
|
+
return async function withTransactionMiddleware(req, res, next) {
|
|
217
|
+
try {
|
|
218
|
+
await wrapper.transaction(async () => {
|
|
219
|
+
await new Promise((resolve, reject) => {
|
|
220
|
+
res.on('finish', resolve);
|
|
221
|
+
res.on('error', reject);
|
|
222
|
+
next();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
/* c8 ignore next 2 */
|
|
228
|
+
next(error);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
package/dist/rate-limit.d.ts
CHANGED
|
@@ -19,4 +19,5 @@ export interface RateLimitOptions {
|
|
|
19
19
|
}
|
|
20
20
|
export declare function rateLimit(redis: RedisClient, { max, windowSizeInSeconds, getKey, }?: RateLimitOptions): RequestHandler<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>> & {
|
|
21
21
|
clear: (key: string) => Promise<void>;
|
|
22
|
+
clearAll: () => Promise<number>;
|
|
22
23
|
};
|
package/dist/rate-limit.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import { config } from '@devmoods/config';
|
|
1
2
|
import {} from 'express';
|
|
2
|
-
import { config } from './config.js';
|
|
3
3
|
import { makeError } from './errors.js';
|
|
4
4
|
import {} from './redis.js';
|
|
5
5
|
export function rateLimit(redis, { max = 100, windowSizeInSeconds = 60, getKey = (req) => req.ip || 'unknown', } = {}) {
|
|
6
|
+
const keyPrefix = `${config.REDIS_KEY_PREFIX}rate-limit:`;
|
|
6
7
|
const handler = async (req, res, next) => {
|
|
7
8
|
try {
|
|
8
|
-
const key = `${
|
|
9
|
+
const key = `${keyPrefix}${getKey(req)}`;
|
|
9
10
|
// Use Redis to track requests
|
|
10
11
|
const currentCount = await redis.get(key);
|
|
11
12
|
const count = currentCount ? parseInt(currentCount, 10) : 0;
|
|
@@ -28,8 +29,29 @@ export function rateLimit(redis, { max = 100, windowSizeInSeconds = 60, getKey =
|
|
|
28
29
|
}
|
|
29
30
|
};
|
|
30
31
|
const clear = async (key) => {
|
|
31
|
-
await redis.del(
|
|
32
|
+
await redis.del(`${keyPrefix}${key}`);
|
|
33
|
+
};
|
|
34
|
+
const clearAll = async () => {
|
|
35
|
+
let cursor = '0';
|
|
36
|
+
let deletedCount = 0;
|
|
37
|
+
const pattern = `${config.REDIS_KEY_PREFIX}rate-limit:*`;
|
|
38
|
+
do {
|
|
39
|
+
// Use SCAN instead of KEYS - non-blocking iteration
|
|
40
|
+
const result = await redis.scan(cursor, {
|
|
41
|
+
MATCH: pattern,
|
|
42
|
+
COUNT: 100, // Process in batches of 100
|
|
43
|
+
});
|
|
44
|
+
cursor = result.cursor;
|
|
45
|
+
const keys = result.keys;
|
|
46
|
+
if (keys.length > 0) {
|
|
47
|
+
// Use UNLINK instead of DEL - asynchronous deletion
|
|
48
|
+
await redis.unlink(keys);
|
|
49
|
+
deletedCount += keys.length;
|
|
50
|
+
}
|
|
51
|
+
} while (cursor !== '0');
|
|
52
|
+
return deletedCount;
|
|
32
53
|
};
|
|
33
54
|
handler.clear = clear;
|
|
55
|
+
handler.clearAll = clearAll;
|
|
34
56
|
return handler;
|
|
35
57
|
}
|
package/dist/redis.d.ts
CHANGED
|
@@ -7,4 +7,4 @@ export type RedisClient = ReturnType<typeof createClient>;
|
|
|
7
7
|
export declare function createRedis({ url, onError, }?: RedisOptions): RedisClient;
|
|
8
8
|
export declare function connectRedis(redis: RedisClient, shouldContinue?: boolean): Promise<void>;
|
|
9
9
|
export declare function stopRedis(redis: RedisClient, timeout?: number): Promise<void>;
|
|
10
|
-
export declare function pingRedis(redis: RedisClient): Promise<string>;
|
|
10
|
+
export declare function pingRedis(redis: RedisClient, timeout?: number): Promise<string>;
|
package/dist/redis.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { config } from '@devmoods/config';
|
|
1
2
|
import { createClient } from 'redis';
|
|
2
|
-
import { config } from './config.js';
|
|
3
3
|
import { withTimeout } from './utils.js';
|
|
4
|
-
export function createRedis({ url = config.
|
|
4
|
+
export function createRedis({ url = config.REDIS_URL, onError, } = {}) {
|
|
5
5
|
const client = createClient({ url });
|
|
6
6
|
// Always attach a listener even if
|
|
7
7
|
// onError is not provided.
|
|
@@ -29,6 +29,6 @@ export async function stopRedis(redis, timeout = 10000) {
|
|
|
29
29
|
return redis.destroy();
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
-
export function pingRedis(redis) {
|
|
33
|
-
return withTimeout(redis.ping(),
|
|
32
|
+
export function pingRedis(redis, timeout = 1000) {
|
|
33
|
+
return withTimeout(redis.ping(), timeout);
|
|
34
34
|
}
|
package/dist/sentry.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import { config } from '@devmoods/config';
|
|
2
|
+
import { eventLoopBlockIntegration } from '@sentry/node-native';
|
|
1
3
|
import { nodeProfilingIntegration } from '@sentry/profiling-node';
|
|
2
|
-
import { config } from './config.js';
|
|
3
4
|
export function sentryOptions(options = {}) {
|
|
4
5
|
return {
|
|
5
|
-
dsn: config.
|
|
6
|
+
dsn: config.SENTRY_DSN,
|
|
6
7
|
tracesSampleRate: 1.0,
|
|
7
8
|
profilesSampleRate: 1.0,
|
|
8
|
-
integrations: [
|
|
9
|
+
integrations: [
|
|
10
|
+
nodeProfilingIntegration(),
|
|
11
|
+
eventLoopBlockIntegration({ threshold: 300 }),
|
|
12
|
+
],
|
|
9
13
|
enableLogs: true,
|
|
10
14
|
...options,
|
|
11
15
|
};
|
package/dist/setupTests.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import 'dotenv/config';
|
|
2
2
|
import EventEmitter from 'node:events';
|
|
3
|
+
import { loadConfig } from '@devmoods/config';
|
|
4
|
+
import { config } from './config.js';
|
|
3
5
|
import { extendMatchers, failOnConsole } from './vitest/index.js';
|
|
4
6
|
EventEmitter.setMaxListeners(20);
|
|
5
7
|
failOnConsole();
|
|
6
8
|
extendMatchers(expect);
|
|
9
|
+
loadConfig(config);
|
package/dist/slack.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { config } from '@devmoods/config';
|
|
1
2
|
import { createFetch } from '@devmoods/fetch';
|
|
2
|
-
import { config } from './config.js';
|
|
3
3
|
import { getLogger } from './logger.js';
|
|
4
4
|
const logger = getLogger('slack');
|
|
5
5
|
/**
|
|
@@ -15,7 +15,7 @@ const logger = getLogger('slack');
|
|
|
15
15
|
*
|
|
16
16
|
* See https://api.slack.com/messaging/webhooks
|
|
17
17
|
*/
|
|
18
|
-
export function createSlack({ webhookUrl = config.
|
|
18
|
+
export function createSlack({ webhookUrl = config.SLACK_WEBHOOK_URL, dryRun = process.env.NODE_ENV !== 'production', } = {}) {
|
|
19
19
|
const fetch = createFetch();
|
|
20
20
|
/**
|
|
21
21
|
* Post a message to Slack
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,19 +1,6 @@
|
|
|
1
1
|
import { type EventEmitter } from 'node:events';
|
|
2
2
|
import { withTimeout, TimeoutError, delay } from '@devmoods/fetch';
|
|
3
3
|
export { withTimeout, TimeoutError, delay };
|
|
4
|
-
/**
|
|
5
|
-
* Start a high resolution timer
|
|
6
|
-
*/
|
|
7
|
-
export declare function startTimer(): {
|
|
8
|
-
/**
|
|
9
|
-
* Reset the timer
|
|
10
|
-
*/
|
|
11
|
-
reset(): void;
|
|
12
|
-
/**
|
|
13
|
-
* Get duration in seconds
|
|
14
|
-
*/
|
|
15
|
-
getDuration(): number;
|
|
16
|
-
};
|
|
17
4
|
export declare function readEnv(key: string): string | undefined;
|
|
18
5
|
export declare function readEnv(key: string, defaultValue: string): string;
|
|
19
6
|
export declare namespace readEnv {
|
package/dist/utils.js
CHANGED
|
@@ -1,27 +1,6 @@
|
|
|
1
1
|
import {} from 'node:events';
|
|
2
2
|
import { withTimeout, TimeoutError, delay } from '@devmoods/fetch';
|
|
3
3
|
export { withTimeout, TimeoutError, delay };
|
|
4
|
-
/**
|
|
5
|
-
* Start a high resolution timer
|
|
6
|
-
*/
|
|
7
|
-
export function startTimer() {
|
|
8
|
-
let start = process.hrtime();
|
|
9
|
-
return {
|
|
10
|
-
/**
|
|
11
|
-
* Reset the timer
|
|
12
|
-
*/
|
|
13
|
-
reset() {
|
|
14
|
-
start = process.hrtime();
|
|
15
|
-
},
|
|
16
|
-
/**
|
|
17
|
-
* Get duration in seconds
|
|
18
|
-
*/
|
|
19
|
-
getDuration() {
|
|
20
|
-
const elapsed = process.hrtime(start);
|
|
21
|
-
return (elapsed[0] * 1e9 + elapsed[1]) / 1e9;
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
4
|
export function readEnv(key, defaultValue) {
|
|
26
5
|
const value = process.env[key];
|
|
27
6
|
return value ?? defaultValue;
|
package/dist/validate.js
CHANGED
|
@@ -28,10 +28,11 @@ export async function validateSchema(data, schema) {
|
|
|
28
28
|
return {
|
|
29
29
|
isValid: false,
|
|
30
30
|
errors: (error instanceof ValidationError
|
|
31
|
-
?
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
? /* c8 ignore next 5*/
|
|
32
|
+
error.issues.map((issue) => [
|
|
33
|
+
String(issue.path ?? '/'),
|
|
34
|
+
issue.message,
|
|
35
|
+
])
|
|
35
36
|
: []).reduce((result, item) => {
|
|
36
37
|
const [key, value] = item;
|
|
37
38
|
if (Array.isArray(result[key])) {
|
|
@@ -61,6 +62,7 @@ export function validateStandardSchema(schemas) {
|
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
next();
|
|
65
|
+
/* c8 ignore next 3 */
|
|
64
66
|
}
|
|
65
67
|
catch (error) {
|
|
66
68
|
next(error);
|
|
@@ -89,6 +91,7 @@ function toJsonSchema(standardSchema) {
|
|
|
89
91
|
if ('toJsonSchema' in standardSchema &&
|
|
90
92
|
typeof standardSchema.toJsonSchema === 'function') {
|
|
91
93
|
return standardSchema.toJsonSchema();
|
|
94
|
+
/* c8 ignore next 3 */
|
|
92
95
|
}
|
|
93
96
|
return {};
|
|
94
97
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { renderToString } from 'react-dom/server';
|
|
3
|
+
import {} from '../../../types.js';
|
|
4
|
+
export async function render(req, options) {
|
|
5
|
+
const helmetContext = {};
|
|
6
|
+
const initialData = await options.getInitialData(req);
|
|
7
|
+
if (initialData.shouldRedirect) {
|
|
8
|
+
throw new Response('redirect', { status: 302 });
|
|
9
|
+
}
|
|
10
|
+
const html = renderToString(_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [options.useDevServer && (_jsx("script", { type: "module", nonce: options.nonce, children: options.preamble })), _jsx("meta", { charSet: "utf-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1, shrink-to-fit=no" }), _jsx("meta", { name: "theme-color", content: "#FFFFFF" }), _jsx("meta", { name: "apple-mobile-web-app-capable", content: "yes" }), _jsx("meta", { name: "apple-mobile-web-app-title", content: "express-extras" }), _jsx("link", { rel: "apple-touch-icon", href: "/apple-touch-icon.png" }), options.placeholders, _jsx("meta", { name: "csrf-token", content: options.csrfToken })] }), _jsxs("body", { children: [_jsx("div", { id: "root", children: _jsx("h1", { children: "Hello World!" }) }), options.useDevServer && (_jsx("script", { type: "module", src: "/src/entry-client.tsx" })), _jsx("script", { defer: true, "data-domain": options.publicUrl.hostname, src: "/analytics.js" }), _jsx("script", { nonce: options.nonce, children: `window.store = ${JSON.stringify(initialData)};` })] })] }));
|
|
11
|
+
return {
|
|
12
|
+
html,
|
|
13
|
+
context: helmetContext.helmet,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/// <reference types="vitest" />
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
2
3
|
import * as path from 'node:path';
|
|
3
4
|
import react from '@vitejs/plugin-react';
|
|
4
5
|
import Sonda from 'sonda/vite';
|
|
@@ -12,6 +13,14 @@ export function defineViteConfig({ base = '', reactCompiler = false, ssr = false
|
|
|
12
13
|
`${process.cwd().split('/').pop()}.local`,
|
|
13
14
|
`${process.cwd().split('/').pop()}.test`,
|
|
14
15
|
];
|
|
16
|
+
/* c8 ignore next 4 */
|
|
17
|
+
const globalSetup = existsSync(path.resolve('./src/setupTests.global.ts'))
|
|
18
|
+
? [path.resolve('./src/setupTests.global.ts')]
|
|
19
|
+
: undefined;
|
|
20
|
+
/* c8 ignore next 4 */
|
|
21
|
+
const setupFiles = existsSync(path.resolve('./src/setupTests.ts'))
|
|
22
|
+
? [path.resolve('./src/setupTests.ts')]
|
|
23
|
+
: undefined;
|
|
15
24
|
const port = readEnv.number('VITE_PORT', 5173);
|
|
16
25
|
return defineConfig({
|
|
17
26
|
plugins: [
|
|
@@ -69,7 +78,8 @@ export function defineViteConfig({ base = '', reactCompiler = false, ssr = false
|
|
|
69
78
|
}
|
|
70
79
|
: undefined,
|
|
71
80
|
test: {
|
|
72
|
-
setupFiles
|
|
81
|
+
setupFiles,
|
|
82
|
+
globalSetup,
|
|
73
83
|
include: ['src/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
|
|
74
84
|
globals: true,
|
|
75
85
|
},
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { defineViteConfig } from './defineViteConfig.js';
|
package/dist/vite/ssr.d.ts
CHANGED
|
@@ -4,3 +4,4 @@ export interface SsrMiddlewareOptions<InitialData = unknown> {
|
|
|
4
4
|
useDevServer?: boolean;
|
|
5
5
|
}
|
|
6
6
|
export declare function createSsrMiddleware<InitialData = unknown>({ root, getInitialData, useDevServer, }?: SsrMiddlewareOptions<InitialData>): Promise<import("compose-middleware").RequestHandler<any, any, void>>;
|
|
7
|
+
export declare function replaceSsrPlaceholders(template: string, values: Record<string, unknown>): string;
|
package/dist/vite/ssr.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
+
import { config } from '@devmoods/config';
|
|
3
4
|
import {} from 'compose-middleware';
|
|
4
5
|
import express, {} from 'express';
|
|
5
6
|
import { createServer as createViteServer } from 'vite';
|
|
6
7
|
import {} from './types.js';
|
|
7
|
-
import { config } from '../config.js';
|
|
8
8
|
import { generateCsrfToken } from '../csrf.js';
|
|
9
9
|
import { composeMiddleware, route, useCspNonce } from '../middleware.js';
|
|
10
10
|
import { createFetchRequest } from '../webapi.js';
|
|
11
|
-
export async function createSsrMiddleware({ root = process.cwd(), getInitialData, useDevServer = config.
|
|
11
|
+
export async function createSsrMiddleware({ root = process.cwd(), getInitialData, useDevServer = config.VITE_USE_DEV_SERVER, } = {}) {
|
|
12
12
|
const resolve = (p) => path.resolve(root, p);
|
|
13
13
|
let vite;
|
|
14
14
|
const middlewares = [];
|
|
@@ -53,7 +53,7 @@ export async function createSsrMiddleware({ root = process.cwd(), getInitialData
|
|
|
53
53
|
window.$RefreshSig$ = () => (type) => type
|
|
54
54
|
window.__vite_plugin_react_preamble_installed__ = true
|
|
55
55
|
`,
|
|
56
|
-
publicUrl: new URL(config.
|
|
56
|
+
publicUrl: new URL(config.PUBLIC_URL),
|
|
57
57
|
csrfToken: generateCsrfToken(req, res),
|
|
58
58
|
});
|
|
59
59
|
const readManifest = async () => {
|
|
@@ -84,6 +84,7 @@ export async function createSsrMiddleware({ root = process.cwd(), getInitialData
|
|
|
84
84
|
error.status >= 300 &&
|
|
85
85
|
error.status <= 399) {
|
|
86
86
|
return res.redirect(error.status, error.headers.get('Location') || '');
|
|
87
|
+
/* c8 ignore next 9 */
|
|
87
88
|
}
|
|
88
89
|
if (useDevServer) {
|
|
89
90
|
vite.ssrFixStacktrace(error);
|
|
@@ -93,7 +94,7 @@ export async function createSsrMiddleware({ root = process.cwd(), getInitialData
|
|
|
93
94
|
}));
|
|
94
95
|
return composeMiddleware(middlewares);
|
|
95
96
|
}
|
|
96
|
-
function replaceSsrPlaceholders(template, values) {
|
|
97
|
+
export function replaceSsrPlaceholders(template, values) {
|
|
97
98
|
return template.replace(/\{\{\s*([\w-]+)\s*\}\}/g, (match, variableName) => {
|
|
98
99
|
if (Object.prototype.hasOwnProperty.call(values, variableName)) {
|
|
99
100
|
const value = values[variableName];
|
|
@@ -107,20 +108,24 @@ function replaceSsrPlaceholders(template, values) {
|
|
|
107
108
|
}
|
|
108
109
|
async function getAssetLinks(manifestPath, input, publicPath = '/') {
|
|
109
110
|
const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf-8'));
|
|
111
|
+
/* c8 ignore next 3 */
|
|
110
112
|
if (!manifest[input]) {
|
|
111
113
|
throw Error('Entry not found in the manifest.json file');
|
|
112
114
|
}
|
|
113
115
|
const chunk = manifest[input];
|
|
116
|
+
/* c8 ignore next 3 */
|
|
114
117
|
const cssHtml = (chunk.css || [])
|
|
115
118
|
.map((href) => `<link rel="stylesheet" href="${publicPath}${href}" />`)
|
|
116
119
|
.join(' ');
|
|
117
120
|
// Generate HTML for preload links
|
|
118
121
|
const preloadList = [input];
|
|
122
|
+
/* c8 ignore next 3 */
|
|
119
123
|
if (Array.isArray(chunk.imports)) {
|
|
120
124
|
preloadList.push(...chunk.imports);
|
|
121
125
|
}
|
|
122
126
|
const preloadHtml = preloadList
|
|
123
127
|
.map((pre) => {
|
|
128
|
+
/* c8 ignore next 3 */
|
|
124
129
|
if (!manifest[pre]?.file) {
|
|
125
130
|
return '';
|
|
126
131
|
}
|
package/dist/vitest/index.d.ts
CHANGED
|
@@ -1 +1,6 @@
|
|
|
1
|
+
import { type TestFunction } from 'vitest';
|
|
1
2
|
export * from './matchers.js';
|
|
3
|
+
export * from './testClient.js';
|
|
4
|
+
export declare function createTransactionWrapper(wrapped: {
|
|
5
|
+
transaction: <T>(fn: () => Promise<T> | T) => Promise<T> | T;
|
|
6
|
+
}): <T>(run: TestFunction<T>) => TestFunction<T>;
|
package/dist/vitest/index.js
CHANGED
|
@@ -1 +1,25 @@
|
|
|
1
|
+
import {} from 'vitest';
|
|
1
2
|
export * from './matchers.js';
|
|
3
|
+
export * from './testClient.js';
|
|
4
|
+
class RollbackError extends Error {
|
|
5
|
+
}
|
|
6
|
+
export function createTransactionWrapper(wrapped) {
|
|
7
|
+
const wrapTransaction = (run) => {
|
|
8
|
+
return async (context) => {
|
|
9
|
+
try {
|
|
10
|
+
await wrapped.transaction(async () => {
|
|
11
|
+
await run(context);
|
|
12
|
+
throw new RollbackError();
|
|
13
|
+
/* c8 ignore next 1 */
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (error instanceof RollbackError) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
return wrapTransaction;
|
|
25
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ExpectStatic } from 'vitest';
|
|
2
2
|
import failOnConsole from 'vitest-fail-on-console';
|
|
3
3
|
interface CustomMatchers<R = unknown> {
|
|
4
|
-
toMatchResponse: (statusCode: number, body
|
|
4
|
+
toMatchResponse: (statusCode: number, body?: unknown) => R;
|
|
5
5
|
}
|
|
6
6
|
declare module 'vitest' {
|
|
7
7
|
interface Matchers<T = any> extends CustomMatchers<T> {
|
package/dist/vitest/matchers.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {} from 'vitest';
|
|
2
2
|
import failOnConsole from 'vitest-fail-on-console';
|
|
3
3
|
export { failOnConsole };
|
|
4
|
+
/* c8 ignore start */
|
|
4
5
|
export function extendMatchers(expect) {
|
|
5
6
|
expect.extend({
|
|
6
7
|
toMatchResponse(received, expectedStatusCode, expectedBody) {
|
|
@@ -8,13 +9,15 @@ export function extendMatchers(expect) {
|
|
|
8
9
|
// Check if received is a proper response object
|
|
9
10
|
if (!received ||
|
|
10
11
|
typeof received !== 'object' ||
|
|
11
|
-
!('status' in received)) {
|
|
12
|
+
!('status' in received || 'response' in received)) {
|
|
12
13
|
return {
|
|
13
14
|
pass: false,
|
|
14
15
|
message: () => `Expected response object with status property, but received: ${this.utils.printReceived(received)}`,
|
|
15
16
|
};
|
|
16
17
|
}
|
|
17
|
-
const
|
|
18
|
+
const response = 'response' in received ? received.response : received;
|
|
19
|
+
const status = response.status;
|
|
20
|
+
const body = 'jsonData' in response ? response.jsonData : response.body;
|
|
18
21
|
// Check status code
|
|
19
22
|
const statusMatches = status === expectedStatusCode;
|
|
20
23
|
// Check body (if provided)
|
|
@@ -59,3 +62,4 @@ export function extendMatchers(expect) {
|
|
|
59
62
|
},
|
|
60
63
|
});
|
|
61
64
|
}
|
|
65
|
+
/* c8 ignore stop */
|