@prairielearn/trpc 0.0.0 → 1.0.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +683 -0
  3. package/README.md +16 -36
  4. package/dist/client.d.ts +47 -0
  5. package/dist/client.d.ts.map +1 -0
  6. package/dist/client.js +29 -0
  7. package/dist/client.js.map +1 -0
  8. package/dist/client.test.d.ts +2 -0
  9. package/dist/client.test.d.ts.map +1 -0
  10. package/dist/client.test.js +61 -0
  11. package/dist/client.test.js.map +1 -0
  12. package/dist/express.d.ts +42 -0
  13. package/dist/express.d.ts.map +1 -0
  14. package/dist/express.js +67 -0
  15. package/dist/express.js.map +1 -0
  16. package/dist/express.test.d.ts +2 -0
  17. package/dist/express.test.d.ts.map +1 -0
  18. package/dist/express.test.js +51 -0
  19. package/dist/express.test.js.map +1 -0
  20. package/dist/react.d.ts +26 -0
  21. package/dist/react.d.ts.map +1 -0
  22. package/dist/react.js +23 -0
  23. package/dist/react.js.map +1 -0
  24. package/dist/react.test.d.ts +2 -0
  25. package/dist/react.test.d.ts.map +1 -0
  26. package/dist/react.test.js +35 -0
  27. package/dist/react.test.js.map +1 -0
  28. package/dist/server.d.ts +46 -0
  29. package/dist/server.d.ts.map +1 -0
  30. package/dist/server.js +31 -0
  31. package/dist/server.js.map +1 -0
  32. package/dist/server.test.d.ts +2 -0
  33. package/dist/server.test.d.ts.map +1 -0
  34. package/dist/server.test.js +34 -0
  35. package/dist/server.test.js.map +1 -0
  36. package/package.json +43 -8
  37. package/src/client.test.tsx +84 -0
  38. package/src/client.ts +54 -0
  39. package/src/express.test.ts +66 -0
  40. package/src/express.ts +103 -0
  41. package/src/react.test.tsx +55 -0
  42. package/src/react.tsx +53 -0
  43. package/src/server.test.ts +51 -0
  44. package/src/server.ts +77 -0
  45. package/tsconfig.json +6 -0
  46. package/vitest.config.ts +11 -0
package/README.md CHANGED
@@ -1,45 +1,25 @@
1
- # @prairielearn/trpc
1
+ # `@prairielearn/trpc`
2
2
 
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
3
+ Shared tRPC infrastructure for PrairieLearn applications. Import from an explicit subpath so that browser builds cannot pull in server-only dependencies.
4
4
 
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
5
+ | Subpath | Public API |
6
+ | ---------------------------- | -------------------------------------------------------------------- |
7
+ | `@prairielearn/trpc/server` | `appErrorFormatter`, `throwAppError`, and application-error types |
8
+ | `@prairielearn/trpc/client` | `AppError`, `AppErrorRenderers`, `getAppError`, and `renderAppError` |
9
+ | `@prairielearn/trpc/react` | `AppErrorAlert` and `QueryClientProviderDebug` |
10
+ | `@prairielearn/trpc/express` | `isTrpcRequest`, `isMultipartRequest`, and `formatTrpcErrorResponse` |
6
11
 
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
12
+ ## Typed error maps
8
13
 
