-
-
Notifications
You must be signed in to change notification settings - Fork 127
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1668 from hey-api/fix/watch-mode-patch
fix: watch mode tweaks
- Loading branch information
Showing
25 changed files
with
1,099 additions
and
706 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
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
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) { | ||
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.