@codewithagents/openapi-react-query 3.5.0 → 3.5.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.
@@ -1,45 +0,0 @@
1
- import { mkdtemp, readFile, writeFile } from 'node:fs/promises'
2
- import { join } from 'node:path'
3
- import { tmpdir } from 'node:os'
4
- import { check } from 'prettier'
5
- import { describe, it, expect, afterEach } from 'vitest'
6
- import { generate } from '../generator.js'
7
-
8
- const taskHooksFixture = join(import.meta.dirname, '../../__fixtures__/specs/task-hooks.json')
9
-
10
- let tmpDir: string | undefined
11
-
12
- afterEach(async () => {
13
- if (tmpDir) {
14
- const { rm } = await import('node:fs/promises')
15
- await rm(tmpDir, { recursive: true, force: true })
16
- tmpDir = undefined
17
- }
18
- })
19
-
20
- async function runGenerator(): Promise<string> {
21
- tmpDir = await mkdtemp(join(tmpdir(), 'openapi-react-query-prettier-test-'))
22
- const configPath = join(tmpDir, 'openapi-react-query.config.json')
23
- const outDir = join(tmpDir, 'generated')
24
- await writeFile(
25
- configPath,
26
- JSON.stringify({ input_openapi: taskHooksFixture, output: outDir }),
27
- 'utf-8'
28
- )
29
- await generate(tmpDir, configPath)
30
- return outDir
31
- }
32
-
33
- describe('generated output is Prettier-clean (task-hooks)', () => {
34
- it('hooks.ts is Prettier-clean', async () => {
35
- const outDir = await runGenerator()
36
- const content = await readFile(join(outDir, 'hooks.ts'), 'utf-8')
37
- expect(await check(content, { parser: 'typescript' })).toBe(true)
38
- })
39
-
40
- it('test-utils.ts is Prettier-clean', async () => {
41
- const outDir = await runGenerator()
42
- const content = await readFile(join(outDir, 'test-utils.ts'), 'utf-8')
43
- expect(await check(content, { parser: 'typescript' })).toBe(true)
44
- })
45
- })
@@ -1,69 +0,0 @@
1
- import { readFileSync } from 'node:fs'
2
- import { join } from 'node:path'
3
- import { describe, expect, it } from 'vitest'
4
- import type { OpenAPIV3_1 } from 'openapi-types'
5
- import { generateTestUtils } from '../plugins/test-utils.js'
6
-
7
- // Load fixture directly (no parseSpec — avoids needing openapi-gen dist built)
8
- const fixturePath = join(import.meta.dirname, '../../__fixtures__/specs/task-hooks.json')
9
- const spec = JSON.parse(readFileSync(fixturePath, 'utf-8')) as OpenAPIV3_1.Document
10
-
11
- const { filename, content } = generateTestUtils(spec)
12
-
13
- describe('generateTestUtils', () => {
14
- it('filename is test-utils.ts', () => {
15
- expect(filename).toBe('test-utils.ts')
16
- })
17
-
18
- it('contains auto-generated header comment', () => {
19
- expect(content).toContain(
20
- '// This file is auto-generated by @codewithagents/openapi-react-query — do not edit'
21
- )
22
- })
23
-
24
- it('imports QueryClient from @tanstack/react-query (existing peer dep — no new deps)', () => {
25
- expect(content).toContain("from '@tanstack/react-query'")
26
- expect(content).toContain('QueryClient')
27
- })
28
-
29
- it('does NOT import from any testing framework (vitest, jest, testing-library)', () => {
30
- expect(content).not.toContain("from 'vitest'")
31
- expect(content).not.toContain("from '@vitest/")
32
- expect(content).not.toContain("from 'jest'")
33
- expect(content).not.toContain("from '@testing-library/")
34
- expect(content).not.toContain("from 'msw'")
35
- })
36
-
37
- it('exports createTestQueryClient function', () => {
38
- expect(content).toContain('export function createTestQueryClient(')
39
- })
40
-
41
- it('createTestQueryClient returns a QueryClient with retry disabled', () => {
42
- expect(content).toContain('retry: false')
43
- })
44
-
45
- it('createTestQueryClient disables gcTime or sets it to a deterministic value', () => {
46
- // Ensures queries don't linger between tests with stale data
47
- expect(content).toContain('gcTime:')
48
- })
49
-
50
- it('exports createWrapper function', () => {
51
- expect(content).toContain('export function createWrapper(')
52
- })
53
-
54
- it('createWrapper uses QueryClientProvider (from the same peer dep)', () => {
55
- expect(content).toContain('QueryClientProvider')
56
- })
57
-
58
- it('createWrapper returns a component usable as wrapper in renderHook', () => {
59
- // The wrapper component must accept { children } — ReactNode from react (also peer dep)
60
- expect(content).toContain('children')
61
- })
62
-
63
- it('does not import from msw or any test-runner-specific module', () => {
64
- // The file should be completely test-runner agnostic
65
- expect(content).not.toContain('msw')
66
- expect(content).not.toContain('vi.fn')
67
- expect(content).not.toContain('jest.fn')
68
- })
69
- })
package/src/cli.ts DELETED
@@ -1,27 +0,0 @@
1
- #!/usr/bin/env node
2
- import { resolve, dirname } from 'node:path'
3
- import { generate } from './generator.js'
4
-
5
- const args = process.argv.slice(2)
6
- const configIdx = args.indexOf('--config')
7
-
8
- let cwd = process.cwd()
9
- let configFile: string | undefined
10
-
11
- if (configIdx !== -1) {
12
- const next = args[configIdx + 1]
13
- if (next === undefined || next.startsWith('--')) {
14
- console.error('Error: --config requires a file path argument')
15
- console.error('Usage: openapi-react-query [--config <path-to-config.json>]')
16
- process.exit(1)
17
- }
18
- configFile = resolve(cwd, next)
19
- // Relative paths inside the config resolve from the config file's directory,
20
- // not from the shell's CWD. This matches how most tooling works.
21
- cwd = dirname(configFile)
22
- }
23
-
24
- generate(cwd, configFile).catch((err: Error) => {
25
- console.error(`Error: ${err.message}`)
26
- process.exit(1)
27
- })
package/src/config.ts DELETED
@@ -1,171 +0,0 @@
1
- import { readFile } from 'node:fs/promises'
2
- import { join, resolve } from 'node:path'
3
-
4
- export interface ReactQueryConfig {
5
- /** Path to OpenAPI 3.1 spec file (JSON or YAML) */
6
- input_openapi: string
7
- /** Directory to write generated files */
8
- output: string
9
- /** staleTime in ms for all useQuery hooks (default: 0) */
10
- stale_time?: number
11
- /** gcTime in ms for all useQuery hooks (default: 300000) */
12
- gc_time?: number
13
- /** When true, generates useSuspense* variants alongside each useQuery hook (default: false) */
14
- suspense?: boolean
15
- /** Per-resource cache timing overrides. Key is the resource name (e.g. "tasks", "platforms"). */
16
- overrides?: Record<string, { stale_time?: number; gc_time?: number }>
17
- /** When true, mutation hooks auto-invalidate related resource queries on success (default: false) */
18
- auto_invalidate?: boolean
19
- }
20
-
21
- const FORBIDDEN_OUTPUT_PREFIXES = [
22
- '/etc',
23
- '/usr',
24
- '/bin',
25
- '/sbin',
26
- '/lib',
27
- '/lib64',
28
- '/sys',
29
- '/proc',
30
- '/dev',
31
- '/boot',
32
- '/run',
33
- 'C:\\Windows',
34
- 'C:\\Program Files',
35
- ]
36
-
37
- const FORBIDDEN_INPUT_PREFIXES = [
38
- '/etc',
39
- '/usr',
40
- '/bin',
41
- '/sbin',
42
- '/lib',
43
- '/lib64',
44
- '/sys',
45
- '/proc',
46
- '/dev',
47
- 'C:\\Windows',
48
- 'C:\\Program Files',
49
- ]
50
-
51
- export function validateConfigPath(configPath: string): void {
52
- // Must end in .json — prevents accidentally loading non-config files
53
- if (!configPath.endsWith('.json')) {
54
- throw new Error(`Config file must be a .json file, got: ${configPath}`)
55
- }
56
- }
57
-
58
- export function validateOutputPath(resolvedOutput: string): void {
59
- const normalized = resolvedOutput.replace(/\\/g, '/')
60
- for (const forbidden of FORBIDDEN_OUTPUT_PREFIXES) {
61
- const normalizedForbidden = forbidden.replace(/\\/g, '/')
62
- if (normalized === normalizedForbidden || normalized.startsWith(normalizedForbidden + '/')) {
63
- throw new Error(
64
- `Output path resolves to a system directory: "${resolvedOutput}". ` +
65
- `This looks like a misconfiguration — please check your config file.`
66
- )
67
- }
68
- }
69
- }
70
-
71
- export function validateInputPath(resolvedInput: string): void {
72
- const normalized = resolvedInput.replace(/\\/g, '/')
73
- for (const forbidden of FORBIDDEN_INPUT_PREFIXES) {
74
- const normalizedForbidden = forbidden.replace(/\\/g, '/')
75
- if (normalized === normalizedForbidden || normalized.startsWith(normalizedForbidden + '/')) {
76
- throw new Error(
77
- `Input spec path resolves to a system directory: "${resolvedInput}". ` +
78
- `This looks like a misconfiguration — please check your config file.`
79
- )
80
- }
81
- }
82
- }
83
-
84
- export async function loadConfig(cwd: string, configPath?: string): Promise<ReactQueryConfig> {
85
- const resolvedConfigPath = configPath ?? join(cwd, 'openapi-react-query.config.json')
86
-
87
- // Security: validate config path extension when explicitly provided
88
- if (configPath !== undefined) {
89
- validateConfigPath(configPath)
90
- }
91
-
92
- let raw: string
93
- try {
94
- raw = await readFile(resolvedConfigPath, 'utf-8')
95
- } catch {
96
- throw new Error(`Config file not found: ${resolvedConfigPath}`)
97
- }
98
-
99
- let parsed: unknown
100
- try {
101
- parsed = JSON.parse(raw)
102
- } catch {
103
- throw new Error(`Config file is not valid JSON: ${resolvedConfigPath}`)
104
- }
105
-
106
- if (typeof parsed !== 'object' || parsed === null) {
107
- throw new Error('Config must be a JSON object')
108
- }
109
-
110
- const config = parsed as Record<string, unknown>
111
-
112
- if (typeof config['input_openapi'] !== 'string' || !config['input_openapi']) {
113
- throw new Error('Config missing required field: "input_openapi" (path to OpenAPI 3.1 spec)')
114
- }
115
- if (typeof config['output'] !== 'string' || !config['output']) {
116
- throw new Error('Config missing required field: "output" (output directory)')
117
- }
118
- if (config['stale_time'] !== undefined && typeof config['stale_time'] !== 'number') {
119
- throw new Error('"stale_time" must be a number (milliseconds)')
120
- }
121
- if (config['gc_time'] !== undefined && typeof config['gc_time'] !== 'number') {
122
- throw new Error('"gc_time" must be a number (milliseconds)')
123
- }
124
- if (config['suspense'] !== undefined && typeof config['suspense'] !== 'boolean') {
125
- throw new Error('"suspense" must be a boolean')
126
- }
127
- if (config['auto_invalidate'] !== undefined && typeof config['auto_invalidate'] !== 'boolean') {
128
- throw new Error('"auto_invalidate" must be a boolean')
129
- }
130
- if (config['overrides'] !== undefined) {
131
- if (
132
- typeof config['overrides'] !== 'object' ||
133
- config['overrides'] === null ||
134
- Array.isArray(config['overrides'])
135
- ) {
136
- throw new Error('"overrides" must be an object')
137
- }
138
- for (const [resource, timing] of Object.entries(
139
- config['overrides'] as Record<string, unknown>
140
- )) {
141
- if (typeof timing !== 'object' || timing === null) {
142
- throw new Error(`"overrides.${resource}" must be an object`)
143
- }
144
- const t = timing as Record<string, unknown>
145
- if (t['stale_time'] !== undefined && typeof t['stale_time'] !== 'number') {
146
- throw new Error(`"overrides.${resource}.stale_time" must be a number`)
147
- }
148
- if (t['gc_time'] !== undefined && typeof t['gc_time'] !== 'number') {
149
- throw new Error(`"overrides.${resource}.gc_time" must be a number`)
150
- }
151
- }
152
- }
153
-
154
- // Security: validate resolved input and output paths
155
- const resolvedInput = resolve(cwd, config['input_openapi'] as string)
156
- const resolvedOutput = resolve(cwd, config['output'] as string)
157
- validateInputPath(resolvedInput)
158
- validateOutputPath(resolvedOutput)
159
-
160
- return {
161
- input_openapi: config['input_openapi'] as string,
162
- output: config['output'] as string,
163
- stale_time: config['stale_time'] as number | undefined,
164
- gc_time: config['gc_time'] as number | undefined,
165
- suspense: config['suspense'] as boolean | undefined,
166
- overrides: config['overrides'] as
167
- | Record<string, { stale_time?: number; gc_time?: number }>
168
- | undefined,
169
- auto_invalidate: config['auto_invalidate'] as boolean | undefined,
170
- }
171
- }
package/src/generator.ts DELETED
@@ -1,50 +0,0 @@
1
- import { mkdir, writeFile } from 'node:fs/promises'
2
- import { join, resolve } from 'node:path'
3
- import { parseSpec } from '@codewithagents/openapi-gen'
4
- import { loadConfig } from './config.js'
5
- import { generateHooks } from './plugins/hooks.js'
6
- import { generateTestUtils } from './plugins/test-utils.js'
7
-
8
- async function formatTs(content: string, filePath: string): Promise<string> {
9
- const { format, resolveConfig } = await import('prettier')
10
- const config = await resolveConfig(filePath)
11
- return format(content, { ...config, parser: 'typescript' })
12
- }
13
-
14
- export async function generate(cwd: string, configPath?: string): Promise<void> {
15
- const config = await loadConfig(cwd, configPath)
16
- const inputPath = resolve(cwd, config.input_openapi)
17
- const outputDir = resolve(cwd, config.output)
18
- const spec = await parseSpec(inputPath)
19
- const globalStaleTime = config.stale_time ?? 0
20
- const globalGcTime = config.gc_time ?? 300_000
21
-
22
- // Convert snake_case config overrides to camelCase options
23
- const overrides: Record<string, { staleTime: number; gcTime: number }> = {}
24
- if (config.overrides) {
25
- for (const [resource, timing] of Object.entries(config.overrides)) {
26
- overrides[resource] = {
27
- staleTime: timing.stale_time ?? globalStaleTime,
28
- gcTime: timing.gc_time ?? globalGcTime,
29
- }
30
- }
31
- }
32
-
33
- const files = [
34
- generateHooks(spec, {
35
- staleTime: globalStaleTime,
36
- gcTime: globalGcTime,
37
- suspense: config.suspense,
38
- overrides: Object.keys(overrides).length > 0 ? overrides : undefined,
39
- autoInvalidate: config.auto_invalidate,
40
- }),
41
- generateTestUtils(spec),
42
- ]
43
-
44
- await mkdir(outputDir, { recursive: true })
45
- for (const file of files) {
46
- const filePath = join(outputDir, file.filename)
47
- await writeFile(filePath, await formatTs(file.content, filePath), 'utf-8')
48
- console.log(`✓ ${file.filename}`)
49
- }
50
- }