diff --git a/packages/hmr/src/index.ts b/packages/hmr/src/index.ts index f1c4369..9a30733 100644 --- a/packages/hmr/src/index.ts +++ b/packages/hmr/src/index.ts @@ -108,7 +108,7 @@ class Watcher extends Service { if (loader.internal!.loadCache.has(filename)) { loader.exit() } else { - await loader.reload() + await loader.start() } } else { if (this.externals.has(filename)) { @@ -290,6 +290,14 @@ class Watcher extends Service { // emit reload event before replacing loader cache this.ctx.emit('hmr/reload', reloads) + const reload = (plugin: any, children: ForkScope[]) => { + for (const oldFork of children) { + const fork = oldFork.parent.plugin(plugin, oldFork.config) + fork.entry = oldFork.entry + if (fork.entry) fork.entry.fork = fork + } + } + try { for (const [plugin, { filename, children }] of reloads) { const path = this.relative(filename) @@ -302,11 +310,7 @@ class Watcher extends Service { } try { - for (const oldFork of children) { - const fork = oldFork.parent.plugin(attempts[filename], oldFork.config) - fork.entry = oldFork.entry - fork.entry.fork = fork - } + reload(attempts[filename], children) this.ctx.logger.info('reload plugin at %c', path) } catch (err) { this.ctx.logger.warn('failed to reload plugin at %c', path) @@ -320,11 +324,7 @@ class Watcher extends Service { for (const [plugin, { filename, children }] of reloads) { try { this.ctx.registry.delete(attempts[filename]) - for (const oldFork of children) { - const fork = oldFork.parent.plugin(plugin, oldFork.config) - fork.entry = oldFork.entry - fork.entry.fork = fork - } + reload(plugin, children) } catch (err) { this.ctx.logger.warn(err) } diff --git a/packages/loader/src/shared.ts b/packages/loader/src/shared.ts index 63caea1..6c9f1a3 100644 --- a/packages/loader/src/shared.ts +++ b/packages/loader/src/shared.ts @@ -139,6 +139,12 @@ export namespace Loader { export interface Options { name: string immutable?: boolean + fallback?: Fallback + } + + export interface Fallback { + extension?: string + config: Omit[] } } @@ -153,7 +159,7 @@ export abstract class Loader extends env: process.env, } - public entryFork!: ForkScope + public entryFork: ForkScope public suspend = false public writable = false public mimeType!: string @@ -167,7 +173,39 @@ export abstract class Loader extends constructor(public app: Context, public options: T) { super(app, 'loader', true) + this.entryFork = this.app.plugin(group, []) this.realms.root = app.root[Context.isolate] + + this.app.on('dispose', () => { + this.exit() + }) + + this.app.on('internal/update', (fork) => { + const entry = this.entries[fork.entry?.options.id!] + if (!entry) return + fork.parent.emit('loader/entry', 'reload', entry) + }) + + this.app.on('internal/before-update', (fork, config) => { + if (!fork.entry) return + if (fork.entry.isUpdate) return fork.entry.isUpdate = false + const { schema } = fork.runtime + fork.entry.options.config = schema ? schema.simplify(config) : config + this.writeConfig() + }) + + this.app.on('internal/fork', (fork) => { + // fork.uid: fork is created (we only care about fork dispose event) + // fork.parent.runtime.plugin !== group: fork is not tracked by loader + if (fork.uid || !fork.entry) return + fork.parent.emit('loader/entry', 'unload', fork.entry) + // fork is disposed by main scope (e.g. hmr plugin) + // normal: ctx.dispose() -> fork / runtime dispose -> delete(plugin) + // hmr: delete(plugin) -> runtime dispose -> fork dispose + if (!this.app.registry.has(fork.runtime.plugin)) return + fork.entry.options.disabled = true + this.writeConfig() + }) } async init(filename?: string) { @@ -200,14 +238,21 @@ export abstract class Loader extends private async findConfig() { const files = await fs.readdir(this.baseDir) - for (const extname of supported) { - const filename = this.options.name + extname + for (const extension of supported) { + const filename = this.options.name + extension if (files.includes(filename)) { - this.mimeType = writable[extname] + this.mimeType = writable[extension] this.filename = path.resolve(this.baseDir, filename) return } } + if (this.options.fallback) { + const { config, extension = '.yml' } = this.options.fallback + this.config = config as any + this.mimeType = writable[extension] + this.filename = path.resolve(this.baseDir, this.options.name + extension) + return this.writeConfig(true) + } throw new Error('config file not found') } @@ -237,12 +282,6 @@ export abstract class Loader extends if (!silent) this.app.emit('config') } - async reload() { - const config = await this.readConfig() - this.entryFork.update(config) - this.app.emit('config') - } - interpolate(source: any) { if (typeof source === 'string') { return interpolate(source, this.params, /\$\{\{(.+?)\}\}/g) @@ -300,38 +339,8 @@ export abstract class Loader extends async start() { await this.readConfig() - this.entryFork = this.app.plugin(group, this.config) - - this.app.on('dispose', () => { - this.exit() - }) - - this.app.on('internal/update', (fork) => { - const entry = this.entries[fork.entry?.options.id!] - if (!entry) return - fork.parent.emit('loader/entry', 'reload', entry) - }) - - this.app.on('internal/before-update', (fork, config) => { - if (!fork.entry) return - if (fork.entry.isUpdate) return fork.entry.isUpdate = false - const { schema } = fork.runtime - fork.entry.options.config = schema ? schema.simplify(config) : config - this.writeConfig() - }) - - this.app.on('internal/fork', (fork) => { - // fork.uid: fork is created (we only care about fork dispose event) - // fork.parent.runtime.plugin !== group: fork is not tracked by loader - if (fork.uid || !fork.entry) return - fork.parent.emit('loader/entry', 'unload', fork.entry) - // fork is disposed by main scope (e.g. hmr plugin) - // normal: ctx.dispose() -> fork / runtime dispose -> delete(plugin) - // hmr: delete(plugin) -> runtime dispose -> fork dispose - if (!this.app.registry.has(fork.runtime.plugin)) return - fork.entry.options.disabled = true - this.writeConfig() - }) + this.entryFork.update(this.config) + this.app.emit('config') while (this.tasks.size) { await Promise.all(this.tasks)