From eff476da51185dcac204e9eabd8158f7208cd4a5 Mon Sep 17 00:00:00 2001 From: Shigma Date: Sun, 10 Nov 2024 22:07:50 +0800 Subject: [PATCH] feat: new runtime api supports hmr --- packages/core/src/registry.ts | 4 ++- packages/core/src/scope.ts | 12 ++++---- packages/core/src/utils.ts | 2 +- packages/hmr/src/index.ts | 45 +++++++++++++++-------------- packages/loader/tests/group.spec.ts | 2 +- 5 files changed, 36 insertions(+), 29 deletions(-) diff --git a/packages/core/src/registry.ts b/packages/core/src/registry.ts index 31cf4e3..0a05ec3 100644 --- a/packages/core/src/registry.ts +++ b/packages/core/src/registry.ts @@ -158,7 +158,9 @@ class Registry { const runtime = key && this._internal.get(key) if (!runtime) return this._internal.delete(key) - runtime.scopes.popAll().forEach(scope => scope.dispose()) + for (const scope of runtime.scopes) { + scope.dispose() + } return runtime } diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index 5d5a5ea..0ae8ad5 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -56,15 +56,17 @@ export class EffectScope { this.uid = parent.registry.counter this.ctx = this.context = parent.extend({ scope: this }) this.dispose = parent.scope.effect(() => { - const remove = this.runtime?.scopes.push(this) + const remove = runtime!.scopes.push(this) this.context.emit('internal/plugin', this) this.setActive(true) return async () => { - remove?.() this.uid = null this.context.emit('internal/plugin', this) - if (this.runtime && !this.runtime.scopes.length) { - this.ctx.registry.delete(this.runtime.plugin) + if (this.ctx.registry.has(runtime!.plugin)) { + remove() + if (!runtime!.scopes.length) { + this.ctx.registry.delete(runtime!.plugin) + } } this.setActive(false) await this._pending @@ -165,7 +167,7 @@ export class EffectScope { } private async _unload() { - await Promise.all(this.disposables.popAll().map(async (dispose) => { + await Promise.all(this.disposables.clear().map(async (dispose) => { try { await dispose() } catch (reason) { diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 1eb1b62..6e050ca 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -22,7 +22,7 @@ export class DisposableList { this.map.delete(this.sn) } - popAll() { + clear() { const values = [...this.map.values()] this.map.clear() return values.reverse() diff --git a/packages/hmr/src/index.ts b/packages/hmr/src/index.ts index 66f3aaa..d07e5fd 100644 --- a/packages/hmr/src/index.ts +++ b/packages/hmr/src/index.ts @@ -33,7 +33,7 @@ async function loadDependencies(job: ModuleJob, ignored = new Set()) { interface Reload { filename: string - children: EffectScope[] + runtime?: Plugin.Runtime } class Watcher extends Service { @@ -95,18 +95,22 @@ class Watcher extends Service { // files independent from any plugins will trigger a full reload const mainJob = await loader.internal!.getModuleJob('cordis/worker', import.meta.url, {})! this.externals = await loadDependencies(mainJob) - const triggerLocalReload = this.ctx.debounce(() => this.triggerLocalReload(), this.config.debounce) + const partialReload = this.ctx.debounce(() => this.partialReload(), this.config.debounce) this.watcher.on('change', async (path) => { this.ctx.logger.debug('change detected:', path) const url = pathToFileURL(resolve(this.base, path)).href + + // full reload if (this.externals.has(url)) return loader.exit() + // partial reload if (loader.internal!.loadCache.has(url)) { this.stashed.add(url) - return triggerLocalReload() + return partialReload() } + // config reload const file = this.ctx.loader.files[url] if (!file) return if (file.suspend) { @@ -114,7 +118,7 @@ class Watcher extends Service { return } for (const tree of file.trees) { - tree.refresh() + tree.start() } }) } @@ -191,11 +195,11 @@ class Watcher extends Service { } } - private async triggerLocalReload() { + private async partialReload() { await this.analyzeChanges() /** plugins pending classification */ - const pending = new Map() + const pending = new Map() /** plugins that should be reloaded */ const reloads = new Map() @@ -213,9 +217,8 @@ class Watcher extends Service { if (this.declined.has(url)) continue const job = this.internal.loadCache.get(url) const plugin = this.ctx.loader.unwrapExports(job?.module?.getNamespace()) - const runtime = this.ctx.registry.get(plugin) if (!job || !plugin) continue - pending.set(job, [plugin, runtime]) + pending.set(job, plugin) this.declined.add(url) } catch (err) { this.ctx.logger.warn(err) @@ -223,7 +226,7 @@ class Watcher extends Service { } } - for (const [job, [plugin, runtime]] of pending) { + for (const [job, plugin] of pending) { // check if it is a dependent of the changed file this.declined.delete(job.url) const dependencies = [...await loadDependencies(job, this.declined)] @@ -235,11 +238,10 @@ class Watcher extends Service { dependencies.forEach(dep => this.accepted.add(dep)) // prepare for reload - if (runtime) { - reloads.set(plugin, { filename: job.url, children: runtime.scopes }) - } else { - reloads.set(plugin, { filename: job.url, children: [] }) - } + reloads.set(plugin, { + filename: job.url, + runtime: this.ctx.registry.get(plugin), + }) } // save cache for rollback @@ -269,16 +271,17 @@ class Watcher extends Service { return rollback() } - const reload = (plugin: any, children: EffectScope[]) => { - for (const oldFiber of children) { + const reload = (plugin: any, runtime?: Plugin.Runtime) => { + if (!runtime) return + for (const oldFiber of runtime.scopes) { const scope = oldFiber.parent.plugin(plugin, oldFiber.config) scope.entry = oldFiber.entry - if (scope.entry) scope.entry.fork = scope + if (scope.entry) scope.entry.scope = scope } } try { - for (const [plugin, { filename, children }] of reloads) { + for (const [plugin, { filename, runtime }] of reloads) { const path = this.relative(fileURLToPath(filename)) try { @@ -289,7 +292,7 @@ class Watcher extends Service { } try { - reload(attempts[filename], children) + reload(attempts[filename], runtime) this.ctx.logger.info('reload plugin at %c', path) } catch (err) { this.ctx.logger.warn('failed to reload plugin at %c', path) @@ -300,10 +303,10 @@ class Watcher extends Service { } catch { // rollback cache and plugin states rollback() - for (const [plugin, { filename, children }] of reloads) { + for (const [plugin, { filename, runtime }] of reloads) { try { this.ctx.registry.delete(attempts[filename]) - reload(plugin, children) + reload(plugin, runtime) } catch (err) { this.ctx.logger.warn(err) } diff --git a/packages/loader/tests/group.spec.ts b/packages/loader/tests/group.spec.ts index 2bd5e53..d790c6b 100644 --- a/packages/loader/tests/group.spec.ts +++ b/packages/loader/tests/group.spec.ts @@ -3,7 +3,7 @@ import { expect } from 'chai' import { Context } from '@cordisjs/core' import MockLoader from './utils' -describe.only('group management: basic support', () => { +describe('group management: basic support', () => { const root = new Context() root.plugin(MockLoader) const loader = root.loader as unknown as MockLoader