-
-
Notifications
You must be signed in to change notification settings - Fork 126
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
16 changed files
with
1,067 additions
and
690 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@hey-api/openapi-ts': patch | ||
--- | ||
|
||
fix: watch mode handles servers not exposing HEAD method for spec |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@hey-api/openapi-ts': patch | ||
--- | ||
|
||
fix: add watch.timeout option |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import path from 'node:path'; | ||
|
||
import { generateLegacyOutput, generateOutput } from './generate/output'; | ||
import { getSpec } from './getSpec'; | ||
import type { IR } from './ir/types'; | ||
import { parseLegacy, parseOpenApiSpec } from './openApi'; | ||
import { processOutput } from './processOutput'; | ||
import type { Client } from './types/client'; | ||
import type { Config } from './types/config'; | ||
import type { WatchValues } from './types/types'; | ||
import { isLegacyClient, legacyNameFromConfig } from './utils/config'; | ||
import type { Templates } from './utils/handlebars'; | ||
import { Performance } from './utils/performance'; | ||
import { postProcessClient } from './utils/postprocess'; | ||
|
||
export const createClient = async ({ | ||
config, | ||
templates, | ||
watch: _watch, | ||
}: { | ||
config: Config; | ||
templates: Templates; | ||
watch?: WatchValues; | ||
}) => { | ||
const inputPath = config.input.path; | ||
const timeout = config.watch.timeout; | ||
|
||
const watch: WatchValues = _watch || { headers: new Headers() }; | ||
|
||
Performance.start('spec'); | ||
const { data, error, response } = await getSpec({ | ||
inputPath, | ||
timeout, | ||
watch, | ||
}); | ||
Performance.end('spec'); | ||
|
||
// throw on first run if there's an error to preserve user experience | ||
// if in watch mode, subsequent errors won't throw to gracefully handle | ||
// cases where server might be reloading | ||
if (error && !_watch) { | ||
throw new Error( | ||
`Request failed with status ${response.status}: ${response.statusText}`, | ||
); | ||
} | ||
|
||
let client: Client | undefined; | ||
let context: IR.Context | undefined; | ||
|
||
if (data) { | ||
if (_watch) { | ||
console.clear(); | ||
console.log(`⏳ Input changed, generating from ${inputPath}`); | ||
} else { | ||
console.log(`⏳ Generating from ${inputPath}`); | ||
} | ||
|
||
Performance.start('parser'); | ||
if ( | ||
config.experimentalParser && | ||
!isLegacyClient(config) && | ||
!legacyNameFromConfig(config) | ||
) { | ||
context = parseOpenApiSpec({ config, spec: data }); | ||
} | ||
|
||
// fallback to legacy parser | ||
if (!context) { | ||
const parsed = parseLegacy({ openApi: data }); | ||
client = postProcessClient(parsed, config); | ||
} | ||
Performance.end('parser'); | ||
|
||
Performance.start('generator'); | ||
if (context) { | ||
await generateOutput({ context }); | ||
} else if (client) { | ||
await generateLegacyOutput({ client, openApi: data, templates }); | ||
} | ||
Performance.end('generator'); | ||
|
||
Performance.start('postprocess'); | ||
if (!config.dryRun) { | ||
processOutput({ config }); | ||
|
||
const outputPath = process.env.INIT_CWD | ||
? `./${path.relative(process.env.INIT_CWD, config.output.path)}` | ||
: config.output.path; | ||
console.log(`🚀 Done! Your output is in ${outputPath}`); | ||
} | ||
Performance.end('postprocess'); | ||
} | ||
|
||
if (config.watch.enabled && typeof inputPath === 'string') { | ||
setTimeout(() => { | ||
createClient({ config, templates, watch }); | ||
}, config.watch.interval); | ||
} | ||
|
||
return context || client; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import type { Config, UserConfig } from './types/config'; | ||
|
||
export const getLogs = (userConfig: UserConfig): Config['logs'] => { | ||
let logs: Config['logs'] = { | ||
level: 'info', | ||
path: process.cwd(), | ||
}; | ||
if (typeof userConfig.logs === 'string') { | ||
logs.path = userConfig.logs; | ||
} else { | ||
logs = { | ||
...logs, | ||
...userConfig.logs, | ||
}; | ||
} | ||
return logs; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
import { | ||
$RefParser, | ||
getResolvedInput, | ||
type JSONSchema, | ||
sendRequest, | ||
} from '@hey-api/json-schema-ref-parser'; | ||
|
||
import type { Config } from './types/config'; | ||
import type { WatchValues } from './types/types'; | ||
|
||
interface SpecResponse { | ||
data: JSONSchema; | ||
error?: undefined; | ||
response?: undefined; | ||
} | ||
|
||
interface SpecError { | ||
data?: undefined; | ||
error: 'not-modified' | 'not-ok'; | ||
response: Response; | ||
} | ||
|
||
export const getSpec = async ({ | ||
inputPath, | ||
timeout, | ||
watch, | ||
}: { | ||
inputPath: Config['input']['path']; | ||
timeout: number; | ||
watch: WatchValues; | ||
}): Promise<SpecResponse | SpecError> => { | ||
const refParser = new $RefParser(); | ||
const resolvedInput = getResolvedInput({ pathOrUrlOrSchema: inputPath }); | ||
|
||
let arrayBuffer: ArrayBuffer | undefined; | ||
// boolean signals whether the file has **definitely** changed | ||
let hasChanged: boolean | undefined; | ||
let response: Response | undefined; | ||
|
||
// no support for watching files and objects for now | ||
if (resolvedInput.type === 'url') { | ||
// do NOT send HEAD request on first run or if unsupported | ||
if (watch.lastValue && watch.isHeadMethodSupported !== false) { | ||
const request = await sendRequest({ | ||
init: { | ||
headers: watch.headers, | ||
method: 'HEAD', | ||
}, | ||
timeout, | ||
url: resolvedInput.path, | ||
}); | ||
response = request.response; | ||
|
||
if (!response.ok && watch.isHeadMethodSupported) { | ||
// assume the server is no longer running | ||
// do nothing, it might be restarted later | ||
return { | ||
error: 'not-ok', | ||
response, | ||
}; | ||
} | ||
|
||
if (watch.isHeadMethodSupported === undefined) { | ||
watch.isHeadMethodSupported = response.ok; | ||
} | ||
|
||
if (response.status === 304) { | ||
return { | ||
error: 'not-modified', | ||
response, | ||
}; | ||
} | ||
|
||
if (hasChanged === undefined) { | ||
Check notice Code scanning / CodeQL Unneeded defensive code Note
This guard always evaluates to true.
|
||
const eTag = response.headers.get('ETag'); | ||
if (eTag) { | ||
hasChanged = eTag !== watch.headers.get('If-None-Match'); | ||
|
||
if (hasChanged) { | ||
watch.headers.set('If-None-Match', eTag); | ||
} | ||
} | ||
} | ||
|
||
if (hasChanged === undefined) { | ||
const lastModified = response.headers.get('Last-Modified'); | ||
if (lastModified) { | ||
hasChanged = lastModified !== watch.headers.get('If-Modified-Since'); | ||
|
||
if (hasChanged) { | ||
watch.headers.set('If-Modified-Since', lastModified); | ||
} | ||
} | ||
} | ||
|
||
// we definitely know the input has not changed | ||
if (hasChanged === false) { | ||
return { | ||
error: 'not-modified', | ||
response, | ||
}; | ||
} | ||
} | ||
|
||
const fileRequest = await sendRequest({ | ||
init: { | ||
method: 'GET', | ||
}, | ||
timeout, | ||
url: resolvedInput.path, | ||
}); | ||
response = fileRequest.response; | ||
|
||
if (!response.ok) { | ||
// assume the server is no longer running | ||
// do nothing, it might be restarted later | ||
return { | ||
error: 'not-ok', | ||
response, | ||
}; | ||
} | ||
|
||
arrayBuffer = response.body | ||
? await response.arrayBuffer() | ||
: new ArrayBuffer(0); | ||
|
||
if (hasChanged === undefined) { | ||
const content = new TextDecoder().decode(arrayBuffer); | ||
hasChanged = content !== watch.lastValue; | ||
watch.lastValue = content; | ||
} | ||
} | ||
|
||
if (hasChanged === false) { | ||
return { | ||
error: 'not-modified', | ||
response: response!, | ||
}; | ||
} | ||
|
||
const data = await refParser.bundle({ | ||
arrayBuffer, | ||
pathOrUrlOrSchema: undefined, | ||
resolvedInput, | ||
}); | ||
|
||
return { | ||
data, | ||
}; | ||
}; |
Oops, something went wrong.