@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.
- package/CHANGELOG.md +7 -0
- package/LICENSE +683 -0
- package/README.md +16 -36
- package/dist/client.d.ts +47 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +29 -0
- package/dist/client.js.map +1 -0
- package/dist/client.test.d.ts +2 -0
- package/dist/client.test.d.ts.map +1 -0
- package/dist/client.test.js +61 -0
- package/dist/client.test.js.map +1 -0
- package/dist/express.d.ts +42 -0
- package/dist/express.d.ts.map +1 -0
- package/dist/express.js +67 -0
- package/dist/express.js.map +1 -0
- package/dist/express.test.d.ts +2 -0
- package/dist/express.test.d.ts.map +1 -0
- package/dist/express.test.js +51 -0
- package/dist/express.test.js.map +1 -0
- package/dist/react.d.ts +26 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +23 -0
- package/dist/react.js.map +1 -0
- package/dist/react.test.d.ts +2 -0
- package/dist/react.test.d.ts.map +1 -0
- package/dist/react.test.js +35 -0
- package/dist/react.test.js.map +1 -0
- package/dist/server.d.ts +46 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +31 -0
- package/dist/server.js.map +1 -0
- package/dist/server.test.d.ts +2 -0
- package/dist/server.test.d.ts.map +1 -0
- package/dist/server.test.js +34 -0
- package/dist/server.test.js.map +1 -0
- package/package.json +43 -8
- package/src/client.test.tsx +84 -0
- package/src/client.ts +54 -0
- package/src/express.test.ts +66 -0
- package/src/express.ts +103 -0
- package/src/react.test.tsx +55 -0
- package/src/react.tsx +53 -0
- package/src/server.test.ts +51 -0
- package/src/server.ts +77 -0
- package/tsconfig.json +6 -0
- package/vitest.config.ts +11 -0
package/README.md
CHANGED
|
@@ -1,45 +1,25 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@prairielearn/trpc`
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12
|
+
## Typed error maps
|
|
8
13
|
|
|
9
|
-
|
|
14
|
+
The server and client intentionally resolve a whole procedure error map differently:
|
|
10
15
|
|
|
11
|
-
This
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21
|
+
## Express errors before the adapter
|
|
19
22
|
|
|
20
|
-
|
|
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
|
-
|
|
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.
|
package/dist/client.d.ts
ADDED
|
@@ -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 @@
|
|
|
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"]}
|
package/dist/express.js
ADDED
|
@@ -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 @@
|
|
|
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":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE1F,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAC9D,MAAM,CAAC,MAAM,CACX,aAAa,CAAC;YACZ,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;YAC1D,WAAW,EAAE,6CAA6C;SAC3D,CAAC,CACH,CAAC;QACF,MAAM,CAAC,OAAO,CACZ,aAAa,CAAC;YACZ,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;YAC1D,WAAW,EAAE,uBAAuB;SACrC,CAAC,CACH,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;IAClC,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,CAAC,MAAM,CACX,kBAAkB,CAAC;YACjB,OAAO,EAAE,EAAE,cAAc,EAAE,uCAAuC,EAAE;SACrE,CAAC,CACH,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,EAAE,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,EAAE,CAAC,CAAC,CAAC;QACxF,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;IACtD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACvC,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACpD,MAAM,QAAQ,GAAG,uBAAuB,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC,CAAC;QAErF,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,uBAAuB,CAAC,SAAS,CAAC,CAAC;QAC1E,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;QACzD,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,QAAQ,GAAG,uBAAuB,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,mBAAmB,EAAE,CAAC,CAAC;QAExF,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,uBAAuB,CAAC,qBAAqB,CAAC,CAAC;QACtF,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,uBAAuB,CAAC,CAAC;QACrE,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACpD,MAAM,SAAS,GAAG,uBAAuB,CAAC;YACxC,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,QAAQ;YACjB,KAAK,EAAE,aAAa;SACrB,CAAC,CAAC;QACH,MAAM,YAAY,GAAG,uBAAuB,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;QAEjF,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC;QAC7D,MAAM,CAAC,WAAW,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","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"]}
|
package/dist/react.d.ts
ADDED
|
@@ -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 @@
|
|
|
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"]}
|