9
- ## Purpose
14
+ The server and client intentionally resolve a whole procedure error map differently:
10
15
 
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@prairielearn/trpc`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
16
+ - `throwAppError<WholeErrorMap>` accepts only variants shared by every procedure. This prevents a helper that can run for every procedure from throwing an error that some procedures did not declare.
17
+ - `getAppError<WholeErrorMap>` returns the union of all procedure variants. This supports a client component that handles failures from several procedures.
15
18
 
16
- ## What is OIDC Trusted Publishing?
19
+ For a single procedure, pass its map entry to either helper, such as `throwAppError<WidgetError['Update']>(...)` or `getAppError<WidgetError['Update']>(error)`.
17
20
 
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
21
+ ## Express errors before the adapter
19
22
 
20
- ## Setup Instructions
23
+ `formatTrpcErrorResponse` builds the SuperJSON-wrapped tRPC error response used when authentication, authorization, CSRF, or other Express middleware fails before the tRPC adapter runs. Its numeric JSON-RPC code is sourced from tRPC's own `TRPC_ERROR_CODES_BY_KEY` constants.
21
24
 
22
- To properly configure OIDC trusted publishing for this package:
23
-
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
28
-
29
- ## DO NOT USE THIS PACKAGE
30
-
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
36
-
37
- ## More Information
38
-
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
42
-
43
- ---
44
-
45
- **Maintained for OIDC setup purposes only**
25
+ Error IDs generated for normal Express errors and adapter errors remain server-side and correlate application logs with error reporting. The shared response shape does not expose an error ID to clients.
@@ -0,0 +1,47 @@
1
+ import type { ReactNode } from 'react';
2
+ /**
3
+ * Resolves client error types to the variants a component may receive.
4
+ *
5
+ * Unlike the server's whole-map resolution, a whole map becomes the union of its procedure errors
6
+ * because a component may handle failures from several procedures.
7
+ */
8
+ type ResolveClientAppError<T> = T extends {
9
+ code: string;
10
+ } ? T : T[keyof T] extends {
11
+ code: string;
12
+ } ? T[keyof T] : never;
13
+ /** The resolved, non-null return type of {@link getAppError}. */
14
+ export type AppError<T> = (ResolveClientAppError<T> & {
15
+ message: string;
16
+ }) | {
17
+ code: 'UNKNOWN';
18
+ message: string;
19
+ };
20
+ /** An exhaustive renderer map for every statically known application-error code. */
21
+ export type AppErrorRenderers<E extends {
22
+ code: string;
23
+ message: string;
24
+ }> = {
25
+ [K in E['code']]: (error: Extract<E, {
26
+ code: K;
27
+ }>) => ReactNode;
28
+ };
29
+ /**
30
+ * Extracts typed application metadata from a tRPC client error.
31
+ *
32
+ * Pass a procedure entry for a single operation. Pass a whole error map only when one component
33
+ * handles failures from several procedures; in that case the result is their union.
34
+ */
35
+ export declare function getAppError<T>(error: unknown): AppError<T> | null;
36
+ /**
37
+ * Renders an application error with an exhaustive map for statically known variants.
38
+ *
39
+ * If a newer server sends a code unknown to a stale browser bundle, the server message is rendered
40
+ * instead of attempting to call a missing renderer.
41
+ */
42
+ export declare function renderAppError<E extends {
43
+ code: string;
44
+ message: string;
45
+ }>(error: E, renderers: AppErrorRenderers<E>): ReactNode;
46
+ export {};
47
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC;;;;;GAKG;AACH,KAAK,qBAAqB,CAAC,CAAC,IAAI,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GACtD,CAAC,GACD,CAAC,CAAC,MAAM,CAAC,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GACjC,CAAC,CAAC,MAAM,CAAC,CAAC,GACV,KAAK,CAAC;AAEZ,iEAAiE;AACjE,MAAM,MAAM,QAAQ,CAAC,CAAC,IAClB,CAAC,qBAAqB,CAAC,CAAC,CAAC,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,GAChD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzC,oFAAoF;AACpF,MAAM,MAAM,iBAAiB,CAAC,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,IAAI;KAC1E,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,EAAE;QAAE,IAAI,EAAE,CAAC,CAAA;KAAE,CAAC,KAAK,SAAS;CAChE,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI,CAQjE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EACxE,KAAK,EAAE,CAAC,EACR,SAAS,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAC9B,SAAS,CAGX","sourcesContent":["import { TRPCClientError } from '@trpc/client';\nimport type { ReactNode } from 'react';\n\n/**\n * Resolves client error types to the variants a component may receive.\n *\n * Unlike the server's whole-map resolution, a whole map becomes the union of its procedure errors\n * because a component may handle failures from several procedures.\n */\ntype ResolveClientAppError<T> = T extends { code: string }\n ? T\n : T[keyof T] extends { code: string }\n ? T[keyof T]\n : never;\n\n/** The resolved, non-null return type of {@link getAppError}. */\nexport type AppError<T> =\n | (ResolveClientAppError<T> & { message: string })\n | { code: 'UNKNOWN'; message: string };\n\n/** An exhaustive renderer map for every statically known application-error code. */\nexport type AppErrorRenderers<E extends { code: string; message: string }> = {\n [K in E['code']]: (error: Extract<E, { code: K }>) => ReactNode;\n};\n\n/**\n * Extracts typed application metadata from a tRPC client error.\n *\n * Pass a procedure entry for a single operation. Pass a whole error map only when one component\n * handles failures from several procedures; in that case the result is their union.\n */\nexport function getAppError<T>(error: unknown): AppError<T> | null {\n if (error instanceof TRPCClientError) {\n const appError = (error.data as { appError?: ResolveClientAppError<T> } | undefined)?.appError;\n if (appError) return { ...appError, message: error.message };\n return { code: 'UNKNOWN', message: error.message };\n }\n if (error instanceof Error) return { code: 'UNKNOWN', message: error.message };\n return null;\n}\n\n/**\n * Renders an application error with an exhaustive map for statically known variants.\n *\n * If a newer server sends a code unknown to a stale browser bundle, the server message is rendered\n * instead of attempting to call a missing renderer.\n */\nexport function renderAppError<E extends { code: string; message: string }>(\n error: E,\n renderers: AppErrorRenderers<E>,\n): ReactNode {\n const render = renderers[error.code as E['code']] as ((error: E) => ReactNode) | undefined;\n return render ? render(error) : error.message;\n}\n"]}
package/dist/client.js ADDED
@@ -0,0 +1,29 @@
1
+ import { TRPCClientError } from '@trpc/client';
2
+ /**
3
+ * Extracts typed application metadata from a tRPC client error.
4
+ *
5
+ * Pass a procedure entry for a single operation. Pass a whole error map only when one component
6
+ * handles failures from several procedures; in that case the result is their union.
7
+ */
8
+ export function getAppError(error) {
9
+ if (error instanceof TRPCClientError) {
10
+ const appError = error.data?.appError;
11
+ if (appError)
12
+ return { ...appError, message: error.message };
13
+ return { code: 'UNKNOWN', message: error.message };
14
+ }
15
+ if (error instanceof Error)
16
+ return { code: 'UNKNOWN', message: error.message };
17
+ return null;
18
+ }
19
+ /**
20
+ * Renders an application error with an exhaustive map for statically known variants.
21
+ *
22
+ * If a newer server sends a code unknown to a stale browser bundle, the server message is rendered
23
+ * instead of attempting to call a missing renderer.
24
+ */
25
+ export function renderAppError(error, renderers) {
26
+ const render = renderers[error.code];
27
+ return render ? render(error) : error.message;
28
+ }
29
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAyB/C;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAI,KAAc;IAC3C,IAAI,KAAK,YAAY,eAAe,EAAE,CAAC;QACrC,MAAM,QAAQ,GAAI,KAAK,CAAC,IAA4D,EAAE,QAAQ,CAAC;QAC/F,IAAI,QAAQ;YAAE,OAAO,EAAE,GAAG,QAAQ,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;QAC7D,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;IACrD,CAAC;IACD,IAAI,KAAK,YAAY,KAAK;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;IAC/E,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAC5B,KAAQ,EACR,SAA+B;IAE/B,MAAM,MAAM,GAAG,SAAS,CAAC,KAAK,CAAC,IAAiB,CAA0C,CAAC;IAC3F,OAAO,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC;AAChD,CAAC","sourcesContent":["import { TRPCClientError } from '@trpc/client';\nimport type { ReactNode } from 'react';\n\n/**\n * Resolves client error types to the variants a component may receive.\n *\n * Unlike the server's whole-map resolution, a whole map becomes the union of its procedure errors\n * because a component may handle failures from several procedures.\n */\ntype ResolveClientAppError<T> = T extends { code: string }\n ? T\n : T[keyof T] extends { code: string }\n ? T[keyof T]\n : never;\n\n/** The resolved, non-null return type of {@link getAppError}. */\nexport type AppError<T> =\n | (ResolveClientAppError<T> & { message: string })\n | { code: 'UNKNOWN'; message: string };\n\n/** An exhaustive renderer map for every statically known application-error code. */\nexport type AppErrorRenderers<E extends { code: string; message: string }> = {\n [K in E['code']]: (error: Extract<E, { code: K }>) => ReactNode;\n};\n\n/**\n * Extracts typed application metadata from a tRPC client error.\n *\n * Pass a procedure entry for a single operation. Pass a whole error map only when one component\n * handles failures from several procedures; in that case the result is their union.\n */\nexport function getAppError<T>(error: unknown): AppError<T> | null {\n if (error instanceof TRPCClientError) {\n const appError = (error.data as { appError?: ResolveClientAppError<T> } | undefined)?.appError;\n if (appError) return { ...appError, message: error.message };\n return { code: 'UNKNOWN', message: error.message };\n }\n if (error instanceof Error) return { code: 'UNKNOWN', message: error.message };\n return null;\n}\n\n/**\n * Renders an application error with an exhaustive map for statically known variants.\n *\n * If a newer server sends a code unknown to a stale browser bundle, the server message is rendered\n * instead of attempting to call a missing renderer.\n */\nexport function renderAppError<E extends { code: string; message: string }>(\n error: E,\n renderers: AppErrorRenderers<E>,\n): ReactNode {\n const render = renderers[error.code as E['code']] as ((error: E) => ReactNode) | undefined;\n return render ? render(error) : error.message;\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=client.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.test.d.ts","sourceRoot":"","sources":["../src/client.test.tsx"],"names":[],"mappings":"","sourcesContent":["import { TRPCClientError } from '@trpc/client';\nimport { TRPCError } from '@trpc/server';\nimport { TRPC_ERROR_CODES_BY_KEY } from '@trpc/server/rpc';\nimport { assert, describe, it } from 'vitest';\n\nimport { getAppError, renderAppError } from './client.js';\nimport { appErrorFormatter, throwAppError } from './server.js';\n\ninterface ExpectedError {\n code: 'EXPECTED';\n detail: string;\n}\n\ndescribe('getAppError', () => {\n it('extracts typed metadata from a tRPC client error', () => {\n const serverError = assert.throws(() => {\n throwAppError<ExpectedError>({\n message: 'Expected failure',\n code: 'EXPECTED',\n detail: 'more context',\n });\n });\n assert.instanceOf(serverError, TRPCError);\n\n const errorShape = appErrorFormatter({\n error: serverError,\n shape: {\n message: serverError.message,\n code: TRPC_ERROR_CODES_BY_KEY.BAD_REQUEST,\n data: {\n code: 'BAD_REQUEST',\n httpStatus: 400,\n },\n },\n });\n const error = new TRPCClientError(errorShape.message, {\n result: { error: errorShape },\n });\n\n assert.deepEqual(getAppError<ExpectedError>(error), {\n code: 'EXPECTED',\n message: 'Expected failure',\n detail: 'more context',\n });\n });\n\n it('uses UNKNOWN for ordinary errors', () => {\n assert.deepEqual(getAppError<ExpectedError>(new Error('Unexpected failure')), {\n code: 'UNKNOWN',\n message: 'Unexpected failure',\n });\n });\n});\n\ndescribe('renderAppError', () => {\n it('uses an exhaustive renderer for a known code', () => {\n const error = { code: 'EXPECTED' as const, message: 'Expected failure', detail: 'context' };\n\n assert.equal(\n renderAppError(error, {\n EXPECTED: ({ message, detail }) => `${message}: ${detail}`,\n }),\n 'Expected failure: context',\n );\n });\n\n it('falls back to the message when a stale client receives an unknown runtime code', () => {\n interface KnownError {\n code: 'EXPECTED';\n message: string;\n }\n const staleClientError = {\n code: 'ADDED_AFTER_DEPLOY',\n message: 'The server still explains the failure',\n } as unknown as KnownError;\n\n assert.equal(\n renderAppError(staleClientError, {\n EXPECTED: ({ message }) => message,\n }),\n 'The server still explains the failure',\n );\n });\n});\n"]}
@@ -0,0 +1,61 @@
1
+ import { TRPCClientError } from '@trpc/client';
2
+ import { TRPCError } from '@trpc/server';
3
+ import { TRPC_ERROR_CODES_BY_KEY } from '@trpc/server/rpc';
4
+ import { assert, describe, it } from 'vitest';
5
+ import { getAppError, renderAppError } from './client.js';
6
+ import { appErrorFormatter, throwAppError } from './server.js';
7
+ describe('getAppError', () => {
8
+ it('extracts typed metadata from a tRPC client error', () => {
9
+ const serverError = assert.throws(() => {
10
+ throwAppError({
11
+ message: 'Expected failure',
12
+ code: 'EXPECTED',
13
+ detail: 'more context',
14
+ });
15
+ });
16
+ assert.instanceOf(serverError, TRPCError);
17
+ const errorShape = appErrorFormatter({
18
+ error: serverError,
19
+ shape: {
20
+ message: serverError.message,
21
+ code: TRPC_ERROR_CODES_BY_KEY.BAD_REQUEST,
22
+ data: {
23
+ code: 'BAD_REQUEST',
24
+ httpStatus: 400,
25
+ },
26
+ },
27
+ });
28
+ const error = new TRPCClientError(errorShape.message, {
29
+ result: { error: errorShape },
30
+ });
31
+ assert.deepEqual(getAppError(error), {
32
+ code: 'EXPECTED',
33
+ message: 'Expected failure',
34
+ detail: 'more context',
35
+ });
36
+ });
37
+ it('uses UNKNOWN for ordinary errors', () => {
38
+ assert.deepEqual(getAppError(new Error('Unexpected failure')), {
39
+ code: 'UNKNOWN',
40
+ message: 'Unexpected failure',
41
+ });
42
+ });
43
+ });
44
+ describe('renderAppError', () => {
45
+ it('uses an exhaustive renderer for a known code', () => {
46
+ const error = { code: 'EXPECTED', message: 'Expected failure', detail: 'context' };
47
+ assert.equal(renderAppError(error, {
48
+ EXPECTED: ({ message, detail }) => `${message}: ${detail}`,
49
+ }), 'Expected failure: context');
50
+ });
51
+ it('falls back to the message when a stale client receives an unknown runtime code', () => {
52
+ const staleClientError = {
53
+ code: 'ADDED_AFTER_DEPLOY',
54
+ message: 'The server still explains the failure',
55
+ };
56
+ assert.equal(renderAppError(staleClientError, {
57
+ EXPECTED: ({ message }) => message,
58
+ }), 'The server still explains the failure');
59
+ });
60
+ });
61
+ //# sourceMappingURL=client.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.test.js","sourceRoot":"","sources":["../src/client.test.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAO/D,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;IAC3B,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE;YACrC,aAAa,CAAgB;gBAC3B,OAAO,EAAE,kBAAkB;gBAC3B,IAAI,EAAE,UAAU;gBAChB,MAAM,EAAE,cAAc;aACvB,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,UAAU,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;QAE1C,MAAM,UAAU,GAAG,iBAAiB,CAAC;YACnC,KAAK,EAAE,WAAW;YAClB,KAAK,EAAE;gBACL,OAAO,EAAE,WAAW,CAAC,OAAO;gBAC5B,IAAI,EAAE,uBAAuB,CAAC,WAAW;gBACzC,IAAI,EAAE;oBACJ,IAAI,EAAE,aAAa;oBACnB,UAAU,EAAE,GAAG;iBAChB;aACF;SACF,CAAC,CAAC;QACH,MAAM,KAAK,GAAG,IAAI,eAAe,CAAC,UAAU,CAAC,OAAO,EAAE;YACpD,MAAM,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE;SAC9B,CAAC,CAAC;QAEH,MAAM,CAAC,SAAS,CAAC,WAAW,CAAgB,KAAK,CAAC,EAAE;YAClD,IAAI,EAAE,UAAU;YAChB,OAAO,EAAE,kBAAkB;YAC3B,MAAM,EAAE,cAAc;SACvB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C,MAAM,CAAC,SAAS,CAAC,WAAW,CAAgB,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC,EAAE;YAC5E,IAAI,EAAE,SAAS;YACf,OAAO,EAAE,oBAAoB;SAC9B,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;QACtD,MAAM,KAAK,GAAG,EAAE,IAAI,EAAE,UAAmB,EAAE,OAAO,EAAE,kBAAkB,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;QAE5F,MAAM,CAAC,KAAK,CACV,cAAc,CAAC,KAAK,EAAE;YACpB,QAAQ,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,OAAO,KAAK,MAAM,EAAE;SAC3D,CAAC,EACF,2BAA2B,CAC5B,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gFAAgF,EAAE,GAAG,EAAE;QAKxF,MAAM,gBAAgB,GAAG;YACvB,IAAI,EAAE,oBAAoB;YAC1B,OAAO,EAAE,uCAAuC;SACxB,CAAC;QAE3B,MAAM,CAAC,KAAK,CACV,cAAc,CAAC,gBAAgB,EAAE;YAC/B,QAAQ,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,OAAO;SACnC,CAAC,EACF,uCAAuC,CACxC,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { TRPCClientError } from '@trpc/client';\nimport { TRPCError } from '@trpc/server';\nimport { TRPC_ERROR_CODES_BY_KEY } from '@trpc/server/rpc';\nimport { assert, describe, it } from 'vitest';\n\nimport { getAppError, renderAppError } from './client.js';\nimport { appErrorFormatter, throwAppError } from './server.js';\n\ninterface ExpectedError {\n code: 'EXPECTED';\n detail: string;\n}\n\ndescribe('getAppError', () => {\n it('extracts typed metadata from a tRPC client error', () => {\n const serverError = assert.throws(() => {\n throwAppError<ExpectedError>({\n message: 'Expected failure',\n code: 'EXPECTED',\n detail: 'more context',\n });\n });\n assert.instanceOf(serverError, TRPCError);\n\n const errorShape = appErrorFormatter({\n error: serverError,\n shape: {\n message: serverError.message,\n code: TRPC_ERROR_CODES_BY_KEY.BAD_REQUEST,\n data: {\n code: 'BAD_REQUEST',\n httpStatus: 400,\n },\n },\n });\n const error = new TRPCClientError(errorShape.message, {\n result: { error: errorShape },\n });\n\n assert.deepEqual(getAppError<ExpectedError>(error), {\n code: 'EXPECTED',\n message: 'Expected failure',\n detail: 'more context',\n });\n });\n\n it('uses UNKNOWN for ordinary errors', () => {\n assert.deepEqual(getAppError<ExpectedError>(new Error('Unexpected failure')), {\n code: 'UNKNOWN',\n message: 'Unexpected failure',\n });\n });\n});\n\ndescribe('renderAppError', () => {\n it('uses an exhaustive renderer for a known code', () => {\n const error = { code: 'EXPECTED' as const, message: 'Expected failure', detail: 'context' };\n\n assert.equal(\n renderAppError(error, {\n EXPECTED: ({ message, detail }) => `${message}: ${detail}`,\n }),\n 'Expected failure: context',\n );\n });\n\n it('falls back to the message when a stale client receives an unknown runtime code', () => {\n interface KnownError {\n code: 'EXPECTED';\n message: string;\n }\n const staleClientError = {\n code: 'ADDED_AFTER_DEPLOY',\n message: 'The server still explains the failure',\n } as unknown as KnownError;\n\n assert.equal(\n renderAppError(staleClientError, {\n EXPECTED: ({ message }) => message,\n }),\n 'The server still explains the failure',\n );\n });\n});\n"]}
@@ -0,0 +1,42 @@
1
+ import { type TRPC_ERROR_CODE_KEY } from '@trpc/server/rpc';
2
+ export interface TrpcRequestLike {
3
+ header(name: string): string | undefined;
4
+ originalUrl: string;
5
+ }
6
+ export interface RequestWithHeaders {
7
+ headers: Record<string, string | string[] | undefined>;
8
+ }
9
+ /**
10
+ * Returns true for a tRPC HTTP-link request whose procedure is mounted below a `trpc` path segment.
11
+ */
12
+ export declare function isTrpcRequest(req: TrpcRequestLike): boolean;
13
+ /**
14
+ * Returns true when a request has a multipart/form-data content type.
15
+ *
16
+ * Use this to skip JSON and URL-encoded body parsers for file uploads and tRPC `FormData` inputs.
17
+ * Even when those parsers do not consume multipart data, they can assign an empty `req.body` that
18
+ * prevents the tRPC Express adapter from reading the original stream.
19
+ */
20
+ export declare function isMultipartRequest(req: RequestWithHeaders): boolean;
21
+ export interface TrpcErrorResponse {
22
+ error: {
23
+ json: {
24
+ message: string;
25
+ code: number;
26
+ data: {
27
+ code: TRPC_ERROR_CODE_KEY;
28
+ httpStatus: number;
29
+ stack?: string;
30
+ };
31
+ };
32
+ };
33
+ }
34
+ /**
35
+ * Formats an error thrown before the tRPC adapter as a SuperJSON-compatible tRPC response.
36
+ */
37
+ export declare function formatTrpcErrorResponse({ status, message, stack }: {
38
+ status: number;
39
+ message: string;
40
+ stack?: string;
41
+ }): TrpcErrorResponse;
42
+ //# sourceMappingURL=express.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../src/express.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,KAAK,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAErF,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACzC,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;CACxD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAM3D;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAOnE;AAyBD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE;QACL,IAAI,EAAE;YACJ,OAAO,EAAE,MAAM,CAAC;YAChB,IAAI,EAAE,MAAM,CAAC;YACb,IAAI,EAAE;gBACJ,IAAI,EAAE,mBAAmB,CAAC;gBAC1B,UAAU,EAAE,MAAM,CAAC;gBACnB,KAAK,CAAC,EAAE,MAAM,CAAC;aAChB,CAAC;SACH,CAAC;KACH,CAAC;CACH;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,MAAM,EACN,OAAO,EACP,KAAK,EACN,EAAE;IACD,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GAAG,iBAAiB,CAgBpB","sourcesContent":["import { TRPC_ERROR_CODES_BY_KEY, type TRPC_ERROR_CODE_KEY } from '@trpc/server/rpc';\n\nexport interface TrpcRequestLike {\n header(name: string): string | undefined;\n originalUrl: string;\n}\n\nexport interface RequestWithHeaders {\n headers: Record<string, string | string[] | undefined>;\n}\n\n/**\n * Returns true for a tRPC HTTP-link request whose procedure is mounted below a `trpc` path segment.\n */\nexport function isTrpcRequest(req: TrpcRequestLike): boolean {\n // This header is spoofable; the URL path is what Express actually uses to dispatch.\n if (req.header('X-TRPC') !== 'true') return false;\n const pathOnly = req.originalUrl.split('?')[0];\n const segments = pathOnly.split('/');\n return segments.at(-2) === 'trpc';\n}\n\n/**\n * Returns true when a request has a multipart/form-data content type.\n *\n * Use this to skip JSON and URL-encoded body parsers for file uploads and tRPC `FormData` inputs.\n * Even when those parsers do not consume multipart data, they can assign an empty `req.body` that\n * prevents the tRPC Express adapter from reading the original stream.\n */\nexport function isMultipartRequest(req: RequestWithHeaders): boolean {\n const contentTypeEntry = Object.entries(req.headers).find(\n ([headerName]) => headerName.toLowerCase() === 'content-type',\n );\n const contentType = contentTypeEntry?.[1];\n const value = Array.isArray(contentType) ? contentType[0] : contentType;\n return value?.toLowerCase().startsWith('multipart/form-data') ?? false;\n}\n\nconst HTTP_STATUS_TO_TRPC_ERROR_CODE: Partial<Record<number, TRPC_ERROR_CODE_KEY>> = {\n 400: 'BAD_REQUEST',\n 401: 'UNAUTHORIZED',\n 402: 'PAYMENT_REQUIRED',\n 403: 'FORBIDDEN',\n 404: 'NOT_FOUND',\n 405: 'METHOD_NOT_SUPPORTED',\n 408: 'TIMEOUT',\n 409: 'CONFLICT',\n 412: 'PRECONDITION_FAILED',\n 413: 'PAYLOAD_TOO_LARGE',\n 415: 'UNSUPPORTED_MEDIA_TYPE',\n 422: 'UNPROCESSABLE_CONTENT',\n 428: 'PRECONDITION_REQUIRED',\n 429: 'TOO_MANY_REQUESTS',\n 499: 'CLIENT_CLOSED_REQUEST',\n 500: 'INTERNAL_SERVER_ERROR',\n 501: 'NOT_IMPLEMENTED',\n 502: 'BAD_GATEWAY',\n 503: 'SERVICE_UNAVAILABLE',\n 504: 'GATEWAY_TIMEOUT',\n};\n\nexport interface TrpcErrorResponse {\n error: {\n json: {\n message: string;\n code: number;\n data: {\n code: TRPC_ERROR_CODE_KEY;\n httpStatus: number;\n stack?: string;\n };\n };\n };\n}\n\n/**\n * Formats an error thrown before the tRPC adapter as a SuperJSON-compatible tRPC response.\n */\nexport function formatTrpcErrorResponse({\n status,\n message,\n stack,\n}: {\n status: number;\n message: string;\n stack?: string;\n}): TrpcErrorResponse {\n const code = HTTP_STATUS_TO_TRPC_ERROR_CODE[status] ?? 'INTERNAL_SERVER_ERROR';\n\n return {\n error: {\n json: {\n message,\n code: TRPC_ERROR_CODES_BY_KEY[code],\n data: {\n code,\n httpStatus: status,\n ...(stack === undefined ? {} : { stack }),\n },\n },\n },\n };\n}\n"]}
@@ -0,0 +1,67 @@
1
+ import { TRPC_ERROR_CODES_BY_KEY } from '@trpc/server/rpc';
2
+ /**
3
+ * Returns true for a tRPC HTTP-link request whose procedure is mounted below a `trpc` path segment.
4
+ */
5
+ export function isTrpcRequest(req) {
6
+ // This header is spoofable; the URL path is what Express actually uses to dispatch.
7
+ if (req.header('X-TRPC') !== 'true')
8
+ return false;
9
+ const pathOnly = req.originalUrl.split('?')[0];
10
+ const segments = pathOnly.split('/');
11
+ return segments.at(-2) === 'trpc';
12
+ }
13
+ /**
14
+ * Returns true when a request has a multipart/form-data content type.
15
+ *
16
+ * Use this to skip JSON and URL-encoded body parsers for file uploads and tRPC `FormData` inputs.
17
+ * Even when those parsers do not consume multipart data, they can assign an empty `req.body` that
18
+ * prevents the tRPC Express adapter from reading the original stream.
19
+ */
20
+ export function isMultipartRequest(req) {
21
+ const contentTypeEntry = Object.entries(req.headers).find(([headerName]) => headerName.toLowerCase() === 'content-type');
22
+ const contentType = contentTypeEntry?.[1];
23
+ const value = Array.isArray(contentType) ? contentType[0] : contentType;
24
+ return value?.toLowerCase().startsWith('multipart/form-data') ?? false;
25
+ }
26
+ const HTTP_STATUS_TO_TRPC_ERROR_CODE = {
27
+ 400: 'BAD_REQUEST',
28
+ 401: 'UNAUTHORIZED',
29
+ 402: 'PAYMENT_REQUIRED',
30
+ 403: 'FORBIDDEN',
31
+ 404: 'NOT_FOUND',
32
+ 405: 'METHOD_NOT_SUPPORTED',
33
+ 408: 'TIMEOUT',
34
+ 409: 'CONFLICT',
35
+ 412: 'PRECONDITION_FAILED',
36
+ 413: 'PAYLOAD_TOO_LARGE',
37
+ 415: 'UNSUPPORTED_MEDIA_TYPE',
38
+ 422: 'UNPROCESSABLE_CONTENT',
39
+ 428: 'PRECONDITION_REQUIRED',
40
+ 429: 'TOO_MANY_REQUESTS',
41
+ 499: 'CLIENT_CLOSED_REQUEST',
42
+ 500: 'INTERNAL_SERVER_ERROR',
43
+ 501: 'NOT_IMPLEMENTED',
44
+ 502: 'BAD_GATEWAY',
45
+ 503: 'SERVICE_UNAVAILABLE',
46
+ 504: 'GATEWAY_TIMEOUT',
47
+ };
48
+ /**
49
+ * Formats an error thrown before the tRPC adapter as a SuperJSON-compatible tRPC response.
50
+ */
51
+ export function formatTrpcErrorResponse({ status, message, stack, }) {
52
+ const code = HTTP_STATUS_TO_TRPC_ERROR_CODE[status] ?? 'INTERNAL_SERVER_ERROR';
53
+ return {
54
+ error: {
55
+ json: {
56
+ message,
57
+ code: TRPC_ERROR_CODES_BY_KEY[code],
58
+ data: {
59
+ code,
60
+ httpStatus: status,
61
+ ...(stack === undefined ? {} : { stack }),
62
+ },
63
+ },
64
+ },
65
+ };
66
+ }
67
+ //# sourceMappingURL=express.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.js","sourceRoot":"","sources":["../src/express.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAA4B,MAAM,kBAAkB,CAAC;AAWrF;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,GAAoB;IAChD,oFAAoF;IACpF,IAAI,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,MAAM;QAAE,OAAO,KAAK,CAAC;IAClD,MAAM,QAAQ,GAAG,GAAG,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/C,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACrC,OAAO,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC;AACpC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAuB;IACxD,MAAM,gBAAgB,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,IAAI,CACvD,CAAC,CAAC,UAAU,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,WAAW,EAAE,KAAK,cAAc,CAC9D,CAAC;IACF,MAAM,WAAW,GAAG,gBAAgB,EAAE,CAAC,CAAC,CAAC,CAAC;IAC1C,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC;IACxE,OAAO,KAAK,EAAE,WAAW,EAAE,CAAC,UAAU,CAAC,qBAAqB,CAAC,IAAI,KAAK,CAAC;AACzE,CAAC;AAED,MAAM,8BAA8B,GAAiD;IACnF,GAAG,EAAE,aAAa;IAClB,GAAG,EAAE,cAAc;IACnB,GAAG,EAAE,kBAAkB;IACvB,GAAG,EAAE,WAAW;IAChB,GAAG,EAAE,WAAW;IAChB,GAAG,EAAE,sBAAsB;IAC3B,GAAG,EAAE,SAAS;IACd,GAAG,EAAE,UAAU;IACf,GAAG,EAAE,qBAAqB;IAC1B,GAAG,EAAE,mBAAmB;IACxB,GAAG,EAAE,wBAAwB;IAC7B,GAAG,EAAE,uBAAuB;IAC5B,GAAG,EAAE,uBAAuB;IAC5B,GAAG,EAAE,mBAAmB;IACxB,GAAG,EAAE,uBAAuB;IAC5B,GAAG,EAAE,uBAAuB;IAC5B,GAAG,EAAE,iBAAiB;IACtB,GAAG,EAAE,aAAa;IAClB,GAAG,EAAE,qBAAqB;IAC1B,GAAG,EAAE,iBAAiB;CACvB,CAAC;AAgBF;;GAEG;AACH,MAAM,UAAU,uBAAuB,CAAC,EACtC,MAAM,EACN,OAAO,EACP,KAAK,GAKN;IACC,MAAM,IAAI,GAAG,8BAA8B,CAAC,MAAM,CAAC,IAAI,uBAAuB,CAAC;IAE/E,OAAO;QACL,KAAK,EAAE;YACL,IAAI,EAAE;gBACJ,OAAO;gBACP,IAAI,EAAE,uBAAuB,CAAC,IAAI,CAAC;gBACnC,IAAI,EAAE;oBACJ,IAAI;oBACJ,UAAU,EAAE,MAAM;oBAClB,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC;iBAC1C;aACF;SACF;KACF,CAAC;AACJ,CAAC","sourcesContent":["import { TRPC_ERROR_CODES_BY_KEY, type TRPC_ERROR_CODE_KEY } from '@trpc/server/rpc';\n\nexport interface TrpcRequestLike {\n header(name: string): string | undefined;\n originalUrl: string;\n}\n\nexport interface RequestWithHeaders {\n headers: Record<string, string | string[] | undefined>;\n}\n\n/**\n * Returns true for a tRPC HTTP-link request whose procedure is mounted below a `trpc` path segment.\n */\nexport function isTrpcRequest(req: TrpcRequestLike): boolean {\n // This header is spoofable; the URL path is what Express actually uses to dispatch.\n if (req.header('X-TRPC') !== 'true') return false;\n const pathOnly = req.originalUrl.split('?')[0];\n const segments = pathOnly.split('/');\n return segments.at(-2) === 'trpc';\n}\n\n/**\n * Returns true when a request has a multipart/form-data content type.\n *\n * Use this to skip JSON and URL-encoded body parsers for file uploads and tRPC `FormData` inputs.\n * Even when those parsers do not consume multipart data, they can assign an empty `req.body` that\n * prevents the tRPC Express adapter from reading the original stream.\n */\nexport function isMultipartRequest(req: RequestWithHeaders): boolean {\n const contentTypeEntry = Object.entries(req.headers).find(\n ([headerName]) => headerName.toLowerCase() === 'content-type',\n );\n const contentType = contentTypeEntry?.[1];\n const value = Array.isArray(contentType) ? contentType[0] : contentType;\n return value?.toLowerCase().startsWith('multipart/form-data') ?? false;\n}\n\nconst HTTP_STATUS_TO_TRPC_ERROR_CODE: Partial<Record<number, TRPC_ERROR_CODE_KEY>> = {\n 400: 'BAD_REQUEST',\n 401: 'UNAUTHORIZED',\n 402: 'PAYMENT_REQUIRED',\n 403: 'FORBIDDEN',\n 404: 'NOT_FOUND',\n 405: 'METHOD_NOT_SUPPORTED',\n 408: 'TIMEOUT',\n 409: 'CONFLICT',\n 412: 'PRECONDITION_FAILED',\n 413: 'PAYLOAD_TOO_LARGE',\n 415: 'UNSUPPORTED_MEDIA_TYPE',\n 422: 'UNPROCESSABLE_CONTENT',\n 428: 'PRECONDITION_REQUIRED',\n 429: 'TOO_MANY_REQUESTS',\n 499: 'CLIENT_CLOSED_REQUEST',\n 500: 'INTERNAL_SERVER_ERROR',\n 501: 'NOT_IMPLEMENTED',\n 502: 'BAD_GATEWAY',\n 503: 'SERVICE_UNAVAILABLE',\n 504: 'GATEWAY_TIMEOUT',\n};\n\nexport interface TrpcErrorResponse {\n error: {\n json: {\n message: string;\n code: number;\n data: {\n code: TRPC_ERROR_CODE_KEY;\n httpStatus: number;\n stack?: string;\n };\n };\n };\n}\n\n/**\n * Formats an error thrown before the tRPC adapter as a SuperJSON-compatible tRPC response.\n */\nexport function formatTrpcErrorResponse({\n status,\n message,\n stack,\n}: {\n status: number;\n message: string;\n stack?: string;\n}): TrpcErrorResponse {\n const code = HTTP_STATUS_TO_TRPC_ERROR_CODE[status] ?? 'INTERNAL_SERVER_ERROR';\n\n return {\n error: {\n json: {\n message,\n code: TRPC_ERROR_CODES_BY_KEY[code],\n data: {\n code,\n httpStatus: status,\n ...(stack === undefined ? {} : { stack }),\n },\n },\n },\n };\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=express.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.test.d.ts","sourceRoot":"","sources":["../src/express.test.ts"],"names":[],"mappings":"","sourcesContent":["import { TRPC_ERROR_CODES_BY_KEY } from '@trpc/server/rpc';\nimport { assert, describe, it } from 'vitest';\n\nimport { formatTrpcErrorResponse, isMultipartRequest, isTrpcRequest } from './express.js';\n\ndescribe('isTrpcRequest', () => {\n it('requires both the tRPC header and mount path segment', () => {\n assert.isTrue(\n isTrpcRequest({\n header: (name) => (name === 'X-TRPC' ? 'true' : undefined),\n originalUrl: '/pl/course/1/trpc/widgets.list?input=%7B%7D',\n }),\n );\n assert.isFalse(\n isTrpcRequest({\n header: (name) => (name === 'X-TRPC' ? 'true' : undefined),\n originalUrl: '/pl/course/1/not-trpc',\n }),\n );\n });\n});\n\ndescribe('isMultipartRequest', () => {\n it('matches the header name and media type case-insensitively', () => {\n assert.isTrue(\n isMultipartRequest({\n headers: { 'Content-Type': 'Multipart/Form-Data; boundary=example' },\n }),\n );\n });\n\n it('rejects non-multipart and missing content types', () => {\n assert.isFalse(isMultipartRequest({ headers: { 'content-type': 'application/json' } }));\n assert.isFalse(isMultipartRequest({ headers: {} }));\n });\n});\n\ndescribe('formatTrpcErrorResponse', () => {\n it('maps an HTTP status to its tRPC error code', () => {\n const response = formatTrpcErrorResponse({ status: 403, message: 'Request failed' });\n\n assert.equal(response.error.json.code, TRPC_ERROR_CODES_BY_KEY.FORBIDDEN);\n assert.equal(response.error.json.data.code, 'FORBIDDEN');\n assert.equal(response.error.json.data.httpStatus, 403);\n });\n\n it('falls back to INTERNAL_SERVER_ERROR for an unknown status', () => {\n const response = formatTrpcErrorResponse({ status: 418, message: 'Unexpected status' });\n\n assert.equal(response.error.json.code, TRPC_ERROR_CODES_BY_KEY.INTERNAL_SERVER_ERROR);\n assert.equal(response.error.json.data.code, 'INTERNAL_SERVER_ERROR');\n assert.equal(response.error.json.data.httpStatus, 418);\n });\n\n it('includes a stack only when one is supplied', () => {\n const withStack = formatTrpcErrorResponse({\n status: 500,\n message: 'Failed',\n stack: 'stack trace',\n });\n const withoutStack = formatTrpcErrorResponse({ status: 500, message: 'Failed' });\n\n assert.equal(withStack.error.json.data.stack, 'stack trace');\n assert.notProperty(withoutStack.error.json.data, 'stack');\n });\n});\n"]}
@@ -0,0 +1,51 @@
1
+ import { TRPC_ERROR_CODES_BY_KEY } from '@trpc/server/rpc';
2
+ import { assert, describe, it } from 'vitest';
3
+ import { formatTrpcErrorResponse, isMultipartRequest, isTrpcRequest } from './express.js';
4
+ describe('isTrpcRequest', () => {
5
+ it('requires both the tRPC header and mount path segment', () => {
6
+ assert.isTrue(isTrpcRequest({
7
+ header: (name) => (name === 'X-TRPC' ? 'true' : undefined),
8
+ originalUrl: '/pl/course/1/trpc/widgets.list?input=%7B%7D',
9
+ }));
10
+ assert.isFalse(isTrpcRequest({
11
+ header: (name) => (name === 'X-TRPC' ? 'true' : undefined),
12
+ originalUrl: '/pl/course/1/not-trpc',
13
+ }));
14
+ });
15
+ });
16
+ describe('isMultipartRequest', () => {
17
+ it('matches the header name and media type case-insensitively', () => {
18
+ assert.isTrue(isMultipartRequest({
19
+ headers: { 'Content-Type': 'Multipart/Form-Data; boundary=example' },
20
+ }));
21
+ });
22
+ it('rejects non-multipart and missing content types', () => {
23
+ assert.isFalse(isMultipartRequest({ headers: { 'content-type': 'application/json' } }));
24
+ assert.isFalse(isMultipartRequest({ headers: {} }));
25
+ });
26
+ });
27
+ describe('formatTrpcErrorResponse', () => {
28
+ it('maps an HTTP status to its tRPC error code', () => {
29
+ const response = formatTrpcErrorResponse({ status: 403, message: 'Request failed' });
30
+ assert.equal(response.error.json.code, TRPC_ERROR_CODES_BY_KEY.FORBIDDEN);
31
+ assert.equal(response.error.json.data.code, 'FORBIDDEN');
32
+ assert.equal(response.error.json.data.httpStatus, 403);
33
+ });
34
+ it('falls back to INTERNAL_SERVER_ERROR for an unknown status', () => {
35
+ const response = formatTrpcErrorResponse({ status: 418, message: 'Unexpected status' });
36
+ assert.equal(response.error.json.code, TRPC_ERROR_CODES_BY_KEY.INTERNAL_SERVER_ERROR);
37
+ assert.equal(response.error.json.data.code, 'INTERNAL_SERVER_ERROR');
38
+ assert.equal(response.error.json.data.httpStatus, 418);
39
+ });
40
+ it('includes a stack only when one is supplied', () => {
41
+ const withStack = formatTrpcErrorResponse({
42
+ status: 500,
43
+ message: 'Failed',
44
+ stack: 'stack trace',
45
+ });
46
+ const withoutStack = formatTrpcErrorResponse({ status: 500, message: 'Failed' });
47
+ assert.equal(withStack.error.json.data.stack, 'stack trace');
48
+ assert.notProperty(withoutStack.error.json.data, 'stack');
49
+ });
50
+ });
51
+ //# sourceMappingURL=express.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.test.js","sourceRoot":"","sources":["../src/express.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { TRPC_ERROR_CODES_BY_KEY } from '@trpc/server/rpc';\nimport { assert, describe, it } from 'vitest';\n\nimport { formatTrpcErrorResponse, isMultipartRequest, isTrpcRequest } from './express.js';\n\ndescribe('isTrpcRequest', () => {\n it('requires both the tRPC header and mount path segment', () => {\n assert.isTrue(\n isTrpcRequest({\n header: (name) => (name === 'X-TRPC' ? 'true' : undefined),\n originalUrl: '/pl/course/1/trpc/widgets.list?input=%7B%7D',\n }),\n );\n assert.isFalse(\n isTrpcRequest({\n header: (name) => (name === 'X-TRPC' ? 'true' : undefined),\n originalUrl: '/pl/course/1/not-trpc',\n }),\n );\n });\n});\n\ndescribe('isMultipartRequest', () => {\n it('matches the header name and media type case-insensitively', () => {\n assert.isTrue(\n isMultipartRequest({\n headers: { 'Content-Type': 'Multipart/Form-Data; boundary=example' },\n }),\n );\n });\n\n it('rejects non-multipart and missing content types', () => {\n assert.isFalse(isMultipartRequest({ headers: { 'content-type': 'application/json' } }));\n assert.isFalse(isMultipartRequest({ headers: {} }));\n });\n});\n\ndescribe('formatTrpcErrorResponse', () => {\n it('maps an HTTP status to its tRPC error code', () => {\n const response = formatTrpcErrorResponse({ status: 403, message: 'Request failed' });\n\n assert.equal(response.error.json.code, TRPC_ERROR_CODES_BY_KEY.FORBIDDEN);\n assert.equal(response.error.json.data.code, 'FORBIDDEN');\n assert.equal(response.error.json.data.httpStatus, 403);\n });\n\n it('falls back to INTERNAL_SERVER_ERROR for an unknown status', () => {\n const response = formatTrpcErrorResponse({ status: 418, message: 'Unexpected status' });\n\n assert.equal(response.error.json.code, TRPC_ERROR_CODES_BY_KEY.INTERNAL_SERVER_ERROR);\n assert.equal(response.error.json.data.code, 'INTERNAL_SERVER_ERROR');\n assert.equal(response.error.json.data.httpStatus, 418);\n });\n\n it('includes a stack only when one is supplied', () => {\n const withStack = formatTrpcErrorResponse({\n status: 500,\n message: 'Failed',\n stack: 'stack trace',\n });\n const withoutStack = formatTrpcErrorResponse({ status: 500, message: 'Failed' });\n\n assert.equal(withStack.error.json.data.stack, 'stack trace');\n assert.notProperty(withoutStack.error.json.data, 'stack');\n });\n});\n"]}
@@ -0,0 +1,26 @@
1
+ import { type QueryClient } from '@tanstack/react-query';
2
+ import { type ReactNode } from 'react';
3
+ import { type AlertProps } from 'react-bootstrap';
4
+ import { type AppErrorRenderers } from './client.js';
5
+ declare global {
6
+ interface Window {
7
+ __TANSTACK_QUERY_CLIENT__?: QueryClient;
8
+ }
9
+ }
10
+ /** Renders an application error inside a Bootstrap alert. */
11
+ export declare function AppErrorAlert<E extends {
12
+ code: string;
13
+ message: string;
14
+ }>({ error, onDismiss, variant, className, render }: {
15
+ error: E | null | undefined;
16
+ onDismiss?: () => void;
17
+ variant?: AlertProps['variant'];
18
+ className?: string;
19
+ render: AppErrorRenderers<E>;
20
+ }): import("react").JSX.Element | null;
21
+ /** Provides a TanStack Query client and exposes it to browser devtools. */
22
+ export declare function QueryClientProviderDebug({ client, children }: {
23
+ client: QueryClient;
24
+ children: ReactNode;
25
+ }): import("react").JSX.Element;
26
+ //# sourceMappingURL=react.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAuB,MAAM,uBAAuB,CAAC;AAC9E,OAAO,EAAE,KAAK,SAAS,EAAa,MAAM,OAAO,CAAC;AAClD,OAAO,EAAS,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,EAAE,KAAK,iBAAiB,EAAkB,MAAM,aAAa,CAAC;AAErE,OAAO,CAAC,MAAM,CAAC,CAAC;IACd,UAAU,MAAM;QACd,yBAAyB,CAAC,EAAE,WAAW,CAAC;KACzC;CACF;AAED,6DAA6D;AAC7D,wBAAgB,aAAa,CAAC,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EAAE,EACzE,KAAK,EACL,SAAS,EACT,OAAkB,EAClB,SAAS,EACT,MAAM,EACP,EAAE;IACD,KAAK,EAAE,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,OAAO,CAAC,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC;CAC9B,sCAQA;AAED,2EAA2E;AAC3E,wBAAgB,wBAAwB,CAAC,EACvC,MAAM,EACN,QAAQ,EACT,EAAE;IACD,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;CACrB,+BAUA","sourcesContent":["import { type QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport { type ReactNode, useEffect } from 'react';\nimport { Alert, type AlertProps } from 'react-bootstrap';\n\nimport { type AppErrorRenderers, renderAppError } from './client.js';\n\ndeclare global {\n interface Window {\n __TANSTACK_QUERY_CLIENT__?: QueryClient;\n }\n}\n\n/** Renders an application error inside a Bootstrap alert. */\nexport function AppErrorAlert<E extends { code: string; message: string }>({\n error,\n onDismiss,\n variant = 'danger',\n className,\n render,\n}: {\n error: E | null | undefined;\n onDismiss?: () => void;\n variant?: AlertProps['variant'];\n className?: string;\n render: AppErrorRenderers<E>;\n}) {\n if (!error) return null;\n\n return (\n <Alert variant={variant} dismissible={!!onDismiss} className={className} onClose={onDismiss}>\n {renderAppError(error, render)}\n </Alert>\n );\n}\n\n/** Provides a TanStack Query client and exposes it to browser devtools. */\nexport function QueryClientProviderDebug({\n client,\n children,\n}: {\n client: QueryClient;\n children: ReactNode;\n}) {\n // Expose the page's query client after mount so the browser extension can discover it.\n useEffect(() => {\n const browserWindow = (globalThis as { window?: Window }).window;\n if (!browserWindow) return;\n\n browserWindow.__TANSTACK_QUERY_CLIENT__ = client;\n }, [client]);\n\n return <QueryClientProvider client={client}>{children}</QueryClientProvider>;\n}\n"]}
package/dist/react.js ADDED
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { QueryClientProvider } from '@tanstack/react-query';
3
+ import { useEffect } from 'react';
4
+ import { Alert } from 'react-bootstrap';
5
+ import { renderAppError } from './client.js';
6
+ /** Renders an application error inside a Bootstrap alert. */
7
+ export function AppErrorAlert({ error, onDismiss, variant = 'danger', className, render, }) {
8
+ if (!error)
9
+ return null;
10
+ return (_jsx(Alert, { variant: variant, dismissible: !!onDismiss, className: className, onClose: onDismiss, children: renderAppError(error, render) }));
11
+ }
12
+ /** Provides a TanStack Query client and exposes it to browser devtools. */
13
+ export function QueryClientProviderDebug({ client, children, }) {
14
+ // Expose the page's query client after mount so the browser extension can discover it.
15
+ useEffect(() => {
16
+ const browserWindow = globalThis.window;
17
+ if (!browserWindow)
18
+ return;
19
+ browserWindow.__TANSTACK_QUERY_CLIENT__ = client;
20
+ }, [client]);
21
+ return _jsx(QueryClientProvider, { client: client, children: children });
22
+ }
23
+ //# sourceMappingURL=react.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.js","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAoB,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC9E,OAAO,EAAkB,SAAS,EAAE,MAAM,OAAO,CAAC;AAClD,OAAO,EAAE,KAAK,EAAmB,MAAM,iBAAiB,CAAC;AAEzD,OAAO,EAA0B,cAAc,EAAE,MAAM,aAAa,CAAC;AAQrE,6DAA6D;AAC7D,MAAM,UAAU,aAAa,CAA8C,EACzE,KAAK,EACL,SAAS,EACT,OAAO,GAAG,QAAQ,EAClB,SAAS,EACT,MAAM,GAOP;IACC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IAExB,OAAO,CACL,KAAC,KAAK,IAAC,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,CAAC,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,YACxF,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,GACxB,CACT,CAAC;AACJ,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,wBAAwB,CAAC,EACvC,MAAM,EACN,QAAQ,GAIT;IACC,uFAAuF;IACvF,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,aAAa,GAAI,UAAkC,CAAC,MAAM,CAAC;QACjE,IAAI,CAAC,aAAa;YAAE,OAAO;QAE3B,aAAa,CAAC,yBAAyB,GAAG,MAAM,CAAC;IACnD,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAEb,OAAO,KAAC,mBAAmB,IAAC,MAAM,EAAE,MAAM,YAAG,QAAQ,GAAuB,CAAC;AAC/E,CAAC","sourcesContent":["import { type QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport { type ReactNode, useEffect } from 'react';\nimport { Alert, type AlertProps } from 'react-bootstrap';\n\nimport { type AppErrorRenderers, renderAppError } from './client.js';\n\ndeclare global {\n interface Window {\n __TANSTACK_QUERY_CLIENT__?: QueryClient;\n }\n}\n\n/** Renders an application error inside a Bootstrap alert. */\nexport function AppErrorAlert<E extends { code: string; message: string }>({\n error,\n onDismiss,\n variant = 'danger',\n className,\n render,\n}: {\n error: E | null | undefined;\n onDismiss?: () => void;\n variant?: AlertProps['variant'];\n className?: string;\n render: AppErrorRenderers<E>;\n}) {\n if (!error) return null;\n\n return (\n <Alert variant={variant} dismissible={!!onDismiss} className={className} onClose={onDismiss}>\n {renderAppError(error, render)}\n </Alert>\n );\n}\n\n/** Provides a TanStack Query client and exposes it to browser devtools. */\nexport function QueryClientProviderDebug({\n client,\n children,\n}: {\n client: QueryClient;\n children: ReactNode;\n}) {\n // Expose the page's query client after mount so the browser extension can discover it.\n useEffect(() => {\n const browserWindow = (globalThis as { window?: Window }).window;\n if (!browserWindow) return;\n\n browserWindow.__TANSTACK_QUERY_CLIENT__ = client;\n }, [client]);\n\n return <QueryClientProvider client={client}>{children}</QueryClientProvider>;\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=react.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.test.d.ts","sourceRoot":"","sources":["../src/react.test.tsx"],"names":[],"mappings":"","sourcesContent":["// @vitest-environment jsdom\n\nimport { QueryClient } from '@tanstack/react-query';\nimport { act } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport { renderToStaticMarkup } from 'react-dom/server';\nimport { afterEach, assert, describe, it } from 'vitest';\n\nimport { AppErrorAlert, QueryClientProviderDebug } from './react.js';\n\nconst testGlobal = globalThis as typeof globalThis & {\n document: { createElement(tagName: string): Parameters<typeof createRoot>[0] };\n IS_REACT_ACT_ENVIRONMENT?: boolean;\n window: Window;\n};\n\nafterEach(() => {\n delete testGlobal.window.__TANSTACK_QUERY_CLIENT__;\n delete testGlobal.IS_REACT_ACT_ENVIRONMENT;\n});\n\ndescribe('AppErrorAlert', () => {\n it('renders an error from a JSX element', () => {\n const error = { code: 'EXPECTED' as const, message: 'Rendered application error' };\n const markup = renderToStaticMarkup(\n <AppErrorAlert\n error={error}\n render={{ EXPECTED: ({ message }) => <span>{message}</span> }}\n />,\n );\n\n assert.include(markup, 'Rendered application error');\n });\n});\n\ndescribe('QueryClientProviderDebug', () => {\n it('exposes the client to browser devtools', async () => {\n testGlobal.IS_REACT_ACT_ENVIRONMENT = true;\n const client = new QueryClient();\n const root = createRoot(testGlobal.document.createElement('div'));\n\n assert.isUndefined(testGlobal.window.__TANSTACK_QUERY_CLIENT__);\n await act(async () => {\n root.render(\n <QueryClientProviderDebug client={client}>\n <span>Child</span>\n </QueryClientProviderDebug>,\n );\n });\n assert.strictEqual(testGlobal.window.__TANSTACK_QUERY_CLIENT__, client);\n\n await act(async () => root.unmount());\n client.clear();\n });\n});\n"]}
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // @vitest-environment jsdom
3
+ import { QueryClient } from '@tanstack/react-query';
4
+ import { act } from 'react';
5
+ import { createRoot } from 'react-dom/client';
6
+ import { renderToStaticMarkup } from 'react-dom/server';
7
+ import { afterEach, assert, describe, it } from 'vitest';
8
+ import { AppErrorAlert, QueryClientProviderDebug } from './react.js';
9
+ const testGlobal = globalThis;
10
+ afterEach(() => {
11
+ delete testGlobal.window.__TANSTACK_QUERY_CLIENT__;
12
+ delete testGlobal.IS_REACT_ACT_ENVIRONMENT;
13
+ });
14
+ describe('AppErrorAlert', () => {
15
+ it('renders an error from a JSX element', () => {
16
+ const error = { code: 'EXPECTED', message: 'Rendered application error' };
17
+ const markup = renderToStaticMarkup(_jsx(AppErrorAlert, { error: error, render: { EXPECTED: ({ message }) => _jsx("span", { children: message }) } }));
18
+ assert.include(markup, 'Rendered application error');
19
+ });
20
+ });
21
+ describe('QueryClientProviderDebug', () => {
22
+ it('exposes the client to browser devtools', async () => {
23
+ testGlobal.IS_REACT_ACT_ENVIRONMENT = true;
24
+ const client = new QueryClient();
25
+ const root = createRoot(testGlobal.document.createElement('div'));
26
+ assert.isUndefined(testGlobal.window.__TANSTACK_QUERY_CLIENT__);
27
+ await act(async () => {
28
+ root.render(_jsx(QueryClientProviderDebug, { client: client, children: _jsx("span", { children: "Child" }) }));
29
+ });
30
+ assert.strictEqual(testGlobal.window.__TANSTACK_QUERY_CLIENT__, client);
31
+ await act(async () => root.unmount());
32
+ client.clear();
33
+ });
34
+ });
35
+ //# sourceMappingURL=react.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.test.js","sourceRoot":"","sources":["../src/react.test.tsx"],"names":[],"mappings":";AAAA,4BAA4B;AAE5B,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAC5B,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAEzD,OAAO,EAAE,aAAa,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAErE,MAAM,UAAU,GAAG,UAIlB,CAAC;AAEF,SAAS,CAAC,GAAG,EAAE;IACb,OAAO,UAAU,CAAC,MAAM,CAAC,yBAAyB,CAAC;IACnD,OAAO,UAAU,CAAC,wBAAwB,CAAC;AAC7C,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,KAAK,GAAG,EAAE,IAAI,EAAE,UAAmB,EAAE,OAAO,EAAE,4BAA4B,EAAE,CAAC;QACnF,MAAM,MAAM,GAAG,oBAAoB,CACjC,KAAC,aAAa,IACZ,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,yBAAO,OAAO,GAAQ,EAAE,GAC7D,CACH,CAAC;QAEF,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,4BAA4B,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,0BAA0B,EAAE,GAAG,EAAE;IACxC,EAAE,CAAC,wCAAwC,EAAE,KAAK,IAAI,EAAE;QACtD,UAAU,CAAC,wBAAwB,GAAG,IAAI,CAAC;QAC3C,MAAM,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,UAAU,CAAC,UAAU,CAAC,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC;QAElE,MAAM,CAAC,WAAW,CAAC,UAAU,CAAC,MAAM,CAAC,yBAAyB,CAAC,CAAC;QAChE,MAAM,GAAG,CAAC,KAAK,IAAI,EAAE;YACnB,IAAI,CAAC,MAAM,CACT,KAAC,wBAAwB,IAAC,MAAM,EAAE,MAAM,YACtC,mCAAkB,GACO,CAC5B,CAAC;QACJ,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,WAAW,CAAC,UAAU,CAAC,MAAM,CAAC,yBAAyB,EAAE,MAAM,CAAC,CAAC;QAExE,MAAM,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QACtC,MAAM,CAAC,KAAK,EAAE,CAAC;IACjB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["// @vitest-environment jsdom\n\nimport { QueryClient } from '@tanstack/react-query';\nimport { act } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport { renderToStaticMarkup } from 'react-dom/server';\nimport { afterEach, assert, describe, it } from 'vitest';\n\nimport { AppErrorAlert, QueryClientProviderDebug } from './react.js';\n\nconst testGlobal = globalThis as typeof globalThis & {\n document: { createElement(tagName: string): Parameters<typeof createRoot>[0] };\n IS_REACT_ACT_ENVIRONMENT?: boolean;\n window: Window;\n};\n\nafterEach(() => {\n delete testGlobal.window.__TANSTACK_QUERY_CLIENT__;\n delete testGlobal.IS_REACT_ACT_ENVIRONMENT;\n});\n\ndescribe('AppErrorAlert', () => {\n it('renders an error from a JSX element', () => {\n const error = { code: 'EXPECTED' as const, message: 'Rendered application error' };\n const markup = renderToStaticMarkup(\n <AppErrorAlert\n error={error}\n render={{ EXPECTED: ({ message }) => <span>{message}</span> }}\n />,\n );\n\n assert.include(markup, 'Rendered application error');\n });\n});\n\ndescribe('QueryClientProviderDebug', () => {\n it('exposes the client to browser devtools', async () => {\n testGlobal.IS_REACT_ACT_ENVIRONMENT = true;\n const client = new QueryClient();\n const root = createRoot(testGlobal.document.createElement('div'));\n\n assert.isUndefined(testGlobal.window.__TANSTACK_QUERY_CLIENT__);\n await act(async () => {\n root.render(\n <QueryClientProviderDebug client={client}>\n <span>Child</span>\n </QueryClientProviderDebug>,\n );\n });\n assert.strictEqual(testGlobal.window.__TANSTACK_QUERY_CLIENT__, client);\n\n await act(async () => root.unmount());\n client.clear();\n });\n});\n"]}