From 8bb0b0fb95e2b5e41a594a2acd9f6fe89b5b4073 Mon Sep 17 00:00:00 2001 From: Shigma Date: Wed, 29 May 2024 22:58:24 +0800 Subject: [PATCH] feat(loader): support tree.entries(), support different baseURL for hmr --- packages/hmr/src/index.ts | 38 ++++++++++++++++++---------------- packages/loader/src/entry.ts | 19 +++++++++++------ packages/loader/src/group.ts | 8 +++---- packages/loader/src/loader.ts | 25 ++++++++++------------ packages/loader/src/tree.ts | 21 ++++++++++++------- packages/loader/tests/utils.ts | 4 ++-- 6 files changed, 64 insertions(+), 51 deletions(-) diff --git a/packages/hmr/src/index.ts b/packages/hmr/src/index.ts index 1823c80..06d4b52 100644 --- a/packages/hmr/src/index.ts +++ b/packages/hmr/src/index.ts @@ -41,7 +41,6 @@ class Watcher extends Service { private base: string private watcher!: FSWatcher - private initialURL!: string /** * changes from externals E will always trigger a full reload @@ -72,8 +71,6 @@ class Watcher extends Service { constructor(ctx: Context, public config: Watcher.Config) { super(ctx, 'hmr') this.base = resolve(ctx.baseDir, config.base || '') - // FIXME resolve deps based on different files - this.initialURL = ctx.loader.url } relative(filename: string) { @@ -111,8 +108,8 @@ class Watcher extends Service { file.suspend = false return } - for (const loader of file.groups) { - loader.refresh() + for (const tree of file.trees) { + tree.refresh() } }) } @@ -200,19 +197,24 @@ class Watcher extends Service { // Plugin entry files should be "atomic". // Which means, reloading them will not cause any other reloads. - const names = new Set(Object.values(this.ctx.loader.entries).map(entry => entry.options.name)) - for (const name of names) { - try { - const { url } = await this.ctx.loader.internal!.resolve(name, this.initialURL, {}) - if (this.declined.has(url)) continue - const job = this.ctx.loader.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]) - this.declined.add(url) - } catch (err) { - this.ctx.logger.warn(err) + const nameMap: Dict> = Object.create(null) + for (const entry of this.ctx.loader.entries()) { + (nameMap[entry.parent.tree.url] ??= new Set()).add(entry.options.name) + } + for (const baseURL in nameMap) { + for (const name of nameMap[baseURL]) { + try { + const { url } = await this.ctx.loader.internal!.resolve(name, baseURL, {}) + if (this.declined.has(url)) continue + const job = this.ctx.loader.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]) + this.declined.add(url) + } catch (err) { + this.ctx.logger.warn(err) + } } } diff --git a/packages/loader/src/entry.ts b/packages/loader/src/entry.ts index ff6c024..b131f7f 100644 --- a/packages/loader/src/entry.ts +++ b/packages/loader/src/entry.ts @@ -62,6 +62,13 @@ export class Entry { } } + hasIsolate(key: string, realm: string) { + if (!this.fork) return false + const label = this.options.isolate?.[key] + if (!label) return false + return realm === this.resolveRealm(label) + } + patch(options: Partial = {}) { // step 1: prepare isolate map const ctx = this.fork?.parent ?? this.parent.ctx.extend({ @@ -112,13 +119,13 @@ export class Entry { swap(ctx[Context.isolate], newMap) swap(ctx[Context.intercept], this.options.intercept) - // step 4.2: update fork when options.config is updated + // step 4.2: update fork (only when options.config is updated) if (this.fork && 'config' in options) { this.suspend = true this.fork.update(this.options.config) } - // step 4.3: update service impl + // step 4.3: replace service impl for (const [, symbol1, symbol2, flag1, flag2] of diff) { if (flag1 === flag2 && ctx[symbol1] && !ctx[symbol2]) { ctx.root[symbol2] = ctx.root[symbol1] @@ -146,13 +153,13 @@ export class Entry { return ctx } - get requiredInjects() { + get requiredDeps() { return Array.isArray(this.options.inject) ? this.options.inject : this.options.inject?.required ?? [] } - get optionalInjects() { + get deps() { return Array.isArray(this.options.inject) ? this.options.inject : [ @@ -163,7 +170,7 @@ export class Entry { _check() { if (this.options.disabled) return false - for (const name of this.requiredInjects) { + for (const name of this.requiredDeps) { let key = this.parent.ctx[Context.isolate][name] const label = this.options.isolate?.[name] if (label) { @@ -176,7 +183,7 @@ export class Entry { } async checkService(name: string) { - if (!this.requiredInjects.includes(name)) return + if (!this.requiredDeps.includes(name)) return const ready = this._check() if (ready && !this.fork) { await this.start() diff --git a/packages/loader/src/group.ts b/packages/loader/src/group.ts index 05160d7..a8243fa 100644 --- a/packages/loader/src/group.ts +++ b/packages/loader/src/group.ts @@ -12,11 +12,11 @@ export class EntryGroup { async create(options: Omit) { const id = this.tree.ensureId(options) - const entry = this.tree.entries[id] ??= new Entry(this.ctx.loader, this) + const entry = this.tree.store[id] ??= new Entry(this.ctx.loader, this) // Entry may be moved from another group, // so we need to update the parent reference. entry.parent = this - await entry.update(options as Entry.Options, true) + await entry.update(options, true) return id } @@ -27,11 +27,11 @@ export class EntryGroup { } remove(id: string) { - const entry = this.tree.entries[id] + const entry = this.tree.store[id] if (!entry) return entry.stop() this.unlink(entry.options) - delete this.tree.entries[id] + delete this.tree.store[id] } update(config: Entry.Options[]) { diff --git a/packages/loader/src/loader.ts b/packages/loader/src/loader.ts index 9db650c..51387a8 100644 --- a/packages/loader/src/loader.ts +++ b/packages/loader/src/loader.ts @@ -8,6 +8,7 @@ import { ImportTree, LoaderFile } from './file.ts' export * from './entry.ts' export * from './file.ts' export * from './group.ts' +export * from './tree.ts' declare module '@cordisjs/core' { interface Events { @@ -100,13 +101,13 @@ export abstract class Loader extends ImportTree { }) this.ctx.on('internal/before-service', (name) => { - for (const entry of Object.values(this.entries)) { + for (const entry of this.entries()) { entry.checkService(name) } }, { global: true }) this.ctx.on('internal/service', (name) => { - for (const entry of Object.values(this.entries)) { + for (const entry of this.entries()) { entry.checkService(name) } }, { global: true }) @@ -116,7 +117,7 @@ export abstract class Loader extends ImportTree { if (scope.runtime === scope) { return scope.runtime.children.every(fork => checkInject(fork, name)) } - if (scope.entry?.optionalInjects.includes(name)) return true + if (scope.entry?.deps.includes(name)) return true return checkInject(scope.parent.scope, name) } @@ -175,17 +176,13 @@ export abstract class Loader extends ImportTree { return exports.default ?? exports } - _clearRealm(key: string, name: string) { - const hasRef = Object.values(this.entries).some((entry) => { - if (!entry.fork) return false - const label = entry.options.isolate?.[key] - if (!label) return false - return name === entry.resolveRealm(label) - }) - if (hasRef) return - delete this.realms[name][key] - if (!Object.keys(this.realms[name]).length) { - delete this.realms[name] + _clearRealm(key: string, realm: string) { + for (const entry of this.entries()) { + if (entry.hasIsolate(key, realm)) return + } + delete this.realms[realm][key] + if (!Object.keys(this.realms[realm]).length) { + delete this.realms[realm] } } } diff --git a/packages/loader/src/tree.ts b/packages/loader/src/tree.ts index cba2b1a..1fd74ec 100644 --- a/packages/loader/src/tree.ts +++ b/packages/loader/src/tree.ts @@ -6,7 +6,7 @@ import { EntryGroup } from './group.ts' export abstract class EntryTree { public url!: string public root: EntryGroup - public entries: Dict = Object.create(null) + public store: Dict = Object.create(null) constructor(public ctx: Context) { this.root = new EntryGroup(ctx, this) @@ -14,17 +14,25 @@ export abstract class EntryTree { if (entry) entry.subtree = this } + * entries(): Generator { + for (const entry of Object.values(this.store)) { + yield entry + if (!entry.subtree) continue + yield* entry.subtree.entries() + } + } + ensureId(options: Partial) { if (!options.id) { do { options.id = Math.random().toString(36).slice(2, 8) - } while (this.entries[options.id]) + } while (this.store[options.id]) } return options.id! } resolveGroup(id: string | null) { - const group = id ? this.entries[id]?.subgroup : this.root + const group = id ? this.store[id]?.subgroup : this.root if (!group) throw new Error(`entry ${id} not found`) return group } @@ -37,20 +45,19 @@ export abstract class EntryTree { } remove(id: string) { - const entry = this.entries[id] + const entry = this.store[id] if (!entry) throw new Error(`entry ${id} not found`) entry.parent.remove(id) entry.parent.tree.write() } async update(id: string, options: Omit, parent?: string | null, position?: number) { - const entry = this.entries[id] + const entry = this.store[id] if (!entry) throw new Error(`entry ${id} not found`) const source = entry.parent source.tree.write() - let target: EntryGroup | undefined if (parent !== undefined) { - target = this.resolveGroup(parent) + const target = this.resolveGroup(parent) source.unlink(entry.options) target.data.splice(position ?? Infinity, 0, entry.options) target.tree.write() diff --git a/packages/loader/tests/utils.ts b/packages/loader/tests/utils.ts index 5e344c7..793ef98 100644 --- a/packages/loader/tests/utils.ts +++ b/packages/loader/tests/utils.ts @@ -61,7 +61,7 @@ export default class MockLoader extends Loader { } expectFork(id: string) { - expect(this.entries[id]?.fork).to.be.ok - return this.entries[id]!.fork! + expect(this.store[id]?.fork).to.be.ok + return this.store[id]!.fork! } }