@devmoods/express-extras 0.64.0 → 0.65.1

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.
Files changed (73) hide show
  1. package/README.md +0 -111
  2. package/dist/auth/emailAndPassword.js +2 -1
  3. package/dist/auth/google.d.ts +4 -4
  4. package/dist/auth/google.js +14 -11
  5. package/dist/auth/passwordless.js +2 -1
  6. package/dist/auth/token.js +6 -6
  7. package/dist/auth/types.d.ts +1 -1
  8. package/dist/auth.d.ts +1 -1
  9. package/dist/auth.js +17 -10
  10. package/dist/cache.d.ts +3 -0
  11. package/dist/cache.js +3 -2
  12. package/dist/channels.d.ts +6 -5
  13. package/dist/channels.js +9 -4
  14. package/dist/cli.js +1 -1
  15. package/dist/config.d.ts +72 -66
  16. package/dist/config.js +38 -36
  17. package/dist/context.d.ts +9 -2
  18. package/dist/context.js +18 -9
  19. package/dist/csrf.js +6 -6
  20. package/dist/email.d.ts +1 -1
  21. package/dist/email.js +3 -3
  22. package/dist/errors.js +1 -1
  23. package/dist/faktory.d.ts +1 -1
  24. package/dist/faktory.js +5 -15
  25. package/dist/index.d.ts +2 -2
  26. package/dist/index.js +4 -3
  27. package/dist/listen.d.ts +4 -4
  28. package/dist/listen.js +41 -43
  29. package/dist/logger.d.ts +1 -0
  30. package/dist/logger.js +11 -5
  31. package/dist/manage.d.ts +4 -6
  32. package/dist/manage.js +41 -37
  33. package/dist/middleware.d.ts +3 -0
  34. package/dist/middleware.js +24 -7
  35. package/dist/rate-limit.d.ts +1 -0
  36. package/dist/rate-limit.js +25 -3
  37. package/dist/redis.d.ts +1 -1
  38. package/dist/redis.js +4 -4
  39. package/dist/sentry.js +11 -5
  40. package/dist/setupTests.global.d.ts +2 -0
  41. package/dist/setupTests.global.js +4 -0
  42. package/dist/setupTests.js +3 -0
  43. package/dist/slack.js +2 -2
  44. package/dist/utils.d.ts +0 -13
  45. package/dist/utils.js +0 -21
  46. package/dist/validate.js +7 -4
  47. package/dist/vite/__tests__/fixtures/src/entry-server.d.ts +5 -0
  48. package/dist/vite/__tests__/fixtures/src/entry-server.js +15 -0
  49. package/dist/vite/defineViteConfig.js +11 -1
  50. package/dist/vite/index.d.ts +1 -0
  51. package/dist/vite/index.js +3 -0
  52. package/dist/vite/ssr.d.ts +1 -0
  53. package/dist/vite/ssr.js +9 -4
  54. package/dist/vitest/index.d.ts +5 -0
  55. package/dist/vitest/index.js +24 -0
  56. package/dist/vitest/matchers.d.ts +1 -1
  57. package/dist/vitest/matchers.js +6 -2
  58. package/dist/vitest/testClient.d.ts +30 -0
  59. package/dist/vitest/testClient.js +98 -0
  60. package/dist/webapi.js +4 -0
  61. package/package.json +31 -42
  62. package/dist/__tests__/__migrations__/1_a.d.ts +0 -3
  63. package/dist/__tests__/__migrations__/1_a.js +0 -9
  64. package/dist/migrate.d.ts +0 -47
  65. package/dist/migrate.js +0 -255
  66. package/dist/postgres-ts-generator.d.ts +0 -11
  67. package/dist/postgres-ts-generator.js +0 -114
  68. package/dist/postgres.d.ts +0 -71
  69. package/dist/postgres.js +0 -261
  70. package/dist/sql.d.ts +0 -33
  71. package/dist/sql.js +0 -135
  72. package/dist/vite.d.ts +0 -1
  73. 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 { program } from 'commander';
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
- export function registerManagementCommand(register) {
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.option('--verbose', 'Verbose mode');
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) => command('routes')
24
- .option('--openapi', 'Output OpenAPI schema')
25
- .option('--json', 'Output JSON')
26
- .action(cli.wrap(async (options) => {
27
- if (!app) {
28
- return;
29
- }
30
- if (options.openapi) {
31
- const routes = toOpenAPISpec(app);
32
- console.log(JSON.stringify(routes, null, 2));
33
- return;
34
- }
35
- const routes = parseRoutes(app);
36
- if (options.json) {
37
- console.log(JSON.stringify(routes, null, 2));
38
- return;
39
- }
40
- for (const route of routes) {
41
- cli.writeLine(`${route.method.padEnd(7)} ${route.path}\n`);
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.value, null, 2));
61
+ console.log(JSON.stringify(config, null, 2));
57
62
  return;
58
63
  }
59
- for (const [key, value] of Object.entries(config.value)) {
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.value.POSTGRES_URL,
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
  }
@@ -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;
@@ -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 { config } from './config.js';
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 useContext(CSP_NONCE_KEY) ?? '__CSP_NOT_SET__';
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
- setContext(CSP_NONCE_KEY, crypto.randomBytes(16).toString('base64'));
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.value.AUTH_COOKIE_SAME_SITE,
153
+ sameSite: config.AUTH_COOKIE_SAME_SITE,
154
154
  });
