Skip to content

Commit

Permalink
feat(loader): support tree.entries(), support different baseURL for hmr
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed May 29, 2024
1 parent 643f4b1 commit 8bb0b0f
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 51 deletions.
38 changes: 20 additions & 18 deletions packages/hmr/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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()
}
})
}
Expand Down Expand Up @@ -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<Set<string>> = 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)
}
}
}

Expand Down
19 changes: 13 additions & 6 deletions packages/loader/src/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Entry.Options> = {}) {
// step 1: prepare isolate map
const ctx = this.fork?.parent ?? this.parent.ctx.extend({
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
: [
Expand All @@ -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) {
Expand All @@ -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()
Expand Down
8 changes: 4 additions & 4 deletions packages/loader/src/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ export class EntryGroup {

async create(options: Omit<Entry.Options, 'id'>) {
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
}

Expand All @@ -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[]) {
Expand Down
25 changes: 11 additions & 14 deletions packages/loader/src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 })
Expand All @@ -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)
}

Expand Down Expand Up @@ -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]
}
}
}
Expand Down
21 changes: 14 additions & 7 deletions packages/loader/src/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,33 @@ import { EntryGroup } from './group.ts'
export abstract class EntryTree {
public url!: string
public root: EntryGroup
public entries: Dict<Entry> = Object.create(null)
public store: Dict<Entry> = Object.create(null)

constructor(public ctx: Context) {
this.root = new EntryGroup(ctx, this)
const entry = ctx.scope.entry
if (entry) entry.subtree = this
}

* entries(): Generator<Entry, void, void> {
for (const entry of Object.values(this.store)) {
yield entry
if (!entry.subtree) continue
yield* entry.subtree.entries()
}
}

ensureId(options: Partial<Entry.Options>) {
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
}
Expand All @@ -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<Entry.Options, 'id' | 'name'>, 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()
Expand Down
4 changes: 2 additions & 2 deletions packages/loader/tests/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!
}
}

0 comments on commit 8bb0b0f

Please sign in to comment.