@intrig/plugin-react 0.0.1 → 0.0.2-2

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 (58) hide show
  1. package/dist/index.cjs +4260 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +4235 -0
  4. package/package.json +6 -3
  5. package/.swcrc +0 -29
  6. package/README.md +0 -7
  7. package/eslint.config.mjs +0 -19
  8. package/project.json +0 -29
  9. package/rollup.config.cjs +0 -54
  10. package/rollup.config.mjs +0 -33
  11. package/src/index.ts +0 -2
  12. package/src/lib/code-generator.ts +0 -79
  13. package/src/lib/get-endpoint-documentation.ts +0 -35
  14. package/src/lib/get-schema-documentation.ts +0 -11
  15. package/src/lib/internal-types.ts +0 -15
  16. package/src/lib/plugin-react.ts +0 -22
  17. package/src/lib/templates/context.template.ts +0 -74
  18. package/src/lib/templates/docs/__snapshots__/async-hook.spec.ts.snap +0 -889
  19. package/src/lib/templates/docs/__snapshots__/download-hook.spec.ts.snap +0 -1445
  20. package/src/lib/templates/docs/__snapshots__/react-hook.spec.ts.snap +0 -1371
  21. package/src/lib/templates/docs/__snapshots__/sse-hook.spec.ts.snap +0 -2008
  22. package/src/lib/templates/docs/async-hook.spec.ts +0 -92
  23. package/src/lib/templates/docs/async-hook.ts +0 -226
  24. package/src/lib/templates/docs/download-hook.spec.ts +0 -182
  25. package/src/lib/templates/docs/download-hook.ts +0 -170
  26. package/src/lib/templates/docs/react-hook.spec.ts +0 -97
  27. package/src/lib/templates/docs/react-hook.ts +0 -323
  28. package/src/lib/templates/docs/schema.ts +0 -105
  29. package/src/lib/templates/docs/sse-hook.spec.ts +0 -207
  30. package/src/lib/templates/docs/sse-hook.ts +0 -221
  31. package/src/lib/templates/extra.template.ts +0 -198
  32. package/src/lib/templates/index.template.ts +0 -14
  33. package/src/lib/templates/intrigMiddleware.template.ts +0 -21
  34. package/src/lib/templates/logger.template.ts +0 -67
  35. package/src/lib/templates/media-type-utils.template.ts +0 -191
  36. package/src/lib/templates/network-state.template.ts +0 -702
  37. package/src/lib/templates/packageJson.template.ts +0 -63
  38. package/src/lib/templates/provider/__tests__/provider-templates.spec.ts +0 -209
  39. package/src/lib/templates/provider/axios-config.template.ts +0 -49
  40. package/src/lib/templates/provider/hooks.template.ts +0 -240
  41. package/src/lib/templates/provider/interfaces.template.ts +0 -72
  42. package/src/lib/templates/provider/intrig-provider-stub.template.ts +0 -73
  43. package/src/lib/templates/provider/intrig-provider.template.ts +0 -185
  44. package/src/lib/templates/provider/main.template.ts +0 -48
  45. package/src/lib/templates/provider/reducer.template.ts +0 -50
  46. package/src/lib/templates/provider/status-trap.template.ts +0 -80
  47. package/src/lib/templates/provider.template.ts +0 -698
  48. package/src/lib/templates/source/controller/method/asyncFunctionHook.template.ts +0 -196
  49. package/src/lib/templates/source/controller/method/clientIndex.template.ts +0 -38
  50. package/src/lib/templates/source/controller/method/download.template.ts +0 -256
  51. package/src/lib/templates/source/controller/method/params.template.ts +0 -31
  52. package/src/lib/templates/source/controller/method/requestHook.template.ts +0 -220
  53. package/src/lib/templates/source/type/typeTemplate.ts +0 -257
  54. package/src/lib/templates/swcrc.template.ts +0 -25
  55. package/src/lib/templates/tsconfig.template.ts +0 -37
  56. package/src/lib/templates/type-utils.template.ts +0 -28
  57. package/tsconfig.json +0 -13
  58. package/tsconfig.lib.json +0 -20
