chore: extract vendored Cordis hot-reload fix

This commit is contained in:
Tianyi Cui
2026-07-22 17:30:45 +08:00
parent ec533d0a77
commit b72410b152
6 changed files with 14 additions and 260 deletions

View File

@@ -77,15 +77,7 @@ export class Include extends EntryTree {
ctx.on('internal/update', (config, _, next) => {
if (config.path !== this.config.path) return next()
// Veto the fiber restart (children update in place), but persist the new
// config ourselves — `Fiber.update` only assigns `this.config` behind
// `next()`, and a stale `this.config.patches` would make the next
// `refresh()` re-apply the old overlay.
this.config = config
this.root.update(this.applyPatches(this.data!, config.patches)).catch((error) => {
this.ctx.logger.warn('config update at %C failed', this.filename)
this.ctx.logger.warn(error)
})
this.root.update(this.data!)
})
}
@@ -101,37 +93,22 @@ export class Include extends EntryTree {
private async read(forced = false) {
const content = await readFile(this.filename, 'utf8')
if (!forced && this.content === content) return false
let data: any
this.content = content
if (this.type === 'application/yaml') {
data = yaml.load(content, { schema })
this.data = yaml.load(this.content, { schema }) as any
} else if (this.type === 'application/json') {
data = JSON.parse(content)
this.data = JSON.parse(this.content) as any
} else {
const module = await import(/* @vite-ignore */ this.filename)
data = module.default || module
this.data = module.default || module
}
// An empty or truncated file (common mid-edit: editors and `sed -i` write
// through temp states) parses to `undefined`, not an error; reject every
// non-array shape here so callers see one "invalid file" signal. Content
// and data commit only on success, so an edit that is later reverted to
// the exact last good content correctly reads as "unchanged".
if (!Array.isArray(data)) {
throw new TypeError(`config file must be a top-level array of entries: ${this.filename}`)
}
this.content = content
this.data = data
await this.checkAccess()
return true
}
private applyPatches(data: EntryOptions[], patches = this.config.patches): EntryOptions[] {
// Always detach from the cached parse: patching shared entry objects would
// bake earlier patch values into `this.data`, so repeated application
// (config hot-reloads) could never revert a removed or changed patch. The
// supported extensions guarantee JSON-safe plain data, so `structuredClone`
// cannot throw here.
if (!patches?.length) return [...data]
data = structuredClone(data)
private applyPatches(data: EntryOptions[]): EntryOptions[] {
const { patches } = this.config
if (!patches?.length) return data
const entryMap = new Map<string, EntryOptions>()
const buildMap = (entries: EntryOptions[]) => {
@@ -197,11 +174,7 @@ export class Include extends EntryTree {
async* [Service.init]() {
try {
await this.read()
} catch (error) {
// Only a missing file falls back to `initial` (or the not-found error):
// an existing-but-invalid file must fail loud with its real parse error,
// never be mislabelled as absent or silently overwritten.
if ((error as NodeJS.ErrnoException | null)?.code !== 'ENOENT') throw error
} catch {
if (this.config.initial) {
this.writeFile(this.config.initial as any)
await this.read()
@@ -211,26 +184,18 @@ export class Include extends EntryTree {
}
yield () => this.stop()
await this.root.update(this.applyPatches(this.data!))
const data = this.applyPatches([...this.data!])
await this.root.update(data)
}
stop() {
this.root.stop()
}
/**
* Re-read the file and refresh child entries when content changed. An
* unreadable or unparsable file logs a warning and keeps the last good
* tree: a hot-reload of a live app must never take the process down.
*/
/** Re-read the file and refresh child entries when content changed. */
async refresh() {
try {
if (!await this.read()) return
await this.root.update(this.applyPatches(this.data!))
} catch (error) {
this.ctx.logger.warn('config reload at %C failed; keeping the running tree', this.filename)
this.ctx.logger.warn(error)
}
if (!await this.read()) return
this.root.update(this.data!)
}
private async _writeFile(config: EntryOptions[]) {