返回 CodeWhale
tree.ts
1 import { composeError, Context } from '@deepseek-ai/cordis'
2 import { isNonNullable, type Dict } from '@deepseek-ai/cosmokit'
3 import { Entry, type EntryOptions } from './entry.ts'
4 import { EntryGroup } from './group.ts'
5
6 /** Mutable tree of loader entries. Persistence is supplied by subclasses. */
7 export abstract class EntryTree {
8 static readonly sep = ':'
9
10 public ctx: Context
11 public enableLogs?: boolean
12 public root: EntryGroup
13 public store: Dict<Entry> = Object.create(null)
14
15 constructor(ctx: Context) {
16 this.ctx = ctx.extend({ baseUrl: ctx.baseUrl })
17 this.root = new EntryGroup(this.ctx, this)
18 const entry = this.ctx.fiber.entry
19 if (entry) entry.subtree = this
20 }
21
22 get context(): Context {
23 return this.ctx
24 }
25
26 /** Iterate entries in this tree and any nested subtrees. */
27 * entries(): Generator<Entry, void, void> {
28 for (const entry of Object.values(this.store)) {
29 yield entry
30 if (!entry.subtree) continue
31 yield* entry.subtree.entries()
32 }
33 }
34
35 /** Return pending import and lifecycle tasks owned by this tree. */
36 getTasks() {
37 return [...this.entries()]
38 .map(entry => entry._initTask || entry.fiber?.inertia)
39 .filter(isNonNullable)
40 }
41
42 /** Wait until this tree has no pending import or lifecycle tasks. */
43 async await() {
44 while (true) {
45 const tasks = this.getTasks()
46 if (!tasks.length) return
47 await Promise.allSettled(tasks)
48 }
49 }
50
51 ensureId(options: Partial<EntryOptions>) {
52 if (!options.id) {
53 do {
54 options.id = Math.random().toString(16).slice(2, 10)
55 } while (this.store[options.id])
56 }
57 return options.id!
58 }
59
60 /** Resolve an entry by id, including nested ids separated by `EntryTree.sep`. */
61 resolve(id: string) {
62 const parts = id.split(EntryTree.sep)
63 let tree: EntryTree | undefined = this
64 const final = parts.pop()!
65 for (const part of parts) {
66 tree = tree.store[part]?.subtree
67 if (!tree) throw new Error(`cannot resolve entry ${id}`)
68 }
69 const entry = tree.store[final]
70 if (!entry) throw new Error(`cannot resolve entry ${id}`)
71 return entry
72 }
73
74 resolveGroup(id: string | null) {
75 if (!id) return this.root
76 const entry = this.resolve(id)
77 if (!entry.subgroup) throw new Error(`entry ${id} is not a group`)
78 return entry.subgroup
79 }
80
81 /** Create an entry in the root group or a nested group. */
82 async create(options: Omit<EntryOptions, 'id'>, parent: string | null = null, position = Infinity) {
83 const group = this.resolveGroup(parent)
84 group.data.splice(position, 0, options as EntryOptions)
85 group.tree.write()
86 return group.create(options)
87 }
88
89 /** Stop and remove an entry from its parent group. */
90 remove(id: string) {
91 const entry = this.resolve(id)
92 entry.parent.remove(id)
93 entry.parent.tree.write()
94 }
95
96 /** Update an entry and optionally move it to another group. */
97 async update(id: string, options: Omit<EntryOptions, 'id' | 'name'>, parent?: string | null, position?: number) {
98 const entry = this.resolve(id)
99 const source = entry.parent
100 if (parent !== undefined) {
101 const target = this.resolveGroup(parent)
102 source.unlink(entry.options)
103 target.data.splice(position ?? Infinity, 0, entry.options)
104 target.tree.write()
105 entry.parent = target
106 }
107 source.tree.write()
108 return entry.update(options, false, true)
109 }
110
111 /** Import a plugin module from a specifier or `cordis:` builtin. */
112 import(name: string, getOuterStack?: () => string[]) {
113 if (name.startsWith('cordis:')) {
114 return this.ctx.loader.builtins[name.slice(7)]
115 }
116 return composeError(async (info) => {
117 // ModuleJob.run
118 // onImport.tracePromise.__proto__
119 // internal.import
120 info.offset += 3
121 if (this.ctx.loader.internal) {
122 return await this.ctx.loader.internal.import(name, this.ctx.baseUrl!, {})
123 } else if (name.startsWith('.')) {
124 return await import(/* @vite-ignore */new URL(name, this.ctx.baseUrl).href)
125 } else {
126 return await import(/* @vite-ignore */name)
127 }
128 }, getOuterStack)
129 }
130
131 /** Persist current tree state. In-memory trees may implement this as a no-op. */
132 abstract write(): void
133 }
134
134 lines TYPESCRIPT