155
155
  }
156
156
  // Store sessionId in request context
157
- setContext(ANONYMOUS_SESSION_KEY, sessionId);
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
+ }
@@ -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
  };
@@ -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 = `${config.value.REDIS_KEY_PREFIX}rate-limit:${getKey(req)}`;
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(`rate-limit:${key}`);
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.value.REDIS_URL, onError, } = {}) {
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(), 1000);
32
+ export function pingRedis(redis, timeout = 1000) {
33
+ return withTimeout(redis.ping(), timeout);
34
34
  }
package/dist/sentry.js CHANGED
@@ -1,11 +1,17 @@
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.value.SENTRY_DSN,
6
- tracesSampleRate: 1.0,
7
- profilesSampleRate: 1.0,
8
- integrations: [nodeProfilingIntegration()],
6
+ dsn: config.SENTRY_DSN,
7
+ tracesSampleRate: config.SENTRY_TRACES_SAMPLE_RATE,
8
+ profilesSampleRate: config.SENTRY_PROFILES_SAMPLE_RATE,
9
+ integrations: [
10
+ nodeProfilingIntegration(),
11
+ eventLoopBlockIntegration({
12
+ threshold: config.SENTRY_EVENT_LOOP_THRESHOLD,
13
+ }),
14
+ ],
9
15
  enableLogs: true,
10
16
  ...options,
11
17
  };
@@ -0,0 +1,2 @@
1
+ export declare function setup(): Promise<void>;
2
+ export declare function teardown(): Promise<void>;
@@ -0,0 +1,4 @@
1
+ /* c8 ignore start */
2
+ export async function setup() { }
3
+ export async function teardown() { }
4
+ /* c8 ignore stop */
@@ -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.value.SLACK_WEBHOOK_URL, dryRun = process.env.NODE_ENV !== 'production', } = {}) {
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
- ? error.issues.map((issue) => [
32
- String(issue.path ?? '/'),
33
- issue.message,
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,5 @@
1
+ import { type SsrRenderOptions } from '../../../types.js';
2
+ export declare function render(req: Request, options: SsrRenderOptions): Promise<{
3
+ html: string;
4
+ context: any;
5
+ }>;
@@ -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: ['./src/setupTests'],
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';
@@ -0,0 +1,3 @@
1
+ /* c8 ignore start */
2
+ export { defineViteConfig } from './defineViteConfig.js';
3
+ /* c8 ignore stop */
@@ -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.value.VITE_USE_DEV_SERVER, } = {}) {
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.value.PUBLIC_URL),
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
  }
@@ -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>;
@@ -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: unknown) => R;
4
+ toMatchResponse: (statusCode: number, body?: unknown) => R;
5
5
  }
6
6
  declare module 'vitest' {
7
7
  interface Matchers<T = any> extends CustomMatchers<T> {
@@ -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 { status, body } = received;
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 */