@@ -1,92 +0,0 @@
1
- import { reactAsyncFunctionHookDocs } from './async-hook';
2
-
3
- // We use a plain object cast to any to avoid direct dependency on the types from "@intrig/plugin-sdk" in test.
4
-
5
- describe('reactAsyncFunctionHookDocs', () => {
6
- it('snapshot — simple REST descriptor (no body, no path params)', async () => {
7
- const descriptor = {
8
- id: '1',
9
- name: 'validateUsername',
10
- type: 'rest' as const,
11
- source: 'demo_api',
12
- path: 'users',
13
- data: {
14
- method: 'GET',
15
- paths: ['/api/users/validate-username'],
16
- operationId: 'validateUsername',
17
- // no requestBody
18
- // no variables
19
- },
20
- };
21
-
22
- const result = await reactAsyncFunctionHookDocs(descriptor as any);
23
-
24
- expect(result.path).toBe('async-hook.md');
25
- expect(result.content).toMatchSnapshot();
26
- });
27
-
28
- it('snapshot — request body only (no path params)', async () => {
29
- const descriptor = {
30
- id: '2',
31
- name: 'checkPasswordStrength',
32
- type: 'rest' as const,
33
- source: 'demo_api',
34
- path: 'security',
35
- data: {
36
- method: 'POST',
37
- paths: ['/api/security/check-password'],
38
- operationId: 'checkPasswordStrength',
39
- requestBody: 'CheckPasswordStrengthRequest',
40
- },
41
- };
42
-
43
- const result = await reactAsyncFunctionHookDocs(descriptor as any);
44
- expect(result.path).toBe('async-hook.md');
45
- expect(result.content).toMatchSnapshot();
46
- });
47
-
48
- it('snapshot — path params only (no request body)', async () => {
49
- const descriptor = {
50
- id: '3',
51
- name: 'verifyUserById',
52
- type: 'rest' as const,
53
- source: 'demo_api',
54
- path: 'users/{id}',
55
- data: {
56
- method: 'GET',
57
- paths: ['/api/users/{id}/verify'],
58
- operationId: 'verifyUserById',
59
- variables: [
60
- { name: 'id', in: 'path', ref: '#/components/schemas/Id' },
61
- ],
62
- },
63
- };
64
-
65
- const result = await reactAsyncFunctionHookDocs(descriptor as any);
66
- expect(result.path).toBe('async-hook.md');
67
- expect(result.content).toMatchSnapshot();
68
- });
69
-
70
- it('snapshot — request body and path params', async () => {
71
- const descriptor = {
72
- id: '4',
73
- name: 'recalculateUserScore',
74
- type: 'rest' as const,
75
- source: 'demo_api',
76
- path: 'users/{id}',
77
- data: {
78
- method: 'PUT',
79
- paths: ['/api/users/{id}/score'],
80
- operationId: 'recalculateUserScore',
81
- requestBody: 'RecalculateUserScoreRequest',
82
- variables: [
83
- { name: 'id', in: 'PATH', ref: '#/components/schemas/Id' },
84
- ],
85
- },
86
- };
87
-
88
- const result = await reactAsyncFunctionHookDocs(descriptor as any);
89
- expect(result.path).toBe('async-hook.md');
90
- expect(result.content).toMatchSnapshot();
91
- });
92
- });
@@ -1,226 +0,0 @@
1
- import { camelCase, mdLiteral, pascalCase, ResourceDescriptor, RestData } from "@intrig/plugin-sdk"
2
-
3
- export function reactAsyncFunctionHookDocs(descriptor: ResourceDescriptor<RestData>) {
4
- const md = mdLiteral('async-hook.md')
5
-
6
- // ===== Derived names (preserve these) =====
7
- const hasPathParams = (descriptor.data.variables ?? []).some(
8
- (v: any) => v.in?.toUpperCase() === 'PATH',
9
- )
10
-
11
- const actionName = camelCase(descriptor.name) // e.g. getUser
12
- const abortName = `abort${pascalCase(descriptor.name)}` // e.g. abortGetUser
13
-
14
- const requestBodyVar = descriptor.data.requestBody
15
- ? camelCase(descriptor.data.requestBody) // e.g. createUser
16
- : undefined
17
- const requestBodyType = descriptor.data.requestBody
18
- ? pascalCase(descriptor.data.requestBody) // e.g. CreateUser
19
- : undefined
20
-
21
- const paramsVar = hasPathParams ? `${actionName}Params` : undefined // e.g. getUserParams
22
- const paramsType = hasPathParams ? `${pascalCase(descriptor.name)}Params` : undefined // e.g. GetUserParams
23
- const responseTypeName = `${pascalCase(descriptor.name)}ResponseBody` // e.g. GetUserResponseBody
24
-
25
- const callArgs = [requestBodyVar, paramsVar].filter(Boolean).join(', ')
26
-
27
- return md`
28
- # Intrig Async Hooks — Quick Guide
29
-
30
- ## Copy-paste starter (fast lane)
31
-
32
- ### 1) Hook import
33
- ${"```ts"}
34
- import { use${pascalCase(descriptor.name)}Async } from '@intrig/react/${descriptor.path}/client';
35
- ${"```"}
36
-
37
- ### 2) Create an instance
38
- ${"```ts"}
39
- const [${actionName}, ${abortName}] = use${pascalCase(descriptor.name)}Async();
40
- ${"```"}
41
-
42
- ### 3) Call it (awaitable)
43
- ${"```ts"}
44
- // body?, params? — pass what your endpoint needs (order: body, params)
45
- await ${actionName}(${callArgs});
46
- ${"```"}
47
-
48
- Async hooks are for one-off, low-friction calls (e.g., validations, submissions). They return an **awaitable function** plus an **abort** function. No NetworkState.
49
-
50
- ---
51
-
52
- ## TL;DR (copy–paste)
53
- ${"```tsx"}
54
- import { use${pascalCase(descriptor.name)}Async } from '@intrig/react/${descriptor.path}/client';
55
- import { useCallback, useEffect } from 'react';
56
-
57
- export default function Example() {
58
- const [${actionName}, ${abortName}] = use${pascalCase(descriptor.name)}Async();
59
-
60
- const run = useCallback(async () => {
61
- try {
62
- const result = await ${actionName}(${callArgs});
63
- // do something with result
64
- console.log(result);
65
- } catch (e) {
66
- // request failed or was aborted
67
- console.error(e);
68
- }
69
- }, [${actionName}]);
70
-
71
- // Optional: abort on unmount
72
- useEffect(() => ${abortName}, [${abortName}]);
73
-
74
- return <button onClick={run}>Call</button>;
75
- }
76
- ${"```"}
77
-
78
- ${requestBodyType || paramsType ? `### Optional types (if generated by your build)
79
- ${"```ts"}
80
- ${requestBodyType ? `import type { ${requestBodyType} } from '@intrig/react/${descriptor.source}/components/schemas/${requestBodyType}';
81
- ` : ''}${paramsType ? `import type { ${paramsType} } from '@intrig/react/${descriptor.path}/${pascalCase(descriptor.name)}.params';
82
- ` : ''}import type { ${responseTypeName} } from '@intrig/react/${descriptor.path}/${pascalCase(descriptor.name)}.response';
83
- ${"```"}
84
- ` : ''}
85
-
86
- ---
87
-
88
- ## Hook API
89
- ${"```ts"}
90
- // Prefer concrete types if your build emits them:
91
- // import type { ${responseTypeName} } from '@intrig/react/${descriptor.path}/${pascalCase(descriptor.name)}.response';
92
- // ${paramsType ? `import type { ${paramsType} } from '@intrig/react/${descriptor.path}/${pascalCase(descriptor.name)}.params';` : ''}
93
-
94
- type ${pascalCase(descriptor.name)}Data = ${'unknown'}; // replace with ${responseTypeName} if generated
95
- type ${pascalCase(descriptor.name)}Request = {
96
- body?: ${requestBodyType ?? 'unknown'};
97
- params?: ${paramsType ?? 'unknown'};
98
- };
99
-
100
- // Signature (shape shown; return type depends on your endpoint)
101
- declare function use${pascalCase(descriptor.name)}Async(): [
102
- (body?: ${pascalCase(descriptor.name)}Request['body'], params?: ${pascalCase(descriptor.name)}Request['params']) => Promise<${pascalCase(descriptor.name)}Data>,
103
- () => void // abort
104
- ];
105
- ${"```"}
106
-
107
- ### Why async hooks?
108
- - **No state machine:** just \`await\` the result.
109
- - **Great for validations & submits:** uniqueness checks, field-level checks, updates.
110
- - **Abortable:** cancel in-flight work on demand.
111
-
112
- ---
113
-
114
- ## Usage Patterns
115
-
116
- ### 1) Simple try/catch (recommended)
117
- ${"```tsx"}
118
- const [${actionName}] = use${pascalCase(descriptor.name)}Async();
119
-
120
- try {
121
- const res = await ${actionName}(${callArgs});
122
- // use res
123
- } catch (e) {
124
- // network error or abort
125
- }
126
- ${"```"}
127
-
128
- <details><summary>Description</summary>
129
- <p><strong>Use when</strong> you just need the value or an error. Ideal for validators, uniqueness checks, or quick lookups.</p>
130
- </details>
131
-
132
- ### 2) Abort on unmount (safe cleanup)
133
- ${"```tsx"}
134
- const [${actionName}, ${abortName}] = use${pascalCase(descriptor.name)}Async();
135
-
136
- useEffect(() => ${abortName}, [${abortName}]);
137
- ${"```"}
138
-
139
- <details><summary>Description</summary>
140
- <p><strong>Use when</strong> the component may unmount while a request is in-flight (route changes, conditional UI).</p>
141
- </details>
142
-
143
- ### 3) Debounced validation (e.g., on input change)
144
- ${"```tsx"}
145
- const [${actionName}, ${abortName}] = use${pascalCase(descriptor.name)}Async();
146
-
147
- const onChange = useMemo(() => {
148
- let t: any;
149
- return (${requestBodyVar ? `${requestBodyVar}: ${requestBodyType ?? 'any'}` : 'value: string'}) => {
150
- clearTimeout(t);
151
- t = setTimeout(async () => {
152
- try {
153
- // Optionally abort before firing a new request
154
- ${abortName}();
155
- await ${actionName}(${[requestBodyVar ?? '/* body from value */', paramsVar ?? '/* params? */'].join(', ')});
156
- } catch {}
157
- }, 250);
158
- };
159
- }, [${actionName}, ${abortName}]);
160
- ${"```"}
161
-
162
- <details><summary>Description</summary>
163
- <p><strong>Use when</strong> validating as the user types. Debounce to reduce chatter; consider <code>${abortName}()</code> before firing a new call.</p>
164
- </details>
165
-
166
- ### 4) Guard against races (latest-only)
167
- ${"```tsx"}
168
- const [${actionName}, ${abortName}] = use${pascalCase(descriptor.name)}Async();
169
-
170
- const latestOnly = async () => {
171
- ${abortName}();
172
- return ${actionName}(${callArgs});
173
- };
174
- ${"```"}
175
-
176
- <details><summary>Description</summary>
177
- <p><strong>Use when</strong> only the most recent call should win (search suggestions, live filters).</p>
178
- </details>
179
-
180
- ---
181
-
182
- ## Full example
183
- ${"```tsx"}
184
- import { use${pascalCase(descriptor.name)}Async } from '@intrig/react/${descriptor.path}/client';
185
- import { useCallback } from 'react';
186
-
187
- function MyComponent() {
188
- const [${actionName}, ${abortName}] = use${pascalCase(descriptor.name)}Async();
189
-
190
- const run = useCallback(async () => {
191
- try {
192
- const data = await ${actionName}(${callArgs});
193
- alert(JSON.stringify(data));
194
- } catch (e) {
195
- console.error('Call failed/aborted', e);
196
- }
197
- }, [${actionName}]);
198
-
199
- return (
200
- <>
201
- <button onClick={run}>Call remote</button>
202
- <button onClick={${abortName}}>Abort</button>
203
- </>
204
- );
205
- }
206
- ${"```"}
207
-
208
- ---
209
-
210
- ## Gotchas & Tips
211
- - **No \`NetworkState\`:** async hooks return a Promise, not a state machine.
212
- - **Abort:** always available; call it to cancel the latest in-flight request.
213
- - **Errors:** wrap calls with \`try/catch\` to handle network failures or abort errors.
214
- - **Debounce & throttle:** combine with timers to cut down chatter for typeahead/validators.
215
- - **Types:** prefer generated \`${responseTypeName}\` and \`${paramsType ?? '...Params'}\` if your build emits them.
216
-
217
- ---
218
-
219
- ## Reference: Minimal cheat sheet
220
- ${"```ts"}
221
- const [fn, abort] = use${pascalCase(descriptor.name)}Async();
222
- await fn(${callArgs});
223
- abort(); // optional
224
- ${"```"}
225
- `
226
- }
@@ -1,182 +0,0 @@
1
- import { reactDownloadHookDocs } from './download-hook';
2
-
3
- // We use a plain object cast to any to avoid direct dependency on the types from "@intrig/plugin-sdk" in test.
4
-
5
- describe('reactDownloadHookDocs', () => {
6
- it('snapshot — simple REST descriptor (no body, no path params)', async () => {
7
- const descriptor = {
8
- id: '1',
9
- name: 'downloadReport',
10
- type: 'rest' as const,
11
- source: 'demo_api',
12
- path: 'reports',
13
- data: {
14
- method: 'GET',
15
- paths: ['/api/reports/download'],
16
- operationId: 'downloadReport',
17
- // no requestBody
18
- // no variables
19
- },
20
- };
21
-
22
- const result = await reactDownloadHookDocs(descriptor as any);
23
-
24
- expect(result.path).toBe('download-hook.md');
25
- expect(result.content).toMatchSnapshot();
26
- });
27
-
28
- it('snapshot — request body only (no path params)', async () => {
29
- const descriptor = {
30
- id: '2',
31
- name: 'downloadCustomReport',
32
- type: 'rest' as const,
33
- source: 'demo_api',
34
- path: 'reports',
35
- data: {
36
- method: 'POST',
37
- paths: ['/api/reports/download-custom'],
38
- operationId: 'downloadCustomReport',
39
- requestBody: 'DownloadCustomReportRequest',
40
- // no variables
41
- },
42
- };
43
-
44
- const result = await reactDownloadHookDocs(descriptor as any);
45
- expect(result.path).toBe('download-hook.md');
46
- expect(result.content).toMatchSnapshot();
47
- });
48
-
49
- it('snapshot — path params only (no request body)', async () => {
50
- const descriptor = {
51
- id: '3',
52
- name: 'downloadFileById',
53
- type: 'rest' as const,
54
- source: 'demo_api',
55
- path: 'files/{id}',
56
- data: {
57
- method: 'GET',
58
- paths: ['/api/files/{id}/download'],
59
- operationId: 'downloadFileById',
60
- variables: [
61
- { name: 'id', in: 'path', ref: '#/components/schemas/Id' },
62
- ],
63
- },
64
- };
65
-
66
- const result = await reactDownloadHookDocs(descriptor as any);
67
- expect(result.path).toBe('download-hook.md');
68
- expect(result.content).toMatchSnapshot();
69
- });
70
-
71
- it('snapshot — request body and path params', async () => {
72
- const descriptor = {
73
- id: '4',
74
- name: 'downloadUserDocument',
75
- type: 'rest' as const,
76
- source: 'demo_api',
77
- path: 'users/{userId}/documents',
78
- data: {
79
- method: 'POST',
80
- paths: ['/api/users/{userId}/documents/download'],
81
- operationId: 'downloadUserDocument',
82
- requestBody: 'DownloadUserDocumentRequest',
83
- variables: [
84
- { name: 'userId', in: 'PATH', ref: '#/components/schemas/UserId' },
85
- ],
86
- },
87
- };
88
-
89
- const result = await reactDownloadHookDocs(descriptor as any);
90
- expect(result.path).toBe('download-hook.md');
91
- expect(result.content).toMatchSnapshot();
92
- });
93
-
94
- it('handles multiple path params', async () => {
95
- const descriptor = {
96
- id: '5',
97
- name: 'downloadProjectAsset',
98
- type: 'rest' as const,
99
- source: 'demo_api',
100
- path: 'projects/{projectId}/assets/{assetId}',
101
- data: {
102
- method: 'GET',
103
- paths: ['/api/projects/{projectId}/assets/{assetId}/download'],
104
- operationId: 'downloadProjectAsset',
105
- variables: [
106
- { name: 'projectId', in: 'path', ref: '#/components/schemas/ProjectId' },
107
- { name: 'assetId', in: 'PATH', ref: '#/components/schemas/AssetId' },
108
- ],
109
- },
110
- };
111
-
112
- const result = await reactDownloadHookDocs(descriptor as any);
113
- expect(result.path).toBe('download-hook.md');
114
- expect(result.content).toMatchSnapshot();
115
- });
116
-
117
- it('handles query params mixed with path params', async () => {
118
- const descriptor = {
119
- id: '6',
120
- name: 'downloadTaskFile',
121
- type: 'rest' as const,
122
- source: 'demo_api',
123
- path: 'tasks/{taskId}/files',
124
- data: {
125
- method: 'GET',
126
- paths: ['/api/tasks/{taskId}/files/download'],
127
- operationId: 'downloadTaskFile',
128
- variables: [
129
- { name: 'taskId', in: 'path', ref: '#/components/schemas/TaskId' },
130
- { name: 'format', in: 'query', ref: '#/components/schemas/Format' },
131
- { name: 'includeMetadata', in: 'QUERY', ref: '#/components/schemas/Boolean' },
132
- ],
133
- },
134
- };
135
-
136
- const result = await reactDownloadHookDocs(descriptor as any);
137
- expect(result.path).toBe('download-hook.md');
138
- expect(result.content).toMatchSnapshot();
139
- });
140
-
141
- it('handles empty variables array', async () => {
142
- const descriptor = {
143
- id: '7',
144
- name: 'downloadBackup',
145
- type: 'rest' as const,
146
- source: 'demo_api',
147
- path: 'backup',
148
- data: {
149
- method: 'GET',
150
- paths: ['/api/backup/download'],
151
- operationId: 'downloadBackup',
152
- variables: [], // empty array instead of undefined
153
- },
154
- };
155
-
156
- const result = await reactDownloadHookDocs(descriptor as any);
157
- expect(result.path).toBe('download-hook.md');
158
- expect(result.content).toMatchSnapshot();
159
- });
160
-
161
- it('handles case-insensitive path parameter detection', async () => {
162
- const descriptor = {
163
- id: '8',
164
- name: 'downloadInvoice',
165
- type: 'rest' as const,
166
- source: 'demo_api',
167
- path: 'invoices/{invoiceId}',
168
- data: {
169
- method: 'GET',
170
- paths: ['/api/invoices/{invoiceId}/download'],
171
- operationId: 'downloadInvoice',
172
- variables: [
173
- { name: 'invoiceId', in: 'Path', ref: '#/components/schemas/InvoiceId' }, // Mixed case
174
- ],
175
- },
176
- };
177
-
178
- const result = await reactDownloadHookDocs(descriptor as any);
179
- expect(result.path).toBe('download-hook.md');
180
- expect(result.content).toMatchSnapshot();
181
- });
182
- });
@@ -1,170 +0,0 @@
1
- import { camelCase, mdLiteral, pascalCase, ResourceDescriptor, RestData } from "@intrig/plugin-sdk";
2
-
3
- export function reactDownloadHookDocs(descriptor: ResourceDescriptor<RestData>) {
4
- const md = mdLiteral('download-hook.md');
5
-
6
- // ===== Derived names (preserve these) =====
7
- const hasPathParams = (descriptor.data.variables ?? []).some(
8
- (v: any) => v.in?.toUpperCase() === 'PATH',
9
- );
10
-
11
- const actionName = camelCase(descriptor.name); // e.g. downloadTaskFile
12
- const respVar = `${actionName}Resp`; // e.g. downloadTaskFileResp
13
-
14
- const paramsVar = hasPathParams ? `${actionName}Params` : undefined; // e.g. downloadTaskFileParams
15
- const paramsType = hasPathParams ? `${pascalCase(descriptor.name)}Params` : undefined; // e.g. DownloadTaskFileParams
16
-
17
- const pascal = pascalCase(descriptor.name);
18
- const responseTypeName = `${pascal}ResponseBody`; // e.g. DownloadTaskFileResponseBody
19
-
20
- return md`
21
- # Intrig Download Hooks — Quick Guide
22
-
23
- ## Copy-paste starter (fast lane)
24
-
25
- ### Auto-download (most common)
26
- ${"```ts"}
27
- import { use${pascal}Download } from '@intrig/react/${descriptor.path}/client';
28
- ${"```"}
29
- ${"```ts"}
30
- import { isPending, isError } from '@intrig/react';
31
- ${"```"}
32
- ${"```tsx"}
33
- const [${respVar}, ${actionName}] = use${pascal}Download({ clearOnUnmount: true });
34
- // e.g., in a click handler:
35
- ${actionName}(${paramsType ? paramsVar ?? '{}' : '{}'});
36
- ${"```"}
37
-
38
- ### Manual/stateful (you handle the blob/UI)
39
- ${"```ts"}
40
- import { use${pascal} } from '@intrig/react/${descriptor.path}/client';
41
- ${"```"}
42
- ${"```ts"}
43
- import { isSuccess, isPending, isError } from '@intrig/react';
44
- ${"```"}
45
- ${"```tsx"}
46
- const [${respVar}, ${actionName}] = use${pascal}({ clearOnUnmount: true });
47
- // later:
48
- ${actionName}(${paramsType ? paramsVar ?? '{}' : '{}'});
49
- ${"```"}
50
-
51
- ---
52
-
53
- ## TL;DR (auto-download)
54
- ${"```tsx"}
55
- import { use${pascal}Download } from '@intrig/react/${descriptor.path}/client';
56
- import { isPending, isError } from '@intrig/react';
57
-
58
- export default function Example() {
59
- const [${respVar}, ${actionName}] = use${pascal}Download({ clearOnUnmount: true });
60
-
61
- return (
62
- <button
63
- onClick={() => ${actionName}(${paramsType ? paramsVar ?? '{}' : '{}'})}
64
- disabled={isPending(${respVar})}
65
- >
66
- {isPending(${respVar}) ? 'Downloading…' : 'Download'}
67
- </button>
68
- );
69
- }
70
- ${"```"}
71
-
72
- ${paramsType ? `### Optional types (if generated by your build)
73
- ${"```ts"}
74
- import type { ${paramsType} } from '@intrig/react/${descriptor.path}/${pascal}.params';
75
- import type { ${responseTypeName} } from '@intrig/react/${descriptor.path}/${pascal}.response';
76
- ${"```"}
77
- ` : ''}
78
-
79
- ---
80
-
81
- ## Hook APIs
82
-
83
- ### \`use${pascal}Download\` (auto-download)
84
- - **What it does:** requests the file with \`responseType: 'blob'\` + \`adapter: 'fetch'\`, derives filename from \`Content-Disposition\` if present, creates a temporary object URL, clicks a hidden \`<a>\`, **downloads**, then resets state to \`init\`.
85
- - **Signature:** \`[state, download, clear]\`
86
- - \`download(params: ${paramsType ?? 'Record<string, unknown>'}) => void\`
87
-
88
- ### \`use${pascal}\` (manual/stateful)
89
- - **What it does:** same request but **does not** auto-save. You control preview/saving using \`state.data\` + \`state.headers\`.
90
- - **Signature:** \`[state, fetchFile, clear]\`
91
- - \`fetchFile(params: ${paramsType ?? 'Record<string, unknown>'}) => void\`
92
-
93
- ---
94
-
95
- ## Usage Patterns
96
-
97
- ### 1) Auto-download on click (recommended)
98
- ${"```tsx"}
99
- const [${respVar}, ${actionName}] = use${pascal}Download({ clearOnUnmount: true });
100
-
101
- <button
102
- onClick={() => ${actionName}(${paramsType ? paramsVar ?? '{}' : '{}'})}
103
- disabled={isPending(${respVar})}
104
- >
105
- {isPending(${respVar}) ? 'Downloading…' : 'Download file'}
106
- </button>
107
- {isError(${respVar}) ? <p className="text-red-500">Failed to download.</p> : null}
108
- ${"```"}
109
-
110
- <details><summary>Description</summary>
111
- <p>Most users just need a button that saves the file. This variant handles object URL creation, filename extraction, click, and state reset.</p>
112
- </details>
113
-
114
- ### 2) Auto-download on mount (e.g., “Your file is ready” page)
115
- ${"```tsx"}
116
- useEffect(() => {
117
- ${actionName}(${paramsType ? paramsVar ?? '{}' : '{}'});
118
- }, [${actionName}]);
119
- ${"```"}
120
-
121
- <details><summary>Description</summary>
122
- <p>Good for post-processing routes that immediately start a download.</p>
123
- </details>
124
-
125
- ### 3) Manual handling (preview or custom filename)
126
- ${"```tsx"}
127
- const [${respVar}, ${actionName}] = use${pascal}({ clearOnUnmount: true });
128
-
129
- useEffect(() => {
130
- if (isSuccess(${respVar})) {
131
- const ct = ${respVar}.headers?.['content-type'] ?? 'application/octet-stream';
132
- const parts = Array.isArray(${respVar}.data) ? ${respVar}.data : [${respVar}.data];
133
- const url = URL.createObjectURL(new Blob(parts, { type: ct }));
134
- // preview/save with your own UI...
135
- return () => URL.revokeObjectURL(url);
136
- }
137
- }, [${respVar}]);
138
- ${"```"}
139
-
140
- <details><summary>Description</summary>
141
- <p>Use when you need to inspect headers, show a preview, or control the filename/UI flow.</p>
142
- </details>
143
-
144
- ---
145
-
146
- ## Behavior notes (what the auto-download variant does)
147
- - Forces \`responseType: 'blob'\` and \`adapter: 'fetch'\`.
148
- - If \`content-type\` is JSON, stringifies payload so the saved file is human-readable.
149
- - Parses \`Content-Disposition\` to derive a filename; falls back to a default.
150
- - Creates and clicks a temporary link, then **resets state to \`init\`**.
151
-
152
- ---
153
-
154
- ## Gotchas & Tips
155
- - **Expose headers in CORS:** server should send
156
- \`Access-Control-Expose-Headers: Content-Disposition, Content-Type\`
157
- - **Disable double clicks:** guard with \`isPending(state)\`.
158
- - **Revoke URLs** when you create them manually in the stateful variant.
159
- - **iOS Safari:** blob downloads may open a new tab—consider server-side direct-download URLs for a smoother UX.
160
-
161
- ---
162
-
163
- ## Troubleshooting
164
- - **No filename shown:** your server didn’t include \`Content-Disposition\`. Add it.
165
- - **Got JSON instead of a file:** server returned \`application/json\` (maybe an error). The auto hook saves it as text; surface the error in UI.
166
- - **Nothing happens on click:** ensure you’re using the **Download** variant and the request succeeds (check Network tab); verify CORS headers.
167
-
168
- ---
169
- `;
170
- }