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:
Tianyi Cui
2026-06-11 10:53:32 +08:00
parent ae2e08b4d6
commit 72688a3888
69 changed files with 6659 additions and 0 deletions

21
vendor/loader/LICENSE vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021-present Shigma
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

48
vendor/loader/README.md vendored Normal file
View File

@@ -0,0 +1,48 @@
# @cordisjs/plugin-loader
Runtime plugin loader for Cordis. The loader owns an `EntryTree`, imports plugin
modules by name, applies their config, and keeps the running plugin graph in
sync with entry updates.
## Usage
```ts
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
const root = new Context()
await root.plugin(Loader, { baseUrl: import.meta.url })
const id = await root.loader.create({
name: './plugins/example',
config: { enabled: true },
})
await root.loader.await()
root.loader.update(id, { config: { enabled: false } })
```
## Entry Options
| Field | Description |
| --- | --- |
| `id` | Stable id for resolving, updating, and removing the entry. |
| `name` | Module specifier imported by the loader. |
| `config` | Config passed to the plugin. |
| `group` | Marks the entry as a group whose `config` is a child entry list. |
| `disabled` | Stops the entry and prevents it from starting. |
| `inject` | Adds required services or intercept config for this entry. |
## API
| API | Description |
| --- | --- |
| `loader.create(options, parent?, position?)` | Add and start an entry. |
| `loader.update(id, options, parent?, position?)` | Update, move, and restart an entry. |
| `loader.remove(id)` | Stop and delete an entry. |
| `loader.resolve(id)` | Resolve an entry by id, including nested `a:b` ids. |
| `loader.resolveGroup(id)` | Resolve the root group or a nested group. |
| `loader.await()` | Wait for pending entry imports and fiber reloads. |
| `loader.locate(fiber?)` | Return the loader entry id that owns a fiber. |
For file-backed trees, use `@cordisjs/plugin-include`.

29
vendor/loader/package.json vendored Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "@cordisjs/plugin-loader",
"description": "Plugin loader for cordis",
"version": "1.0.0-rc.4",
"private": true,
"type": "module",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"exports": {
".": {
"types": "./lib/index.d.ts",
"default": "./lib/index.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib",
"src"
],
"author": "Shigma <shigma10826@gmail.com>",
"license": "MIT",
"peerDependencies": {
"cordis": "^4.0.0-rc.6"
},
"dependencies": {
"cosmokit": "^1.8.1"
}
}

184
vendor/loader/src/config/entry.ts vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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' })
}
}
}

12
vendor/loader/tsconfig.json vendored Normal file
View File

@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib"
},
"include": ["src"],
"references": [
{ "path": "../cosmokit" },
{ "path": "../cordis" }
]
}