Vendor Cordis framework packages as source
cordis 4.0.0-rc.6, plugin-loader, -include, -group, -timer, -hmr, -logger-console, cosmokit 1.8.1, schemastery 3.18.0 — copied from the cordis-workspace checkout, flattened under vendor/, original npm names, private: true. vendor/README.md is the manifest: upstream repos + commit SHAs, local-modification log, sync procedure. Local modification: hmr's locale YAML imports and .i18n() call removed (avoids a runtime YAML import hook we don't vendor).
This commit is contained in:
184
vendor/loader/src/config/entry.ts
vendored
Normal file
184
vendor/loader/src/config/entry.ts
vendored
Normal file
@@ -0,0 +1,184 @@
|
||||
import { Context, Fiber, Inject } from 'cordis'
|
||||
import { deepEqual, isNullable } from 'cosmokit'
|
||||
import { Loader } from '../index.ts'
|
||||
import { EntryGroup } from './group.ts'
|
||||
import { EntryTree } from './tree.ts'
|
||||
import { evaluate, interpolate } from './utils.ts'
|
||||
|
||||
/** Serialized plugin entry options stored in loader config files. */
|
||||
export interface EntryOptions {
|
||||
/** Stable id inside the containing entry tree. */
|
||||
id: string
|
||||
/** Module specifier imported by the entry tree. */
|
||||
name: string
|
||||
/** Config passed to the plugin. */
|
||||
config?: any
|
||||
/** Marks this entry as a nested group. */
|
||||
group?: boolean | null
|
||||
/** Prevents this entry and descendants from running. */
|
||||
disabled?: boolean | null
|
||||
/** Required services or service intercept config for this entry. */
|
||||
inject?: Inject | null
|
||||
}
|
||||
|
||||
function takeEntries(object: {}, keys: string[]) {
|
||||
const result: [string, any][] = []
|
||||
for (const key of keys) {
|
||||
if (!(key in object)) continue
|
||||
result.push([key, object[key]])
|
||||
delete object[key]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function sortKeys<T extends {}>(object: T, prepend = ['id', 'name'], append = ['config']): T {
|
||||
const part1 = takeEntries(object, prepend)
|
||||
const part2 = takeEntries(object, append)
|
||||
const rest = takeEntries(object, Object.keys(object)).sort(([a], [b]) => a.localeCompare(b))
|
||||
return Object.assign(object, Object.fromEntries([...part1, ...rest, ...part2]))
|
||||
}
|
||||
|
||||
/** One configured plugin node inside an `EntryTree`. */
|
||||
export class Entry {
|
||||
static readonly key = Symbol.for('cordis.entry')
|
||||
|
||||
public ctx: Context
|
||||
public fiber?: Fiber
|
||||
public parent!: EntryGroup
|
||||
// safety: call `entry.update()` immediately after creating an entry
|
||||
public options = {} as EntryOptions
|
||||
public subgroup?: EntryGroup
|
||||
public subtree?: EntryTree
|
||||
|
||||
_initTask?: Promise<void>
|
||||
|
||||
constructor(public loader: Loader) {
|
||||
this.ctx = loader.ctx.extend({ [Entry.key]: this })
|
||||
this.context.emit('loader/entry-init', this)
|
||||
}
|
||||
|
||||
get context(): Context {
|
||||
return this.ctx
|
||||
}
|
||||
|
||||
get id() {
|
||||
let id = this.options.id
|
||||
if (this.parent.tree.ctx.fiber.entry) {
|
||||
id = this.parent.tree.ctx.fiber.entry.id + EntryTree.sep + id
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
/** True when this entry or any owning parent entry is disabled. */
|
||||
get disabled() {
|
||||
// group is always enabled
|
||||
if (this.options.group) return false
|
||||
let entry: Entry | undefined = this
|
||||
do {
|
||||
if (entry.options.disabled) return true
|
||||
entry = entry.parent.ctx.fiber.entry
|
||||
} while (entry)
|
||||
return false
|
||||
}
|
||||
|
||||
evaluate(expr: string) {
|
||||
return evaluate(this.ctx, expr)
|
||||
}
|
||||
|
||||
_resolveConfig(plugin: any): [any, any?] {
|
||||
if (plugin[EntryGroup.key]) return this.options.config
|
||||
return interpolate(this.ctx, this.options.config)
|
||||
}
|
||||
|
||||
private _patchContext(diff: string[]) {
|
||||
this.context.waterfall('loader/patch-context', this, () => {
|
||||
Object.setPrototypeOf(this.ctx, this.parent.ctx)
|
||||
|
||||
if (this.fiber?.uid && (diff.includes('config') || this.options.group)) {
|
||||
this.fiber.update(this._resolveConfig(this.fiber.runtime!.callback), true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
if (this.fiber) return
|
||||
if (this.disabled) return
|
||||
await this.init()
|
||||
}
|
||||
|
||||
/** Merge new options, restart as needed, and persist through the parent tree. */
|
||||
async update(options: Partial<EntryOptions>, create = false, force = false) {
|
||||
const legacy = { ...this.options }
|
||||
|
||||
// step 1: update options
|
||||
if (create) {
|
||||
this.options = options as EntryOptions
|
||||
} else {
|
||||
for (const [key, value] of Object.entries(options)) {
|
||||
if (isNullable(value)) {
|
||||
delete this.options[key]
|
||||
} else {
|
||||
this.options[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
sortKeys(this.options)
|
||||
|
||||
// step 2: execute
|
||||
if (this.disabled) {
|
||||
this.fiber?.dispose()
|
||||
return
|
||||
}
|
||||
|
||||
// step 3: check if options are changed
|
||||
if (this.fiber?.uid) {
|
||||
const diff = Object
|
||||
.keys({ ...this.options, ...legacy })
|
||||
.filter(key => !deepEqual(this.options[key], legacy[key]))
|
||||
if (!diff.length && !force) return
|
||||
this.context.emit('loader/partial-dispose', this, legacy, true)
|
||||
this._patchContext(diff)
|
||||
} else {
|
||||
await this.init()
|
||||
}
|
||||
}
|
||||
|
||||
getOuterStack = () => {
|
||||
let entry: Entry | undefined = this
|
||||
const result: string[] = []
|
||||
do {
|
||||
result.push(` at ${entry.parent.tree.ctx.baseUrl}#${entry.options.id}`)
|
||||
entry = entry.parent.ctx.fiber.entry
|
||||
} while (entry)
|
||||
return result
|
||||
}
|
||||
|
||||
/** Import and start the configured plugin if it is not already running. */
|
||||
async init() {
|
||||
try {
|
||||
await (this._initTask ??= this._init())
|
||||
} finally {
|
||||
this._initTask = undefined
|
||||
}
|
||||
this.fiber?.await().finally(() => {
|
||||
if (this.loader.getTasks().length) return
|
||||
this.ctx.reflect.notify(['loader'])
|
||||
})
|
||||
}
|
||||
|
||||
private async _init() {
|
||||
let exports: any
|
||||
try {
|
||||
exports = await this.parent.tree.import(this.options.name, this.getOuterStack)
|
||||
} catch (error) {
|
||||
this.ctx.logger.error(error)
|
||||
return
|
||||
} finally {
|
||||
this._initTask = undefined
|
||||
}
|
||||
const plugin = this.loader.unwrapExports(exports)
|
||||
this._patchContext([])
|
||||
this.loader.showLog(this, 'apply')
|
||||
this.fiber = this.ctx.registry.plugin(plugin, this._resolveConfig(plugin), this.getOuterStack)
|
||||
}
|
||||
}
|
||||
90
vendor/loader/src/config/group.ts
vendored
Normal file
90
vendor/loader/src/config/group.ts
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Context, Service } from 'cordis'
|
||||
import { Entry, EntryOptions } from './entry.ts'
|
||||
import { EntryTree } from './tree.ts'
|
||||
|
||||
/** Runtime owner for a list of child loader entries. */
|
||||
export class EntryGroup {
|
||||
static readonly key = Symbol.for('cordis.group')
|
||||
|
||||
public data: EntryOptions[] = []
|
||||
|
||||
constructor(public ctx: Context, public tree: EntryTree) {
|
||||
const entry = ctx.fiber.entry
|
||||
if (entry) entry.subgroup = this
|
||||
}
|
||||
|
||||
get context(): Context {
|
||||
return this.ctx
|
||||
}
|
||||
|
||||
async create(options: Omit<EntryOptions, 'id'>) {
|
||||
const id = this.tree.ensureId(options)
|
||||
const entry: Entry = this.tree.store[id] ??= new Entry(this.ctx.loader)
|
||||
// Entry may be moved from another group,
|
||||
// so we need to update the parent reference.
|
||||
entry.parent = this
|
||||
// Use `create: true` to replace existing entry.options.
|
||||
await entry.update(options, true, true)
|
||||
return entry.id
|
||||
}
|
||||
|
||||
unlink(options: EntryOptions) {
|
||||
const config = this.data
|
||||
const index = config.indexOf(options)
|
||||
if (index >= 0) config.splice(index, 1)
|
||||
}
|
||||
|
||||
remove(id: string, isDispose = false) {
|
||||
const entry = this.tree.store[id]
|
||||
if (!entry) return
|
||||
entry.fiber?.dispose()
|
||||
if (!isDispose) {
|
||||
this.unlink(entry.options)
|
||||
}
|
||||
delete this.tree.store[id]
|
||||
this.context.emit('loader/partial-dispose', entry, entry.options, false)
|
||||
}
|
||||
|
||||
async update(config: EntryOptions[]) {
|
||||
const oldConfig = this.data as EntryOptions[]
|
||||
this.data = config
|
||||
const oldMap = Object.fromEntries(oldConfig.map(options => [options.id, options]))
|
||||
const newMap = Object.fromEntries(config.map(options => [options.id ?? Symbol('anonymous'), options]))
|
||||
|
||||
// update inner plugins
|
||||
const ids = Reflect.ownKeys({ ...oldMap, ...newMap }) as string[]
|
||||
await Promise.all(ids.map(async (id) => {
|
||||
if (newMap[id]) {
|
||||
await this.create(newMap[id]).catch((error) => {
|
||||
this.ctx.logger.error(error)
|
||||
})
|
||||
} else {
|
||||
this.remove(id)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
stop() {
|
||||
for (const options of this.data) {
|
||||
this.remove(options.id, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Plugin that mounts a nested loader entry group. */
|
||||
export class Group extends EntryGroup {
|
||||
static initial: Omit<EntryOptions, 'id'>[] = []
|
||||
static readonly [EntryGroup.key] = true
|
||||
|
||||
constructor(public ctx: Context, public config: EntryOptions[]) {
|
||||
super(ctx, ctx.fiber.entry!.parent.tree)
|
||||
ctx.on('internal/update', (config) => {
|
||||
this.update(config)
|
||||
})
|
||||
}
|
||||
|
||||
async* [Service.init]() {
|
||||
yield () => this.stop()
|
||||
await this.update(this.config)
|
||||
}
|
||||
}
|
||||
173
vendor/loader/src/config/isolate.ts
vendored
Normal file
173
vendor/loader/src/config/isolate.ts
vendored
Normal file
@@ -0,0 +1,173 @@
|
||||
import { Context } from 'cordis'
|
||||
import { Dict } from 'cosmokit'
|
||||
import { Entry } from './entry.ts'
|
||||
|
||||
declare module './entry.ts' {
|
||||
interface EntryOptions {
|
||||
intercept?: Dict | null
|
||||
isolate?: Dict<true | string> | null
|
||||
}
|
||||
|
||||
interface Entry {
|
||||
realm: LocalRealm
|
||||
}
|
||||
}
|
||||
|
||||
function swap<T extends {}>(target: T, source?: T | null) {
|
||||
for (const key of Reflect.ownKeys(target)) {
|
||||
Reflect.deleteProperty(target, key)
|
||||
}
|
||||
for (const key of Reflect.ownKeys(source || {})) {
|
||||
Reflect.defineProperty(target, key, Reflect.getOwnPropertyDescriptor(source!, key)!)
|
||||
}
|
||||
}
|
||||
|
||||
/** Symbol realm used to isolate service implementations by entry or label. */
|
||||
export abstract class Realm {
|
||||
protected store: Dict<symbol> = Object.create(null)
|
||||
|
||||
abstract get suffix(): string
|
||||
|
||||
access(key: string, create = false) {
|
||||
if (create) {
|
||||
return this.store[key] ??= Symbol(`${key}${this.suffix}`)
|
||||
} else {
|
||||
return this.store[key] ?? Symbol(`${key}${this.suffix}`)
|
||||
}
|
||||
}
|
||||
|
||||
delete(key: string) {
|
||||
delete this.store[key]
|
||||
}
|
||||
|
||||
get size() {
|
||||
return Object.keys(this.store).length
|
||||
}
|
||||
}
|
||||
|
||||
/** Entry-local isolation realm. */
|
||||
export class LocalRealm extends Realm {
|
||||
constructor(private entry: Entry) {
|
||||
super()
|
||||
}
|
||||
|
||||
get suffix() {
|
||||
return '#' + this.entry.options.id
|
||||
}
|
||||
}
|
||||
|
||||
/** Named isolation realm shared by entries that use the same label. */
|
||||
export class GlobalRealm extends Realm {
|
||||
constructor(public label: string) {
|
||||
super()
|
||||
}
|
||||
|
||||
get suffix() {
|
||||
return '@' + this.label
|
||||
}
|
||||
}
|
||||
|
||||
/** Install loader hooks that apply `intercept` and `isolate` entry options. */
|
||||
export default function isolate(ctx: Context) {
|
||||
const realms: Dict<GlobalRealm> = Object.create(null)
|
||||
const delims: Dict<symbol> = Object.create(null)
|
||||
|
||||
function access(entry: Entry, name: string, create: true): symbol
|
||||
function access(entry: Entry, name: string, create?: boolean): symbol | undefined
|
||||
function access(entry: Entry, name: string, create = false) {
|
||||
let realm: Realm | undefined
|
||||
const label = entry.options.isolate?.[name]
|
||||
if (!label) return
|
||||
if (label === true) {
|
||||
realm = entry.realm ??= new LocalRealm(entry)
|
||||
} else if (create) {
|
||||
realm = realms[label] ??= new GlobalRealm(label)
|
||||
} else {
|
||||
realm = realms[label]
|
||||
}
|
||||
return realm?.access(name, create)
|
||||
}
|
||||
|
||||
ctx.on('loader/entry-init', (entry) => {
|
||||
entry.ctx[Context.intercept] = Object.create(entry.ctx[Context.intercept])
|
||||
entry.ctx[Context.isolate] = Object.create(entry.ctx[Context.isolate])
|
||||
})
|
||||
|
||||
ctx.on('loader/patch-context', (entry, next) => {
|
||||
// step 1: generate new isolate map
|
||||
const newMap: Dict<symbol> = Object.create(entry.parent.ctx[Context.isolate])
|
||||
for (const name of Object.keys(entry.options.isolate ?? {})) {
|
||||
newMap[name] = access(entry, name, true)
|
||||
}
|
||||
|
||||
// step 2: generate service diff
|
||||
const diff: Dict<[symbol, symbol, symbol, symbol]> = Object.create(null)
|
||||
const oldMap = entry.ctx[Context.isolate]
|
||||
for (const name in { ...newMap, ...delims }) {
|
||||
if (newMap[name] === oldMap[name]) continue
|
||||
const delim = delims[name] ??= Symbol(`delim:${name}`)
|
||||
entry.ctx[delim] = Symbol(`${name}#${entry.id}`)
|
||||
for (const symbol of [oldMap[name], newMap[name]]) {
|
||||
const impl = symbol && entry.ctx.reflect.store[symbol]
|
||||
if (!impl) continue
|
||||
if (!impl.fiber) {
|
||||
entry.ctx.logger.warn(new Error(`expected service ${name} to be implemented`))
|
||||
continue
|
||||
}
|
||||
diff[name] = [oldMap[name], newMap[name], entry.ctx[delim], impl.fiber.ctx[delim]]
|
||||
if (entry.ctx[delim] !== impl.fiber.ctx[delim]) break
|
||||
}
|
||||
}
|
||||
|
||||
// step 3: set prototype for transferred context
|
||||
Object.setPrototypeOf(entry.ctx[Context.isolate], entry.parent.ctx[Context.isolate])
|
||||
Object.setPrototypeOf(entry.ctx[Context.intercept], entry.parent.ctx[Context.intercept])
|
||||
swap(entry.ctx[Context.isolate], newMap)
|
||||
swap(entry.ctx[Context.intercept], entry.options.intercept)
|
||||
|
||||
// step 4: reload fiber
|
||||
next()
|
||||
|
||||
// step 5: replace service impl
|
||||
for (const [symbol1, symbol2, flag1, flag2] of Object.values(diff)) {
|
||||
if (flag1 === flag2 && entry.ctx.reflect.store[symbol1] && !entry.ctx.reflect.store[symbol2]) {
|
||||
entry.ctx.reflect.store[symbol2] = entry.ctx.reflect.store[symbol1]
|
||||
delete entry.ctx.reflect.store[symbol1]
|
||||
}
|
||||
}
|
||||
|
||||
// step 6: reflect notify
|
||||
ctx.reflect.notify(Object.keys(diff), (ctx, name) => {
|
||||
const [symbol1, symbol2, flag1, flag2] = diff[name]
|
||||
const symbol3 = ctx[Context.isolate][name]
|
||||
const flag3 = ctx[delims[name]]
|
||||
return (symbol1 === symbol3 || symbol2 === symbol3) && (flag1 === flag3) !== (flag1 === flag2)
|
||||
})
|
||||
|
||||
// step 7: clean up delimiters
|
||||
for (const name in delims) {
|
||||
if (!Reflect.ownKeys(newMap).includes(name)) {
|
||||
delete entry.ctx[delims[name]]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ctx.on('loader/partial-dispose', (entry, legacy, active) => {
|
||||
for (const [name, label] of Object.entries(legacy.isolate ?? {})) {
|
||||
if (label === true) continue
|
||||
if (active && entry.options.isolate?.[name] === label) continue
|
||||
const realm = realms[label]
|
||||
if (!realm) continue
|
||||
|
||||
// realm garbage collection
|
||||
for (const entry of ctx.loader.entries()) {
|
||||
// has reference to this realm
|
||||
if (entry.options.isolate?.[name] === realm.label) return
|
||||
}
|
||||
realm.delete(name)
|
||||
if (!realm.size) {
|
||||
delete realms[realm.label]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
133
vendor/loader/src/config/tree.ts
vendored
Normal file
133
vendor/loader/src/config/tree.ts
vendored
Normal file
@@ -0,0 +1,133 @@
|
||||
import { composeError, Context } from 'cordis'
|
||||
import { Dict, isNonNullable } from 'cosmokit'
|
||||
import { Entry, EntryOptions } from './entry.ts'
|
||||
import { EntryGroup } from './group.ts'
|
||||
|
||||
/** Mutable tree of loader entries. Persistence is supplied by subclasses. */
|
||||
export abstract class EntryTree {
|
||||
static readonly sep = ':'
|
||||
|
||||
public ctx: Context
|
||||
public enableLogs?: boolean
|
||||
public root: EntryGroup
|
||||
public store: Dict<Entry> = Object.create(null)
|
||||
|
||||
constructor(ctx: Context) {
|
||||
this.ctx = ctx.extend({ baseUrl: ctx.baseUrl })
|
||||
this.root = new EntryGroup(this.ctx, this)
|
||||
const entry = this.ctx.fiber.entry
|
||||
if (entry) entry.subtree = this
|
||||
}
|
||||
|
||||
get context(): Context {
|
||||
return this.ctx
|
||||
}
|
||||
|
||||
/** Iterate entries in this tree and any nested subtrees. */
|
||||
* entries(): Generator<Entry, void, void> {
|
||||
for (const entry of Object.values(this.store)) {
|
||||
yield entry
|
||||
if (!entry.subtree) continue
|
||||
yield* entry.subtree.entries()
|
||||
}
|
||||
}
|
||||
|
||||
/** Return pending import and lifecycle tasks owned by this tree. */
|
||||
getTasks() {
|
||||
return [...this.entries()]
|
||||
.map(entry => entry._initTask || entry.fiber?.inertia)
|
||||
.filter(isNonNullable)
|
||||
}
|
||||
|
||||
/** Wait until this tree has no pending import or lifecycle tasks. */
|
||||
async await() {
|
||||
while (true) {
|
||||
const tasks = this.getTasks()
|
||||
if (!tasks.length) return
|
||||
await Promise.allSettled(tasks)
|
||||
}
|
||||
}
|
||||
|
||||
ensureId(options: Partial<EntryOptions>) {
|
||||
if (!options.id) {
|
||||
do {
|
||||
options.id = Math.random().toString(16).slice(2, 10)
|
||||
} while (this.store[options.id])
|
||||
}
|
||||
return options.id!
|
||||
}
|
||||
|
||||
/** Resolve an entry by id, including nested ids separated by `EntryTree.sep`. */
|
||||
resolve(id: string) {
|
||||
const parts = id.split(EntryTree.sep)
|
||||
let tree: EntryTree | undefined = this
|
||||
const final = parts.pop()!
|
||||
for (const part of parts) {
|
||||
tree = tree.store[part]?.subtree
|
||||
if (!tree) throw new Error(`cannot resolve entry ${id}`)
|
||||
}
|
||||
const entry = tree.store[final]
|
||||
if (!entry) throw new Error(`cannot resolve entry ${id}`)
|
||||
return entry
|
||||
}
|
||||
|
||||
resolveGroup(id: string | null) {
|
||||
if (!id) return this.root
|
||||
const entry = this.resolve(id)
|
||||
if (!entry.subgroup) throw new Error(`entry ${id} is not a group`)
|
||||
return entry.subgroup
|
||||
}
|
||||
|
||||
/** Create an entry in the root group or a nested group. */
|
||||
async create(options: Omit<EntryOptions, 'id'>, parent: string | null = null, position = Infinity) {
|
||||
const group = this.resolveGroup(parent)
|
||||
group.data.splice(position, 0, options as EntryOptions)
|
||||
group.tree.write()
|
||||
return group.create(options)
|
||||
}
|
||||
|
||||
/** Stop and remove an entry from its parent group. */
|
||||
remove(id: string) {
|
||||
const entry = this.resolve(id)
|
||||
entry.parent.remove(id)
|
||||
entry.parent.tree.write()
|
||||
}
|
||||
|
||||
/** Update an entry and optionally move it to another group. */
|
||||
async update(id: string, options: Omit<EntryOptions, 'id' | 'name'>, parent?: string | null, position?: number) {
|
||||
const entry = this.resolve(id)
|
||||
const source = entry.parent
|
||||
if (parent !== undefined) {
|
||||
const target = this.resolveGroup(parent)
|
||||
source.unlink(entry.options)
|
||||
target.data.splice(position ?? Infinity, 0, entry.options)
|
||||
target.tree.write()
|
||||
entry.parent = target
|
||||
}
|
||||
source.tree.write()
|
||||
return entry.update(options, false, true)
|
||||
}
|
||||
|
||||
/** Import a plugin module from a specifier or `cordis:` builtin. */
|
||||
import(name: string, getOuterStack?: () => string[]) {
|
||||
if (name.startsWith('cordis:')) {
|
||||
return this.ctx.loader.builtins[name.slice(7)]
|
||||
}
|
||||
return composeError(async (info) => {
|
||||
// ModuleJob.run
|
||||
// onImport.tracePromise.__proto__
|
||||
// internal.import
|
||||
info.offset += 3
|
||||
if (this.ctx.loader.internal) {
|
||||
return await this.ctx.loader.internal.import(name, this.ctx.baseUrl!, {})
|
||||
} else if (name.startsWith('.')) {
|
||||
return await import(/* @vite-ignore */new URL(name, this.ctx.baseUrl).href)
|
||||
} else {
|
||||
return await import(/* @vite-ignore */name)
|
||||
}
|
||||
}, getOuterStack)
|
||||
}
|
||||
|
||||
/** Persist current tree state. In-memory trees may implement this as a no-op. */
|
||||
abstract write(): void
|
||||
}
|
||||
32
vendor/loader/src/config/utils.ts
vendored
Normal file
32
vendor/loader/src/config/utils.ts
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
import { valueMap } from 'cosmokit'
|
||||
|
||||
// eslint-disable-next-line no-new-func
|
||||
/** Evaluate a JavaScript expression against a loader context scope. */
|
||||
export const evaluate = new Function('ctx', 'expr', `
|
||||
with (ctx) {
|
||||
return eval(expr)
|
||||
}
|
||||
`) as ((ctx: object, expr: string) => any)
|
||||
|
||||
/** Recursively replace YAML `!js` expression nodes with evaluated values. */
|
||||
export function interpolate(ctx: object, value: any) {
|
||||
if (isJsExpr(value)) {
|
||||
return evaluate(ctx, value.__jsExpr)
|
||||
} else if (!value || typeof value !== 'object') {
|
||||
return value
|
||||
} else if (Array.isArray(value)) {
|
||||
return value.map(item => interpolate(ctx, item))
|
||||
} else {
|
||||
return valueMap(value, item => interpolate(ctx, item))
|
||||
}
|
||||
}
|
||||
|
||||
/** Return true when a value is a serialized loader JavaScript expression. */
|
||||
export function isJsExpr(value: any): value is JsExpr {
|
||||
return value instanceof Object && '__jsExpr' in value
|
||||
}
|
||||
|
||||
/** Serialized JavaScript expression produced by the include YAML tag. */
|
||||
export interface JsExpr {
|
||||
__jsExpr: string
|
||||
}
|
||||
185
vendor/loader/src/index.ts
vendored
Normal file
185
vendor/loader/src/index.ts
vendored
Normal file
@@ -0,0 +1,185 @@
|
||||
import { Context, Inject, Service } from 'cordis'
|
||||
import { defineProperty, Dict, isNullable } from 'cosmokit'
|
||||
import { ModuleLoader } from './internal.ts'
|
||||
import { Entry, EntryOptions } from './config/entry.ts'
|
||||
import isolate from './config/isolate.ts'
|
||||
import { EntryTree } from './config/tree.ts'
|
||||
|
||||
/** Re-export entry node APIs. */
|
||||
export * from './config/entry.ts'
|
||||
/** Re-export nested entry group APIs. */
|
||||
export * from './config/group.ts'
|
||||
/** Re-export service isolation helpers. */
|
||||
export * from './config/isolate.ts'
|
||||
/** Re-export entry tree persistence APIs. */
|
||||
export * from './config/tree.ts'
|
||||
/** Re-export loader config expression helpers. */
|
||||
export * from './config/utils.ts'
|
||||
/** Re-export Node internal module loader compatibility types. */
|
||||
export * from './internal.ts'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Events {
|
||||
'exit'(signal: NodeJS.Signals): Promise<void>
|
||||
'loader/config-update'(): void
|
||||
'loader/entry-init'(entry: Entry): void
|
||||
'loader/partial-dispose'(entry: Entry, legacy: Partial<EntryOptions>, active: boolean): void
|
||||
'loader/patch-context'(entry: Entry, next: () => void): void
|
||||
}
|
||||
|
||||
interface Context {
|
||||
loader: Loader
|
||||
}
|
||||
|
||||
interface EnvData {
|
||||
startTime?: number
|
||||
}
|
||||
|
||||
interface Fiber {
|
||||
entry?: Entry
|
||||
}
|
||||
}
|
||||
|
||||
/** Loader config and dependency intercept namespace. */
|
||||
export namespace Loader {
|
||||
/** Root loader configuration. */
|
||||
export interface Config {
|
||||
/** Base URL used to resolve relative plugin specifiers and config paths. */
|
||||
baseUrl?: string
|
||||
}
|
||||
|
||||
/** Intercept config used when other plugins depend on `loader`. */
|
||||
export interface Intercept {
|
||||
/** Keep dependent plugins pending while loader entries are still loading. */
|
||||
await?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Service that owns a loader entry tree and imports configured plugins.
|
||||
*
|
||||
* Subclasses provide persistence by implementing `write()` on `EntryTree`.
|
||||
*/
|
||||
export class Loader extends EntryTree {
|
||||
declare [Service.config]: Loader.Intercept
|
||||
|
||||
public envData = process.env.CORDIS_SHARED
|
||||
? JSON.parse(process.env.CORDIS_SHARED)
|
||||
: { startTime: Date.now() }
|
||||
|
||||
public name = 'loader'
|
||||
public internal = ModuleLoader.fromInternal()
|
||||
|
||||
public builtins: Dict<any> = Object.create(null)
|
||||
|
||||
constructor(ctx: Context, public config: Loader.Config = {}) {
|
||||
super(ctx)
|
||||
if (config.baseUrl) {
|
||||
this.ctx.baseUrl = config.baseUrl
|
||||
}
|
||||
const self = this
|
||||
|
||||
defineProperty(this, Service.tracker, {
|
||||
associate: 'loader',
|
||||
property: 'ctx',
|
||||
noShadow: true,
|
||||
})
|
||||
|
||||
ctx.reflect.provide('loader', this, this[Service.check])
|
||||
|
||||
ctx.on('internal/update', function (config, noSave, next) {
|
||||
if (!this.entry || noSave || this.parent.fiber?.entry === this.entry) return next()
|
||||
const unparse = this.runtime?.Config?.['simplify']
|
||||
this.entry.options.config = unparse ? unparse(config) : config
|
||||
this.entry.parent.tree.write()
|
||||
return next()
|
||||
}, { global: true, prepend: true })
|
||||
|
||||
ctx.on('internal/update', function (config, _, next) {
|
||||
if (!this.entry || this.parent.fiber?.entry === this.entry) return next()
|
||||
self.showLog(this.entry, 'reload')
|
||||
return next()
|
||||
}, { global: true })
|
||||
|
||||
ctx.on('internal/plugin', (fiber) => {
|
||||
// 1. set `fiber.entry`
|
||||
if (fiber.parent[Entry.key] && !fiber.entry) {
|
||||
fiber.entry = fiber.parent[Entry.key]
|
||||
// FIXME merge config
|
||||
Inject.resolve(fiber.entry!.options.inject, fiber.inject)
|
||||
}
|
||||
|
||||
// 2. handle self-dispose
|
||||
// We only care about `ctx.fiber.dispose()`, so we need to filter out other cases.
|
||||
|
||||
// case 1: fiber is created
|
||||
if (fiber.uid) return
|
||||
|
||||
// case 2: fiber is not tracked by loader
|
||||
if (!fiber.entry) return
|
||||
|
||||
// case 3: fiber is a child plugin under the entry (not the entry's root fiber)
|
||||
if (fiber.parent.fiber?.entry === fiber.entry) return
|
||||
|
||||
// case 4: fiber is disposed on behalf of plugin deletion (such as plugin hmr)
|
||||
// self-dispose: ctx.fiber.dispose() -> fiber / runtime dispose -> delete(plugin)
|
||||
// plugin hmr: delete(plugin) -> runtime dispose -> fiber dispose
|
||||
if (!ctx.registry.has(fiber.runtime!.callback)) return
|
||||
|
||||
// case 5: the entry's tree is being disposed
|
||||
if (!fiber.entry.parent.tree.ctx.fiber.uid) return
|
||||
|
||||
this.showLog(fiber.entry, 'unload')
|
||||
|
||||
// case 6: fiber is disposed by loader behavior
|
||||
// such as inject checker, config file update, ancestor group disable
|
||||
if (fiber.entry.disabled) return
|
||||
|
||||
fiber.entry.options.disabled = true
|
||||
fiber.entry.parent.tree.write()
|
||||
})
|
||||
|
||||
ctx.plugin(isolate)
|
||||
}
|
||||
|
||||
write() {
|
||||
// Loader's root tree is in-memory; writes are no-ops.
|
||||
}
|
||||
|
||||
[Service.check]() {
|
||||
const config: Loader.Intercept = Service.prototype[Service.resolveConfig].call(this)
|
||||
if (config.await && this.getTasks().length) return false
|
||||
return true
|
||||
}
|
||||
|
||||
showLog(entry: Entry, type: string) {
|
||||
if (entry.options.group || !entry.parent.tree.enableLogs) return
|
||||
this.ctx.root.logger?.('loader').info('%s plugin %C', type, entry.options.name)
|
||||
}
|
||||
|
||||
/** Return the loader entry id that owns `fiber`, if any. */
|
||||
locate(fiber = this.ctx.fiber) {
|
||||
while (1) {
|
||||
if (fiber.entry) return fiber.entry.id
|
||||
const next = fiber.parent.fiber
|
||||
if (fiber === next) return
|
||||
fiber = next
|
||||
}
|
||||
}
|
||||
|
||||
/** Hook for hosts that can restart the process on full-reload requests. */
|
||||
exit() {
|
||||
}
|
||||
|
||||
/** Normalize ESM/CJS/default export shapes before applying a plugin. */
|
||||
unwrapExports(exports: any) {
|
||||
if (isNullable(exports)) return exports
|
||||
exports = exports.default ?? exports
|
||||
// https://github.com/evanw/esbuild/issues/2623
|
||||
// https://esbuild.github.io/content-types/#default-interop
|
||||
if (!exports.__esModule) return exports
|
||||
return exports.default ?? exports
|
||||
}
|
||||
}
|
||||
|
||||
export default Loader
|
||||
122
vendor/loader/src/internal.ts
vendored
Normal file
122
vendor/loader/src/internal.ts
vendored
Normal file
@@ -0,0 +1,122 @@
|
||||
import { createRequire, LoadHookContext } from 'node:module'
|
||||
import { Dict } from 'cosmokit'
|
||||
|
||||
/** Node internal module format names handled by loader hooks. */
|
||||
export type ModuleFormat = 'builtin' | 'commonjs' | 'json' | 'module' | 'wasm'
|
||||
/** Source payload accepted by Node internal module load hooks. */
|
||||
export type ModuleSource = string | ArrayBuffer
|
||||
|
||||
/** Result returned by a Node internal resolve hook. */
|
||||
export interface ResolveResult {
|
||||
format: ModuleFormat
|
||||
url: string
|
||||
}
|
||||
|
||||
/** Result returned by a Node internal load hook. */
|
||||
export interface LoadResult {
|
||||
format: ModuleFormat
|
||||
source?: ModuleSource
|
||||
}
|
||||
|
||||
type LoadCacheData = ModuleJob // | Function
|
||||
|
||||
/** @see https://github.com/nodejs/node/blob/main/lib/internal/modules/esm/module_map.js */
|
||||
interface LoadCache extends Omit<Map<string, Dict<LoadCacheData>>, 'get' | 'set' | 'has'> {
|
||||
get(url: string, type?: string): LoadCacheData | undefined
|
||||
set(url: string, type?: string, job?: LoadCacheData): this
|
||||
has(url: string, type?: string): boolean
|
||||
}
|
||||
|
||||
/** Minimal Node internal ModuleWrap surface used by HMR helpers. */
|
||||
export interface ModuleWrap {
|
||||
url: string
|
||||
getNamespace(): any
|
||||
}
|
||||
|
||||
/** @see https://github.com/nodejs/node/blob/main/lib/internal/modules/esm/module_job.js */
|
||||
export interface ModuleJob {
|
||||
url: string
|
||||
loader: ModuleLoader
|
||||
module?: ModuleWrap
|
||||
importAttributes: ImportAttributes
|
||||
linked: Promise<ModuleJob[]>
|
||||
instantiate(): Promise<void>
|
||||
run(): Promise<{ module: ModuleWrap }>
|
||||
}
|
||||
|
||||
/**
|
||||
* Node 22/23 ModuleLoader interface.
|
||||
*
|
||||
* Key methods:
|
||||
* - getModuleJobForImport(specifier, parentURL, importAttributes)
|
||||
* - resolve(specifier, parentURL, importAttributes) → Promise<ResolveResult>
|
||||
* - resolveSync(specifier, parentURL, importAttributes) → ResolveResult
|
||||
*/
|
||||
export interface ModuleLoaderV1 {
|
||||
version: 'v1'
|
||||
loadCache: LoadCache
|
||||
import(specifier: string, parentURL: string, importAttributes: ImportAttributes): Promise<any>
|
||||
register(specifier: string | URL, parentURL?: string | URL, data?: any, transferList?: any[]): void
|
||||
getModuleJobForImport(specifier: string, parentURL: string, importAttributes: ImportAttributes): Promise<ModuleJob>
|
||||
resolve(specifier: string, parentURL: string, importAttributes: ImportAttributes): Promise<ResolveResult>
|
||||
resolveSync(specifier: string, parentURL: string, importAttributes: ImportAttributes): ResolveResult
|
||||
load(specifier: string, context: Pick<LoadHookContext, 'format' | 'importAttributes'>): Promise<LoadResult>
|
||||
}
|
||||
|
||||
/** Node 24+ module request object. */
|
||||
export interface ModuleRequest {
|
||||
specifier: string
|
||||
attributes?: ImportAttributes
|
||||
phase?: ModulePhase
|
||||
}
|
||||
|
||||
/** @see https://github.com/nodejs/node/blob/main/src/module_wrap.h */
|
||||
export const enum ModulePhase {
|
||||
Source = 1,
|
||||
Evaluation = 2,
|
||||
}
|
||||
|
||||
/** Opaque Node internal module request type marker. */
|
||||
export type ModuleRequestType = unknown // internal symbols
|
||||
|
||||
/**
|
||||
* Node 24+ ModuleLoader interface.
|
||||
*
|
||||
* Breaking changes from v1:
|
||||
* - getModuleJobForImport removed → getOrCreateModuleJob(parentURL, request, requestType)
|
||||
* - resolve removed (became private #resolve) → resolveSync(parentURL, request)
|
||||
* - Parameter order reversed for resolveSync, request object { specifier, attributes }
|
||||
* - LoadCache became typed Map<url, { [type]: ModuleJob }> with delete only setting undefined
|
||||
*/
|
||||
export interface ModuleLoaderV2 {
|
||||
version: 'v2'
|
||||
loadCache: LoadCache
|
||||
import(specifier: string, parentURL: string, importAttributes: ImportAttributes, phase?: ModulePhase, isEntryPoint?: boolean): Promise<any>
|
||||
register(specifier: string | URL, parentURL?: string | URL, data?: any, transferList?: any[], isInternal?: boolean): void
|
||||
getOrCreateModuleJob(parentURL: string, request: ModuleRequest, requestType?: ModuleRequestType): Promise<ModuleJob>
|
||||
resolveSync(parentURL: string, request: ModuleRequest): ResolveResult
|
||||
load(url: string, context: Pick<LoadHookContext, 'format' | 'importAttributes'>): Promise<LoadResult>
|
||||
}
|
||||
|
||||
/** Supported Node internal ESM loader shapes. */
|
||||
export type ModuleLoader = ModuleLoaderV1 | ModuleLoaderV2
|
||||
|
||||
/** Helpers for locating the current Node internal module loader. */
|
||||
export namespace ModuleLoader {
|
||||
let _cachedLoader: ModuleLoader | undefined
|
||||
|
||||
export function fromInternal(): ModuleLoader | undefined {
|
||||
if (!process.execArgv.includes('--expose-internals')) return
|
||||
if (_cachedLoader) return _cachedLoader
|
||||
const require = createRequire(import.meta.url)
|
||||
const [major] = process.versions.node.split('.').map(Number)
|
||||
|
||||
if (major >= 24) {
|
||||
const raw = require('internal/modules/esm/loader').getOrInitializeCascadedLoader()
|
||||
return _cachedLoader = Object.assign(raw, { version: 'v2' })
|
||||
} else if (major >= 22) {
|
||||
const raw = require('internal/modules/esm/loader').getOrInitializeCascadedLoader()
|
||||
return _cachedLoader = Object.assign(raw, { version: 'v1' })
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user