tree.ts 4.1 KB

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