@codewithagents/openapi-react-query 3.5.1 → 3.6.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/dist/cli-args.d.ts +23 -0
- package/dist/cli-args.d.ts.map +1 -0
- package/dist/cli-args.js +37 -0
- package/dist/cli-args.js.map +1 -0
- package/dist/cli.js +25 -17
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/hooks.js +7 -7
- package/dist/plugins/hooks.js.map +1 -1
- package/package.json +5 -2
- package/CHANGELOG.md +0 -161
- package/CLAUDE.md +0 -30
- package/__fixtures__/specs/task-hooks.json +0 -170
- package/src/__tests__/__snapshots__/hooks.test.ts.snap +0 -457
- package/src/__tests__/compat-matrix.test.ts +0 -25
- package/src/__tests__/config.test.ts +0 -291
- package/src/__tests__/hooks.property.test.ts +0 -310
- package/src/__tests__/hooks.test.ts +0 -1845
- package/src/__tests__/prettier-output.test.ts +0 -45
- package/src/__tests__/test-utils.test.ts +0 -69
- package/src/cli.ts +0 -27
- package/src/config.ts +0 -171
- package/src/generator.ts +0 -50
- package/src/plugins/hooks.ts +0 -823
- package/src/plugins/test-utils.ts +0 -77
- package/stryker.config.json +0 -19
- package/tsconfig.build.json +0 -10
- package/tsconfig.json +0 -9
- package/vitest.config.ts +0 -12
|
@@ -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
|
-
}
|