mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
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:
64
vendor/README.md
vendored
Normal file
64
vendor/README.md
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
# Vendored Packages
|
||||
|
||||
This directory contains source-vendored copies of the Cordis framework and its
|
||||
foundation libraries. They are copied into this monorepo instead of being
|
||||
depended on via npm, so that the harness fully owns its framework layer
|
||||
(auditable, patchable, pinned).
|
||||
|
||||
All vendored packages keep their **original npm names** (they are resolved
|
||||
through Yarn workspaces) and are marked `private: true` — they are never
|
||||
published from this repo. Upstream MIT `LICENSE` files are preserved in each
|
||||
package directory.
|
||||
|
||||
## Manifest
|
||||
|
||||
Upstream workspace: `cordis-workspace` (local checkout: `~/repos/cordis-workspace`).
|
||||
|
||||
| Directory | npm name | Version | Upstream repo | Commit |
|
||||
|---|---|---|---|---|
|
||||
| `cosmokit/` | `cosmokit` | 1.8.1 | https://github.com/deepseek-harness/cosmokit | `16f6fc058ade66e8ac5da0033d35a8d0f279f544` |
|
||||
| `schemastery/` | `schemastery` | 3.18.0 | https://github.com/deepseek-harness/schemastery (`packages/core`) | `e67cee00ad725bd1534aee930a979ea3eec6f698` |
|
||||
| `cordis/` | `cordis` | 4.0.0-rc.6 | https://github.com/deepseek-harness/cordis (`packages/core`) | `abb0a307cb1d3b0947f455d590cf5ba922d4caa4` |
|
||||
| `loader/` | `@cordisjs/plugin-loader` | 1.0.0-rc.4 | https://github.com/deepseek-harness/cordis (`packages/loader`) | `abb0a307cb1d3b0947f455d590cf5ba922d4caa4` |
|
||||
| `include/` | `@cordisjs/plugin-include` | 1.0.4 | https://github.com/deepseek-harness/cordis (`packages/include`) | `abb0a307cb1d3b0947f455d590cf5ba922d4caa4` |
|
||||
| `group/` | `@cordisjs/plugin-group` | 1.0.0 | https://github.com/deepseek-harness/cordis (`packages/group`) | `abb0a307cb1d3b0947f455d590cf5ba922d4caa4` |
|
||||
| `timer/` | `@cordisjs/plugin-timer` | 1.1.2 | https://github.com/deepseek-harness/cordis (`packages/timer`) | `abb0a307cb1d3b0947f455d590cf5ba922d4caa4` |
|
||||
| `hmr/` | `@cordisjs/plugin-hmr` | 1.0.15 | https://github.com/deepseek-harness/cordis (`packages/hmr`) | `abb0a307cb1d3b0947f455d590cf5ba922d4caa4` |
|
||||
| `logger-console/` | `@cordisjs/plugin-logger-console` | 1.0.0 | https://github.com/deepseek-harness/cordis (`packages/logger-console`) | `abb0a307cb1d3b0947f455d590cf5ba922d4caa4` |
|
||||
|
||||
Third-party dependencies of the vendored packages stay on npm:
|
||||
`@standard-schema/spec`, `js-yaml`, `chokidar`, `picomatch`,
|
||||
`@babel/code-frame`, `supports-color`.
|
||||
|
||||
Intentionally **not** vendored (verified unused by this set): `reggol`,
|
||||
`@cordisjs/utils`, `@cordisjs/element`, `@cordisjs/unyaml` (dev-time YAML
|
||||
import hook only).
|
||||
|
||||
## Local modifications
|
||||
|
||||
Keep this log exhaustive — every divergence from upstream must be listed.
|
||||
|
||||
1. **`hmr/src/index.ts`**: removed the `./locales/en-US.yml` /
|
||||
`./locales/zh-CN.yml` imports, the `.i18n({...})` call on the `Config`
|
||||
schema, and the `src/locales/` directory. Rationale: those imports require
|
||||
a runtime YAML loader hook (`@cordisjs/unyaml`) that we do not vendor;
|
||||
the i18n texts only localize config descriptions.
|
||||
2. **All `package.json` files**: regenerated — added `private: true`, added
|
||||
`src` to `files` and a `./src/*` export where missing, removed upstream
|
||||
`devDependencies`/`scripts`/`repository` fields. Dependency and
|
||||
peer-dependency ranges preserved.
|
||||
3. **All `tsconfig.json` files**: regenerated to extend the repo-root
|
||||
`tsconfig.base.json` and declare project references.
|
||||
|
||||
## Sync procedure
|
||||
|
||||
To update a vendored package from upstream:
|
||||
|
||||
1. In the upstream workspace, note `git rev-parse HEAD` of the relevant
|
||||
submodule.
|
||||
2. Copy the package's `src/` (and `bin.js`, `README.md`, `LICENSE` if changed)
|
||||
over the vendored directory.
|
||||
3. Re-apply the local modifications listed above (or drop them if upstream
|
||||
made them unnecessary — update the log either way).
|
||||
4. Update the version and commit hash in the manifest table.
|
||||
5. Run `yarn install && yarn test && yarn build` at the repo root.
|
||||
21
vendor/cordis/LICENSE
vendored
Normal file
21
vendor/cordis/LICENSE
vendored
Normal 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.
|
||||
101
vendor/cordis/README.md
vendored
Normal file
101
vendor/cordis/README.md
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
# Cordis
|
||||
|
||||
Cordis is a TypeScript plugin framework for applications that need explicit
|
||||
dependency injection, scoped services, lifecycle-managed cleanup, and optional
|
||||
configuration-driven loading. The core package is published as `cordis`; the
|
||||
official packages in this repository add a loader, config-file includes, HMR,
|
||||
console logging, timers, and project scaffolding.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
yarn add cordis
|
||||
```
|
||||
|
||||
Cordis is ESM-first. The repository is tested on current Node releases, and the
|
||||
scaffolder requires Node 22 or newer.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import { Context, Service } from 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
counter: Counter
|
||||
}
|
||||
|
||||
interface Events {
|
||||
'app/ready'(message: string): void
|
||||
}
|
||||
}
|
||||
|
||||
class Counter extends Service {
|
||||
value = 0
|
||||
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'counter')
|
||||
}
|
||||
|
||||
next() {
|
||||
return ++this.value
|
||||
}
|
||||
}
|
||||
|
||||
const greeter = Object.assign((ctx: Context) => {
|
||||
ctx.on('app/ready', (message) => {
|
||||
ctx.logger.info('%s #%d', message, ctx.counter.next())
|
||||
})
|
||||
}, {
|
||||
inject: ['counter'],
|
||||
})
|
||||
|
||||
const root = new Context()
|
||||
await root.plugin(Counter)
|
||||
await root.plugin(greeter)
|
||||
|
||||
root.emit('app/ready', 'started')
|
||||
await root.fiber.dispose()
|
||||
```
|
||||
|
||||
The important pieces are:
|
||||
|
||||
- `new Context()` creates the root dependency container.
|
||||
- `ctx.plugin()` starts a plugin and returns a `Fiber`.
|
||||
- `inject` tells Cordis which services must exist before the plugin runs.
|
||||
- Effects, event listeners, and services are removed when their owning fiber is
|
||||
disposed.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Tutorial: build a plugin](../../docs/tutorials/build-a-plugin.md)
|
||||
- [Guide: plugin lifecycle](../../docs/guides/plugin-lifecycle.md)
|
||||
- [Guide: loader configuration](../../docs/guides/loader-config.md)
|
||||
- [API reference](../../docs/api/core.md)
|
||||
|
||||
## Packages
|
||||
|
||||
| Package | Purpose |
|
||||
| --- | --- |
|
||||
| `cordis` | Core context, plugin registry, fiber lifecycle, events, services, and logger. |
|
||||
| `create-cordis` | Interactive project scaffolder. |
|
||||
| `@cordisjs/plugin-loader` | Runtime plugin tree and loader service. |
|
||||
| `@cordisjs/plugin-include` | YAML/JSON config-file include support for the loader. |
|
||||
| `@cordisjs/plugin-group` | Nested plugin groups for loader configs. |
|
||||
| `@cordisjs/plugin-hmr` | Hot module replacement for loader-managed plugins. |
|
||||
| `@cordisjs/plugin-logger-console` | Console exporter for the built-in logger. |
|
||||
| `@cordisjs/plugin-timer` | Disposal-aware timeout, interval, throttle, and debounce helpers. |
|
||||
| `@cordisjs/utils` | Shared utilities used by Cordis packages. |
|
||||
|
||||
## Development
|
||||
|
||||
```sh
|
||||
yarn install
|
||||
yarn build
|
||||
yarn test
|
||||
yarn lint
|
||||
```
|
||||
|
||||
The monorepo uses Yakumo to build and test all packages. Most examples in the
|
||||
docs use public APIs from `cordis`; loader examples additionally use
|
||||
`@cordisjs/plugin-loader` and `@cordisjs/plugin-include`.
|
||||
16
vendor/cordis/bin.js
vendored
Executable file
16
vendor/cordis/bin.js
vendored
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Context } from 'cordis'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
|
||||
const ctx = new Context()
|
||||
ctx.baseUrl = pathToFileURL(process.cwd()).href + '/'
|
||||
|
||||
await ctx.plugin(Loader)
|
||||
await ctx.loader.create({
|
||||
name: '@cordisjs/plugin-include',
|
||||
config: {
|
||||
path: './cordis.yml',
|
||||
},
|
||||
})
|
||||
41
vendor/cordis/package.json
vendored
Normal file
41
vendor/cordis/package.json
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "cordis",
|
||||
"description": "Meta-Framework for Modern JavaScript Applications",
|
||||
"version": "4.0.0-rc.6",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"bin": "bin.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./lib/index.d.ts",
|
||||
"default": "./lib/index.js"
|
||||
},
|
||||
"./src/*": "./src/*",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
"bin.js"
|
||||
],
|
||||
"author": "Shigma <shigma10826@gmail.com>",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@cordisjs/plugin-include": "^1.0.4",
|
||||
"@cordisjs/plugin-loader": "^1.0.0-rc.4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@cordisjs/plugin-include": {
|
||||
"optional": true
|
||||
},
|
||||
"@cordisjs/plugin-loader": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.1.0",
|
||||
"cosmokit": "^1.8.1"
|
||||
}
|
||||
}
|
||||
97
vendor/cordis/src/context.ts
vendored
Normal file
97
vendor/cordis/src/context.ts
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
import { Dict } from 'cosmokit'
|
||||
import { EventsService } from './events'
|
||||
import { LoggerService } from './logger'
|
||||
import { ReflectService } from './reflect'
|
||||
import { InjectKey, RegistryService } from './registry'
|
||||
import { getTraceable, symbols } from './utils'
|
||||
import { Fiber } from './fiber'
|
||||
|
||||
/**
|
||||
* Public shape of a Cordis context.
|
||||
*
|
||||
* The concrete `Context` class is proxied at runtime, so this interface is
|
||||
* augmented by core services and plugins to describe the properties that may
|
||||
* be read from `ctx`.
|
||||
*/
|
||||
export interface Context {
|
||||
[symbols.isolate]: Dict<symbol>
|
||||
[symbols.intercept]: Dict
|
||||
/** @experimental */
|
||||
root: this
|
||||
baseUrl?: string
|
||||
events: EventsService
|
||||
logger: LoggerService
|
||||
reflect: ReflectService
|
||||
registry: RegistryService
|
||||
}
|
||||
|
||||
/**
|
||||
* Root and child dependency containers for Cordis plugins.
|
||||
*
|
||||
* A context is a proxy: normal property reads go through the service resolver,
|
||||
* while `extend()`, `isolate()`, and `intercept()` create scoped child
|
||||
* contexts without mutating their parent.
|
||||
*/
|
||||
export class Context {
|
||||
static readonly effect: unique symbol = symbols.effect
|
||||
static readonly filter: unique symbol = symbols.filter
|
||||
static readonly isolate: unique symbol = symbols.isolate
|
||||
static readonly intercept: unique symbol = symbols.intercept
|
||||
|
||||
/** Returns true for Cordis context proxies and context prototypes. */
|
||||
static is(value: any): value is Context {
|
||||
return !!value?.[Context.is as any]
|
||||
}
|
||||
|
||||
static {
|
||||
Context.is[Symbol.toPrimitive] = () => Symbol.for('cordis.is')
|
||||
Context.prototype[Context.is as any] = true
|
||||
}
|
||||
|
||||
/** Create the root context and install the built-in services. */
|
||||
constructor() {
|
||||
this[symbols.isolate] = Object.create(null)
|
||||
this[symbols.intercept] = Object.create(null)
|
||||
const self = new Proxy<this>(this, ReflectService.handler)
|
||||
this.root = self
|
||||
this.baseUrl = undefined
|
||||
this.fiber = new Fiber(self, {}, Object.create(null), null, () => [])
|
||||
this.reflect = new ReflectService(self)
|
||||
this.registry = new RegistryService(self)
|
||||
this.events = new EventsService(self)
|
||||
this.logger = new LoggerService(self)
|
||||
this.fiber._disposables.clear()
|
||||
return self
|
||||
}
|
||||
|
||||
[Symbol.for('nodejs.util.inspect.custom')]() {
|
||||
return `Context <${this.fiber.name}>`
|
||||
}
|
||||
|
||||
/** Create a child context with extra metadata on top of the current scope. */
|
||||
extend(meta = {}): this {
|
||||
const shadow = Reflect.getOwnPropertyDescriptor(this, symbols.shadow)?.value
|
||||
const self = Object.create(getTraceable(this, this))
|
||||
for (const prop of Reflect.ownKeys(meta)) {
|
||||
Object.defineProperty(self, prop, Reflect.getOwnPropertyDescriptor(meta, prop)!)
|
||||
}
|
||||
if (!shadow) return self
|
||||
return Object.assign(Object.create(self), { [symbols.shadow]: shadow })
|
||||
}
|
||||
|
||||
/** Create a child context with an independent service scope for `name`. */
|
||||
isolate(name: string, label?: symbol) {
|
||||
const shadow = Object.create(this[symbols.isolate])
|
||||
shadow[name] = label ?? Symbol(name)
|
||||
return this.extend({ [symbols.isolate]: shadow })
|
||||
}
|
||||
|
||||
/** Add service-specific intercept config for plugins started below this context. */
|
||||
intercept<K extends InjectKey>(name: K, config: Context[K] extends { [symbols.config]: infer T } ? T : never): this
|
||||
intercept(name: string, config: any): this
|
||||
intercept(name: string, config: any) {
|
||||
const intercept = Object.create(this[symbols.intercept])
|
||||
intercept[name] = config
|
||||
return this.extend({ [symbols.intercept]: intercept })
|
||||
}
|
||||
}
|
||||
205
vendor/cordis/src/events.ts
vendored
Normal file
205
vendor/cordis/src/events.ts
vendored
Normal file
@@ -0,0 +1,205 @@
|
||||
import { defineProperty, Promisify } from 'cosmokit'
|
||||
import { Context } from './context'
|
||||
import { Fiber, FiberState } from './fiber'
|
||||
import { DisposableList, symbols } from './utils'
|
||||
|
||||
/** Return whether an event result should stop a bail-style dispatch. */
|
||||
export function isBailed(value: any) {
|
||||
return value !== null && value !== false && value !== undefined
|
||||
}
|
||||
|
||||
/** Extract the parameter tuple from a function type. */
|
||||
export type Parameters<F> = F extends (...args: infer P) => any ? P : never
|
||||
/** Extract the return type from a function type. */
|
||||
export type ReturnType<F> = F extends (...args: any) => infer R ? R : never
|
||||
/** Extract the explicit `this` type from a function type. */
|
||||
export type ThisType<F> = F extends (this: infer T, ...args: any) => any ? T : never
|
||||
|
||||
/**
|
||||
* Event dispatch strategy used by the event service.
|
||||
*
|
||||
* `emit` runs synchronous listeners without awaiting them, `parallel` awaits
|
||||
* all listeners together, `serial` awaits them in order until one bails,
|
||||
* `bail` stops on the first synchronous bail value, and `waterfall` composes
|
||||
* listeners around a final `next` callback.
|
||||
*/
|
||||
export type DispatchMode = 'emit' | 'parallel' | 'serial' | 'bail' | 'waterfall'
|
||||
|
||||
declare module './context' {
|
||||
export interface Context {
|
||||
/* eslint-disable max-len */
|
||||
parallel<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): Promise<void>
|
||||
parallel<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): Promise<void>
|
||||
emit<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): void
|
||||
emit<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): void
|
||||
serial<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): Promisify<ReturnType<Events[K]>>
|
||||
serial<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): Promisify<ReturnType<Events[K]>>
|
||||
bail<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
|
||||
bail<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
|
||||
waterfall<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
|
||||
waterfall<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
|
||||
on<K extends keyof Events>(name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean
|
||||
once<K extends keyof Events>(name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean
|
||||
/* eslint-enable max-len */
|
||||
}
|
||||
}
|
||||
|
||||
/** Options accepted by `ctx.on()` and `ctx.once()`. */
|
||||
export interface EventOptions {
|
||||
/** Add the listener before existing listeners for the same event. */
|
||||
prepend?: boolean
|
||||
/** Receive the event regardless of context filter checks. */
|
||||
global?: boolean
|
||||
}
|
||||
|
||||
/** Registered listener record stored by the event service. */
|
||||
export interface Hook extends EventOptions {
|
||||
ctx: Context
|
||||
callback: (...args: any[]) => any
|
||||
}
|
||||
|
||||
/**
|
||||
* Event bus installed as `ctx.events` and mixed into every context.
|
||||
*
|
||||
* The service supports concurrent, synchronous, serial, bail, and waterfall
|
||||
* dispatch and automatically disposes listeners with their owning fiber.
|
||||
*/
|
||||
export class EventsService {
|
||||
_hooks: Record<keyof any, Hook[]> = {}
|
||||
|
||||
constructor(private ctx: Context) {
|
||||
defineProperty(this, symbols.tracker, {
|
||||
property: 'ctx',
|
||||
noShadow: true,
|
||||
})
|
||||
|
||||
this.on('internal/listener', function (this: Context, name, listener, options: EventOptions) {
|
||||
if (name === 'internal/update' && !options.global) {
|
||||
const hooks = this.fiber._hooks['internal/update'] ??= new DisposableList()
|
||||
const method = options.prepend ? 'unshift' : 'push'
|
||||
return hooks[method](listener)
|
||||
}
|
||||
})
|
||||
|
||||
this.on('internal/update', function (config, noSave, next) {
|
||||
const cbs = [...this._hooks['internal/update'] || []]
|
||||
const _next = () => {
|
||||
const cb = cbs.shift() ?? next
|
||||
return cb.call(this, config, noSave, _next)
|
||||
}
|
||||
return _next()
|
||||
}, { global: true, prepend: true })
|
||||
}
|
||||
|
||||
/** Resolve listeners for one dispatch and apply context filtering. */
|
||||
dispatch(type: string, args: any[]) {
|
||||
const thisArg = typeof args[0] === 'object' || typeof args[0] === 'function' ? args.shift() : null
|
||||
const name: string = args.shift()
|
||||
if (!name.startsWith('internal/')) {
|
||||
this.emit('internal/dispatch', type, name, args, thisArg)
|
||||
}
|
||||
const filter = thisArg?.[Context.filter]
|
||||
return (this._hooks[name] || [])
|
||||
.filter(hook => hook.global || !filter || filter.call(thisArg, hook.ctx))
|
||||
.map(hook => hook.callback.bind(thisArg))
|
||||
}
|
||||
|
||||
/** Run listeners concurrently and wait for all of them. */
|
||||
async parallel(...args: any[]) {
|
||||
await Promise.all(this.dispatch('emit', args).map(cb => cb(...args)))
|
||||
}
|
||||
|
||||
/** Run listeners synchronously without waiting for returned promises. */
|
||||
emit(...args: any[]) {
|
||||
this.dispatch('emit', args).map(cb => cb(...args))
|
||||
}
|
||||
|
||||
/** Run listeners in order until one returns a bail value. */
|
||||
async serial(...args: any[]) {
|
||||
for (const cb of this.dispatch('serial', args)) {
|
||||
const result = await cb(...args)
|
||||
if (isBailed(result)) return result
|
||||
}
|
||||
}
|
||||
|
||||
/** Run listeners synchronously until one returns a bail value. */
|
||||
bail(...args: any[]) {
|
||||
for (const cb of this.dispatch('bail', args)) {
|
||||
const result = cb(...args)
|
||||
if (isBailed(result)) return result
|
||||
}
|
||||
}
|
||||
|
||||
/** Compose listeners around the final `next` callback. */
|
||||
waterfall(...args: any[]) {
|
||||
const cbs = this.dispatch('waterfall', args)
|
||||
const inner = args.pop()
|
||||
const next = () => {
|
||||
const cb = cbs.shift() ?? inner
|
||||
return cb(...args)
|
||||
}
|
||||
args.push(next)
|
||||
return next()
|
||||
}
|
||||
|
||||
register(label: string, hooks: Hook[], callback: any, options: EventOptions): () => void {
|
||||
const method = options.prepend ? 'unshift' : 'push'
|
||||
return this.ctx.fiber.effect(() => {
|
||||
hooks[method]({ ctx: this.ctx, callback, ...options })
|
||||
return () => this.unregister(hooks, callback)
|
||||
}, label)
|
||||
}
|
||||
|
||||
unregister(hooks: Hook[], callback: any) {
|
||||
const index = hooks.findIndex(hook => hook.callback === callback)
|
||||
if (index >= 0) {
|
||||
hooks.splice(index, 1)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/** Register an event listener owned by the current fiber. */
|
||||
on(name: string | symbol, listener: (...args: any) => any, options?: boolean | EventOptions) {
|
||||
if (typeof options !== 'object') {
|
||||
options = { prepend: options }
|
||||
}
|
||||
|
||||
// handle special events
|
||||
this.ctx.fiber.assertActive()
|
||||
listener = this.ctx.reflect.bind(listener)
|
||||
const result = this.bail(this.ctx, 'internal/listener', name, listener, options)
|
||||
if (result) return result
|
||||
|
||||
const hooks = this._hooks[name] ||= []
|
||||
const label = `ctx.on(${typeof name === 'string' ? JSON.stringify(name) : name.toString()})`
|
||||
return this.register(label, hooks, listener, options)
|
||||
}
|
||||
|
||||
/** Register an event listener that disposes itself after the first call. */
|
||||
once(name: string, listener: (...args: any) => any, options?: boolean | EventOptions) {
|
||||
const dispose = this.on(name, function (...args: any[]) {
|
||||
dispose()
|
||||
return listener.apply(this, args)
|
||||
}, options)
|
||||
return dispose
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Built-in framework events used by core services and extension points.
|
||||
*
|
||||
* Plugin and status events track fiber lifecycle, service events observe
|
||||
* dependency registration, update/get/set/listener events allow core services
|
||||
* to intercept runtime operations, and `internal/dispatch` exposes event-bus
|
||||
* diagnostics before public events are delivered.
|
||||
*/
|
||||
export interface Events {
|
||||
'internal/plugin'(fiber: Fiber): void
|
||||
'internal/status'(fiber: Fiber, oldValue: FiberState): void
|
||||
'internal/service'(this: Context, name: string, value: any): void
|
||||
'internal/update'(this: Fiber, config: any, noSave: boolean, next: () => void): void
|
||||
'internal/get'(ctx: Context, name: string, error: Error, next: () => any): any
|
||||
'internal/set'(ctx: Context, name: string, value: any, error: Error, next: () => boolean): boolean
|
||||
'internal/listener'(this: Context, name: string, listener: any, prepend: boolean): void
|
||||
'internal/dispatch'(mode: DispatchMode, name: string, args: any[], thisArg: any): void
|
||||
}
|
||||
504
vendor/cordis/src/fiber.ts
vendored
Normal file
504
vendor/cordis/src/fiber.ts
vendored
Normal file
@@ -0,0 +1,504 @@
|
||||
import { Awaitable, defineProperty, Dict, isNullable } from 'cosmokit'
|
||||
import { Context } from './context'
|
||||
import { Plugin } from './registry'
|
||||
import { buildOuterStack, composeError, DisposableList, getTraceable, isConstructor, isObject, symbols } from './utils'
|
||||
import { Impl } from './reflect'
|
||||
import { StandardSchemaV1 } from '@standard-schema/spec'
|
||||
|
||||
declare module './context' {
|
||||
export interface Context extends Pick<Fiber, 'effect'> {
|
||||
fiber: Fiber
|
||||
}
|
||||
}
|
||||
|
||||
const kValidationError = Symbol.for('ValidationError')
|
||||
|
||||
/** Error raised when plugin configuration fails standard-schema validation. */
|
||||
export class ValidationError extends TypeError {
|
||||
name = 'ValidationError'
|
||||
|
||||
constructor(issues: readonly StandardSchemaV1.Issue[]) {
|
||||
super(`invalid config:\n` + issues.map(issue => {
|
||||
if (issue.path) {
|
||||
return ` - ${issue.message} (at ${issue.path.join('.')})`
|
||||
} else {
|
||||
return ` - ${issue.message}`
|
||||
}
|
||||
}).join('\n'))
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(ValidationError.prototype, kValidationError, {
|
||||
value: true,
|
||||
})
|
||||
|
||||
/** Validate and normalize config for a plugin runtime before it starts. */
|
||||
export function resolveConfig(runtime: Plugin.Runtime, config: any) {
|
||||
if (!runtime.Config) return config
|
||||
// TODO: async validation
|
||||
const result = runtime.Config['~standard'].validate(config)
|
||||
if ('then' in result) {
|
||||
throw new TypeError('Async config validation is not supported')
|
||||
}
|
||||
if (result.issues) {
|
||||
throw new ValidationError(result.issues)
|
||||
} else {
|
||||
return result.value
|
||||
}
|
||||
}
|
||||
|
||||
interface AsyncDisposable<T extends Awaitable<void> = Awaitable<void>> extends PromiseLike<() => T> {
|
||||
(): T
|
||||
}
|
||||
|
||||
/** Function returned by an effect to release resources during disposal. */
|
||||
export type Disposable<T = any> = () => T
|
||||
|
||||
/** Effect body result accepted by `ctx.effect()` and plugin startup. */
|
||||
export type Effect<T = any> =
|
||||
| SyncEffect<T>
|
||||
| AsyncEffect<T>
|
||||
|
||||
type SyncEffect<T = any> =
|
||||
| Disposable<T>
|
||||
| Iterable<Disposable<T>, void, void>
|
||||
|
||||
type AsyncEffect<T = any> =
|
||||
| Promise<Disposable<T>>
|
||||
| AsyncIterable<Disposable<T>, void, void>
|
||||
|
||||
/** Tree node used to expose nested effect labels for diagnostics. */
|
||||
export interface EffectMeta {
|
||||
label: string
|
||||
children: EffectMeta[]
|
||||
}
|
||||
|
||||
interface EffectRunner<T> {
|
||||
epoch: T
|
||||
execute: () => any
|
||||
collect: (dispose: Disposable) => void
|
||||
getOuterStack: () => string[]
|
||||
}
|
||||
|
||||
/** Lifecycle state for one plugin fiber. */
|
||||
export const enum FiberState {
|
||||
PENDING,
|
||||
LOADING,
|
||||
ACTIVE,
|
||||
FAILED,
|
||||
DISPOSED,
|
||||
UNLOADING,
|
||||
}
|
||||
|
||||
/** Framework error with a stable machine-readable code. */
|
||||
export class CordisError extends Error {
|
||||
constructor(public code: CordisError.Code, message?: string) {
|
||||
super(message ?? CordisError.Code[code])
|
||||
}
|
||||
}
|
||||
|
||||
/** Cordis error code definitions. */
|
||||
export namespace CordisError {
|
||||
export type Code = keyof typeof Code
|
||||
|
||||
export const Code = {
|
||||
INACTIVE_EFFECT: 'cannot create effect on inactive context',
|
||||
} as const
|
||||
}
|
||||
|
||||
const INACTIVE = '__INACTIVE__'
|
||||
|
||||
/**
|
||||
* Runtime instance of one plugin application.
|
||||
*
|
||||
* A fiber tracks dependency state, validated config, lifecycle effects, and
|
||||
* cleanup for the plugin context returned by `ctx.plugin()`.
|
||||
*/
|
||||
export class Fiber {
|
||||
public uid: number | null
|
||||
public readonly ctx: Context
|
||||
public config: any
|
||||
public state = FiberState.PENDING
|
||||
public readonly dispose: () => Promise<void>
|
||||
public store: Dict<Impl> | undefined
|
||||
public inertia: Promise<void> | undefined
|
||||
|
||||
public readonly _hooks: Dict<DisposableList<Function>> = Object.create(null)
|
||||
public readonly _disposables = new DisposableList<Disposable>()
|
||||
|
||||
// Same as `this.ctx`, but with a more specific type.
|
||||
protected context: Context
|
||||
|
||||
private _error: any
|
||||
private _runner: EffectRunner<string>
|
||||
private _store: Dict<Impl> = Object.create(null)
|
||||
|
||||
constructor(
|
||||
public parent: Context,
|
||||
config: any,
|
||||
public inject: Dict<any>,
|
||||
public runtime: Plugin.Runtime | null,
|
||||
getOuterStack: () => string[],
|
||||
) {
|
||||
const collect = (dispose: Disposable) => {
|
||||
this._disposables.push(dispose)
|
||||
}
|
||||
|
||||
if (runtime) {
|
||||
this.uid = parent.registry.counter
|
||||
this.ctx = this.context = parent.extend({ fiber: this })
|
||||
|
||||
const injectEntries = Object.entries(this.inject)
|
||||
if (injectEntries.length) {
|
||||
this.ctx[Context.intercept] = Object.create(parent[Context.intercept])
|
||||
for (const [name, config] of injectEntries) {
|
||||
if (isNullable(config)) continue
|
||||
this.ctx[Context.intercept][name] = config
|
||||
}
|
||||
}
|
||||
|
||||
this._runner = {
|
||||
epoch: INACTIVE,
|
||||
getOuterStack,
|
||||
execute: () => {
|
||||
if (isConstructor(runtime.callback)) {
|
||||
// eslint-disable-next-line new-cap
|
||||
const instance = new runtime.callback(this.ctx, this.config)
|
||||
for (const hook of instance?.[symbols.initHooks] ?? []) {
|
||||
hook()
|
||||
}
|
||||
return instance?.[symbols.init]?.()
|
||||
} else {
|
||||
return runtime.callback(this.ctx, this.config)
|
||||
}
|
||||
},
|
||||
collect,
|
||||
}
|
||||
|
||||
this.context.emit('internal/plugin', this)
|
||||
|
||||
for (const name of Object.keys(this.inject)) {
|
||||
this._checkImpl(name)
|
||||
}
|
||||
|
||||
this.dispose = parent.fiber.effect(() => {
|
||||
const remove = runtime.fibers.push(this)
|
||||
try {
|
||||
this.config = resolveConfig(runtime, config)
|
||||
this._refresh()
|
||||
} catch (error) {
|
||||
this.ctx.logger.error(error)
|
||||
this._error = error
|
||||
}
|
||||
return async () => {
|
||||
this.uid = null
|
||||
this.context.emit('internal/plugin', this)
|
||||
if (this.ctx.registry.has(runtime.callback)) {
|
||||
remove()
|
||||
if (!runtime.fibers.length) {
|
||||
this.ctx.registry.delete(runtime.callback)
|
||||
}
|
||||
}
|
||||
this._setEpoch(INACTIVE)
|
||||
// `this.inertia` itself should never reject — both `_reload` and
|
||||
// `_unload` swallow their own work errors via `ctx.logger.error`.
|
||||
// If it *does* reject, the only remaining cause is the logger
|
||||
// itself failing, which we can't recover from in this exact spot
|
||||
// (calling the logger again is what just failed). Let the
|
||||
// rejection propagate; process-level crash is the honest outcome.
|
||||
while (this.inertia) {
|
||||
await this.inertia
|
||||
}
|
||||
}
|
||||
}, 'ctx.plugin()')
|
||||
} else {
|
||||
this.uid = 0
|
||||
this.ctx = this.context = parent
|
||||
this.state = FiberState.ACTIVE
|
||||
this.store = Object.create(null)
|
||||
this._runner = {
|
||||
epoch: '',
|
||||
getOuterStack,
|
||||
execute: () => {},
|
||||
collect,
|
||||
}
|
||||
this.dispose = () => this.restart()
|
||||
}
|
||||
}
|
||||
|
||||
get name() {
|
||||
let fiber: Fiber = this
|
||||
do {
|
||||
if (fiber.runtime?.name) return fiber.runtime.name
|
||||
fiber = fiber.parent.fiber
|
||||
} while (fiber !== fiber.parent.fiber)
|
||||
return 'root'
|
||||
}
|
||||
|
||||
/** Throw if the fiber has already been disposed. */
|
||||
assertActive() {
|
||||
if (this.uid !== null) return
|
||||
throw new CordisError('INACTIVE_EFFECT')
|
||||
}
|
||||
|
||||
private _execute<T>(runner: EffectRunner<T>) {
|
||||
const oldEpoch = runner.epoch
|
||||
return composeError((info) => {
|
||||
const safeCollect = (dispose: void | Disposable) => {
|
||||
if (typeof dispose === 'function') {
|
||||
runner.collect(dispose)
|
||||
} else if (!isNullable(dispose)) {
|
||||
throw new TypeError('Invalid effect')
|
||||
}
|
||||
}
|
||||
const effect: Effect = runner.execute()
|
||||
if (typeof effect === 'function') {
|
||||
return runner.collect(effect)
|
||||
} else if (isNullable(effect)) {
|
||||
// return
|
||||
} else if (!isObject(effect)) {
|
||||
throw new TypeError('Invalid effect')
|
||||
} else if ('then' in effect) {
|
||||
return effect.then(safeCollect)
|
||||
} else if (Symbol.iterator in effect) {
|
||||
info.error = new Error()
|
||||
const iter = effect[Symbol.iterator]()
|
||||
while (true) {
|
||||
const result = iter.next()
|
||||
safeCollect(result.value)
|
||||
if (result.done) return
|
||||
}
|
||||
} else if (Symbol.asyncIterator in effect) {
|
||||
const iter = effect[Symbol.asyncIterator]()
|
||||
return (async () => {
|
||||
// force async stack trace
|
||||
await Promise.resolve()
|
||||
info.error = new Error()
|
||||
while (true) {
|
||||
if (runner.epoch !== oldEpoch) return
|
||||
const result = await iter.next()
|
||||
safeCollect(result.value)
|
||||
if (result.done) return
|
||||
}
|
||||
})()
|
||||
} else {
|
||||
throw new TypeError('Invalid effect')
|
||||
}
|
||||
}, runner.getOuterStack)
|
||||
}
|
||||
|
||||
/** Register a cleanup-aware effect on this fiber. */
|
||||
effect(execute: () => SyncEffect, label?: string): Disposable<Promise<void>>
|
||||
effect(execute: () => Effect, label?: string): AsyncDisposable<Promise<void>>
|
||||
effect(execute: () => Effect, label = 'anonymous'): any {
|
||||
this.assertActive()
|
||||
|
||||
const disposables: Disposable[] = []
|
||||
const dispose = () => {
|
||||
let task!: void | Promise<void>
|
||||
for (const dispose of disposables.splice(0).reverse()) {
|
||||
if (task) {
|
||||
task = task.then(dispose)
|
||||
} else {
|
||||
const result = dispose()
|
||||
if (isObject(result) && 'then' in result) {
|
||||
task = result as any
|
||||
}
|
||||
}
|
||||
}
|
||||
return task
|
||||
}
|
||||
|
||||
const meta: EffectMeta = { label, children: [] }
|
||||
const runner: EffectRunner<boolean> = {
|
||||
execute,
|
||||
epoch: true,
|
||||
collect: (dispose) => {
|
||||
disposables.push(dispose)
|
||||
this._disposables.delete(dispose)
|
||||
if (dispose[symbols.effect]) {
|
||||
meta.children.push(dispose[symbols.effect])
|
||||
}
|
||||
},
|
||||
getOuterStack: buildOuterStack(),
|
||||
}
|
||||
|
||||
let task: void | Promise<void>
|
||||
try {
|
||||
task = this._execute(runner)
|
||||
} catch (reason) {
|
||||
dispose()
|
||||
throw reason
|
||||
}
|
||||
|
||||
// prevent unhandled rejection — both from `task` itself and from the
|
||||
// disposer chain if it fails to settle cleanly.
|
||||
task?.catch(dispose).catch((error) => this.ctx.logger.error(error))
|
||||
|
||||
const wrapper = defineProperty(() => {
|
||||
if (!runner.epoch) return
|
||||
runner.epoch = false
|
||||
return task ? task.then(dispose) : dispose()
|
||||
}, symbols.effect, meta) as AsyncDisposable
|
||||
|
||||
const disposeAsync = () => {
|
||||
if (!runner.epoch) return
|
||||
runner.epoch = false
|
||||
return dispose()
|
||||
}
|
||||
wrapper.then = async (onFulfilled, onRejected) => {
|
||||
return Promise.resolve(task)
|
||||
.then(() => disposeAsync)
|
||||
.then(onFulfilled, onRejected)
|
||||
}
|
||||
disposables.push(this._disposables.push(wrapper))
|
||||
return wrapper
|
||||
}
|
||||
|
||||
/** Return metadata for currently registered effects. */
|
||||
getEffects() {
|
||||
return [...this._disposables]
|
||||
.map<EffectMeta>(dispose => dispose[symbols.effect])
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
private _getState() {
|
||||
if (this.uid === null) return FiberState.DISPOSED
|
||||
if (this._error) return FiberState.FAILED
|
||||
if (this._runner.epoch !== INACTIVE) return FiberState.ACTIVE
|
||||
return FiberState.PENDING
|
||||
}
|
||||
|
||||
private _updateState(callback: () => void | FiberState) {
|
||||
const oldState = this.state
|
||||
this.state = callback() ?? this._getState()
|
||||
if (oldState === this.state) return
|
||||
// FIXME internal/fiber-info
|
||||
this.context.emit('internal/status', this, oldState)
|
||||
|
||||
// only notify changes between ACTIVE and NON-ACTIVE states
|
||||
if (oldState !== FiberState.ACTIVE && this.state !== FiberState.ACTIVE) return
|
||||
for (const key of Reflect.ownKeys(this.ctx.reflect.store)) {
|
||||
const impl = this.ctx.reflect.store[key as symbol]
|
||||
if (impl.fiber !== this) continue
|
||||
this.ctx.reflect.notify([impl.name])
|
||||
}
|
||||
}
|
||||
|
||||
_checkImpl(name: string) {
|
||||
const impl = this.ctx.reflect._getImpl(name, true)
|
||||
if (!impl) return delete this._store[name]
|
||||
try {
|
||||
if (impl.check && !impl.check.call(getTraceable(this.ctx, impl.value))) {
|
||||
return delete this._store[name]
|
||||
}
|
||||
} catch (error) {
|
||||
impl.fiber.ctx.logger.error(error)
|
||||
return delete this._store[name]
|
||||
}
|
||||
this._store[name] = impl
|
||||
}
|
||||
|
||||
_refresh() {
|
||||
let epoch: string | boolean = false
|
||||
epoch = ''
|
||||
for (const name of Object.keys(this.inject)) {
|
||||
const impl = this._store[name]
|
||||
if (!impl) {
|
||||
epoch = INACTIVE
|
||||
break
|
||||
}
|
||||
epoch += ':' + impl.fiber.uid
|
||||
}
|
||||
this._setEpoch(epoch)
|
||||
}
|
||||
|
||||
private _setEpoch(epoch: string) {
|
||||
const oldEpoch = this._runner.epoch
|
||||
if (epoch === oldEpoch) return
|
||||
this._runner.epoch = epoch
|
||||
if (this.inertia) return
|
||||
this._updateState(() => {
|
||||
if (epoch !== INACTIVE && oldEpoch === INACTIVE) {
|
||||
this.inertia = this._reload()
|
||||
return FiberState.LOADING
|
||||
} else {
|
||||
this.inertia = this._unload()
|
||||
return FiberState.UNLOADING
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private async _reload() {
|
||||
this.store = { ...this._store }
|
||||
const oldEpoch = this._runner.epoch
|
||||
try {
|
||||
await Promise.resolve()
|
||||
await this._execute(this._runner)
|
||||
} catch (reason) {
|
||||
// impl guarantees that the error is non-null (?)
|
||||
this.ctx.logger.error(reason)
|
||||
this._error = reason
|
||||
this._runner.epoch = INACTIVE
|
||||
}
|
||||
this._updateState(() => {
|
||||
if (this._runner.epoch === oldEpoch) {
|
||||
this.inertia = undefined
|
||||
} else {
|
||||
this.inertia = this._unload()
|
||||
return FiberState.UNLOADING
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private async _unload() {
|
||||
await Promise.all(this._disposables.clear().map(async (dispose) => {
|
||||
try {
|
||||
await composeError(async (info) => {
|
||||
await Promise.resolve()
|
||||
info.error = new Error()
|
||||
await dispose()
|
||||
}, this._runner.getOuterStack)
|
||||
} catch (reason) {
|
||||
this.ctx.logger.error(reason)
|
||||
}
|
||||
}))
|
||||
this.store = undefined
|
||||
this._updateState(() => {
|
||||
if (this._runner.epoch === INACTIVE) {
|
||||
this.inertia = undefined
|
||||
} else {
|
||||
this.inertia = this._reload()
|
||||
return FiberState.LOADING
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** Wait for current lifecycle work and rethrow startup errors. */
|
||||
async await() {
|
||||
while (this.inertia) {
|
||||
await this.inertia
|
||||
}
|
||||
if (this._error) throw this._error
|
||||
return this
|
||||
}
|
||||
|
||||
/** Dispose and immediately reload this plugin with its current config. */
|
||||
async restart() {
|
||||
this.assertActive()
|
||||
this._setEpoch(INACTIVE)
|
||||
this._refresh()
|
||||
await this.await()
|
||||
}
|
||||
|
||||
/** Validate and apply new config, then restart the plugin. */
|
||||
update(config: any, noSave = false) {
|
||||
this.assertActive()
|
||||
config = resolveConfig(this.runtime!, config)
|
||||
this.context.waterfall(this, 'internal/update', config, noSave, () => {
|
||||
this.config = config
|
||||
this._error = undefined
|
||||
return this.restart()
|
||||
})
|
||||
}
|
||||
}
|
||||
14
vendor/cordis/src/index.ts
vendored
Normal file
14
vendor/cordis/src/index.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
/** Core context type and root context implementation. */
|
||||
export * from './context'
|
||||
/** Event bus, dispatch modes, and event augmentation types. */
|
||||
export * from './events'
|
||||
/** Plugin fiber lifecycle, effects, and config validation helpers. */
|
||||
export * from './fiber'
|
||||
/** Logger facade, logger service, message, exporter, and formatting types. */
|
||||
export * from './logger'
|
||||
/** Plugin registry, dependency injection, and plugin entrypoint types. */
|
||||
export * from './registry'
|
||||
/** Base service class and service lifecycle symbols. */
|
||||
export * from './service'
|
||||
/** Shared internal helpers used by context, services, and plugin fibers. */
|
||||
export * from './utils'
|
||||
262
vendor/cordis/src/logger.ts
vendored
Normal file
262
vendor/cordis/src/logger.ts
vendored
Normal file
@@ -0,0 +1,262 @@
|
||||
import { defineProperty, hyphenate } from 'cosmokit'
|
||||
import { Context } from './context'
|
||||
import { Fiber } from './fiber'
|
||||
import { createCallable, joinPrototype, symbols, Tracker } from './utils'
|
||||
|
||||
declare module './context' {
|
||||
interface Intercept {
|
||||
logger: LoggerService.Intercept
|
||||
}
|
||||
}
|
||||
|
||||
/** Logger method name and severity category. */
|
||||
export type LoggerType = 'error' | 'info' | 'warn' | 'debug'
|
||||
|
||||
/** Callable shape for one logger severity method. */
|
||||
export type LoggerMethod = (format: any, ...param: any[]) => void
|
||||
|
||||
/** Formatter used to resolve a printf-style placeholder. */
|
||||
export type Formatter = (value: any, exporter: Exporter, message: Message) => any
|
||||
|
||||
/** Numeric severity used when exporters decide whether to emit a message. */
|
||||
export const enum LoggerLevel {
|
||||
ERROR = 0,
|
||||
INFO = 1,
|
||||
WARN = 2,
|
||||
DEBUG = 3,
|
||||
}
|
||||
|
||||
/** Structured log record delivered to exporters. */
|
||||
export interface Message {
|
||||
sn: number
|
||||
ts: number
|
||||
name: string
|
||||
type: LoggerType
|
||||
level: number
|
||||
args: any[]
|
||||
fiber?: WeakRef<Fiber>
|
||||
}
|
||||
|
||||
/** Sink that receives structured log messages. */
|
||||
export interface Exporter {
|
||||
colors?: number | false
|
||||
maxLength?: number
|
||||
levels?: Record<string, number>
|
||||
formatters?: Record<string, Formatter>
|
||||
export(message: Message): void
|
||||
}
|
||||
|
||||
/** Built-in placeholder formatters used by `Logger.format()`. */
|
||||
export const defaultFormatters: Record<string, Formatter> = {
|
||||
s: (value) => String(value),
|
||||
d: (value) => Math.trunc(Number(value)),
|
||||
i: (value) => Math.trunc(Number(value)),
|
||||
f: (value) => Number(value),
|
||||
o: (value) => JSON.stringify(value),
|
||||
O: (value) => JSON.stringify(value),
|
||||
c: () => '',
|
||||
C: (value, exporter, message) => {
|
||||
return Logger.color(exporter, Logger.code(message.name, exporter.colors), value)
|
||||
},
|
||||
}
|
||||
|
||||
/** Options used when creating a named logger facade. */
|
||||
export interface LoggerOptions {
|
||||
name: string
|
||||
meta?: Partial<Message>
|
||||
level?: number
|
||||
}
|
||||
|
||||
/** Logger facade identity, inherited message metadata, and optional minimum level. */
|
||||
export interface Logger extends LoggerOptions {}
|
||||
/** Logger facade severity methods. */
|
||||
export interface Logger extends Record<LoggerType, LoggerMethod> {}
|
||||
|
||||
function isAggregateError(error: any): error is Error & { errors: Error[] } {
|
||||
return error instanceof Error && Array.isArray(error['errors'])
|
||||
}
|
||||
|
||||
/** Logger facade for one named subsystem. */
|
||||
export class Logger {
|
||||
static color(exporter: Exporter, code: number, value: any, decoration = '') {
|
||||
if (!exporter.colors) return '' + value
|
||||
return `\u001b[3${code < 8 ? code : '8;5;' + code}${exporter.colors >= 2 ? decoration : ''}m${value}\u001b[0m`
|
||||
}
|
||||
|
||||
static code(name: string, level?: false | number) {
|
||||
let hash = 0
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
hash = ((hash << 3) - hash) + name.charCodeAt(i) + 13
|
||||
hash |= 0
|
||||
}
|
||||
const colors = !level ? [] : level >= 2 ? c256 : c16
|
||||
return colors[Math.abs(hash) % colors.length]
|
||||
}
|
||||
|
||||
static format(exporter: Exporter, message: Message): string {
|
||||
const args = message.args.slice()
|
||||
if (args[0] instanceof Error) {
|
||||
args[0] = args[0].stack || args[0].message
|
||||
args.unshift('%s')
|
||||
} else if (typeof args[0] !== 'string') {
|
||||
args.unshift('%o')
|
||||
}
|
||||
|
||||
let format: string = args.shift()
|
||||
format = format.replace(/%([a-zA-Z%])/g, (match, char) => {
|
||||
if (match === '%%') return '%'
|
||||
const formatter = exporter.formatters?.[char] ?? defaultFormatters[char]
|
||||
if (typeof formatter === 'function') {
|
||||
const value = args.shift()
|
||||
return formatter(value, exporter, message)
|
||||
}
|
||||
return match
|
||||
})
|
||||
|
||||
const oFormatter = exporter.formatters?.o ?? defaultFormatters.o
|
||||
for (let arg of args) {
|
||||
if (typeof arg === 'object' && arg) {
|
||||
arg = oFormatter(arg, exporter, message)
|
||||
}
|
||||
format += ' ' + arg
|
||||
}
|
||||
|
||||
const { maxLength = 10240 } = exporter
|
||||
return format.split(/\r?\n/g).map(line => {
|
||||
return line.slice(0, maxLength) + (line.length > maxLength ? '...' : '')
|
||||
}).join('\n')
|
||||
}
|
||||
|
||||
constructor(options: LoggerOptions, private service: LoggerService) {
|
||||
Object.assign(this, options)
|
||||
this.error = this._method('error', LoggerLevel.ERROR)
|
||||
this.info = this._method('info', LoggerLevel.INFO)
|
||||
this.warn = this._method('warn', LoggerLevel.WARN)
|
||||
this.debug = this._method('debug', LoggerLevel.DEBUG)
|
||||
}
|
||||
|
||||
private _method(type: LoggerType, level: number): LoggerMethod {
|
||||
return (...args: any[]) => {
|
||||
if (args.length === 1 && args[0] instanceof Error) {
|
||||
if (args[0].cause) {
|
||||
this[type](args[0].cause)
|
||||
} else if (isAggregateError(args[0])) {
|
||||
args[0].errors.forEach(error => this[type](error))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const sn = ++this.service._snMessage
|
||||
const ts = Date.now()
|
||||
for (const exporter of this.service.exporters.values()) {
|
||||
const targetLevel = exporter.levels?.[this.name] ?? exporter.levels?.default ?? this.level ?? LoggerLevel.INFO
|
||||
if (targetLevel < level) continue
|
||||
const message: Message = { sn, ts, type, level, name: this.name, ...this.meta, args }
|
||||
exporter.export(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** ANSI 16-color palette indexes used for logger name coloring. */
|
||||
export const c16 = [6, 2, 3, 4, 5, 1]
|
||||
/** ANSI 256-color palette indexes used for logger name coloring. */
|
||||
export const c256 = [
|
||||
20, 21, 26, 27, 32, 33, 38, 39, 40, 41, 42, 43, 44, 45, 56, 57, 62,
|
||||
63, 68, 69, 74, 75, 76, 77, 78, 79, 80, 81, 92, 93, 98, 99, 112, 113,
|
||||
129, 134, 135, 148, 149, 160, 161, 162, 163, 164, 165, 166, 167, 168,
|
||||
169, 170, 171, 172, 173, 178, 179, 184, 185, 196, 197, 198, 199, 200,
|
||||
201, 202, 203, 204, 205, 206, 207, 208, 209, 214, 215, 220, 221,
|
||||
]
|
||||
|
||||
/** Logger service configuration merged from context intercepts. */
|
||||
export namespace LoggerService {
|
||||
export interface Intercept {
|
||||
name?: string
|
||||
level?: number
|
||||
}
|
||||
}
|
||||
|
||||
/** Callable `ctx.logger` service shape. */
|
||||
export interface LoggerService extends Record<LoggerType, LoggerMethod> {
|
||||
(name?: string): Logger
|
||||
}
|
||||
|
||||
/**
|
||||
* Built-in logging service.
|
||||
*
|
||||
* Call `ctx.logger()` to create a named logger, or call `ctx.logger.info()`
|
||||
* directly to log with the current fiber-derived name.
|
||||
*/
|
||||
export class LoggerService {
|
||||
bufferSize = 1000
|
||||
buffer: Message[] = []
|
||||
ctx!: Context
|
||||
|
||||
_snMessage = 0
|
||||
_snExporter = 0
|
||||
exporters = new Map<number, Exporter>()
|
||||
|
||||
constructor(ctx: Context) {
|
||||
const tracker: Tracker = {
|
||||
property: 'ctx',
|
||||
noShadow: true,
|
||||
}
|
||||
const self = createCallable('logger', joinPrototype(Object.getPrototypeOf(this), Function.prototype), tracker) as unknown as LoggerService
|
||||
Object.assign(self, this)
|
||||
self.ctx = ctx
|
||||
defineProperty(self, symbols.tracker, tracker)
|
||||
|
||||
self.exporter({
|
||||
colors: 3,
|
||||
export: (message) => {
|
||||
self.buffer.push(message)
|
||||
if (self.buffer.length > self.bufferSize) {
|
||||
self.buffer = self.buffer.slice(-self.bufferSize)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
/** Register an exporter and dispose it with the current fiber. */
|
||||
exporter(exporter: Exporter) {
|
||||
return this.ctx.effect(() => {
|
||||
this.exporters.set(++this._snExporter, exporter)
|
||||
return () => this.exporters.delete(this._snExporter)
|
||||
}, 'ctx.logger.exporter()')
|
||||
}
|
||||
|
||||
private _resolveConfig(): LoggerService.Intercept {
|
||||
let intercept = this.ctx[symbols.intercept]
|
||||
const configs: LoggerService.Intercept[] = []
|
||||
while ('logger' in intercept) {
|
||||
if (Object.hasOwn(intercept, 'logger')) {
|
||||
configs.unshift(intercept['logger'])
|
||||
}
|
||||
intercept = Object.getPrototypeOf(intercept)
|
||||
}
|
||||
return Object.assign({}, ...configs)
|
||||
}
|
||||
|
||||
[symbols.invoke](name?: string): Logger {
|
||||
const config = this._resolveConfig()
|
||||
const fiber = ((this.ctx as any)[symbols.shadow] ?? this.ctx).fiber
|
||||
name ??= config.name
|
||||
name ??= hyphenate(fiber.name)
|
||||
return new Logger({
|
||||
name,
|
||||
level: config.level,
|
||||
meta: { fiber: new WeakRef(fiber) },
|
||||
}, this)
|
||||
}
|
||||
|
||||
static {
|
||||
for (const type of ['error', 'info', 'warn', 'debug'] as const) {
|
||||
;(LoggerService.prototype as any)[type] = function (this: LoggerService, ...args: any[]) {
|
||||
return (this as any)()[type](...args)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
287
vendor/cordis/src/reflect.ts
vendored
Normal file
287
vendor/cordis/src/reflect.ts
vendored
Normal file
@@ -0,0 +1,287 @@
|
||||
import { defineProperty, Dict, isNullable } from 'cosmokit'
|
||||
import { Context } from './context'
|
||||
import { getTraceable, symbols, withProps } from './utils'
|
||||
import { Fiber, FiberState } from './fiber'
|
||||
|
||||
declare module './context' {
|
||||
interface Context {
|
||||
get<K extends string & keyof this>(name: K, strict?: boolean): undefined | this[K]
|
||||
get(name: string, strict?: boolean): any
|
||||
set<K extends string & keyof this>(name: K, value: undefined | this[K]): void
|
||||
set(name: string, value: any): void
|
||||
provide<K extends string & keyof this>(name: K, value: undefined | this[K]): () => void
|
||||
provide(name: string, value?: any): () => void
|
||||
accessor(name: string, options: Omit<Property.Accessor, 'type'>): void
|
||||
mixin<K extends string & keyof this>(name: K, mixins: (keyof this & keyof this[K])[] | Dict<string>): void
|
||||
mixin<T extends {}>(source: T, mixins: (keyof this & keyof T)[] | Dict<string>): void
|
||||
}
|
||||
}
|
||||
|
||||
function enhanceError(error: Error) {
|
||||
const lines = error.stack!.split('\n')
|
||||
lines.splice(0, 2, `Error: ${error.message}`)
|
||||
error.stack = lines.join('\n')
|
||||
return error
|
||||
}
|
||||
|
||||
const RESERVED_WORDS = ['prototype', 'then']
|
||||
|
||||
// - is a symbol
|
||||
// - is a reserved word (prototype, then)
|
||||
// - is a number string (0, 1, 2, ...)
|
||||
// - starts with `_`
|
||||
function isSpecialProperty(prop: string | symbol): prop is symbol {
|
||||
return typeof prop === 'symbol'
|
||||
|| RESERVED_WORDS.includes(prop)
|
||||
|| parseInt(prop).toString() === prop
|
||||
|| prop.startsWith('_')
|
||||
}
|
||||
|
||||
/** Context property definition known by the reflection service. */
|
||||
export type Property = Property.Service | Property.Accessor
|
||||
|
||||
/** Property definition variants understood by `ReflectService`. */
|
||||
export namespace Property {
|
||||
/** Service property backed by a provided implementation. */
|
||||
export interface Service {
|
||||
type: 'service'
|
||||
}
|
||||
|
||||
/** Computed context property backed by custom get/set hooks. */
|
||||
export interface Accessor {
|
||||
type: 'accessor'
|
||||
get: (this: Context, receiver: any, error: Error) => any
|
||||
set?: (this: Context, value: any, receiver: any, error: Error) => boolean
|
||||
}
|
||||
}
|
||||
|
||||
/** Concrete service implementation record stored in the root reflect service. */
|
||||
export interface Impl {
|
||||
name: string
|
||||
fiber: Fiber
|
||||
value?: any
|
||||
check?: () => boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Reflection and service-resolution layer installed as `ctx.reflect`.
|
||||
*
|
||||
* This service powers the context proxy, service registration, accessors, and
|
||||
* the mixins that expose core service methods directly on `ctx`.
|
||||
*/
|
||||
export class ReflectService {
|
||||
static handler: ProxyHandler<Context> = {
|
||||
get: (target, prop, ctx: Context) => {
|
||||
if (isSpecialProperty(prop)) {
|
||||
return Reflect.get(target, prop, ctx)
|
||||
}
|
||||
if (Reflect.has(target, prop)) {
|
||||
return getTraceable(ctx, Reflect.get(target, prop, ctx))
|
||||
}
|
||||
|
||||
const error = new Error(`cannot get property "${prop}" without inject`)
|
||||
|
||||
try {
|
||||
const def = target.reflect.props[prop]
|
||||
if (def?.type === 'accessor') {
|
||||
return def.get.call(ctx, ctx[symbols.receiver], error)
|
||||
}
|
||||
|
||||
if (!ctx.fiber.runtime) return ctx.reflect.get(prop, false)
|
||||
return ctx.events.waterfall('internal/get', ctx, prop, error, () => {
|
||||
const key = target[symbols.isolate][prop]
|
||||
let fiber = (ctx[symbols.shadow] as Context ?? ctx).fiber
|
||||
while (true) {
|
||||
const impl = fiber.store?.[prop]
|
||||
if (impl) return getTraceable(ctx, impl.value)
|
||||
if (prop in fiber.inject) {
|
||||
error.message = `cannot get required service "${prop}" in inactive context`
|
||||
throw error
|
||||
}
|
||||
if (!fiber.runtime) throw error
|
||||
if (fiber.parent[symbols.isolate][prop] !== key) throw error
|
||||
fiber = fiber.parent.fiber
|
||||
}
|
||||
})
|
||||
} catch (e: any) {
|
||||
throw e === error ? enhanceError(e) : e
|
||||
}
|
||||
},
|
||||
|
||||
set: (target, prop, value, ctx: Context) => {
|
||||
if (isSpecialProperty(prop)) {
|
||||
return Reflect.set(target, prop, value, ctx)
|
||||
}
|
||||
|
||||
const error = new Error(`cannot set property "${prop}" without provide`)
|
||||
const def = target.reflect.props[prop]
|
||||
if (!def) {
|
||||
if (!ctx.fiber.runtime) return Reflect.set(target, prop, value, ctx)
|
||||
throw enhanceError(error)
|
||||
}
|
||||
|
||||
try {
|
||||
if (def.type === 'accessor') {
|
||||
if (!def.set) return false
|
||||
return def.set.call(ctx, value, ctx[symbols.receiver], error)
|
||||
}
|
||||
|
||||
return ctx.events.waterfall('internal/set', ctx, prop, value, error, () => {
|
||||
return ctx.reflect.set(prop, value, error)
|
||||
})
|
||||
} catch (e: any) {
|
||||
throw e === error ? enhanceError(e) : e
|
||||
}
|
||||
},
|
||||
|
||||
has: (target, prop) => {
|
||||
if (isSpecialProperty(prop)) {
|
||||
return Reflect.has(target, prop)
|
||||
}
|
||||
if (Reflect.has(target, prop)) return true
|
||||
return !!target.reflect.props[prop]
|
||||
},
|
||||
}
|
||||
|
||||
public store: Dict<Impl, symbol> = Object.create(null)
|
||||
public props: Dict<Property> = Object.create(null)
|
||||
|
||||
constructor(public ctx: Context) {
|
||||
defineProperty(this, symbols.tracker, {
|
||||
property: 'ctx',
|
||||
noShadow: true,
|
||||
})
|
||||
|
||||
this.mixin('reflect', ['get', 'set', 'provide', 'accessor', 'mixin'])
|
||||
this.mixin('fiber', ['runtime', 'effect'])
|
||||
this.mixin('registry', ['inject', 'plugin'])
|
||||
this.mixin('events', ['on', 'once', 'parallel', 'emit', 'serial', 'bail', 'waterfall'])
|
||||
}
|
||||
|
||||
get(name: string, strict = true) {
|
||||
return getTraceable(this.ctx, this._getImpl(name, strict)?.value)
|
||||
}
|
||||
|
||||
_getImpl(name: string, strict = true) {
|
||||
const key = this.ctx[symbols.isolate][name]
|
||||
const impl = key && this.store[key]
|
||||
if (!impl) return
|
||||
if (strict && impl.fiber.state !== FiberState.ACTIVE) return
|
||||
return impl
|
||||
}
|
||||
|
||||
set(name: string, value: any, error?: Error) {
|
||||
const key = this.ctx[symbols.isolate][name]
|
||||
const impl = this.store[key]
|
||||
if (!impl) {
|
||||
throw new Error(`cannot set property "${name}" without provide`)
|
||||
}
|
||||
if (impl.fiber !== this.ctx.fiber) {
|
||||
throw new Error(`cannot set property "${name}" in multiple fibers`)
|
||||
}
|
||||
impl.value = value
|
||||
return true
|
||||
}
|
||||
|
||||
provide(name: string, value?: any, check?: () => boolean) {
|
||||
return this.ctx.fiber.effect(() => {
|
||||
if (!this.props[name]) {
|
||||
this.props[name] ??= { type: 'service' }
|
||||
} else if (this.props[name].type !== 'service') {
|
||||
throw new Error(`property "${name}" is already declared as ${this.props[name].type}`)
|
||||
}
|
||||
this.props[name] = { type: 'service' }
|
||||
|
||||
this.ctx.root[symbols.isolate][name] ??= Symbol(name)
|
||||
const key = this.ctx[symbols.isolate][name]
|
||||
const impl: Impl = { name, value, fiber: this.ctx.fiber, check }
|
||||
if (this.store[key]) {
|
||||
throw new Error(`service "${name}" has been registered at <${this.store[key].fiber.name}>`)
|
||||
}
|
||||
this.store[key] = impl
|
||||
this.ctx.fiber.store![name] = impl
|
||||
if (this.ctx.fiber.state === FiberState.ACTIVE) {
|
||||
this.notify([name])
|
||||
}
|
||||
return async () => {
|
||||
delete this.store[key]
|
||||
const fibers = this.notify([name])
|
||||
await Promise.allSettled(fibers.map(fiber => fiber.await()))
|
||||
// ensure self access before dependencies cleanup
|
||||
delete this.ctx.fiber.store![name]
|
||||
}
|
||||
}, `ctx.provide(${JSON.stringify(name)})`)
|
||||
}
|
||||
|
||||
notify(names: string[], filter = (ctx: Context, name: string) => ctx[symbols.isolate][name] === this.ctx[symbols.isolate][name]) {
|
||||
const fibers: Fiber[] = []
|
||||
for (const runtime of this.ctx.registry.values()) {
|
||||
for (const fiber of runtime.fibers) {
|
||||
let hasUpdate = false
|
||||
for (const name of names) {
|
||||
if (!(name in fiber.inject)) continue
|
||||
if (!filter(fiber.ctx, name)) continue
|
||||
hasUpdate = true
|
||||
fiber._checkImpl(name)
|
||||
}
|
||||
if (!hasUpdate) continue
|
||||
fiber._refresh()
|
||||
fibers.push(fiber)
|
||||
}
|
||||
}
|
||||
return fibers
|
||||
}
|
||||
|
||||
accessor(name: string, options: Omit<Property.Accessor, 'type'>) {
|
||||
return this.ctx.fiber.effect(() => {
|
||||
if (name in this.props) {
|
||||
throw new Error(`property "${name}" is already declared as ${this.props[name].type}`)
|
||||
}
|
||||
this.props[name] = { type: 'accessor', ...options }
|
||||
return () => delete this.props[name]
|
||||
}, `ctx.accessor(${JSON.stringify(name)})`)
|
||||
}
|
||||
|
||||
mixin(source: any, mixins: string[] | Dict<string>) {
|
||||
const self = this
|
||||
return this.ctx.fiber.effect(function* () {
|
||||
const entries = Array.isArray(mixins) ? mixins.map(key => [key, key]) : Object.entries(mixins)
|
||||
const getTarget = (ctx: Context, error: Error) => {
|
||||
// TODO enhance error message
|
||||
return ctx[source]
|
||||
}
|
||||
for (const [key, value] of entries) {
|
||||
yield self.accessor(value, {
|
||||
get(receiver, error) {
|
||||
const service = getTarget(this, error)
|
||||
if (isNullable(service)) return service
|
||||
const mixin = receiver ? withProps(receiver, service) : service
|
||||
const value = Reflect.get(service, key, mixin)
|
||||
if (typeof value !== 'function') return value
|
||||
return value.bind(mixin ?? service)
|
||||
},
|
||||
set(value, receiver, error) {
|
||||
const service = getTarget(this, error)
|
||||
const mixin = receiver ? withProps(receiver, service) : service
|
||||
return Reflect.set(service, key, value, mixin)
|
||||
},
|
||||
})
|
||||
}
|
||||
}, `ctx.mixin(${JSON.stringify(source)})`)
|
||||
}
|
||||
|
||||
trace<T>(value: T) {
|
||||
return getTraceable(this.ctx, value)
|
||||
}
|
||||
|
||||
bind<T extends Function>(callback: T) {
|
||||
return new Proxy(callback, {
|
||||
apply: (target, thisArg, args) => {
|
||||
return Reflect.apply(target, this.trace(thisArg), args.map(arg => this.trace(arg)))
|
||||
},
|
||||
construct: (target, args, newTarget) => {
|
||||
return Reflect.construct(target, args.map(arg => this.trace(arg)), newTarget)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
249
vendor/cordis/src/registry.ts
vendored
Normal file
249
vendor/cordis/src/registry.ts
vendored
Normal file
@@ -0,0 +1,249 @@
|
||||
import { defineProperty, Dict } from 'cosmokit'
|
||||
import { StandardSchemaV1 } from '@standard-schema/spec'
|
||||
import { Context } from './context'
|
||||
import { Fiber } from './fiber'
|
||||
import { buildOuterStack, DisposableList, symbols, withProps } from './utils'
|
||||
|
||||
function isApplicable(object: Plugin) {
|
||||
return object && typeof object === 'object' && typeof object.apply === 'function'
|
||||
}
|
||||
|
||||
/**
|
||||
* Service dependency declaration accepted by plugins and the `@Inject`
|
||||
* decorator.
|
||||
*
|
||||
* Array form requests services without intercept config. Object form maps each
|
||||
* service name to optional intercept config for the plugin context.
|
||||
*/
|
||||
export type Inject<M = Dict> = (keyof M)[] | { [K in keyof M]?: M[K] }
|
||||
|
||||
/** Context keys that correspond to services with typed intercept config. */
|
||||
export type InjectKey = keyof {
|
||||
[K in keyof Context & string as Context[K] extends { [symbols.config]: any } ? K : never]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorator for declaring service dependencies on classes or class methods.
|
||||
*
|
||||
* On classes it contributes to the plugin's static `inject` map. On methods it
|
||||
* delays the method call until the declared services are available.
|
||||
*/
|
||||
export function Inject<K extends InjectKey>(name: K, config?: Context[K] extends { [symbols.config]: infer T } ? T : never) {
|
||||
return function (value: any, decorator: ClassDecoratorContext<any> | ClassMethodDecoratorContext<any>) {
|
||||
if (decorator.kind === 'class') {
|
||||
if (!Object.hasOwn(value, 'inject')) {
|
||||
defineProperty(value, 'inject', Object.create(Object.getPrototypeOf(value).inject ?? null))
|
||||
defineProperty(value.inject, symbols.checkProto, true)
|
||||
}
|
||||
value.inject[name] = config
|
||||
} else if (decorator.kind === 'method') {
|
||||
const inject = (value[symbols.metadata] ??= {}).inject ??= Object.create(null)
|
||||
inject[name] = config
|
||||
decorator.addInitializer(function () {
|
||||
const property = this[symbols.tracker]?.property
|
||||
;(this[symbols.initHooks] ??= []).push(() => {
|
||||
(this.ctx as Context).inject(inject, (ctx) => {
|
||||
return value.call(property ? withProps(this, { [property]: ctx }) : this)
|
||||
})
|
||||
})
|
||||
})
|
||||
} else {
|
||||
throw new Error('@Inject() can only be used on class or class methods')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Utilities for normalizing plugin dependency declarations. */
|
||||
export namespace Inject {
|
||||
/** Convert array/object/class-inherited inject metadata into a plain map. */
|
||||
export function resolve(inject: Inject | null | undefined, result: Dict = Object.create(null)) {
|
||||
if (!inject) return result
|
||||
if (Array.isArray(inject)) {
|
||||
for (const name of inject) {
|
||||
result[name] = null
|
||||
}
|
||||
} else if (Reflect.has(inject, symbols.checkProto)) {
|
||||
Object.assign(result, resolve(Object.getPrototypeOf(inject)))
|
||||
for (const name of Object.keys(inject)) {
|
||||
result[name] = inject[name] ?? null
|
||||
}
|
||||
} else {
|
||||
for (const name of Object.keys(inject)) {
|
||||
result[name] = inject[name] ?? null
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/** Supported plugin entrypoint shapes. */
|
||||
export type Plugin<T = any> =
|
||||
| Plugin.Function<T>
|
||||
| Plugin.Constructor<T>
|
||||
| Plugin.Object<T>
|
||||
|
||||
/** Types associated with plugin entrypoints and runtime records. */
|
||||
export namespace Plugin {
|
||||
/** Shared metadata understood by the plugin registry and related tooling. */
|
||||
export interface Base<T = any> {
|
||||
name?: string
|
||||
Config?: StandardSchemaV1<any, T>
|
||||
inject?: Inject
|
||||
provide?: string | string[]
|
||||
intercept?: Dict<boolean>
|
||||
}
|
||||
|
||||
export interface Transform<S, T> {
|
||||
/** Marks the transform object as a schema/config transform. */
|
||||
schema?: true
|
||||
/** Convert user-facing config to runtime config. */
|
||||
Config: (config: S) => T
|
||||
}
|
||||
|
||||
/** Function plugin called with `(ctx, config)`. */
|
||||
export interface Function<T = any> extends Base<T> {
|
||||
(ctx: Context, config: T): any
|
||||
}
|
||||
|
||||
/** Class plugin constructed with `(ctx, config)`. */
|
||||
export interface Constructor<T = any> extends Base<T> {
|
||||
new (ctx: Context, config: T): any
|
||||
}
|
||||
|
||||
/** Object plugin with an `apply(ctx, config)` method. */
|
||||
export interface Object<T = any> extends Base<T> {
|
||||
apply(ctx: Context, config: T): any
|
||||
}
|
||||
|
||||
/** Mutable registry record shared by all fibers of one plugin callback. */
|
||||
export interface Runtime {
|
||||
name?: string
|
||||
fibers: DisposableList<Fiber>
|
||||
callback: globalThis.Function
|
||||
Config?: StandardSchemaV1
|
||||
}
|
||||
}
|
||||
|
||||
type Spread<T> = undefined extends T ? [config?: T] : [config: T]
|
||||
|
||||
type GetPluginParameters<P> =
|
||||
| P extends (ctx: Context, ...args: infer R) => any
|
||||
? R
|
||||
: P extends new (ctx: Context, ...args: infer R) => any
|
||||
? R
|
||||
: P extends { apply(ctx: Context, ...args: infer R): any }
|
||||
? R
|
||||
: never
|
||||
|
||||
type GetPluginConfig<P> =
|
||||
| P extends Plugin.Transform<infer S, any>
|
||||
? S
|
||||
: GetPluginParameters<P>[0]
|
||||
|
||||
declare module './context' {
|
||||
export interface Context {
|
||||
inject(deps: Inject, callback: Plugin.Function<void>): Fiber & PromiseLike<Fiber>
|
||||
plugin<P extends Plugin>(plugin: P, ...args: Spread<GetPluginConfig<P>>): Fiber & PromiseLike<Fiber>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin registry installed as `ctx.registry` and mixed into every context.
|
||||
*
|
||||
* It normalizes plugin shapes, tracks plugin runtimes, starts fibers, and
|
||||
* exposes map-like inspection over active plugin callbacks.
|
||||
*/
|
||||
export class RegistryService {
|
||||
private _counter = 0
|
||||
private _internal = new Map<Function, Plugin.Runtime>()
|
||||
|
||||
constructor(public ctx: Context) {
|
||||
defineProperty(this, symbols.tracker, {
|
||||
property: 'ctx',
|
||||
noShadow: true,
|
||||
})
|
||||
}
|
||||
|
||||
get counter() {
|
||||
return ++this._counter
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this._internal.size
|
||||
}
|
||||
|
||||
/** Resolve a supported plugin shape to its executable callback. */
|
||||
resolve(plugin: Plugin): Function | undefined {
|
||||
// plugin.apply may throw
|
||||
try {
|
||||
if (typeof plugin === 'function') return plugin
|
||||
if (isApplicable(plugin)) return plugin.apply
|
||||
} catch {}
|
||||
}
|
||||
|
||||
get(plugin: Plugin) {
|
||||
const key = this.resolve(plugin)
|
||||
return key && this._internal.get(key)
|
||||
}
|
||||
|
||||
has(plugin: Plugin) {
|
||||
const key = this.resolve(plugin)
|
||||
return !!key && this._internal.has(key)
|
||||
}
|
||||
|
||||
/** Dispose every running fiber for a plugin and remove its runtime record. */
|
||||
delete(plugin: Plugin) {
|
||||
const key = this.resolve(plugin)
|
||||
const runtime = key && this._internal.get(key)
|
||||
if (!runtime) return
|
||||
this._internal.delete(key)
|
||||
for (const fiber of runtime.fibers) {
|
||||
fiber.dispose()
|
||||
}
|
||||
return runtime
|
||||
}
|
||||
|
||||
keys() {
|
||||
return this._internal.keys()
|
||||
}
|
||||
|
||||
values() {
|
||||
return this._internal.values()
|
||||
}
|
||||
|
||||
entries() {
|
||||
return this._internal.entries()
|
||||
}
|
||||
|
||||
forEach(callback: (value: Plugin.Runtime, key: Function) => void) {
|
||||
return this._internal.forEach(callback)
|
||||
}
|
||||
|
||||
/** Start a callback once the requested dependencies are available. */
|
||||
inject(inject: Inject, callback: Plugin.Function<void>) {
|
||||
return this.plugin({ inject, apply: callback, name: callback.name })
|
||||
}
|
||||
|
||||
/** Start a plugin in the current context and return its fiber. */
|
||||
plugin(plugin: Plugin, config?: any, getOuterStack = buildOuterStack()) {
|
||||
// check if it's a valid plugin
|
||||
const callback = this.resolve(plugin)
|
||||
if (!callback) throw new Error('invalid plugin, expect function or object with an "apply" method, received ' + typeof plugin)
|
||||
this.ctx.fiber.assertActive()
|
||||
|
||||
let runtime = this._internal.get(callback)
|
||||
if (!runtime) {
|
||||
let name = plugin.name
|
||||
if (name === 'apply') name = undefined
|
||||
runtime = { name, callback, fibers: new DisposableList(), Config: plugin.Config }
|
||||
this._internal.set(callback, runtime)
|
||||
}
|
||||
|
||||
const fiber = new Fiber(this.ctx, config, Inject.resolve(plugin.inject), runtime, getOuterStack)
|
||||
const wrapped = Object.create(fiber) as Fiber & PromiseLike<Fiber>
|
||||
wrapped.then = (onFulfilled, onRejected) => {
|
||||
return fiber.await().then(onFulfilled, onRejected)
|
||||
}
|
||||
return wrapped
|
||||
}
|
||||
}
|
||||
88
vendor/cordis/src/service.ts
vendored
Normal file
88
vendor/cordis/src/service.ts
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
import { defineProperty } from 'cosmokit'
|
||||
import { Context } from './context'
|
||||
import { createCallable, joinPrototype, symbols, Tracker } from './utils'
|
||||
|
||||
/**
|
||||
* Base class for services that expose a named API on `ctx`.
|
||||
*
|
||||
* Subclasses call `super(ctx, name)` from their constructor. The service is
|
||||
* registered immediately and is automatically removed with the owning fiber.
|
||||
*/
|
||||
export abstract class Service<out T = never> {
|
||||
static readonly init: unique symbol = symbols.init
|
||||
static readonly check: unique symbol = symbols.check
|
||||
static readonly config: unique symbol = symbols.config
|
||||
static readonly invoke: unique symbol = symbols.invoke
|
||||
static readonly extend: unique symbol = symbols.extend
|
||||
static readonly tracker: unique symbol = symbols.tracker
|
||||
static readonly resolveConfig: unique symbol = symbols.resolveConfig
|
||||
|
||||
declare [symbols.config]: T
|
||||
|
||||
public name!: string
|
||||
|
||||
/** Register this instance as `name` in the current context. */
|
||||
constructor(protected ctx: Context, name: string) {
|
||||
name ??= this.constructor['provide'] as string
|
||||
|
||||
let self = this
|
||||
const tracker: Tracker = {
|
||||
associate: name,
|
||||
property: 'ctx',
|
||||
}
|
||||
if (self[symbols.invoke]) {
|
||||
self = createCallable(name, joinPrototype(Object.getPrototypeOf(this), Function.prototype), tracker)
|
||||
}
|
||||
self.ctx = ctx
|
||||
self.name = name
|
||||
defineProperty(self, symbols.tracker, tracker)
|
||||
|
||||
self.ctx.reflect.provide(name, self, this[symbols.check])
|
||||
return self
|
||||
}
|
||||
|
||||
protected [symbols.filter](ctx: Context) {
|
||||
return ctx[symbols.isolate][this.name] === this.ctx[symbols.isolate][this.name]
|
||||
}
|
||||
|
||||
protected [symbols.extend](props?: any) {
|
||||
let self: any
|
||||
if (this[Service.invoke]) {
|
||||
self = createCallable(this.name, this, this[symbols.tracker])
|
||||
} else {
|
||||
self = Object.create(this)
|
||||
}
|
||||
return Object.assign(self, props)
|
||||
}
|
||||
|
||||
/** Merge intercept config from ancestors with optional base and head values. */
|
||||
[symbols.resolveConfig](base?: T, head?: T): T {
|
||||
let intercept = this.ctx[Context.intercept]
|
||||
const configs: any[] = []
|
||||
while (this.name in intercept) {
|
||||
if (Object.hasOwn(intercept, this.name)) {
|
||||
configs.unshift(intercept[this.name])
|
||||
}
|
||||
intercept = Object.getPrototypeOf(intercept)
|
||||
}
|
||||
if (base) configs.unshift(base)
|
||||
if (head) configs.push(head)
|
||||
if (this['Config']?.merge) {
|
||||
return this['Config'].merge(...configs)
|
||||
} else {
|
||||
return Object.assign({}, ...configs)
|
||||
}
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance](instance: any) {
|
||||
if (!instance) return false
|
||||
let constructor = instance.constructor
|
||||
while (constructor) {
|
||||
// constructor may be a proxy
|
||||
constructor = constructor.prototype?.constructor
|
||||
if (constructor === this) return true
|
||||
constructor &&= Object.getPrototypeOf(constructor)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
287
vendor/cordis/src/utils.ts
vendored
Normal file
287
vendor/cordis/src/utils.ts
vendored
Normal file
@@ -0,0 +1,287 @@
|
||||
import { defineProperty } from 'cosmokit'
|
||||
import type { Context, Service } from '.'
|
||||
|
||||
/** Ordered collection of disposable values with O(1) deletion by value. */
|
||||
export class DisposableList<T extends WeakKey> {
|
||||
private sn = 0
|
||||
private map = new Map<number, T>()
|
||||
private weak = new WeakMap<T, number>()
|
||||
|
||||
get length() {
|
||||
return this.map.size
|
||||
}
|
||||
|
||||
push(value: T) {
|
||||
const sn = ++this.sn
|
||||
this.map.set(sn, value)
|
||||
this.weak.set(value, sn)
|
||||
return () => this.map.delete(sn)
|
||||
}
|
||||
|
||||
delete(value: T) {
|
||||
const sn = this.weak.get(value)
|
||||
if (!sn) return false
|
||||
return this.map.delete(sn)
|
||||
}
|
||||
|
||||
clear() {
|
||||
const values = [...this.map.values()]
|
||||
this.map.clear()
|
||||
return values.reverse()
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return this.map.values()
|
||||
}
|
||||
|
||||
[Symbol.for('nodejs.util.inspect.custom')]() {
|
||||
return [...this]
|
||||
}
|
||||
}
|
||||
|
||||
/** Metadata used by traceable proxies to rebind `ctx` and associated services. */
|
||||
export interface Tracker {
|
||||
associate?: string
|
||||
property?: string
|
||||
noShadow?: boolean
|
||||
}
|
||||
|
||||
/** Shared symbols used to avoid public property-name collisions. */
|
||||
export const symbols = {
|
||||
// internal symbols
|
||||
shadow: Symbol.for('cordis.shadow'),
|
||||
receiver: Symbol.for('cordis.receiver'),
|
||||
original: Symbol.for('cordis.original'),
|
||||
metadata: Symbol.for('cordis.metadata'),
|
||||
initHooks: Symbol.for('cordis.initHooks'),
|
||||
checkProto: Symbol.for('cordis.checkProto'),
|
||||
|
||||
// context symbols
|
||||
effect: Symbol.for('cordis.effect') as typeof Context.effect,
|
||||
filter: Symbol.for('cordis.filter') as typeof Context.filter,
|
||||
isolate: Symbol.for('cordis.isolate') as typeof Context.isolate,
|
||||
intercept: Symbol.for('cordis.intercept') as typeof Context.intercept,
|
||||
|
||||
// service symbols
|
||||
init: Symbol.for('cordis.init') as typeof Service.init,
|
||||
check: Symbol.for('cordis.check') as typeof Service.check,
|
||||
config: Symbol.for('cordis.config') as typeof Service.config,
|
||||
invoke: Symbol.for('cordis.invoke') as typeof Service.invoke,
|
||||
extend: Symbol.for('cordis.extend') as typeof Service.extend,
|
||||
tracker: Symbol.for('cordis.tracker') as typeof Service.tracker,
|
||||
resolveConfig: Symbol.for('cordis.resolveConfig') as typeof Service.resolveConfig,
|
||||
}
|
||||
|
||||
const GeneratorFunction = function* () {}.constructor
|
||||
const AsyncGeneratorFunction = async function* () {}.constructor
|
||||
|
||||
/** Return true when a plugin callback should be constructed with `new`. */
|
||||
export function isConstructor(func: any): func is new (...args: any) => any {
|
||||
// async function or arrow function
|
||||
if (!func.prototype) return false
|
||||
// generator function or malformed definition
|
||||
// we cannot use below check because `mock.fn()` is proxied
|
||||
// if (func.prototype.constructor !== func) return false
|
||||
if (func instanceof GeneratorFunction) return false
|
||||
// polyfilled AsyncGeneratorFunction === Function
|
||||
if (AsyncGeneratorFunction !== Function && func instanceof AsyncGeneratorFunction) return false
|
||||
return true
|
||||
}
|
||||
|
||||
/** Merge two prototype chains while preserving descriptors from `proto1`. */
|
||||
export function joinPrototype(proto1: {}, proto2: {}) {
|
||||
if (proto1 === Object.prototype) return proto2
|
||||
const result = Object.create(joinPrototype(Object.getPrototypeOf(proto1), proto2))
|
||||
for (const key of Reflect.ownKeys(proto1)) {
|
||||
Object.defineProperty(result, key, Object.getOwnPropertyDescriptor(proto1, key)!)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/** Return true for non-null objects and functions. */
|
||||
export function isObject(value: any): value is {} {
|
||||
return value && (typeof value === 'object' || typeof value === 'function')
|
||||
}
|
||||
|
||||
/** Find a property descriptor by walking an object's prototype chain. */
|
||||
export function getPropertyDescriptor(target: any, prop: string | symbol) {
|
||||
let proto = target
|
||||
while (proto) {
|
||||
const desc = Reflect.getOwnPropertyDescriptor(proto, prop)
|
||||
if (desc) return desc
|
||||
proto = Object.getPrototypeOf(proto)
|
||||
}
|
||||
}
|
||||
|
||||
/** Wrap services/functions so method calls see the caller's active context. */
|
||||
export function getTraceable<T>(ctx: Context, value: T): T {
|
||||
if (!isObject(value)) return value
|
||||
if (Object.hasOwn(value, symbols.shadow)) {
|
||||
return Object.getPrototypeOf(value)
|
||||
}
|
||||
const tracker = value[symbols.tracker]
|
||||
if (!tracker) return value
|
||||
return createTraceable(ctx, value, tracker)
|
||||
}
|
||||
|
||||
/** Return a proxy that overlays readonly or writable properties onto a target. */
|
||||
export function withProps(target: any, props?: {}) {
|
||||
if (!props) return target
|
||||
return new Proxy(target, {
|
||||
get: (target, prop, receiver) => {
|
||||
if (prop in props && prop !== 'constructor') return Reflect.get(props, prop, receiver)
|
||||
return Reflect.get(target, prop, receiver)
|
||||
},
|
||||
set: (target, prop, value, receiver) => {
|
||||
if (prop in props && prop !== 'constructor') return Reflect.set(props, prop, value, receiver)
|
||||
return Reflect.set(target, prop, value, receiver)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function withProp(target: any, prop: string | symbol, value: any) {
|
||||
return withProps(target, Object.defineProperty(Object.create(null), prop, {
|
||||
value,
|
||||
writable: false,
|
||||
}))
|
||||
}
|
||||
|
||||
function createShadow(ctx: Context, target: any, property: string | undefined, receiver: any) {
|
||||
if (!property) return receiver
|
||||
const origin = Reflect.getOwnPropertyDescriptor(target, property)?.value
|
||||
if (!origin) return receiver
|
||||
return withProp(receiver, property, ctx.extend({ [symbols.shadow]: origin }))
|
||||
}
|
||||
|
||||
function createShadowMethod(ctx: Context, value: any, outer: any, shadow: {}) {
|
||||
return new Proxy(value, {
|
||||
apply: (target, thisArg, args) => {
|
||||
if (thisArg === outer) thisArg = shadow
|
||||
return getTraceable(ctx, Reflect.apply(target, thisArg, args))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function createTraceable(ctx: Context, value: any, tracker: Tracker) {
|
||||
// noShadow services are identity-aware (e.g. logger uses the origin fiber to
|
||||
// derive its name): keep the shadow ctx so they can read [symbols.shadow]
|
||||
// and resolve the origin. Non-noShadow services strip — their side effects
|
||||
// bind to caller, not origin.
|
||||
if (ctx[symbols.shadow] && !tracker.noShadow) {
|
||||
ctx = Object.getPrototypeOf(ctx)
|
||||
}
|
||||
const proxy = new Proxy(value, {
|
||||
get: (target, prop, receiver) => {
|
||||
if (prop === symbols.original) return target
|
||||
if (prop === tracker.property) return ctx
|
||||
if (typeof prop === 'symbol') {
|
||||
return Reflect.get(target, prop, receiver)
|
||||
}
|
||||
if (tracker.associate && ctx.reflect.props[`${tracker.associate}.${prop}`]) {
|
||||
return Reflect.get(ctx, `${tracker.associate}.${prop}`, withProp(ctx, symbols.receiver, receiver))
|
||||
}
|
||||
let shadow: any, innerValue: any
|
||||
const desc = getPropertyDescriptor(target, prop)
|
||||
if (desc && 'value' in desc) {
|
||||
innerValue = desc.value
|
||||
} else {
|
||||
shadow = createShadow(ctx, target, tracker.property, receiver)
|
||||
innerValue = Reflect.get(target, prop, shadow)
|
||||
}
|
||||
const innerTracker = innerValue?.[symbols.tracker]
|
||||
if (innerTracker) {
|
||||
return createTraceable(ctx, innerValue, innerTracker)
|
||||
} else if (!tracker.noShadow && typeof innerValue === 'function') {
|
||||
shadow ??= createShadow(ctx, target, tracker.property, receiver)
|
||||
return createShadowMethod(ctx, innerValue, receiver, shadow)
|
||||
} else {
|
||||
return innerValue
|
||||
}
|
||||
},
|
||||
set: (target, prop, value, receiver) => {
|
||||
if (prop === symbols.original) return false
|
||||
if (prop === tracker.property) return false
|
||||
if (typeof prop === 'symbol') {
|
||||
return Reflect.set(target, prop, value, receiver)
|
||||
}
|
||||
if (tracker.associate && ctx.reflect.props[`${tracker.associate}.${prop}`]) {
|
||||
return Reflect.set(ctx, `${tracker.associate}.${prop}`, value, withProp(ctx, symbols.receiver, receiver))
|
||||
}
|
||||
const shadow = createShadow(ctx, target, tracker.property, receiver)
|
||||
return Reflect.set(target, prop, value, shadow)
|
||||
},
|
||||
apply: (target, thisArg, args) => {
|
||||
return applyTraceable(proxy, target, thisArg, args)
|
||||
},
|
||||
})
|
||||
return proxy
|
||||
}
|
||||
|
||||
function applyTraceable(proxy: any, value: any, thisArg: any, args: any[]) {
|
||||
if (!value[symbols.invoke]) return Reflect.apply(value, thisArg, args)
|
||||
return value[symbols.invoke].apply(proxy, args)
|
||||
}
|
||||
|
||||
/** Create a callable service object that dispatches through `symbols.invoke`. */
|
||||
export function createCallable(name: string, proto: {}, tracker: Tracker) {
|
||||
const self = function (...args: any[]) {
|
||||
const proxy = createTraceable(self['ctx'], self, tracker)
|
||||
return applyTraceable(proxy, self, this, args)
|
||||
}
|
||||
defineProperty(self, 'name', name)
|
||||
return Object.setPrototypeOf(self, proto)
|
||||
}
|
||||
|
||||
interface StackInfo {
|
||||
offset: number
|
||||
error: Error
|
||||
}
|
||||
|
||||
function handleError(info: StackInfo, reason: any, getOuterStack: () => string[]): never {
|
||||
const innerLines = info.error.stack!.split('\n')
|
||||
|
||||
// malformed error
|
||||
if (typeof reason?.stack !== 'string') {
|
||||
const outerError = new Error(reason)
|
||||
const lines = outerError.stack!.split('\n')
|
||||
lines.splice(1, Infinity, ...getOuterStack())
|
||||
outerError.stack = lines.join('\n')
|
||||
throw outerError
|
||||
}
|
||||
|
||||
// long stack trace
|
||||
const lines: string[] = reason.stack.split('\n')
|
||||
let index = lines.indexOf(innerLines[2])
|
||||
if (index === -1) throw reason
|
||||
|
||||
index -= info.offset
|
||||
while (index > 0) {
|
||||
if (!lines[index - 1].endsWith(' (<anonymous>)')) break
|
||||
index -= 1
|
||||
}
|
||||
lines.splice(index, Infinity, ...getOuterStack())
|
||||
reason.stack = lines.join('\n')
|
||||
throw reason
|
||||
}
|
||||
|
||||
/** Run a callback and splice outer call-site frames into thrown async errors. */
|
||||
export function composeError<T>(callback: (info: StackInfo) => T, getOuterStack = buildOuterStack()): T {
|
||||
const info: StackInfo = { offset: 1, error: new Error() }
|
||||
|
||||
try {
|
||||
const result: any = callback(info)
|
||||
if (isObject(result) && 'then' in result) {
|
||||
return (result as any).then(undefined, (reason) => handleError(info, reason, getOuterStack)) as T
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
} catch (reason: any) {
|
||||
handleError(info, reason, getOuterStack)
|
||||
}
|
||||
}
|
||||
|
||||
/** Capture a lazy stack-frame supplier for later error composition. */
|
||||
export function buildOuterStack(offset = 0) {
|
||||
const outerError = new Error()
|
||||
return () => outerError.stack!.split('\n').slice(3 + offset)
|
||||
}
|
||||
14
vendor/cordis/tsconfig.json
vendored
Normal file
14
vendor/cordis/tsconfig.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib",
|
||||
"noImplicitAny": false,
|
||||
"noImplicitThis": false,
|
||||
"strictFunctionTypes": false
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{ "path": "../cosmokit" }
|
||||
]
|
||||
}
|
||||
21
vendor/cosmokit/LICENSE
vendored
Normal file
21
vendor/cosmokit/LICENSE
vendored
Normal 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.
|
||||
24
vendor/cosmokit/README.md
vendored
Normal file
24
vendor/cosmokit/README.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# cosmokit
|
||||
|
||||
[](https://codecov.io/gh/shigma/cosmokit)
|
||||
[](https://www.npmjs.com/package/cosmokit)
|
||||
|
||||
A collection of common utilities.
|
||||
|
||||
## Usage
|
||||
|
||||
### Node.js
|
||||
|
||||
```sh
|
||||
npm install cosmokit
|
||||
```
|
||||
|
||||
```ts
|
||||
import cosmokit from 'cosmokit'
|
||||
```
|
||||
|
||||
### Deno
|
||||
|
||||
```ts
|
||||
import cosmokit from 'npm:cosmokit@latest'
|
||||
```
|
||||
23
vendor/cosmokit/package.json
vendored
Normal file
23
vendor/cosmokit/package.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "cosmokit",
|
||||
"description": "A collection of common utilities",
|
||||
"version": "1.8.1",
|
||||
"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"
|
||||
}
|
||||
42
vendor/cosmokit/src/array.ts
vendored
Normal file
42
vendor/cosmokit/src/array.ts
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
import { isNullable } from './misc'
|
||||
|
||||
/** Return true when every item in `array2` is present in `array1`. */
|
||||
export function contain(array1: readonly any[], array2: readonly any[]) {
|
||||
return array2.every(item => array1.includes(item))
|
||||
}
|
||||
|
||||
/** Return items that appear in both arrays. */
|
||||
export function intersection<T>(array1: readonly T[], array2: readonly T[]) {
|
||||
return array1.filter(item => array2.includes(item))
|
||||
}
|
||||
|
||||
/** Return items from `array1` that do not appear in `array2`. */
|
||||
export function difference<S>(array1: readonly S[], array2: readonly any[]) {
|
||||
return array1.filter(item => !array2.includes(item))
|
||||
}
|
||||
|
||||
/** Return the set-union of two arrays while preserving first occurrence order. */
|
||||
export function union<T>(array1: readonly T[], array2: readonly T[]) {
|
||||
return Array.from(new Set([...array1, ...array2]))
|
||||
}
|
||||
|
||||
/** Remove duplicate values while preserving first occurrence order. */
|
||||
export function deduplicate<T>(array: readonly T[]) {
|
||||
return [...new Set(array)]
|
||||
}
|
||||
|
||||
/** Remove one item from an array and report whether it was found. */
|
||||
export function remove<T>(list: T[], item: T) {
|
||||
const index = list?.indexOf(item)
|
||||
if (index >= 0) {
|
||||
list.splice(index, 1)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** Normalize nullish, scalar, or array input to an array. */
|
||||
export function makeArray<T>(source: null | undefined | T | T[]) {
|
||||
return Array.isArray(source) ? source : isNullable(source) ? [] : [source]
|
||||
}
|
||||
10
vendor/cosmokit/src/index.ts
vendored
Normal file
10
vendor/cosmokit/src/index.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
/** Array set and normalization helpers. */
|
||||
export * from './array'
|
||||
/** Runtime type, binary, clone, and equality helpers. */
|
||||
export * from './types'
|
||||
/** Shared utility types and object helpers. */
|
||||
export * from './misc'
|
||||
/** String case, path, and property formatting helpers. */
|
||||
export * from './string'
|
||||
/** Time constants, parsing, and formatting helpers. */
|
||||
export * from './time'
|
||||
78
vendor/cosmokit/src/misc.ts
vendored
Normal file
78
vendor/cosmokit/src/misc.ts
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
/** String/symbol keyed dictionary type. */
|
||||
export type Dict<T = any, K extends string | symbol = string> = { [key in K]: T }
|
||||
/** Safely read `T[K]`, returning `never` when `K` is not a key of `T`. */
|
||||
export type Get<T extends {}, K> = K extends keyof T ? T[K] : never
|
||||
/** Conditional extraction helper with a configurable return type. */
|
||||
export type Extract<S, T, U = S> = S extends T ? U : never
|
||||
/** Accept a value or an array, unless the value is already an array type. */
|
||||
export type MaybeArray<T> = [T] extends [unknown[]] ? T : T | T[]
|
||||
/** Wrap a value in `Promise`, preserving the resolved type of existing promises. */
|
||||
export type Promisify<T> = Promise<T extends Promise<infer S> ? S : T>
|
||||
/** Accept a value or promise unless the value type is already promise-like. */
|
||||
export type Awaitable<T> = [T] extends [Promise<unknown>] ? T : T | Promise<T>
|
||||
/** Convert a union type to an intersection type. */
|
||||
export type Intersect<U> = (U extends any ? (arg: U) => void : never) extends ((arg: infer I) => void) ? I : never
|
||||
|
||||
/** No-op callback returning `undefined` at runtime and `any` at type level. */
|
||||
export function noop(): any {}
|
||||
|
||||
/** Return true when a value is `null` or `undefined`. */
|
||||
export function isNullable(value: any): value is null | undefined | void {
|
||||
return value === null || value === undefined
|
||||
}
|
||||
|
||||
/** Return true when a value is neither `null` nor `undefined`. */
|
||||
export function isNonNullable<T>(value: T): value is NonNullable<T> {
|
||||
return !isNullable(value)
|
||||
}
|
||||
|
||||
/** Return true for non-array object values. */
|
||||
export function isPlainObject(data: any) {
|
||||
return data && typeof data === 'object' && !Array.isArray(data)
|
||||
}
|
||||
|
||||
/** Filter object entries with a key type guard. */
|
||||
export function filterKeys<T, K extends string, U extends K>(object: Dict<T, K>, filter: (key: K, value: T) => key is U): Dict<T, U>
|
||||
/** Filter object entries with a boolean predicate. */
|
||||
export function filterKeys<T, K extends string>(object: Dict<T, K>, filter: (key: K, value: T) => boolean): Dict<T, K>
|
||||
/** Filter object entries and return a new object. */
|
||||
export function filterKeys(object: {}, filter: (key: string, value: any) => boolean) {
|
||||
return Object.fromEntries(Object.entries(object).filter(([key, value]) => filter(key, value)))
|
||||
}
|
||||
|
||||
/** Map object values while preserving the original key set. */
|
||||
export function mapValues<U, T, K extends string>(object: Dict<T, K>, transform: (value: T, key: K) => U) {
|
||||
return Object.fromEntries(Object.entries(object).map(([key, value]) => [key, (transform as any)(value, key)])) as Dict<U, K>
|
||||
}
|
||||
|
||||
/** Alias for `mapValues`. */
|
||||
export { mapValues as valueMap }
|
||||
|
||||
/** Pick selected keys from an object, optionally including `undefined` values. */
|
||||
export function pick<T extends object, K extends keyof T>(source: T, keys?: Iterable<K>, forced?: boolean) {
|
||||
if (!keys) return { ...source }
|
||||
const result = {} as Pick<T, K>
|
||||
for (const key of keys) {
|
||||
if (forced || source[key] !== undefined) result[key] = source[key]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/** Omit selected keys from a shallow object copy. */
|
||||
export function omit<T, K extends keyof T>(source: T, keys?: Iterable<K>) {
|
||||
if (!keys) return { ...source }
|
||||
const result = { ...source } as Omit<T, K>
|
||||
for (const key of keys) {
|
||||
Reflect.deleteProperty(result, key)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/** Define a non-enumerable writable property with a typed key. */
|
||||
export function defineProperty<T, K extends keyof T>(object: T, key: K, value: T[K]): T
|
||||
/** Define a non-enumerable writable property with an arbitrary key. */
|
||||
export function defineProperty<T, K extends keyof any>(object: T, key: K, value: any): T
|
||||
/** Define a non-enumerable writable property and return the object. */
|
||||
export function defineProperty<T, K extends keyof any>(object: T, key: K, value: any) {
|
||||
return Object.defineProperty(object, key, { writable: true, value, enumerable: false })
|
||||
}
|
||||
113
vendor/cosmokit/src/string.ts
vendored
Normal file
113
vendor/cosmokit/src/string.ts
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
/** Uppercase the first character of a string. */
|
||||
export function capitalize(source: string) {
|
||||
return source.charAt(0).toUpperCase() + source.slice(1)
|
||||
}
|
||||
|
||||
/** Lowercase the first character of a string. */
|
||||
export function uncapitalize(source: string) {
|
||||
return source.charAt(0).toLowerCase() + source.slice(1)
|
||||
}
|
||||
|
||||
/** Convert dash or underscore delimited text to camelCase. */
|
||||
export function camelCase(source: string) {
|
||||
return source.replace(/[_-][a-z]/g, str => str.slice(1).toUpperCase())
|
||||
}
|
||||
|
||||
const enum State {
|
||||
DELIM,
|
||||
UPPER,
|
||||
LOWER,
|
||||
}
|
||||
|
||||
function tokenize(source: string, delimiters: number[], delimiter: number) {
|
||||
const output: number[] = []
|
||||
let state = State.DELIM
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
const code = source.charCodeAt(i)
|
||||
if (code >= 65 && code <= 90) {
|
||||
if (state === State.UPPER) {
|
||||
const next = source.charCodeAt(i + 1)
|
||||
if (next >= 97 && next <= 122) {
|
||||
output.push(delimiter)
|
||||
}
|
||||
output.push(code + 32)
|
||||
} else {
|
||||
if (state !== State.DELIM) {
|
||||
output.push(delimiter)
|
||||
}
|
||||
output.push(code + 32)
|
||||
}
|
||||
state = State.UPPER
|
||||
} else if (code >= 97 && code <= 122) {
|
||||
output.push(code)
|
||||
state = State.LOWER
|
||||
} else if (delimiters.includes(code)) {
|
||||
if (state !== State.DELIM) {
|
||||
output.push(delimiter)
|
||||
}
|
||||
state = State.DELIM
|
||||
} else {
|
||||
output.push(code)
|
||||
}
|
||||
}
|
||||
return String.fromCharCode(...output)
|
||||
}
|
||||
|
||||
/** Convert text to dash-delimited parameter case. */
|
||||
export function paramCase(source: string) {
|
||||
return tokenize(source, [45, 95], 45)
|
||||
}
|
||||
|
||||
/** Convert text to underscore-delimited snake case. */
|
||||
export function snakeCase(source: string) {
|
||||
return tokenize(source, [45, 95], 95)
|
||||
}
|
||||
|
||||
/** Runtime alias for `camelCase`. */
|
||||
export const camelize = camelCase
|
||||
/** Runtime alias for `paramCase`. */
|
||||
export const hyphenate = paramCase
|
||||
|
||||
namespace Letter {
|
||||
/* eslint-disable @typescript-eslint/member-delimiter-style */
|
||||
interface LowerToUpper {
|
||||
a: 'A', b: 'B', c: 'C', d: 'D', e: 'E', f: 'F', g: 'G', h: 'H', i: 'I', j: 'J', k: 'K', l: 'L', m: 'M',
|
||||
n: 'N', o: 'O', p: 'P', q: 'Q', r: 'R', s: 'S', t: 'T', u: 'U', v: 'V', w: 'W', x: 'X', y: 'Y', z: 'Z',
|
||||
}
|
||||
|
||||
interface UpperToLower {
|
||||
A: 'a', B: 'b', C: 'c', D: 'd', E: 'e', F: 'f', G: 'g', H: 'h', I: 'i', J: 'j', K: 'k', L: 'l', M: 'm',
|
||||
N: 'n', O: 'o', P: 'p', Q: 'q', R: 'r', S: 's', T: 't', U: 'u', V: 'v', W: 'w', X: 'x', Y: 'y', Z: 'z',
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/member-delimiter-style */
|
||||
|
||||
export type Upper = keyof UpperToLower
|
||||
export type Lower = keyof LowerToUpper
|
||||
|
||||
export type ToUpper<S extends string> = S extends Lower ? LowerToUpper[S] : S
|
||||
export type ToLower<S extends string, P extends string = ''> = S extends Upper ? `${P}${UpperToLower[S]}` : S
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
/** Type-level conversion from dash-delimited text to camelCase. */
|
||||
export type camelize<S extends string> = S extends `${infer L}-${infer M}${infer R}` ? `${L}${Letter.ToUpper<M>}${camelize<R>}` : S
|
||||
/** Type-level conversion from camelCase text to dash-delimited text. */
|
||||
export type hyphenate<S extends string> = S extends `${infer L}${infer R}` ? `${Letter.ToLower<L, '-'>}${hyphenate<R>}` : S
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
/** Format a property key as a JavaScript member access suffix. */
|
||||
export function formatProperty(key: keyof any) {
|
||||
if (typeof key !== 'string') return `[${key.toString()}]`
|
||||
return /^[a-z_$][\w$]*$/i.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`
|
||||
}
|
||||
|
||||
/** Remove one trailing slash from a path string. */
|
||||
export function trimSlash(source: string) {
|
||||
return source.replace(/\/$/, '')
|
||||
}
|
||||
|
||||
/** Ensure a path starts with `/` and has no trailing slash. */
|
||||
export function sanitize(source: string) {
|
||||
if (!source.startsWith('/')) source = '/' + source
|
||||
return trimSlash(source)
|
||||
}
|
||||
92
vendor/cosmokit/src/time.ts
vendored
Normal file
92
vendor/cosmokit/src/time.ts
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
/** Time constants plus parsing and formatting helpers. */
|
||||
export namespace Time {
|
||||
export const millisecond = 1
|
||||
export const second = 1000
|
||||
export const minute = second * 60
|
||||
export const hour = minute * 60
|
||||
export const day = hour * 24
|
||||
export const week = day * 7
|
||||
|
||||
let timezoneOffset = new Date().getTimezoneOffset()
|
||||
|
||||
export function setTimezoneOffset(offset: number) {
|
||||
timezoneOffset = offset
|
||||
}
|
||||
|
||||
export function getTimezoneOffset() {
|
||||
return timezoneOffset
|
||||
}
|
||||
|
||||
export function getDateNumber(date: number | Date = new Date(), offset?: number) {
|
||||
if (typeof date === 'number') date = new Date(date)
|
||||
if (offset === undefined) offset = timezoneOffset
|
||||
return Math.floor((date.valueOf() / minute - offset) / 1440)
|
||||
}
|
||||
|
||||
export function fromDateNumber(value: number, offset?: number) {
|
||||
const date = new Date(value * day)
|
||||
if (offset === undefined) offset = timezoneOffset
|
||||
return new Date(+date + offset * minute)
|
||||
}
|
||||
|
||||
const numeric = /\d+(?:\.\d+)?/.source
|
||||
const timeRegExp = new RegExp(`^${[
|
||||
'w(?:eek(?:s)?)?',
|
||||
'd(?:ay(?:s)?)?',
|
||||
'h(?:our(?:s)?)?',
|
||||
'm(?:in(?:ute)?(?:s)?)?',
|
||||
's(?:ec(?:ond)?(?:s)?)?',
|
||||
].map(unit => `(${numeric}${unit})?`).join('')}$`)
|
||||
|
||||
export function parseTime(source: string) {
|
||||
const capture = timeRegExp.exec(source)
|
||||
if (!capture) return 0
|
||||
return (parseFloat(capture[1]) * week || 0)
|
||||
+ (parseFloat(capture[2]) * day || 0)
|
||||
+ (parseFloat(capture[3]) * hour || 0)
|
||||
+ (parseFloat(capture[4]) * minute || 0)
|
||||
+ (parseFloat(capture[5]) * second || 0)
|
||||
}
|
||||
|
||||
export function parseDate(date: string) {
|
||||
const parsed = parseTime(date)
|
||||
if (parsed) {
|
||||
date = Date.now() + parsed as any
|
||||
} else if (/^\d{1,2}(:\d{1,2}){1,2}$/.test(date)) {
|
||||
date = `${new Date().toLocaleDateString()}-${date}`
|
||||
} else if (/^\d{1,2}-\d{1,2}-\d{1,2}(:\d{1,2}){1,2}$/.test(date)) {
|
||||
date = `${new Date().getFullYear()}-${date}`
|
||||
}
|
||||
return date ? new Date(date) : new Date()
|
||||
}
|
||||
|
||||
export function format(ms: number) {
|
||||
const abs = Math.abs(ms)
|
||||
if (abs >= day - hour / 2) {
|
||||
return Math.round(ms / day) + 'd'
|
||||
} else if (abs >= hour - minute / 2) {
|
||||
return Math.round(ms / hour) + 'h'
|
||||
} else if (abs >= minute - second / 2) {
|
||||
return Math.round(ms / minute) + 'm'
|
||||
} else if (abs >= second) {
|
||||
return Math.round(ms / second) + 's'
|
||||
}
|
||||
return ms + 'ms'
|
||||
}
|
||||
|
||||
export function toDigits(source: number, length = 2) {
|
||||
return source.toString().padStart(length, '0')
|
||||
}
|
||||
|
||||
export function template(template: string, time = new Date()) {
|
||||
return template
|
||||
.replace('yyyy', time.getFullYear().toString())
|
||||
.replace('yy', time.getFullYear().toString().slice(2))
|
||||
.replace('MM', toDigits(time.getMonth() + 1))
|
||||
.replace('dd', toDigits(time.getDate()))
|
||||
.replace('hh', toDigits(time.getHours()))
|
||||
.replace('mm', toDigits(time.getMinutes()))
|
||||
.replace('ss', toDigits(time.getSeconds()))
|
||||
.replace('SSS', toDigits(time.getMilliseconds(), 3))
|
||||
}
|
||||
}
|
||||
142
vendor/cosmokit/src/types.ts
vendored
Normal file
142
vendor/cosmokit/src/types.ts
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
import { isNullable } from './misc'
|
||||
|
||||
type GlobalConstructorNames = keyof {
|
||||
[K in keyof typeof globalThis as typeof globalThis[K] extends abstract new (...args: any) => any ? K : never]: K
|
||||
}
|
||||
|
||||
/** Create a predicate for a global constructor name. */
|
||||
export function is<K extends GlobalConstructorNames>(type: K): (value: any) => value is InstanceType<typeof globalThis[K]>
|
||||
/** Test whether a value matches a global constructor name. */
|
||||
export function is<K extends GlobalConstructorNames>(type: K, value: any): value is InstanceType<typeof globalThis[K]>
|
||||
/** Test values using `instanceof` with a `toStringTag` fallback. */
|
||||
export function is<K extends GlobalConstructorNames>(type: K, value?: any): any {
|
||||
if (arguments.length === 1) return (value: any) => is(type, value)
|
||||
return type in globalThis && value instanceof (globalThis[type] as any)
|
||||
|| Object.prototype.toString.call(value).slice(8, -1) === type
|
||||
}
|
||||
|
||||
function isArrayBufferLike(value: any): value is ArrayBufferLike {
|
||||
return is('ArrayBuffer', value) || is('SharedArrayBuffer', value)
|
||||
}
|
||||
|
||||
function isArrayBufferSource(value: any): value is Binary.Source {
|
||||
return isArrayBufferLike(value) || ArrayBuffer.isView(value)
|
||||
}
|
||||
|
||||
/** Binary source detection and base64/hex conversion helpers. */
|
||||
export namespace Binary {
|
||||
export type Source<T extends ArrayBufferLike = ArrayBufferLike> = T | ArrayBufferView<T>
|
||||
|
||||
export const is = isArrayBufferLike
|
||||
export const isSource = isArrayBufferSource
|
||||
|
||||
export function fromSource<T extends ArrayBufferLike>(source: Source<T>): T {
|
||||
if (ArrayBuffer.isView(source)) {
|
||||
// https://stackoverflow.com/questions/8609289/convert-a-binary-nodejs-buffer-to-javascript-arraybuffer#answer-31394257
|
||||
return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength) as T
|
||||
} else {
|
||||
return source
|
||||
}
|
||||
}
|
||||
|
||||
export function toBase64(source: Source) {
|
||||
source = fromSource(source)
|
||||
if (typeof Buffer !== 'undefined') {
|
||||
return Buffer.from(source).toString('base64')
|
||||
}
|
||||
let binary = ''
|
||||
const bytes = new Uint8Array(source)
|
||||
for (let i = 0; i < bytes.byteLength; i++) {
|
||||
binary += String.fromCharCode(bytes[i])
|
||||
}
|
||||
return btoa(binary)
|
||||
}
|
||||
|
||||
export function fromBase64(source: string) {
|
||||
if (typeof Buffer !== 'undefined') return fromSource(Buffer.from(source, 'base64'))
|
||||
return Uint8Array.from(atob(source), c => c.charCodeAt(0))
|
||||
}
|
||||
|
||||
export function toHex(source: Source) {
|
||||
source = fromSource(source)
|
||||
if (typeof Buffer !== 'undefined') return Buffer.from(source).toString('hex')
|
||||
return Array.from(new Uint8Array(source), byte => byte.toString(16).padStart(2, '0')).join('')
|
||||
}
|
||||
|
||||
export function fromHex(source: string) {
|
||||
if (typeof Buffer !== 'undefined') return fromSource(Buffer.from(source, 'hex'))
|
||||
const hex = source.length % 2 === 0 ? source : source.slice(0, source.length - 1)
|
||||
const buffer: number[] = []
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
buffer.push(parseInt(`${hex[i]}${hex[i + 1]}`, 16))
|
||||
}
|
||||
return Uint8Array.from(buffer).buffer
|
||||
}
|
||||
}
|
||||
|
||||
/** Decode a base64 string into binary data. */
|
||||
export const base64ToArrayBuffer = Binary.fromBase64
|
||||
/** Encode binary data as base64. */
|
||||
export const arrayBufferToBase64 = Binary.toBase64
|
||||
/** Decode a hex string into binary data. */
|
||||
export const hexToArrayBuffer = Binary.fromHex
|
||||
/** Encode binary data as hex. */
|
||||
export const arrayBufferToHex = Binary.toHex
|
||||
|
||||
/** Deep-clone common JavaScript values while preserving prototypes. */
|
||||
export function clone<T>(source: T): T
|
||||
/** Deep-clone common JavaScript values while preserving prototypes and cycles. */
|
||||
export function clone(source: any, refs = new Map<any, any>()) {
|
||||
if (!source || typeof source !== 'object') return source
|
||||
if (is('Date', source)) return new Date(source.valueOf())
|
||||
if (is('RegExp', source)) return new RegExp(source.source, source.flags)
|
||||
if (isArrayBufferLike(source)) return source.slice(0)
|
||||
if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength)
|
||||
const cached = refs.get(source)
|
||||
if (cached) return cached
|
||||
if (Array.isArray(source)) {
|
||||
const result: any[] = []
|
||||
refs.set(source, result)
|
||||
source.forEach((value, index) => {
|
||||
result[index] = Reflect.apply(clone, null, [value, refs])
|
||||
})
|
||||
return result
|
||||
}
|
||||
const result = Object.create(Object.getPrototypeOf(source))
|
||||
refs.set(source, result)
|
||||
for (const key of Reflect.ownKeys(source)) {
|
||||
const descriptor = { ...Reflect.getOwnPropertyDescriptor(source, key) }
|
||||
if ('value' in descriptor) {
|
||||
descriptor.value = Reflect.apply(clone, null, [descriptor.value, refs])
|
||||
}
|
||||
Reflect.defineProperty(result, key, descriptor)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/** Deeply compare arrays, dates, regexps, buffers, and plain object fields. */
|
||||
export function deepEqual(a: any, b: any, strict?: boolean): boolean {
|
||||
if (a === b) return true
|
||||
if (!strict && isNullable(a) && isNullable(b)) return true
|
||||
if (typeof a !== typeof b) return false
|
||||
if (typeof a !== 'object') return false
|
||||
if (!a || !b) return false
|
||||
|
||||
function check<T>(test: (x: any) => x is T, then: (a: T, b: T) => boolean) {
|
||||
return test(a) ? test(b) ? then(a, b) : false : test(b) ? false : undefined
|
||||
}
|
||||
|
||||
return check(Array.isArray, (a, b) => a.length === b.length && a.every((item, index) => deepEqual(item, b[index])))
|
||||
?? check(is('Date'), (a, b) => a.valueOf() === b.valueOf())
|
||||
?? check(is('RegExp'), (a, b) => a.source === b.source && a.flags === b.flags)
|
||||
?? check(isArrayBufferLike, (a, b) => {
|
||||
if (a.byteLength !== b.byteLength) return false
|
||||
const viewA = new Uint8Array(a)
|
||||
const viewB = new Uint8Array(b)
|
||||
for (let i = 0; i < viewA.length; i++) {
|
||||
if (viewA[i] !== viewB[i]) return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
?? Object.keys({ ...a, ...b }).every(key => deepEqual(a[key], b[key], strict))
|
||||
}
|
||||
8
vendor/cosmokit/tsconfig.json
vendored
Normal file
8
vendor/cosmokit/tsconfig.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
21
vendor/group/LICENSE
vendored
Normal file
21
vendor/group/LICENSE
vendored
Normal 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.
|
||||
21
vendor/group/README.md
vendored
Normal file
21
vendor/group/README.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# @cordisjs/plugin-group
|
||||
|
||||
Loader group plugin for nesting Cordis entries.
|
||||
|
||||
## Usage
|
||||
|
||||
```yaml
|
||||
- id: tools
|
||||
name: '@cordisjs/plugin-group'
|
||||
group: true
|
||||
config:
|
||||
- id: logger
|
||||
name: '@cordisjs/plugin-logger-console'
|
||||
```
|
||||
|
||||
Groups are always considered enabled themselves, but disabling a group entry
|
||||
prevents its child entries from running. Nested entry ids use `:` separators,
|
||||
for example `tools:logger`.
|
||||
|
||||
The package re-exports the `Group` implementation from
|
||||
`@cordisjs/plugin-loader` as its default plugin.
|
||||
27
vendor/group/package.json
vendored
Normal file
27
vendor/group/package.json
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "@cordisjs/plugin-group",
|
||||
"description": "Nested plugin group for cordis",
|
||||
"version": "1.0.0",
|
||||
"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": {
|
||||
"@cordisjs/plugin-loader": "^1.0.0-rc.4",
|
||||
"cordis": "^4.0.0-rc.6"
|
||||
}
|
||||
}
|
||||
3
vendor/group/src/index.ts
vendored
Normal file
3
vendor/group/src/index.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import { Group } from '@cordisjs/plugin-loader'
|
||||
|
||||
export default Group
|
||||
12
vendor/group/tsconfig.json
vendored
Normal file
12
vendor/group/tsconfig.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{ "path": "../cordis" },
|
||||
{ "path": "../loader" }
|
||||
]
|
||||
}
|
||||
21
vendor/hmr/LICENSE
vendored
Normal file
21
vendor/hmr/LICENSE
vendored
Normal 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.
|
||||
47
vendor/hmr/README.md
vendored
Normal file
47
vendor/hmr/README.md
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
# @cordisjs/plugin-hmr
|
||||
|
||||
Hot module replacement for loader-managed Cordis plugins.
|
||||
|
||||
The HMR plugin watches source files, traces Node's module graph, clears affected
|
||||
module caches, and reloads only the plugin entries that depend on changed
|
||||
application files. Changes to framework-level dependencies fall back to
|
||||
`loader.exit()`, letting the host process restart.
|
||||
|
||||
## Requirements
|
||||
|
||||
- `@cordisjs/plugin-loader`
|
||||
- `@cordisjs/plugin-timer`
|
||||
- A runtime that exposes Node's internal module loader. The package throws if
|
||||
the loader service has no internal module loader available.
|
||||
|
||||
## Usage
|
||||
|
||||
```yaml
|
||||
- id: timer
|
||||
name: '@cordisjs/plugin-timer'
|
||||
- id: hmr
|
||||
name: '@cordisjs/plugin-hmr'
|
||||
config:
|
||||
root:
|
||||
- src
|
||||
ignored:
|
||||
- '**/node_modules'
|
||||
- '**/.*'
|
||||
debounce: 100
|
||||
```
|
||||
|
||||
## Config
|
||||
|
||||
| Field | Description |
|
||||
| --- | --- |
|
||||
| `base` | Optional base directory resolved from `ctx.baseUrl`. |
|
||||
| `root` | Chokidar roots to watch. Defaults to `['.']`. |
|
||||
| `ignored` | Picomatch patterns excluded from watch and reload analysis. |
|
||||
| `debounce` | Milliseconds to wait before processing a burst of changes. |
|
||||
|
||||
## Events
|
||||
|
||||
| Event | Description |
|
||||
| --- | --- |
|
||||
| `hmr/change` | Emitted for changed files that are not handled by plugin reload or config reload. |
|
||||
| `hmr/reload` | Emitted after one or more plugin entries are reloaded. |
|
||||
49
vendor/hmr/package.json
vendored
Normal file
49
vendor/hmr/package.json
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@cordisjs/plugin-hmr",
|
||||
"description": "Hot Module Replacement Plugin for Cordis",
|
||||
"version": "1.0.15",
|
||||
"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",
|
||||
"cordis": {
|
||||
"services": {
|
||||
"required": [
|
||||
"timer"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"en": "Hot Module Replacement",
|
||||
"zh": "模块热替换"
|
||||
}
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@cordisjs/plugin-timer": "^1.1.2",
|
||||
"cordis": "^4.0.0-rc.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"chokidar": "^4.0.3",
|
||||
"cosmokit": "^1.8.1",
|
||||
"picomatch": "^4.0.3",
|
||||
"schemastery": "^3.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/babel__code-frame": "^7.27.0",
|
||||
"@types/picomatch": "^3.0.2"
|
||||
}
|
||||
}
|
||||
36
vendor/hmr/src/error.ts
vendored
Normal file
36
vendor/hmr/src/error.ts
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Context } from 'cordis'
|
||||
import { BuildFailure } from 'esbuild'
|
||||
import { codeFrameColumns } from '@babel/code-frame'
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
function isBuildFailure(e: any): e is BuildFailure {
|
||||
return Array.isArray(e?.errors) && e.errors.every((error: any) => error.text)
|
||||
}
|
||||
|
||||
/** Log HMR build failures with code frames when source locations are available. */
|
||||
export function handleError(ctx: Context, e: any) {
|
||||
if (!isBuildFailure(e)) {
|
||||
ctx.logger.warn(e)
|
||||
return
|
||||
}
|
||||
|
||||
for (const error of e.errors) {
|
||||
if (!error.location) {
|
||||
ctx.logger.warn(error.text)
|
||||
continue
|
||||
}
|
||||
try {
|
||||
const { file, line, column } = error.location
|
||||
const source = readFileSync(file, 'utf8')
|
||||
const formatted = codeFrameColumns(source, {
|
||||
start: { line, column },
|
||||
}, {
|
||||
highlightCode: true,
|
||||
message: error.text,
|
||||
})
|
||||
ctx.logger.warn(`File: ${file}:${line}:${column}\n` + formatted)
|
||||
} catch (e) {
|
||||
ctx.logger.warn(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
403
vendor/hmr/src/index.ts
vendored
Normal file
403
vendor/hmr/src/index.ts
vendored
Normal file
@@ -0,0 +1,403 @@
|
||||
import { Context, Inject, Plugin, Service } from 'cordis'
|
||||
import { Dict } from 'cosmokit'
|
||||
import { ModuleJob, ModuleLoader, ResolveResult } from '@cordisjs/plugin-loader'
|
||||
import type { Include } from '@cordisjs/plugin-include'
|
||||
import { ChokidarOptions, FSWatcher, watch } from 'chokidar'
|
||||
import { relative, resolve } from 'node:path'
|
||||
import { handleError } from './error.ts'
|
||||
import type {} from '@cordisjs/plugin-timer'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import { createRequire } from 'node:module'
|
||||
import picomatch from 'picomatch'
|
||||
import z from 'schemastery'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
hmr: Hmr
|
||||
}
|
||||
|
||||
interface Events {
|
||||
'hmr/change'(url: string): void
|
||||
'hmr/reload'(reloads: Map<Plugin, Reload>): void
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively collect all module dependencies from a ModuleJob.
|
||||
* Skips node: builtins and node_modules to focus on user code.
|
||||
*/
|
||||
async function loadDependencies(job: ModuleJob, ignored = new Set<string>()) {
|
||||
const dependencies = new Set<string>()
|
||||
async function traverse(job: ModuleJob) {
|
||||
if (ignored.has(job.url) || dependencies.has(job.url)) return
|
||||
if (job.url.startsWith('node:') || job.url.includes('/node_modules/')) return
|
||||
dependencies.add(job.url)
|
||||
const children = await job.linked
|
||||
await Promise.all(Array.prototype.map.call(children, traverse))
|
||||
}
|
||||
await traverse(job)
|
||||
return dependencies
|
||||
}
|
||||
|
||||
interface Reload {
|
||||
filename: string
|
||||
runtime?: Plugin.Runtime
|
||||
}
|
||||
|
||||
@Inject('loader')
|
||||
@Inject('timer')
|
||||
class Hmr extends Service {
|
||||
public baseDir: string
|
||||
|
||||
private internal: ModuleLoader
|
||||
private watcher!: FSWatcher
|
||||
|
||||
/**
|
||||
* Changes from externals will always trigger a full reload.
|
||||
* Externals are the dependency tree of the CLI worker entry point.
|
||||
*/
|
||||
private externals!: Set<string>
|
||||
|
||||
/**
|
||||
* Files that should be reloaded (accepted changes).
|
||||
* Includes all stashed files and their dependents.
|
||||
*/
|
||||
private accepted!: Set<string>
|
||||
|
||||
/**
|
||||
* Files that should NOT be reloaded.
|
||||
* Includes externals and files whose dependents are all declined.
|
||||
*/
|
||||
private declined!: Set<string>
|
||||
|
||||
/** Stashed file changes waiting to be processed */
|
||||
private stashed = new Set<string>()
|
||||
|
||||
constructor(ctx: Context, public config: Hmr.Config) {
|
||||
super(ctx, 'hmr')
|
||||
if (!this.ctx.loader.internal) {
|
||||
throw new Error('--expose-internals is required for HMR service')
|
||||
}
|
||||
this.internal = this.ctx.loader.internal
|
||||
this.baseDir = fileURLToPath(new URL(config.base || '.', ctx.baseUrl))
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a module specifier to a URL, compatible with Node 22-24.
|
||||
*/
|
||||
private async _resolve(specifier: string, parentURL: string, attrs: ImportAttributes): Promise<ResolveResult> {
|
||||
switch (this.internal.version) {
|
||||
case 'v1': return await this.internal.resolve(specifier, parentURL, attrs)
|
||||
case 'v2': return this.internal.resolveSync(parentURL, { specifier, attributes: attrs })
|
||||
}
|
||||
}
|
||||
|
||||
async* [Service.init]() {
|
||||
yield () => this.watcher?.close()
|
||||
|
||||
const { loader } = this.ctx
|
||||
const { root, ignored } = this.config
|
||||
if (!this.config.base) {
|
||||
this.ctx.logger.info('watching %o', root)
|
||||
} else {
|
||||
this.ctx.logger.info('watching %o in %s', root, this.baseDir)
|
||||
}
|
||||
|
||||
const match = picomatch(ignored)
|
||||
this.watcher = watch(root, {
|
||||
...this.config,
|
||||
cwd: this.baseDir,
|
||||
ignored: path => match(relative(this.baseDir, path)),
|
||||
})
|
||||
|
||||
// Collect externals: framework modules reachable from the main entry.
|
||||
// Changes to these files require a full process restart, not HMR.
|
||||
const mainUrl = pathToFileURL(resolve(process.argv[1])).href
|
||||
const mainJob = this.internal.loadCache.get(mainUrl)
|
||||
if (mainJob) {
|
||||
this.externals = await loadDependencies(mainJob)
|
||||
} else {
|
||||
this.externals = new Set()
|
||||
}
|
||||
|
||||
const partialReload = this.ctx.debounce(() => this.partialReload(), this.config.debounce)
|
||||
|
||||
this.watcher.on('change', async (path) => {
|
||||
this.ctx.logger.debug('change detected at %C', path)
|
||||
const filename = resolve(this.baseDir, path)
|
||||
const url = pathToFileURL(filename).href
|
||||
|
||||
// Full reload: the changed file is part of the framework
|
||||
if (this.externals.has(url)) return loader.exit()
|
||||
|
||||
// Partial reload: the file is in the ESM loadCache
|
||||
// In Node 24, both CJS and ESM modules imported via import() end up
|
||||
// in loadCache, so this check covers all module formats.
|
||||
if (loader.internal!.loadCache.has(url)) {
|
||||
this.stashed.add(url)
|
||||
return partialReload()
|
||||
}
|
||||
|
||||
// Config reload: the file is a loader config file (e.g. cordis.yml)
|
||||
for (const entry of this.ctx.loader.entries()) {
|
||||
const include = entry.subtree as Include | undefined
|
||||
if (include?.filename !== filename) continue
|
||||
await include.refresh()
|
||||
return
|
||||
}
|
||||
|
||||
this.ctx.emit('hmr/change', url)
|
||||
})
|
||||
}
|
||||
|
||||
// hide stack trace from HMR
|
||||
getOuterStack = (): string[] => [
|
||||
// ' at HMR.partialReload (<anonymous>)',
|
||||
]
|
||||
|
||||
async getLinked(url: string) {
|
||||
const job = this.internal.loadCache.get(url)
|
||||
if (!job) return []
|
||||
const linked = await job.linked
|
||||
return Array.prototype.map.call(linked, (job: ModuleJob) => job.url) as string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify changed files into accepted (should reload) and declined (should not).
|
||||
*
|
||||
* A file is accepted if it's directly changed (stashed) or if any of its
|
||||
* dependents are accepted. A file is declined if all its dependents are
|
||||
* declined or if it's an external.
|
||||
*/
|
||||
private async analyzeChanges() {
|
||||
const pending: string[] = []
|
||||
|
||||
this.accepted = new Set(this.stashed)
|
||||
this.declined = new Set(this.externals)
|
||||
|
||||
const isExcluded = (url: string) => url.startsWith('node:') || url.includes('/node_modules/')
|
||||
|
||||
await Promise.all([...this.stashed].map(async (url) => {
|
||||
const children = await this.getLinked(url)
|
||||
for (const child of children) {
|
||||
if (this.accepted.has(child) || this.declined.has(child) || isExcluded(child)) continue
|
||||
pending.push(child)
|
||||
}
|
||||
}))
|
||||
|
||||
while (pending.length) {
|
||||
let index = 0, hasUpdate = false
|
||||
while (index < pending.length) {
|
||||
const url = pending[index]
|
||||
const children = await this.getLinked(url)
|
||||
let isDeclined = true, isAccepted = false
|
||||
for (const child of children) {
|
||||
if (this.declined.has(child) || isExcluded(child)) continue
|
||||
if (this.accepted.has(child)) {
|
||||
isAccepted = true
|
||||
break
|
||||
} else {
|
||||
isDeclined = false
|
||||
if (!pending.includes(child)) {
|
||||
hasUpdate = true
|
||||
pending.push(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isAccepted || isDeclined) {
|
||||
hasUpdate = true
|
||||
pending.splice(index, 1)
|
||||
if (isAccepted) {
|
||||
this.accepted.add(url)
|
||||
} else {
|
||||
this.declined.add(url)
|
||||
}
|
||||
} else {
|
||||
index++
|
||||
}
|
||||
}
|
||||
if (!hasUpdate) break
|
||||
}
|
||||
|
||||
for (const url of pending) {
|
||||
this.declined.add(url)
|
||||
}
|
||||
}
|
||||
|
||||
private async partialReload() {
|
||||
await this.analyzeChanges()
|
||||
|
||||
const pending = new Map<ModuleJob, Plugin>()
|
||||
const reloads = new Map<Plugin, Reload>()
|
||||
|
||||
// Build a map of plugin names per config tree URL.
|
||||
// Plugin entry files are treated as atomic reload units.
|
||||
const nameMap: Dict<Set<string>> = Object.create(null)
|
||||
for (const entry of this.ctx.loader.entries()) {
|
||||
(nameMap[entry.parent.tree.ctx.baseUrl!] ??= new Set()).add(entry.options.name)
|
||||
}
|
||||
|
||||
// Resolve each plugin name to its file URL and check if it needs reload
|
||||
for (const baseUrl in nameMap) {
|
||||
for (const name of nameMap[baseUrl]) {
|
||||
try {
|
||||
const { url } = await this._resolve(name, baseUrl, {})
|
||||
if (this.declined.has(url)) continue
|
||||
const job = this.internal.loadCache.get(url)
|
||||
const plugin = this.ctx.loader.unwrapExports(job?.module?.getNamespace())
|
||||
if (!job || !plugin) continue
|
||||
pending.set(job, plugin)
|
||||
this.declined.add(url)
|
||||
} catch (err) {
|
||||
this.ctx.logger.warn(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check each pending plugin's dependency tree for accepted files
|
||||
for (const [job, plugin] of pending) {
|
||||
this.declined.delete(job.url)
|
||||
const dependencies = [...await loadDependencies(job, this.declined)]
|
||||
this.declined.add(job.url)
|
||||
|
||||
if (!dependencies.some(dep => this.accepted.has(dep))) continue
|
||||
dependencies.forEach(dep => this.accepted.add(dep))
|
||||
|
||||
reloads.set(plugin, {
|
||||
filename: job.url,
|
||||
runtime: this.ctx.registry.get(plugin),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear module caches for all accepted files before re-importing.
|
||||
*
|
||||
* We need to clear both:
|
||||
* 1. ESM loadCache — managed by Node's internal ModuleLoader
|
||||
* 2. CJS Module._cache — for CJS modules that were imported via import()
|
||||
*
|
||||
* In Node 24, CJS modules loaded via import() appear in both caches.
|
||||
* If we only clear loadCache, the CJS cache may serve stale modules.
|
||||
*
|
||||
* We use Map.prototype methods directly on loadCache because:
|
||||
* - In Node 22/23, loadCache is a plain Map<url, ModuleJob>
|
||||
* - In Node 24, loadCache is a LoadCache extends Map<url, { [type]: ModuleJob }>
|
||||
* where .delete() only sets the type slot to undefined (doesn't remove the entry)
|
||||
* Using Map.prototype.delete ensures complete removal in both versions.
|
||||
*/
|
||||
const esmBackup: Dict = Object.create(null)
|
||||
const cjsBackup: Dict = Object.create(null)
|
||||
const require = createRequire(import.meta.url)
|
||||
for (const filename of this.accepted) {
|
||||
// Backup and clear ESM loadCache
|
||||
const job = Map.prototype.get.call(this.internal.loadCache, filename)
|
||||
esmBackup[filename] = job
|
||||
Map.prototype.delete.call(this.internal.loadCache, filename)
|
||||
|
||||
// Backup and clear CJS Module._cache
|
||||
try {
|
||||
const filepath = fileURLToPath(filename)
|
||||
if (require.cache[filepath]) {
|
||||
cjsBackup[filepath] = require.cache[filepath]
|
||||
delete require.cache[filepath]
|
||||
}
|
||||
} catch {
|
||||
// filename might not be a file: URL (e.g. node: protocol), ignore
|
||||
}
|
||||
}
|
||||
|
||||
const rollback = () => {
|
||||
for (const filename in esmBackup) {
|
||||
Map.prototype.set.call(this.internal.loadCache, filename, esmBackup[filename])
|
||||
}
|
||||
for (const filepath in cjsBackup) {
|
||||
require.cache[filepath] = cjsBackup[filepath]
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to re-import all plugin entry files
|
||||
const attempts: Dict = {}
|
||||
try {
|
||||
for (const [, { filename }] of reloads) {
|
||||
attempts[filename] = this.ctx.loader.unwrapExports(await this.ctx.loader.import(filename, this.getOuterStack))
|
||||
}
|
||||
} catch (e) {
|
||||
handleError(this.ctx, e)
|
||||
return rollback()
|
||||
}
|
||||
|
||||
const reload = (plugin: any, runtime: Plugin.Runtime) => {
|
||||
if (!runtime) return
|
||||
for (const oldFiber of runtime.fibers) {
|
||||
const fiber = oldFiber.parent.registry.plugin(plugin, oldFiber.config, this.getOuterStack)
|
||||
fiber.entry = oldFiber.entry
|
||||
if (fiber.entry) fiber.entry.fiber = fiber
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
for (const [plugin, { filename, runtime }] of reloads) {
|
||||
if (!runtime) continue
|
||||
const path = relative(this.baseDir, fileURLToPath(filename))
|
||||
|
||||
try {
|
||||
this.ctx.registry.delete(plugin)
|
||||
} catch (err) {
|
||||
this.ctx.logger.warn('failed to dispose plugin at %C', path)
|
||||
this.ctx.logger.warn(err)
|
||||
}
|
||||
|
||||
try {
|
||||
reload(attempts[filename], runtime)
|
||||
this.ctx.logger.info('reload plugin at %C', path)
|
||||
} catch (err) {
|
||||
this.ctx.logger.warn('failed to reload plugin at %C', path)
|
||||
this.ctx.logger.warn(err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Rollback: restore caches and re-register old plugins
|
||||
rollback()
|
||||
for (const [plugin, { filename, runtime }] of reloads) {
|
||||
if (!runtime) continue
|
||||
try {
|
||||
this.ctx.registry.delete(attempts[filename])
|
||||
reload(plugin, runtime)
|
||||
} catch (err) {
|
||||
this.ctx.logger.warn(err)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
this.ctx.emit('hmr/reload', reloads)
|
||||
this.stashed = new Set()
|
||||
}
|
||||
}
|
||||
|
||||
namespace Hmr {
|
||||
export interface Config extends ChokidarOptions {
|
||||
base?: string
|
||||
root: string[]
|
||||
debounce: number
|
||||
ignored: string[]
|
||||
}
|
||||
|
||||
export const Config: z<Config> = z.object({
|
||||
base: z.string(),
|
||||
root: z.array(String).role('table').default(['.']),
|
||||
ignored: z.array(String).role('table').default([
|
||||
'**/node_modules',
|
||||
'**/.*',
|
||||
'cache',
|
||||
'data',
|
||||
]),
|
||||
debounce: z.natural().role('ms').default(100),
|
||||
})
|
||||
// [deepseek-harness] vendored modification: removed `.i18n({ 'en-US': enUS, 'zh-CN': zhCN })`
|
||||
// and the corresponding `./locales/*.yml` imports, to avoid a runtime YAML import hook
|
||||
// (@cordisjs/unyaml) that we don't vendor. See vendor/README.md.
|
||||
}
|
||||
|
||||
export default Hmr
|
||||
16
vendor/hmr/tsconfig.json
vendored
Normal file
16
vendor/hmr/tsconfig.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{ "path": "../cosmokit" },
|
||||
{ "path": "../cordis" },
|
||||
{ "path": "../loader" },
|
||||
{ "path": "../include" },
|
||||
{ "path": "../timer" },
|
||||
{ "path": "../schemastery" }
|
||||
]
|
||||
}
|
||||
21
vendor/include/LICENSE
vendored
Normal file
21
vendor/include/LICENSE
vendored
Normal 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.
|
||||
43
vendor/include/README.md
vendored
Normal file
43
vendor/include/README.md
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
# @cordisjs/plugin-include
|
||||
|
||||
File-backed loader tree for Cordis. The include plugin reads a YAML or JSON
|
||||
file, turns it into loader entries, and writes updates back when the file is
|
||||
writable.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { Context } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import Include from '@cordisjs/plugin-include'
|
||||
|
||||
const root = new Context()
|
||||
await root.plugin(Loader, { baseUrl: import.meta.url })
|
||||
await root.plugin(Include, {
|
||||
path: './cordis.yml',
|
||||
initial: [],
|
||||
enableLogs: true,
|
||||
})
|
||||
```
|
||||
|
||||
Example `cordis.yml`:
|
||||
|
||||
```yaml
|
||||
- id: timer
|
||||
name: '@cordisjs/plugin-timer'
|
||||
- id: app
|
||||
name: ./plugins/app
|
||||
config:
|
||||
message: hello
|
||||
```
|
||||
|
||||
## Config
|
||||
|
||||
| Field | Description |
|
||||
| --- | --- |
|
||||
| `path` | YAML or JSON file path resolved from `ctx.baseUrl`. |
|
||||
| `initial` | Entry list written when the file is missing. |
|
||||
| `patches` | Runtime patches applied after reading the file. |
|
||||
| `enableLogs` | Enables loader apply, reload, and unload logs. |
|
||||
|
||||
Patches can insert entries or override fields on entries with a matching `id`.
|
||||
31
vendor/include/package.json
vendored
Normal file
31
vendor/include/package.json
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@cordisjs/plugin-include",
|
||||
"description": "Include files in cordis configurations",
|
||||
"version": "1.0.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": {
|
||||
"@cordisjs/plugin-loader": "^1.0.0-rc.4",
|
||||
"cordis": "^4.0.0-rc.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"cosmokit": "^1.8.1",
|
||||
"js-yaml": "^4.1.0"
|
||||
}
|
||||
}
|
||||
229
vendor/include/src/index.ts
vendored
Normal file
229
vendor/include/src/index.ts
vendored
Normal file
@@ -0,0 +1,229 @@
|
||||
import { EntryOptions, EntryTree, isJsExpr } from '@cordisjs/plugin-loader'
|
||||
import { Context, Service } from 'cordis'
|
||||
import { extname } from 'node:path'
|
||||
import { access, constants, readFile, rename, writeFile } from 'node:fs/promises'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import * as yaml from 'js-yaml'
|
||||
|
||||
const JsExpr = new yaml.Type('tag:yaml.org,2002:js', {
|
||||
kind: 'scalar',
|
||||
resolve: (data) => typeof data === 'string',
|
||||
construct: (data) => ({ __jsExpr: data }),
|
||||
predicate: isJsExpr,
|
||||
represent: (data) => data['__jsExpr'],
|
||||
})
|
||||
|
||||
const schema = yaml.JSON_SCHEMA.extend(JsExpr)
|
||||
|
||||
const writable: Record<string, string> = {
|
||||
'.json': 'application/json',
|
||||
'.yaml': 'application/yaml',
|
||||
'.yml': 'application/yaml',
|
||||
}
|
||||
|
||||
const supported = new Set(Object.keys(writable))
|
||||
|
||||
/** Runtime patch applied to entries loaded from an included config file. */
|
||||
export interface PatchOptions {
|
||||
id?: string
|
||||
insert?: EntryOptions[]
|
||||
name?: string
|
||||
config?: any
|
||||
group?: boolean | null
|
||||
disabled?: boolean | null
|
||||
inject?: any
|
||||
intercept?: any
|
||||
isolate?: any
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/** Config namespace for the file-backed include loader. */
|
||||
export namespace Include {
|
||||
/** Config for a file-backed loader subtree. */
|
||||
export interface Config {
|
||||
/** YAML or JSON path resolved from `ctx.baseUrl`. */
|
||||
path: string
|
||||
/** Entry list written when the file does not already exist. */
|
||||
initial?: any[]
|
||||
/** Runtime patches applied after reading the file. */
|
||||
patches?: PatchOptions[]
|
||||
/** Enables loader apply/reload/unload logs for this subtree. */
|
||||
enableLogs?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
/** Loader entry tree backed by a YAML or JSON file. */
|
||||
export class Include extends EntryTree {
|
||||
static inject = ['loader']
|
||||
|
||||
public filename: string
|
||||
private type?: string
|
||||
private readonly: boolean
|
||||
private content?: string
|
||||
private data?: EntryOptions[]
|
||||
private writeTask?: NodeJS.Timeout
|
||||
|
||||
constructor(ctx: Context, public config: Include.Config) {
|
||||
super(ctx)
|
||||
this.enableLogs = config.enableLogs ?? ctx.fiber.entry?.parent.tree.enableLogs ?? false
|
||||
this.filename = fileURLToPath(new URL(this.config.path, this.ctx.baseUrl))
|
||||
const ext = extname(this.filename)
|
||||
if (!supported.has(ext)) {
|
||||
throw new Error(`extension "${ext}" not supported`)
|
||||
}
|
||||
this.type = writable[ext]
|
||||
this.readonly = !this.type
|
||||
this.ctx.baseUrl = new URL('.', pathToFileURL(this.filename)).href
|
||||
|
||||
ctx.on('internal/update', (config, _, next) => {
|
||||
if (config.path !== this.config.path) return next()
|
||||
this.root.update(this.data!)
|
||||
})
|
||||
}
|
||||
|
||||
private async checkAccess() {
|
||||
if (!this.type) return
|
||||
try {
|
||||
await access(this.filename, constants.W_OK)
|
||||
} catch {
|
||||
this.readonly = true
|
||||
}
|
||||
}
|
||||
|
||||
private async read(forced = false) {
|
||||
const content = await readFile(this.filename, 'utf8')
|
||||
if (!forced && this.content === content) return false
|
||||
this.content = content
|
||||
if (this.type === 'application/yaml') {
|
||||
this.data = yaml.load(this.content, { schema }) as any
|
||||
} else if (this.type === 'application/json') {
|
||||
this.data = JSON.parse(this.content) as any
|
||||
} else {
|
||||
const module = await import(/* @vite-ignore */ this.filename)
|
||||
this.data = module.default || module
|
||||
}
|
||||
await this.checkAccess()
|
||||
return true
|
||||
}
|
||||
|
||||
private applyPatches(data: EntryOptions[]): EntryOptions[] {
|
||||
const { patches } = this.config
|
||||
if (!patches?.length) return data
|
||||
|
||||
const entryMap = new Map<string, EntryOptions>()
|
||||
const buildMap = (entries: EntryOptions[]) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.id) entryMap.set(entry.id, entry)
|
||||
if (entry.group && Array.isArray(entry.config)) {
|
||||
buildMap(entry.config)
|
||||
}
|
||||
}
|
||||
}
|
||||
buildMap(data)
|
||||
|
||||
for (const patch of patches) {
|
||||
const { id, insert, name, ...overrides } = patch
|
||||
|
||||
if (insert) {
|
||||
if (id) {
|
||||
const target = entryMap.get(id)
|
||||
if (!target) {
|
||||
this.ctx.root.logger?.('loader').warn('patch insert: entry %C not found', id)
|
||||
continue
|
||||
}
|
||||
if (!target.group) {
|
||||
this.ctx.root.logger?.('loader').warn('patch insert: entry %C is not a group', id)
|
||||
continue
|
||||
}
|
||||
if (!Array.isArray(target.config)) target.config = []
|
||||
target.config.push(...insert)
|
||||
} else {
|
||||
data.push(...insert)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
this.ctx.root.logger?.('loader').warn('patch: id is required for non-insert patches')
|
||||
continue
|
||||
}
|
||||
|
||||
const target = entryMap.get(id)
|
||||
if (!target) {
|
||||
this.ctx.root.logger?.('loader').warn('patch: entry %C not found', id)
|
||||
continue
|
||||
}
|
||||
|
||||
if (name && name !== target.name) {
|
||||
this.ctx.root.logger?.('loader').warn(
|
||||
'patch: name mismatch for %C (expected %C, got %C), skipping',
|
||||
id, target.name, name,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(overrides)) {
|
||||
if (key === 'id') continue
|
||||
target[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async* [Service.init]() {
|
||||
try {
|
||||
await this.read()
|
||||
} catch {
|
||||
if (this.config.initial) {
|
||||
this.writeFile(this.config.initial as any)
|
||||
await this.read()
|
||||
} else {
|
||||
throw new Error(`config file not found: ${this.filename}`)
|
||||
}
|
||||
}
|
||||
|
||||
yield () => this.stop()
|
||||
const data = this.applyPatches([...this.data!])
|
||||
await this.root.update(data)
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.root.stop()
|
||||
}
|
||||
|
||||
/** Re-read the file and refresh child entries when content changed. */
|
||||
async refresh() {
|
||||
if (!await this.read()) return
|
||||
this.root.update(this.data!)
|
||||
}
|
||||
|
||||
private async _writeFile(config: EntryOptions[]) {
|
||||
if (this.readonly) {
|
||||
throw new Error(`cannot overwrite readonly config`)
|
||||
}
|
||||
if (this.type === 'application/yaml') {
|
||||
this.content = yaml.dump(config, { schema })
|
||||
} else if (this.type === 'application/json') {
|
||||
this.content = JSON.stringify(config, null, 2)
|
||||
}
|
||||
await writeFile(this.filename + '.tmp', this.content!)
|
||||
await rename(this.filename + '.tmp', this.filename)
|
||||
}
|
||||
|
||||
private writeFile(config: EntryOptions[]) {
|
||||
clearTimeout(this.writeTask)
|
||||
this.writeTask = setTimeout(() => {
|
||||
this.writeTask = undefined
|
||||
this._writeFile(config)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
/** Schedule a write of the current root entry data. */
|
||||
write() {
|
||||
this.context.emit('loader/config-update')
|
||||
return this.writeFile(this.root.data)
|
||||
}
|
||||
}
|
||||
|
||||
export default Include
|
||||
13
vendor/include/tsconfig.json
vendored
Normal file
13
vendor/include/tsconfig.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{ "path": "../cosmokit" },
|
||||
{ "path": "../cordis" },
|
||||
{ "path": "../loader" }
|
||||
]
|
||||
}
|
||||
21
vendor/loader/LICENSE
vendored
Normal file
21
vendor/loader/LICENSE
vendored
Normal 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
48
vendor/loader/README.md
vendored
Normal 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
29
vendor/loader/package.json
vendored
Normal 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
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' })
|
||||
}
|
||||
}
|
||||
}
|
||||
12
vendor/loader/tsconfig.json
vendored
Normal file
12
vendor/loader/tsconfig.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{ "path": "../cosmokit" },
|
||||
{ "path": "../cordis" }
|
||||
]
|
||||
}
|
||||
21
vendor/logger-console/LICENSE
vendored
Normal file
21
vendor/logger-console/LICENSE
vendored
Normal 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.
|
||||
35
vendor/logger-console/README.md
vendored
Normal file
35
vendor/logger-console/README.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# @cordisjs/plugin-logger-console
|
||||
|
||||
Console exporter for the built-in Cordis logger service.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { Context } from 'cordis'
|
||||
import ConsoleLogger from '@cordisjs/plugin-logger-console'
|
||||
|
||||
const root = new Context()
|
||||
await root.plugin(ConsoleLogger, {
|
||||
showDiff: true,
|
||||
levels: {
|
||||
default: 2,
|
||||
hmr: 3,
|
||||
},
|
||||
})
|
||||
|
||||
root.logger('app').info('started')
|
||||
```
|
||||
|
||||
## Config
|
||||
|
||||
| Field | Description |
|
||||
| --- | --- |
|
||||
| `colors` | Color support level, or `false` to disable colors. |
|
||||
| `maxLength` | Maximum rendered line length before truncation. |
|
||||
| `levels` | Per-logger minimum level map. |
|
||||
| `showDiff` | Show elapsed time since the previous message. |
|
||||
| `showTime` | Timestamp template. |
|
||||
| `label` | Label width, margin, and alignment options. |
|
||||
|
||||
The Node entry uses `node:util.inspect` for `%o` and `%O`; the browser entry
|
||||
passes log arguments through to `console`.
|
||||
32
vendor/logger-console/package.json
vendored
Normal file
32
vendor/logger-console/package.json
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@cordisjs/plugin-logger-console",
|
||||
"description": "Console logger exporter for cordis",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/shared.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./lib/shared.d.ts",
|
||||
"node": "./lib/index.js",
|
||||
"default": "./lib/browser.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",
|
||||
"schemastery": "^3.18.0",
|
||||
"supports-color": "^9.4.0"
|
||||
}
|
||||
}
|
||||
17
vendor/logger-console/src/browser.ts
vendored
Normal file
17
vendor/logger-console/src/browser.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Message } from 'cordis'
|
||||
import { ConsoleExporter as Base } from './shared.js'
|
||||
|
||||
/** Re-export shared console exporter config and base implementation. */
|
||||
export * from './shared.js'
|
||||
|
||||
/** Browser console exporter that dispatches to native console methods. */
|
||||
export class ConsoleExporter extends Base {
|
||||
export(message: Message) {
|
||||
const prefix = `[${message.type[0].toUpperCase()}] ${message.name}`
|
||||
const method = message.type === 'error' ? 'error' : message.type === 'warn' ? 'warn' : 'log'
|
||||
// eslint-disable-next-line no-console
|
||||
console[method](prefix, ...message.args)
|
||||
}
|
||||
}
|
||||
|
||||
export default ConsoleExporter
|
||||
28
vendor/logger-console/src/index.ts
vendored
Normal file
28
vendor/logger-console/src/index.ts
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Formatter } from 'cordis'
|
||||
import { inspect } from 'node:util'
|
||||
import supportsColor from 'supports-color'
|
||||
import { ConsoleExporter as Base } from './shared.js'
|
||||
|
||||
/** Re-export shared console exporter config and base implementation. */
|
||||
export * from './shared.js'
|
||||
|
||||
const inspectFormatter: Formatter = (value, target) => {
|
||||
return inspect(value, { colors: !!target.colors, depth: Infinity, compact: true, breakLength: Infinity })
|
||||
}
|
||||
|
||||
/** Node console exporter with `util.inspect` object formatting. */
|
||||
export class ConsoleExporter extends Base {
|
||||
formatters: Record<string, Formatter> = {
|
||||
o: inspectFormatter,
|
||||
O: inspectFormatter,
|
||||
}
|
||||
|
||||
getDefaults() {
|
||||
return {
|
||||
...super.getDefaults(),
|
||||
colors: (supportsColor.stdout ? supportsColor.stdout.level : 0) as false | 0 | 1 | 2 | 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ConsoleExporter
|
||||
100
vendor/logger-console/src/shared.ts
vendored
Normal file
100
vendor/logger-console/src/shared.ts
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
import { Context, Exporter, Formatter, Logger, Message } from 'cordis'
|
||||
import { Time } from 'cosmokit'
|
||||
import z from 'schemastery'
|
||||
|
||||
/** Terminal color support level compatible with supports-color. */
|
||||
export type ColorSupportLevel = 0 | 1 | 2 | 3
|
||||
|
||||
/** Formatting options for the logger name label. */
|
||||
export interface LabelStyle {
|
||||
width?: number
|
||||
margin?: number
|
||||
align?: 'left' | 'right'
|
||||
}
|
||||
|
||||
/** Config namespace for console logger exporters. */
|
||||
export namespace ConsoleExporter {
|
||||
export interface Config {
|
||||
colors?: false | ColorSupportLevel
|
||||
maxLength?: number
|
||||
levels?: Record<string, number>
|
||||
showDiff?: boolean
|
||||
showTime?: string
|
||||
label?: LabelStyle
|
||||
}
|
||||
}
|
||||
|
||||
/** Shared console log exporter implementation used by Node and browser builds. */
|
||||
export class ConsoleExporter implements Exporter {
|
||||
static readonly name = 'logger-console'
|
||||
|
||||
static readonly Config: z<ConsoleExporter.Config> = z.object({
|
||||
colors: z.union([z.const(false), z.number()]),
|
||||
maxLength: z.number(),
|
||||
levels: z.dict(z.number()),
|
||||
showDiff: z.boolean().default(false),
|
||||
showTime: z.string().default('yyyy-MM-dd hh:mm:ss '),
|
||||
label: z.object({
|
||||
width: z.number(),
|
||||
margin: z.number(),
|
||||
align: z.union(['left', 'right']),
|
||||
}),
|
||||
}) as z<ConsoleExporter.Config>
|
||||
|
||||
colors!: false | ColorSupportLevel
|
||||
maxLength?: number
|
||||
levels?: Record<string, number>
|
||||
showDiff!: boolean
|
||||
showTime!: string
|
||||
label?: LabelStyle
|
||||
timestamp: number
|
||||
|
||||
formatters: Record<string, Formatter> = {}
|
||||
|
||||
constructor(public ctx: Context, config: ConsoleExporter.Config = {}) {
|
||||
Object.assign(this, this.getDefaults(), config)
|
||||
this.timestamp = Date.now()
|
||||
ctx.logger.exporter(this)
|
||||
}
|
||||
|
||||
getDefaults() {
|
||||
return {
|
||||
colors: false as false | ColorSupportLevel,
|
||||
showTime: 'yyyy-MM-dd hh:mm:ss ',
|
||||
showDiff: false,
|
||||
}
|
||||
}
|
||||
|
||||
export(message: Message) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(this.render(message))
|
||||
}
|
||||
|
||||
render(message: Message) {
|
||||
const prefix = `[${message.type[0].toUpperCase()}]`
|
||||
const space = ' '.repeat(this.label?.margin ?? 1)
|
||||
let indent = 3 + space.length, output = ''
|
||||
if (this.showTime) {
|
||||
indent += this.showTime.length
|
||||
output += Logger.color(this, 8, Time.template(this.showTime))
|
||||
}
|
||||
const code = Logger.code(message.name, this.colors)
|
||||
const label = Logger.color(this, code, message.name, ';1')
|
||||
const padLength = (this.label?.width ?? 0) + label.length - message.name.length
|
||||
if (this.label?.align === 'right') {
|
||||
output += label.padStart(padLength) + space + prefix + space
|
||||
indent += (this.label.width ?? 0) + space.length
|
||||
} else {
|
||||
output += prefix + space + label.padEnd(padLength) + space
|
||||
}
|
||||
output += Logger.format(this, message).replace(/\n/g, '\n' + ' '.repeat(indent))
|
||||
if (this.showDiff && this.timestamp) {
|
||||
const diff = message.ts - this.timestamp
|
||||
output += Logger.color(this, code, ' +' + Time.format(diff))
|
||||
}
|
||||
this.timestamp = message.ts
|
||||
return output
|
||||
}
|
||||
}
|
||||
|
||||
export default ConsoleExporter
|
||||
13
vendor/logger-console/tsconfig.json
vendored
Normal file
13
vendor/logger-console/tsconfig.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{ "path": "../cosmokit" },
|
||||
{ "path": "../cordis" },
|
||||
{ "path": "../schemastery" }
|
||||
]
|
||||
}
|
||||
21
vendor/schemastery/LICENSE
vendored
Normal file
21
vendor/schemastery/LICENSE
vendored
Normal 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.
|
||||
389
vendor/schemastery/README.md
vendored
Normal file
389
vendor/schemastery/README.md
vendored
Normal file
@@ -0,0 +1,389 @@
|
||||
# Schemastery
|
||||
|
||||
[](https://codecov.io/gh/shigma/schemastery)
|
||||
[](https://www.npmjs.com/package/schemastery)
|
||||
[](https://www.npmjs.com/package/schemastery)
|
||||
[](https://github.com/shigma/schemastery/blob/master/LICENSE)
|
||||
|
||||
Type Driven Schema Validator.
|
||||
|
||||
## Features
|
||||
|
||||
- **Lightweight.** Much smaller than other validation libraries.
|
||||
- **Easy to use.** You can use any schema as a function or constructor directly.
|
||||
- **Powerful.** Schemastery supports some advanced types such as `union`, `intersect` and `transform`.
|
||||
- **Extensible.** You can create your own schema types via `Schema.extend()`.
|
||||
- **Serializable.** Schema objects can be serialized into JSON and then be hydrated in another environment.
|
||||
|
||||
## Basic Examples
|
||||
|
||||
### use as validator (JavaScript)
|
||||
|
||||
```js
|
||||
const Schema = require('schemastery')
|
||||
|
||||
const validate = Schema.number().default(10)
|
||||
|
||||
validate(0) // 0
|
||||
validate(null) // 10
|
||||
validate('') // TypeError
|
||||
```
|
||||
|
||||
### use as constructor (TypeScript)
|
||||
|
||||
```ts
|
||||
import Schema from 'schemastery'
|
||||
|
||||
interface Config {
|
||||
foo: Record<string, string>
|
||||
bar: string[]
|
||||
}
|
||||
|
||||
const Config = Schema.object({
|
||||
foo: Schema.dict(Schema.string()).default({}),
|
||||
bar: Schema.array(Schema.string()).default([]),
|
||||
})
|
||||
|
||||
// config is an instance of Config
|
||||
// in this case, that is { foo: {}, bar: [] }
|
||||
const config = new Config()
|
||||
```
|
||||
|
||||
## General Types
|
||||
|
||||
### Schema.any()
|
||||
|
||||
Assert that the value is of any type.
|
||||
|
||||
```js
|
||||
const validate = Schema.any()
|
||||
|
||||
validate() // undefined
|
||||
validate(0) // 0
|
||||
validate({}) // {}
|
||||
```
|
||||
|
||||
### Schema.never()
|
||||
|
||||
Assert that the value is nullable.
|
||||
|
||||
```js
|
||||
const validate = Schema.never()
|
||||
|
||||
validate() // undefined
|
||||
validate(0) // TypeError
|
||||
validate({}) // TypeError
|
||||
```
|
||||
|
||||
### Schema.const(value)
|
||||
|
||||
Assert that the value is equal to the given constant.
|
||||
|
||||
```js
|
||||
const validate = Schema.const(10)
|
||||
|
||||
validate(10) // 10
|
||||
validate(0) // TypeError
|
||||
```
|
||||
|
||||
### Schema.number()
|
||||
|
||||
Assert that the value is a number.
|
||||
|
||||
```js
|
||||
const validate = Schema.number()
|
||||
|
||||
validate() // undefined
|
||||
validate(1) // 1
|
||||
validate('') // TypeError
|
||||
```
|
||||
|
||||
### Schema.string()
|
||||
|
||||
Assert that the value is a string.
|
||||
|
||||
```js
|
||||
const validate = Schema.string()
|
||||
|
||||
validate() // undefined
|
||||
validate(0) // TypeError
|
||||
validate('foo') // 'foo'
|
||||
```
|
||||
|
||||
### Schema.boolean()
|
||||
|
||||
Assert that the value is a boolean.
|
||||
|
||||
```js
|
||||
const validate = Schema.boolean()
|
||||
|
||||
validate() // undefined
|
||||
validate(0) // TypeError
|
||||
validate(true) // true
|
||||
```
|
||||
|
||||
### Schema.is(constructor)
|
||||
|
||||
Assert that the value is an instance of the given constructor.
|
||||
|
||||
```js
|
||||
const validate = Schema.is(RegExp)
|
||||
|
||||
validate() // undefined
|
||||
validate(/foo/) // /foo/
|
||||
validate('foo') // TypeError
|
||||
```
|
||||
|
||||
### Schema.array(inner)
|
||||
|
||||
Assert that the value is an array of `inner`. The default value will be `[]` if not specified.
|
||||
|
||||
```js
|
||||
const validate = Schema.array(Schema.number())
|
||||
|
||||
validate() // []
|
||||
validate(0) // TypeError
|
||||
validate([0, 1]) // [0, 1]
|
||||
validate([0, '1']) // TypeError
|
||||
```
|
||||
|
||||
### Schema.dict(inner)
|
||||
|
||||
Assert that the value is a dictionary of `inner`. The default value will be `{}` if not specified.
|
||||
|
||||
```js
|
||||
const validate = Schema.dict(Schema.number())
|
||||
|
||||
validate() // {}
|
||||
validate(0) // TypeError
|
||||
validate({ a: 0, b: 1 }) // { a: 0, b: 1 }
|
||||
validate({ a: 0, b: '1' }) // TypeError
|
||||
```
|
||||
|
||||
### Schema.tuple(list)
|
||||
|
||||
Assert that the value is a tuple whose each element is of corresponding subtype. The default value will be `[]` if not specified.
|
||||
|
||||
```js
|
||||
const validate = Schema.tuple([
|
||||
Schema.number(),
|
||||
Schema.string(),
|
||||
])
|
||||
|
||||
validate() // []
|
||||
validate([0]) // { a: 0 }
|
||||
validate([0, 1]) // TypeError
|
||||
validate([0, '1']) // [0, '1']
|
||||
```
|
||||
|
||||
### Schema.object(dict)
|
||||
|
||||
Assert that the value is an object whose each property is of corresponding subtype. The default value will be `{}` if not specified.
|
||||
|
||||
```js
|
||||
const validate = Schema.object({
|
||||
a: Schema.number(),
|
||||
b: Schema.string(),
|
||||
})
|
||||
|
||||
validate() // {}
|
||||
validate({ a: 0 }) // { a: 0 }
|
||||
validate({ a: 0, b: 1 }) // TypeError
|
||||
validate({ a: 0, b: '1' }) // { a: 0, b: '1' }
|
||||
```
|
||||
|
||||
### Schema.union(list)
|
||||
|
||||
Assert that the value is one of the specified types.
|
||||
|
||||
```js
|
||||
const validate = Schema.union([
|
||||
Schema.number(),
|
||||
Schema.string(),
|
||||
])
|
||||
|
||||
validate() // undefined
|
||||
validate(0) // 0
|
||||
validate('1') // '1'
|
||||
validate(true) // TypeError
|
||||
```
|
||||
|
||||
### Schema.intersect(list)
|
||||
|
||||
Assert that the value should match each specified type.
|
||||
|
||||
```js
|
||||
const validate = Schema.intersect([
|
||||
Schema.object({ a: Schema.string().required() }),
|
||||
Schema.object({ b: Schema.number().default(0) }),
|
||||
])
|
||||
|
||||
validate() // TypeError
|
||||
validate({ a: '' }) // { a: '', b: 0 }
|
||||
validate({ a: '', b: 1 }) // { a: '', b: 1 }
|
||||
validate({ a: '', b: '2' }) // TypeError
|
||||
```
|
||||
|
||||
### Schema.transform(inner, callback)
|
||||
|
||||
Assert that the value is of the specified subtype and then transformed by `callback`.
|
||||
|
||||
```js
|
||||
const validate = Schema.transform(Schema.number().default(0), n => n + 1)
|
||||
|
||||
validate() // 1
|
||||
validate('0') // TypeError
|
||||
validate(10) // 11
|
||||
```
|
||||
|
||||
## Instance Methods
|
||||
|
||||
Note: `default` and `required` are mutually exclusive.
|
||||
|
||||
### schema.required()
|
||||
|
||||
Assert that the value is not nullable.
|
||||
|
||||
### schema.default(value)
|
||||
|
||||
Set the fallback value when nullable.
|
||||
|
||||
### schema.description(text)
|
||||
|
||||
Set the description of the schema.
|
||||
|
||||
### schema.simplify(value)
|
||||
|
||||
Normalize a value by removing parts that are equal to schema defaults. This is
|
||||
useful when storing user configuration and keeping persisted files compact.
|
||||
|
||||
```js
|
||||
const Config = Schema.object({
|
||||
foo: Schema.string().default(''),
|
||||
bar: Schema.number().default(0),
|
||||
})
|
||||
|
||||
Config.simplify({ foo: '', bar: 1 }) // { bar: 1 }
|
||||
```
|
||||
|
||||
## Validation Options
|
||||
|
||||
All schemas are callable. The second argument accepts validation options:
|
||||
|
||||
```js
|
||||
const Config = Schema.object({
|
||||
foo: Schema.number(),
|
||||
})
|
||||
|
||||
Config({ foo: '1' }, { autofix: true }) // {}
|
||||
```
|
||||
|
||||
- `autofix`: remove invalid object properties where possible.
|
||||
- `ignore`: skip validation for selected values and schema nodes.
|
||||
- `path`: provide an initial path for nested validation errors.
|
||||
|
||||
## Shorthand Syntax
|
||||
|
||||
Some shorthand syntax is available for inner types.
|
||||
|
||||
- `undefined` -> `Schema.any()`
|
||||
- `String` -> `Schema.string()`
|
||||
- `Number` -> `Schema.number()`
|
||||
- `Boolean` -> `Schema.boolean()`
|
||||
- `1` -> `Schema.const(1)` (only for primitive types)
|
||||
- `Date` -> `Schema.is(Date)`
|
||||
|
||||
```js
|
||||
Schema.array(String) // Schema.array(Schema.string())
|
||||
Schema.dict(RegExp) // Schema.dict(Schema.is(RegExp))
|
||||
Schema.union([1, 2]) // Schema.union([Schema.const(1), Schema.const(2)])
|
||||
```
|
||||
|
||||
You can also use `Schema.from()` to get the inferred schema from a shorthand value.
|
||||
|
||||
```js
|
||||
Schema.from() // Schema.any()
|
||||
Schema.from(Date) // Schema.is(Date)
|
||||
Schema.from('foo') // Schema.const('foo')
|
||||
```
|
||||
|
||||
## Advanced Examples
|
||||
|
||||
Here are some examples which demonstrate how to define advanced types.
|
||||
|
||||
### Enumeration
|
||||
|
||||
```js
|
||||
const Enum = Schema.union(['red', 'blue'])
|
||||
|
||||
Enum('red') // 'red'
|
||||
Enum('blue') // 'blue'
|
||||
Enum('green') // TypeError
|
||||
```
|
||||
|
||||
### ToString
|
||||
|
||||
```js
|
||||
const ToString = Schema.transform(Schema.any(), v => String(v))
|
||||
|
||||
ToString('') // ''
|
||||
ToString(0) // '0'
|
||||
ToString({}) // '{}'
|
||||
```
|
||||
|
||||
### Listable
|
||||
|
||||
```js
|
||||
const Listable = Schema.union([
|
||||
Schema.array(Number),
|
||||
Schema.transform(Number, n => [n]),
|
||||
]).default([])
|
||||
|
||||
Listable() // []
|
||||
Listable(0) // [0]
|
||||
Listable([1, 2]) // [1, 2]
|
||||
```
|
||||
|
||||
### Alias
|
||||
|
||||
```js
|
||||
const Config = Schema.dict(Number, Schema.union([
|
||||
'foo',
|
||||
Schema.transform('bar', () => 'foo'),
|
||||
]))
|
||||
|
||||
Config({ foo: 1 }) // { foo: 1 }
|
||||
Config({ bar: 2 }) // { foo: 2 }
|
||||
Config({ bar: '3' }) // TypeError
|
||||
```
|
||||
|
||||
## Extensibility
|
||||
|
||||
Custom schema types are registered with `Schema.extend(type, resolve)`. A
|
||||
resolver receives the input value, schema node, validation options, and a strict
|
||||
flag. Return `[value]` for accepted input, or `[value, adapted]` when the caller
|
||||
should write an adapted value back to the source object.
|
||||
|
||||
```js
|
||||
Schema.extend('trimmed', (data, schema, options) => {
|
||||
if (typeof data !== 'string') {
|
||||
throw new Schema.ValidationError(`expected string but got ${data}`, options)
|
||||
}
|
||||
return [data.trim()]
|
||||
})
|
||||
```
|
||||
|
||||
## Serializability
|
||||
|
||||
```js
|
||||
const schema1 = Schema.object({
|
||||
foo: Schema.string(),
|
||||
bar: Schema.number(),
|
||||
})
|
||||
|
||||
// should have the same effect as schema1
|
||||
const schema2 = new Schema(JSON.parse(JSON.stringify(schema1)))
|
||||
```
|
||||
|
||||
Schemastery also exposes the Standard Schema `~standard` property, so compatible
|
||||
tools can validate values without depending on Schemastery-specific APIs.
|
||||
19
vendor/schemastery/package.json
vendored
Normal file
19
vendor/schemastery/package.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "schemastery",
|
||||
"description": "Type driven schema validator",
|
||||
"version": "3.18.0",
|
||||
"private": true,
|
||||
"main": "lib/index.cjs",
|
||||
"module": "lib/index.mjs",
|
||||
"types": "lib/index.d.ts",
|
||||
"files": [
|
||||
"lib",
|
||||
"src"
|
||||
],
|
||||
"author": "Shigma <shigma10826@gmail.com>",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.1.0",
|
||||
"cosmokit": "^1.8.1"
|
||||
}
|
||||
}
|
||||
902
vendor/schemastery/src/index.ts
vendored
Normal file
902
vendor/schemastery/src/index.ts
vendored
Normal file
@@ -0,0 +1,902 @@
|
||||
import { Binary, clone, deepEqual, Dict, filterKeys, isNullable, isPlainObject, pick, valueMap } from 'cosmokit'
|
||||
import type { StandardSchemaV1 } from '@standard-schema/spec'
|
||||
|
||||
const kSchema = Symbol.for('schemastery')
|
||||
const kValidationError = Symbol.for('ValidationError')
|
||||
|
||||
declare global {
|
||||
namespace Schemastery {
|
||||
/** Convert primitive constructors, constants, and existing schemas into a schema type. */
|
||||
export type From<X> =
|
||||
| X extends string | number | boolean ? Schema<X>
|
||||
: X extends Schema ? X
|
||||
: X extends typeof String ? Schema<string>
|
||||
: X extends typeof Number ? Schema<number>
|
||||
: X extends typeof Boolean ? Schema<boolean>
|
||||
: X extends typeof Function ? Schema<Function, (...args: any[]) => any>
|
||||
: X extends Constructor<infer S> ? Schema<S>
|
||||
: never
|
||||
|
||||
type TypeS1<X> = X extends Schema<infer S, unknown> ? S : never
|
||||
type Inverse<X> = X extends Schema<any, infer Y> ? (arg: Y) => void : never
|
||||
|
||||
/** Input type accepted by a schema-like value. */
|
||||
export type TypeS<X> = TypeS1<From<X>>
|
||||
/** Output type returned by a schema-like value after validation. */
|
||||
export type TypeT<X> = ReturnType<From<X>>
|
||||
/** Resolver callback used by custom schema types registered with `Schema.extend()`. */
|
||||
export type Resolve = (data: any, schema: Schema, options: Options, strict?: boolean) => [any, any?]
|
||||
|
||||
/** Input type accepted by one schema in an intersection. */
|
||||
export type IntersectS<X> = From<X> extends Schema<infer S, unknown> ? S : never
|
||||
/** Output type returned by one schema in an intersection. */
|
||||
export type IntersectT<X> = Inverse<From<X>> extends ((arg: infer T) => void) ? T : never
|
||||
|
||||
type TupleS<X extends readonly any[]> = X extends readonly [infer L, ...infer R] ? [TypeS<L>?, ...TupleS<R>] : any[]
|
||||
type TupleT<X extends readonly any[]> = X extends readonly [infer L, ...infer R] ? [TypeT<L>?, ...TupleT<R>] : any[]
|
||||
type ObjectS<X extends Dict> = { [K in keyof X]?: TypeS<X[K]> | null } & Dict
|
||||
type ObjectT<X extends Dict> = { [K in keyof X]: TypeT<X[K]> } & Dict
|
||||
type Constructor<T = any> = new (...args: any[]) => T
|
||||
|
||||
/** Static constructor and factory methods exposed by the default `Schema` export. */
|
||||
export interface Static {
|
||||
<T = any>(options: Partial<Schema<T>>): Schema<T>
|
||||
new <T = any>(options: Partial<Schema<T>>): Schema<T>
|
||||
prototype: Schema
|
||||
/** Validate a value against a schema node and return `[output, adaptedInput?]`. */
|
||||
resolve: Resolve
|
||||
/** Infer a schema from a primitive value, constructor, or existing schema. */
|
||||
from<X = any>(source?: X): From<X>
|
||||
/** Register a resolver for a custom schema `type`. */
|
||||
extend(type: string, resolve: Resolve): void
|
||||
/** Accept any value without validation. */
|
||||
any<T = any>(): Schema<T>
|
||||
/** Accept only nullable input. */
|
||||
never(): Schema<never>
|
||||
/** Accept exactly one constant value. */
|
||||
const<const T>(value: T): Schema<T>
|
||||
/** Accept strings, with optional metadata constraints added by instance methods. */
|
||||
string(): Schema<string>
|
||||
/** Accept numbers, with optional range and step constraints. */
|
||||
number(): Schema<number>
|
||||
/** Accept non-negative integer numbers. */
|
||||
natural(): Schema<number>
|
||||
/** Accept a number between 0 and 1 and mark it as a slider. */
|
||||
percent(): Schema<number>
|
||||
/** Accept booleans. */
|
||||
boolean(): Schema<boolean>
|
||||
/** Accept `Date` instances or parse datetime strings into `Date` objects. */
|
||||
date(): Schema<string | Date, Date>
|
||||
/** Accept `RegExp` instances or parse strings into regular expressions. */
|
||||
regExp(flag?: string): Schema<string | RegExp, RegExp>
|
||||
/** Accept binary sources and normalize them to `ArrayBufferLike`. */
|
||||
arrayBuffer(): Schema<Binary.Source, ArrayBufferLike>
|
||||
arrayBuffer(encoding: 'hex' | 'base64'): Schema<Binary.Source | string, ArrayBufferLike>
|
||||
/** Accept a numeric bitset or string keys and normalize to a number. */
|
||||
bitset<K extends string>(bits: Partial<Record<K, number>>): Schema<number | readonly K[], number>
|
||||
/** Accept functions. */
|
||||
function(): Schema<Function, (...args: any[]) => any>
|
||||
/** Accept instances of a constructor or objects whose constructor name matches. */
|
||||
is(constructor: string): Schema
|
||||
is<T>(constructor: Constructor<T>): Schema<T>
|
||||
/** Accept arrays whose elements match `inner`. */
|
||||
array<X>(inner: X): Schema<TypeS<X>[], TypeT<X>[]>
|
||||
/** Accept plain objects with values matching `inner` and optional key schema. */
|
||||
dict<X, Y extends Schema<any, string> = Schema<string>>(inner: X, sKey?: Y): Schema<Dict<TypeS<X>, TypeS<Y>>, Dict<TypeT<X>, TypeT<Y>>>
|
||||
/** Accept tuple arrays where each index matches the corresponding schema. */
|
||||
tuple<const X extends readonly any[]>(list: X): Schema<TupleS<X>, TupleT<X>>
|
||||
/** Accept plain objects whose declared properties match the schema dictionary. */
|
||||
object<X extends Dict>(dict: X): Schema<ObjectS<X>, ObjectT<X>>
|
||||
/** Accept values matching at least one schema in `list`. */
|
||||
union<const X>(list: readonly X[]): Schema<TypeS<X>, TypeT<X>>
|
||||
/** Accept values matching every schema in `list`, merging object outputs. */
|
||||
intersect<const X>(list: readonly X[]): Schema<IntersectS<X>, IntersectT<X>>
|
||||
/** Validate with `inner`, then convert the result with `callback`. */
|
||||
transform<X, T>(inner: X, callback: (value: TypeS<X>, options: Schemastery.Options) => T, preserve?: boolean): Schema<TypeS<X>, T>
|
||||
/** Defer construction of a recursive schema until validation or serialization. */
|
||||
lazy<X extends Schema>(callback: () => X): X
|
||||
ValidationError: typeof ValidationError
|
||||
}
|
||||
|
||||
/** Runtime validation options shared by all schema calls. */
|
||||
interface Options {
|
||||
/** Remove invalid object properties instead of throwing when possible. */
|
||||
autofix?: boolean
|
||||
/** Skip validation for selected values and schema nodes. */
|
||||
ignore?(data: any, schema: Schema): boolean
|
||||
/** Path used to format nested validation errors. */
|
||||
path?: (keyof any)[]
|
||||
}
|
||||
|
||||
/** UI and validation metadata attached by schema builder methods. */
|
||||
export interface Meta<T = any> {
|
||||
default?: T extends {} ? Partial<T> : T
|
||||
required?: boolean
|
||||
disabled?: boolean
|
||||
collapse?: boolean
|
||||
badges?: { text: string; type: string }[]
|
||||
hidden?: boolean
|
||||
loose?: boolean
|
||||
role?: string
|
||||
extra?: any
|
||||
link?: string
|
||||
description?: string | Dict<string>
|
||||
comment?: string
|
||||
pattern?: { source: string; flags?: string }
|
||||
max?: number
|
||||
min?: number
|
||||
step?: number
|
||||
}
|
||||
}
|
||||
|
||||
/** Callable schema instance that validates input and returns normalized output. */
|
||||
interface Schemastery<S = any, T = S> {
|
||||
(data?: S | null, options?: Schemastery.Options): T
|
||||
new (data?: S | null, options?: Schemastery.Options): T
|
||||
[kSchema]: true
|
||||
uid: number
|
||||
meta: Schemastery.Meta<T>
|
||||
type: string
|
||||
sKey?: Schema
|
||||
inner?: Schema
|
||||
list?: Schema[]
|
||||
dict?: Dict<Schema>
|
||||
bits?: Dict<number>
|
||||
callback?: Function
|
||||
constructor?: string | Function
|
||||
builder?: Function
|
||||
value?: T
|
||||
refs?: Dict<Schema>
|
||||
preserve?: boolean
|
||||
'~standard': StandardSchemaV1.Props // <S, T>
|
||||
/** Format this schema as a compact TypeScript-like type string. */
|
||||
toString(inline?: boolean): string
|
||||
/** Serialize this schema, preserving shared and recursive references. */
|
||||
toJSON(): Schema<S, T>
|
||||
/** Mark nullable input as invalid unless a default supplies a fallback. */
|
||||
required(value?: boolean): Schema<S, T>
|
||||
/** Hide this schema node from UI renderers. */
|
||||
hidden(value?: boolean): Schema<S, T>
|
||||
/** Return the default value instead of throwing when validation fails. */
|
||||
loose(value?: boolean): Schema<S, T>
|
||||
/** Attach a renderer role and optional role-specific metadata. */
|
||||
role(text: string, extra?: any): Schema<S, T>
|
||||
/** Attach an external documentation link. */
|
||||
link(link: string): Schema<S, T>
|
||||
/** Set the fallback value used for nullable input. */
|
||||
default(value: T): Schema<S, T>
|
||||
/** Attach an auxiliary comment for documentation or form UIs. */
|
||||
comment(text: string): Schema<S, T>
|
||||
/** Attach a localized or plain description for documentation or form UIs. */
|
||||
description(text: string): Schema<S, T>
|
||||
/** Mark this schema node as disabled for form UIs. */
|
||||
disabled(value?: boolean): Schema<S, T>
|
||||
/** Request collapsed rendering for nested form UIs. */
|
||||
collapse(value?: boolean): Schema<S, T>
|
||||
/** Add a deprecated badge to this schema node. */
|
||||
deprecated(): Schema<S, T>
|
||||
/** Add an experimental badge to this schema node. */
|
||||
experimental(): Schema<S, T>
|
||||
/** Require strings to match a regular expression. */
|
||||
pattern(regexp: RegExp): Schema<S, T>
|
||||
/** Set an inclusive maximum for numbers or collection lengths. */
|
||||
max(value: number): Schema<S, T>
|
||||
/** Set an inclusive minimum for numbers or collection lengths. */
|
||||
min(value: number): Schema<S, T>
|
||||
/** Set the numeric increment constraint. */
|
||||
step(value: number): Schema<S, T>
|
||||
/** Add or replace an object property schema. */
|
||||
set(key: string, value: Schema): Schema<S, T>
|
||||
/** Append a tuple, union, or intersection member schema. */
|
||||
push(value: Schema): Schema<S, T>
|
||||
/** Remove values equal to schema defaults from normalized output. */
|
||||
simplify(value?: any): any
|
||||
/** Return a schema clone with descriptions merged from locale messages. */
|
||||
i18n(messages: Dict): Schema<S, T>
|
||||
/** Attach arbitrary metadata consumed by form renderers and downstream tools. */
|
||||
extra<K extends keyof Schemastery.Meta>(key: K, value: Schemastery.Meta[K]): Schema<S, T>
|
||||
}
|
||||
}
|
||||
|
||||
declare namespace globalThis {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export let __schemastery_index__: number
|
||||
export let __schemastery_refs__: Record<number, Schema> | undefined
|
||||
}
|
||||
|
||||
globalThis.__schemastery_index__ ??= 0
|
||||
globalThis.__schemastery_refs__ = undefined
|
||||
|
||||
class ValidationError extends TypeError {
|
||||
name = 'ValidationError'
|
||||
|
||||
constructor(message: string, public options: Schemastery.Options) {
|
||||
let prefix = '$'
|
||||
for (const segment of options.path || []) {
|
||||
if (typeof segment === 'string') {
|
||||
prefix += '.' + segment
|
||||
} else if (typeof segment === 'number') {
|
||||
prefix += '[' + segment + ']'
|
||||
} else if (typeof segment === 'symbol') {
|
||||
prefix += `[Symbol(${segment.toString()})]`
|
||||
}
|
||||
}
|
||||
if (prefix.startsWith('.')) prefix = prefix.slice(1)
|
||||
super((prefix === '$' ? '' : `${prefix} `) + message)
|
||||
}
|
||||
|
||||
static is(error: any): error is ValidationError {
|
||||
return !!error?.[kValidationError]
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(ValidationError.prototype, kValidationError, {
|
||||
value: true,
|
||||
})
|
||||
|
||||
type Schema<S = any, T = S> = Schemastery<S, T>
|
||||
|
||||
const Schema = function (options: Schema) {
|
||||
const schema = function (data: any, options: Schemastery.Options = {}) {
|
||||
return Schema.resolve(data, schema, options)[0]
|
||||
} as Schema
|
||||
|
||||
if (options.refs) {
|
||||
const refs = valueMap(options.refs, options => new Schema(options))
|
||||
const getRef = (uid: any) => refs[uid]!
|
||||
for (const key in refs) {
|
||||
const options = refs[key]!
|
||||
options.sKey = getRef(options.sKey)
|
||||
options.inner = getRef(options.inner)
|
||||
options.list = options.list && options.list.map(getRef)
|
||||
options.dict = options.dict && valueMap(options.dict, getRef)
|
||||
}
|
||||
return refs[options.uid!]
|
||||
}
|
||||
|
||||
Object.assign(schema, options)
|
||||
if (typeof schema.callback === 'string') {
|
||||
try {
|
||||
// eslint-disable-next-line no-new-func
|
||||
schema.callback = new Function('return ' + schema.callback)()
|
||||
} catch {}
|
||||
}
|
||||
Object.defineProperty(schema, 'uid', { value: globalThis.__schemastery_index__++ })
|
||||
Object.setPrototypeOf(schema, Schema.prototype)
|
||||
schema.meta ||= {}
|
||||
schema.toString = schema.toString.bind(schema)
|
||||
return schema
|
||||
} as Schemastery.Static
|
||||
|
||||
Schema.prototype = Object.create(Function.prototype)
|
||||
|
||||
Schema.prototype[kSchema] = true
|
||||
|
||||
Object.defineProperty(Schema.prototype, '~standard', {
|
||||
get(this: Schema) {
|
||||
return {
|
||||
version: 1,
|
||||
vendor: 'schemastery',
|
||||
validate: (value: unknown) => {
|
||||
try {
|
||||
return { value: Schema.resolve(value, this, {})[0] }
|
||||
} catch (error) {
|
||||
if (ValidationError.is(error)) {
|
||||
return { issues: [{ message: error.message, path: error.options.path }] }
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
Schema.ValidationError = ValidationError
|
||||
|
||||
Schema.prototype.toJSON = function toJSON() {
|
||||
if (globalThis.__schemastery_refs__) {
|
||||
globalThis.__schemastery_refs__[this.uid] ??= JSON.parse(JSON.stringify({ ...this }))
|
||||
return this.uid as any
|
||||
}
|
||||
|
||||
globalThis.__schemastery_refs__ = { [this.uid]: { ...this } as Schema }
|
||||
globalThis.__schemastery_refs__[this.uid] = JSON.parse(JSON.stringify({ ...this }))
|
||||
const result = { uid: this.uid, refs: globalThis.__schemastery_refs__ }
|
||||
globalThis.__schemastery_refs__ = undefined
|
||||
return result
|
||||
}
|
||||
|
||||
Schema.prototype.set = function set(key, value) {
|
||||
this.dict![key] = value
|
||||
return this
|
||||
}
|
||||
|
||||
Schema.prototype.push = function push(value) {
|
||||
this.list!.push(value)
|
||||
return this
|
||||
}
|
||||
|
||||
function mergeDesc(original: undefined | string | Dict<string>, messages: Dict) {
|
||||
const result: Dict<string> = typeof original === 'string' ? { '': original } : { ...original }
|
||||
for (const locale in messages) {
|
||||
const value = messages[locale]
|
||||
if (value?.$description || value?.$desc) {
|
||||
result[locale] = value.$description || value.$desc
|
||||
} else if (typeof value === 'string') {
|
||||
result[locale] = value
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function getInner(value: any) {
|
||||
return value?.$value ?? value?.$inner
|
||||
}
|
||||
|
||||
function extractKeys(data: any) {
|
||||
return filterKeys(data ?? {}, key => !key.startsWith('$'))
|
||||
}
|
||||
|
||||
Schema.prototype.i18n = function i18n(messages) {
|
||||
const schema = Schema(this)
|
||||
const desc = mergeDesc(schema.meta.description, messages)
|
||||
if (Object.keys(desc).length) schema.meta.description = desc
|
||||
if (schema.dict) {
|
||||
schema.dict = valueMap(schema.dict, (inner, key) => {
|
||||
return inner.i18n(valueMap(messages, (data) => getInner(data)?.[key] ?? data?.[key]))
|
||||
})
|
||||
}
|
||||
if (schema.list) {
|
||||
schema.list = schema.list!.map((inner, index) => {
|
||||
return inner.i18n(valueMap(messages, (data = {}) => {
|
||||
if (Array.isArray(getInner(data))) return getInner(data)[index]
|
||||
if (Array.isArray(data)) return data[index]
|
||||
return extractKeys(data)
|
||||
}))
|
||||
})
|
||||
}
|
||||
if (schema.inner) {
|
||||
schema.inner = schema.inner.i18n(valueMap(messages, (data) => {
|
||||
if (getInner(data)) return getInner(data)
|
||||
return extractKeys(data)
|
||||
}))
|
||||
}
|
||||
if (schema.sKey) {
|
||||
schema.sKey = schema.sKey.i18n(valueMap(messages, (data) => data?.$key))
|
||||
}
|
||||
return schema
|
||||
}
|
||||
|
||||
Schema.prototype.extra = function extra(key, value) {
|
||||
const schema = Schema(this)
|
||||
schema.meta = { ...schema.meta, [key]: value }
|
||||
return schema
|
||||
}
|
||||
|
||||
for (const key of ['required', 'disabled', 'collapse', 'hidden', 'loose']) {
|
||||
Object.assign(Schema.prototype, {
|
||||
[key](this: Schema, value = true) {
|
||||
const schema = Schema(this)
|
||||
schema.meta = { ...schema.meta, [key]: value }
|
||||
return schema
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
Schema.prototype.deprecated = function deprecated() {
|
||||
const schema = Schema(this)
|
||||
schema.meta.badges ||= []
|
||||
schema.meta.badges.push({ text: 'deprecated', type: 'danger' })
|
||||
return schema
|
||||
}
|
||||
|
||||
Schema.prototype.experimental = function experimental() {
|
||||
const schema = Schema(this)
|
||||
schema.meta.badges ||= []
|
||||
schema.meta.badges.push({ text: 'experimental', type: 'warning' })
|
||||
return schema
|
||||
}
|
||||
|
||||
Schema.prototype.pattern = function pattern(regexp) {
|
||||
const schema = Schema(this)
|
||||
const pattern = pick(regexp, ['source', 'flags'])
|
||||
schema.meta = { ...schema.meta, pattern }
|
||||
return schema
|
||||
}
|
||||
|
||||
Schema.prototype.simplify = function simplify(this: Schema, value) {
|
||||
if (deepEqual(value, this.meta.default, this.type === 'dict')) return null
|
||||
if (isNullable(value)) return value
|
||||
if (this.type === 'object' || this.type === 'dict') {
|
||||
const result: Dict = {}
|
||||
for (const key in value) {
|
||||
const schema = this.type === 'object' ? this.dict![key] : this.inner
|
||||
const item = schema?.simplify(value[key])
|
||||
if (this.type === 'dict' || !isNullable(item)) result[key] = item
|
||||
}
|
||||
if (deepEqual(result, this.meta.default, this.type === 'dict')) return null
|
||||
return result
|
||||
} else if (this.type === 'array' || this.type === 'tuple') {
|
||||
const result: any[] = []
|
||||
;(value as any[]).forEach((value, index) => {
|
||||
const schema = this.type === 'array' ? this.inner : this.list![index]
|
||||
const item = schema ? schema.simplify(value) : value
|
||||
result.push(item)
|
||||
})
|
||||
return result
|
||||
} else if (this.type === 'intersect') {
|
||||
const result: Dict = {}
|
||||
for (const item of this.list!) {
|
||||
Object.assign(result, item.simplify(value))
|
||||
}
|
||||
return result
|
||||
} else if (this.type === 'union') {
|
||||
for (const schema of this.list!) {
|
||||
try {
|
||||
Schema.resolve(value, schema, {})
|
||||
return schema.simplify(value)
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
Schema.prototype.toString = function toString(inline?: boolean) {
|
||||
return formatters[this.type]?.(this, inline) ?? `Schema<${this.type}>`
|
||||
}
|
||||
|
||||
Schema.prototype.role = function role(role, extra) {
|
||||
const schema = Schema(this)
|
||||
schema.meta = { ...schema.meta, role, extra }
|
||||
return schema
|
||||
}
|
||||
|
||||
for (const key of ['default', 'link', 'comment', 'description', 'max', 'min', 'step']) {
|
||||
Object.assign(Schema.prototype, {
|
||||
[key](this: Schema, value: any) {
|
||||
const schema = Schema(this)
|
||||
schema.meta = { ...schema.meta, [key]: value }
|
||||
return schema
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const resolvers: Dict<Schemastery.Resolve> = {}
|
||||
|
||||
Schema.extend = function extend(type, resolve) {
|
||||
resolvers[type] = resolve
|
||||
}
|
||||
|
||||
Schema.resolve = function resolve(data, schema, options = {}, strict = false) {
|
||||
if (!schema) return [data]
|
||||
if (options.ignore?.(data, schema)) return [data]
|
||||
|
||||
if (isNullable(data) && schema.type !== 'lazy') {
|
||||
if (schema.meta.required) throw new ValidationError(`missing required value`, options)
|
||||
let current = schema
|
||||
let fallback = schema.meta.default
|
||||
while (current?.type === 'intersect' && isNullable(fallback)) {
|
||||
current = current.list![0]
|
||||
fallback = current?.meta.default
|
||||
}
|
||||
if (isNullable(fallback)) return [data]
|
||||
data = clone(fallback)
|
||||
}
|
||||
|
||||
const callback = resolvers[schema.type]
|
||||
if (!callback) throw new ValidationError(`unsupported type "${schema.type}"`, options)
|
||||
|
||||
try {
|
||||
return callback(data, schema, options, strict)
|
||||
} catch (error) {
|
||||
if (!schema.meta.loose) throw error
|
||||
return [schema.meta.default]
|
||||
}
|
||||
}
|
||||
|
||||
Schema.from = function from(source: any) {
|
||||
if (isNullable(source)) {
|
||||
return Schema.any()
|
||||
} else if (['string', 'number', 'boolean'].includes(typeof source)) {
|
||||
return Schema.const(source).required()
|
||||
} else if (source[kSchema]) {
|
||||
return source
|
||||
} else if (typeof source === 'function') {
|
||||
switch (source) {
|
||||
case String: return Schema.string().required()
|
||||
case Number: return Schema.number().required()
|
||||
case Boolean: return Schema.boolean().required()
|
||||
case Function: return Schema.function().required()
|
||||
default: return Schema.is(source).required()
|
||||
}
|
||||
} else {
|
||||
throw new TypeError(`cannot infer schema from ${source}`)
|
||||
}
|
||||
}
|
||||
|
||||
Schema.lazy = function lazy(builder) {
|
||||
const toJSON = () => {
|
||||
if (!schema.inner![kSchema]) {
|
||||
schema.inner = schema.builder!()
|
||||
schema.inner!.meta = { ...schema.meta, ...schema.inner!.meta }
|
||||
}
|
||||
return schema.inner!.toJSON()
|
||||
}
|
||||
const schema = new Schema({ type: 'lazy', builder, inner: { toJSON } as any })
|
||||
return schema as any
|
||||
}
|
||||
|
||||
Schema.natural = function natural() {
|
||||
return Schema.number().step(1).min(0)
|
||||
}
|
||||
|
||||
Schema.percent = function percent() {
|
||||
return Schema.number().step(0.01).min(0).max(1).role('slider')
|
||||
}
|
||||
|
||||
Schema.date = function date() {
|
||||
return Schema.union([
|
||||
Schema.is(Date),
|
||||
Schema.transform(Schema.string().role('datetime'), (value, options) => {
|
||||
const date = new Date(value)
|
||||
if (isNaN(+date)) throw new ValidationError(`invalid date "${value}"`, options)
|
||||
return date
|
||||
}, true),
|
||||
])
|
||||
}
|
||||
|
||||
Schema.regExp = function regExp(flag = '') {
|
||||
return Schema.union([
|
||||
Schema.is(RegExp),
|
||||
Schema.transform(Schema.string().role('regexp', { flag }), (value, options) => {
|
||||
try {
|
||||
return new RegExp(value, flag)
|
||||
} catch (e: any) {
|
||||
throw new ValidationError(e.message, options)
|
||||
}
|
||||
}, true),
|
||||
])
|
||||
}
|
||||
|
||||
Schema.arrayBuffer = function arrayBuffer(encoding?: 'hex' | 'base64'): any {
|
||||
return Schema.union([
|
||||
Schema.is(ArrayBuffer),
|
||||
Schema.is(SharedArrayBuffer),
|
||||
Schema.transform(Schema.any<ArrayBufferView>(), (value, options) => {
|
||||
if (Binary.isSource(value)) return Binary.fromSource(value)
|
||||
throw new ValidationError(`expected ArrayBufferSource but got ${value}`, options)
|
||||
}, true),
|
||||
...encoding ? [Schema.transform(Schema.string(), (value, options) => {
|
||||
try {
|
||||
return encoding === 'base64'
|
||||
? Binary.fromBase64(value)
|
||||
: Binary.fromHex(value)
|
||||
} catch (e: any) {
|
||||
throw new ValidationError(e.message, options)
|
||||
}
|
||||
}, true)] as const : [],
|
||||
])
|
||||
}
|
||||
|
||||
Schema.extend('lazy', (data, schema, options, strict) => {
|
||||
if (!schema.inner![kSchema]) {
|
||||
schema.inner = schema.builder!()
|
||||
schema.inner!.meta = { ...schema.meta, ...schema.inner!.meta }
|
||||
}
|
||||
return Schema.resolve(data, schema.inner!, options, strict)
|
||||
})
|
||||
|
||||
Schema.extend('any', (data) => {
|
||||
return [data]
|
||||
})
|
||||
|
||||
Schema.extend('never', (data, _, options) => {
|
||||
throw new ValidationError(`expected nullable but got ${data}`, options)
|
||||
})
|
||||
|
||||
Schema.extend('const', (data, { value }, options) => {
|
||||
if (deepEqual(data, value)) return [value]
|
||||
throw new ValidationError(`expected ${value} but got ${data}`, options)
|
||||
})
|
||||
|
||||
function checkWithinRange(data: number, meta: Schemastery.Meta<any>, description: string, options: Schemastery.Options, skipMin = false) {
|
||||
const { max = Infinity, min = -Infinity } = meta
|
||||
if (data > max) throw new ValidationError(`expected ${description} <= ${max} but got ${data}`, options)
|
||||
if (data < min && !skipMin) throw new ValidationError(`expected ${description} >= ${min} but got ${data}`, options)
|
||||
}
|
||||
|
||||
Schema.extend('string', (data, { meta }, options) => {
|
||||
if (typeof data !== 'string') throw new ValidationError(`expected string but got ${data}`, options)
|
||||
if (meta.pattern) {
|
||||
const regexp = new RegExp(meta.pattern.source, meta.pattern.flags)
|
||||
if (!regexp.test(data)) throw new ValidationError(`expect string to match regexp ${regexp}`, options)
|
||||
}
|
||||
checkWithinRange(data.length, meta, 'string length', options)
|
||||
return [data]
|
||||
})
|
||||
|
||||
function decimalShift(data: number, digits: number) {
|
||||
const str = data.toString()
|
||||
if (str.includes('e')) return data * Math.pow(10, digits)
|
||||
const index = str.indexOf('.')
|
||||
if (index === -1) return data * Math.pow(10, digits)
|
||||
const frac = str.slice(index + 1)
|
||||
const integer = str.slice(0, index)
|
||||
if (frac.length <= digits) return +(integer + frac.padEnd(digits, '0'))
|
||||
return +(integer + frac.slice(0, digits) + '.' + frac.slice(digits))
|
||||
}
|
||||
|
||||
function isMultipleOf(data: number, min: number, step: number) {
|
||||
step = Math.abs(step)
|
||||
if (!/^\d+\.\d+$/.test(step.toString())) {
|
||||
return (data - min) % step === 0
|
||||
}
|
||||
const index = step.toString().indexOf('.')
|
||||
const digits = step.toString().slice(index + 1).length
|
||||
return Math.abs(decimalShift(data, digits) - decimalShift(min, digits)) % decimalShift(step, digits) === 0
|
||||
}
|
||||
|
||||
Schema.extend('number', (data, { meta }, options) => {
|
||||
if (typeof data !== 'number') throw new ValidationError(`expected number but got ${data}`, options)
|
||||
checkWithinRange(data, meta, 'number', options)
|
||||
const { step } = meta
|
||||
if (step && !isMultipleOf(data, meta.min ?? 0, step)) {
|
||||
throw new ValidationError(`expected number multiple of ${step} but got ${data}`, options)
|
||||
}
|
||||
return [data]
|
||||
})
|
||||
|
||||
Schema.extend('boolean', (data, _, options) => {
|
||||
if (typeof data === 'boolean') return [data]
|
||||
throw new ValidationError(`expected boolean but got ${data}`, options)
|
||||
})
|
||||
|
||||
Schema.extend('bitset', (data, { bits, meta }, options) => {
|
||||
let value = 0, keys: string[] = []
|
||||
if (typeof data === 'number') {
|
||||
value = data
|
||||
for (const key in bits!) {
|
||||
if (data & bits![key]!) {
|
||||
keys.push(key)
|
||||
}
|
||||
}
|
||||
} else if (Array.isArray(data)) {
|
||||
keys = data
|
||||
for (const key of keys) {
|
||||
if (typeof key !== 'string') throw new ValidationError(`expected string but got ${key}`, options)
|
||||
if (key in bits!) value |= bits![key]!
|
||||
}
|
||||
} else {
|
||||
throw new ValidationError(`expected number or array but got ${data}`, options)
|
||||
}
|
||||
if (value === meta.default) return [value]
|
||||
return [value, keys]
|
||||
})
|
||||
|
||||
Schema.extend('function', (data, _, options) => {
|
||||
if (typeof data === 'function') return [data]
|
||||
throw new ValidationError(`expected function but got ${data}`, options)
|
||||
})
|
||||
|
||||
Schema.extend('is', (data, { constructor }, options) => {
|
||||
if (typeof constructor === 'function') {
|
||||
if (data instanceof constructor) return [data]
|
||||
throw new ValidationError(`expected ${constructor.name} but got ${data}`, options)
|
||||
} else {
|
||||
if (isNullable(data)) {
|
||||
throw new ValidationError(`expected ${constructor} but got ${data}`, options)
|
||||
}
|
||||
let prototype = Object.getPrototypeOf(data)
|
||||
while (prototype) {
|
||||
if (prototype.constructor?.name === constructor) return [data]
|
||||
prototype = Object.getPrototypeOf(prototype)
|
||||
}
|
||||
throw new ValidationError(`expected ${constructor} but got ${data}`, options)
|
||||
}
|
||||
})
|
||||
|
||||
function property(data: any, key: keyof any, schema: Schema, options: Schemastery.Options) {
|
||||
try {
|
||||
const [value, adapted] = Schema.resolve(data[key], schema, {
|
||||
...options,
|
||||
path: [...options.path || [], key],
|
||||
})
|
||||
if (adapted !== undefined) data[key] = adapted
|
||||
return value
|
||||
} catch (e) {
|
||||
if (!options?.autofix) throw e
|
||||
delete data[key]
|
||||
return schema.meta.default
|
||||
}
|
||||
}
|
||||
|
||||
Schema.extend('array', (data, { inner, meta }, options) => {
|
||||
if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options)
|
||||
checkWithinRange(data.length, meta, 'array length', options, !isNullable(inner!.meta.default))
|
||||
return [data.map((_, index) => property(data, index, inner!, options))]
|
||||
})
|
||||
|
||||
Schema.extend('dict', (data, { inner, sKey }, options, strict) => {
|
||||
if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options)
|
||||
const result: any = {}
|
||||
for (const key in data) {
|
||||
let rKey: string
|
||||
try {
|
||||
rKey = Schema.resolve(key, sKey!, options)[0]
|
||||
} catch (error) {
|
||||
if (strict) continue
|
||||
throw error
|
||||
}
|
||||
result[rKey] = property(data, key, inner!, options)
|
||||
data[rKey] = data[key]
|
||||
if (key !== rKey) delete data[key]
|
||||
}
|
||||
return [result]
|
||||
})
|
||||
|
||||
Schema.extend('tuple', (data, { list }, options, strict) => {
|
||||
if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options)
|
||||
const result = list!.map((inner, index) => property(data, index, inner, options))
|
||||
if (strict) return [result]
|
||||
result.push(...data.slice(list!.length))
|
||||
return [result]
|
||||
})
|
||||
|
||||
function merge(result: any, data: any) {
|
||||
for (const key in data) {
|
||||
if (key in result) continue
|
||||
result[key] = data[key]
|
||||
}
|
||||
}
|
||||
|
||||
Schema.extend('object', (data, { dict }, options, strict) => {
|
||||
if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options)
|
||||
const result: any = {}
|
||||
for (const key in dict) {
|
||||
const value = property(data, key, dict![key]!, options)
|
||||
if (!isNullable(value) || key in data) {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
if (!strict) merge(result, data)
|
||||
return [result]
|
||||
})
|
||||
|
||||
Schema.extend('union', (data, { list, toString }, options, strict) => {
|
||||
const messages: any[] = []
|
||||
for (const inner of list!) {
|
||||
try {
|
||||
return Schema.resolve(data, inner, options, strict)
|
||||
} catch (error) {
|
||||
messages.push(error)
|
||||
}
|
||||
}
|
||||
throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options)
|
||||
})
|
||||
|
||||
Schema.extend('intersect', (data, { list, toString }, options, strict) => {
|
||||
if (!list!.length) return [data]
|
||||
let result
|
||||
for (const inner of list!) {
|
||||
const value: any = Schema.resolve(data, inner, options, true)[0]
|
||||
if (isNullable(value)) continue
|
||||
if (isNullable(result)) {
|
||||
result = value
|
||||
} else if (typeof result !== typeof value) {
|
||||
throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options)
|
||||
} else if (typeof value === 'object') {
|
||||
merge(result ??= {}, value)
|
||||
} else if (result !== value) {
|
||||
throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options)
|
||||
}
|
||||
}
|
||||
if (!strict && isPlainObject(data)) merge(result, data)
|
||||
return [result]
|
||||
})
|
||||
|
||||
Schema.extend('transform', (data, { inner, callback, preserve }, options) => {
|
||||
const [result, adapted = data] = Schema.resolve(data, inner!, options, true)
|
||||
if (preserve) {
|
||||
return [callback!(result)]
|
||||
// } else if (isPlainObject(data)) {
|
||||
// const temp: any = {}
|
||||
// for (const key in result) {
|
||||
// if (!(key in data)) continue
|
||||
// temp[key] = data[key]
|
||||
// delete data[key]
|
||||
// }
|
||||
// Object.assign(data, callback!(temp))
|
||||
// return [callback!(result)]
|
||||
} else {
|
||||
return [callback!(result), callback!(adapted)]
|
||||
}
|
||||
})
|
||||
|
||||
type Formatter = (schema: Schema, inline?: boolean) => string
|
||||
const formatters: Dict<Formatter> = {}
|
||||
|
||||
function defineMethod(name: string, keys: (keyof Schema)[], format: Formatter) {
|
||||
formatters[name] = format
|
||||
Object.assign(Schema, {
|
||||
[name](...args: any[]) {
|
||||
const schema = new Schema({ type: name } as Schema)
|
||||
keys.forEach((key, index) => {
|
||||
switch (key) {
|
||||
case 'sKey': schema.sKey = args[index] ?? Schema.string(); break
|
||||
case 'inner': schema.inner = Schema.from(args[index]); break
|
||||
case 'list': schema.list = args[index].map(Schema.from); break
|
||||
case 'dict': schema.dict = valueMap(args[index], Schema.from); break
|
||||
case 'bits': {
|
||||
schema.bits = {}
|
||||
for (const key in args[index]) {
|
||||
if (typeof args[index][key] !== 'number') continue
|
||||
schema.bits[key] = args[index][key]
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'callback': {
|
||||
const callback = schema.callback = args[index]
|
||||
;callback['toJSON'] ||= () => callback.toString()
|
||||
break
|
||||
}
|
||||
case 'constructor': {
|
||||
const constructor = schema.constructor = args[index]
|
||||
if (typeof constructor === 'function') {
|
||||
;constructor['toJSON'] ||= () => constructor['name']
|
||||
}
|
||||
break
|
||||
}
|
||||
default: schema[key] = args[index] as never
|
||||
}
|
||||
})
|
||||
if (name === 'object' || name === 'dict') {
|
||||
schema.meta.default = {}
|
||||
} else if (name === 'array' || name === 'tuple') {
|
||||
schema.meta.default = []
|
||||
} else if (name === 'bitset') {
|
||||
schema.meta.default = 0
|
||||
}
|
||||
return schema
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
defineMethod('is', ['constructor'], ({ constructor }) => {
|
||||
if (typeof constructor === 'function') {
|
||||
return constructor.name
|
||||
} else {
|
||||
return constructor!
|
||||
}
|
||||
})
|
||||
|
||||
defineMethod('any', [], () => 'any')
|
||||
defineMethod('never', [], () => 'never')
|
||||
defineMethod('const', ['value'], ({ value }) => typeof value === 'string' ? JSON.stringify(value) : value)
|
||||
defineMethod('string', [], () => 'string')
|
||||
defineMethod('number', [], () => 'number')
|
||||
defineMethod('boolean', [], () => 'boolean')
|
||||
defineMethod('bitset', ['bits'], () => 'bitset')
|
||||
defineMethod('function', [], () => 'function')
|
||||
defineMethod('array', ['inner'], ({ inner }) => `${inner!.toString(true)}[]`)
|
||||
defineMethod('dict', ['inner', 'sKey'], ({ inner, sKey }) => `{ [key: ${sKey!.toString()}]: ${inner!.toString()} }`)
|
||||
defineMethod('tuple', ['list'], ({ list }) => `[${list!.map((inner) => inner.toString()).join(', ')}]`)
|
||||
|
||||
defineMethod('object', ['dict'], ({ dict }) => {
|
||||
if (Object.keys(dict!).length === 0) return '{}'
|
||||
return `{ ${Object.entries(dict!).map(([key, inner]) => {
|
||||
return `${key}${inner!.meta.required ? '' : '?'}: ${inner!.toString()}`
|
||||
}).join(', ')} }`
|
||||
})
|
||||
|
||||
defineMethod('union', ['list'], ({ list }, inline) => {
|
||||
const result = list!.map(({ toString: format }) => format()).join(' | ')
|
||||
return inline ? `(${result})` : result
|
||||
})
|
||||
|
||||
defineMethod('intersect', ['list'], ({ list }) => {
|
||||
return `${list!.map((inner) => inner.toString(true)).join(' & ')}`
|
||||
})
|
||||
|
||||
defineMethod('transform', ['inner', 'callback', 'preserve'], ({ inner }, isInner) => inner!.toString(isInner))
|
||||
|
||||
export = Schema
|
||||
12
vendor/schemastery/tsconfig.json
vendored
Normal file
12
vendor/schemastery/tsconfig.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib",
|
||||
"module": "preserve"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{ "path": "../cosmokit" }
|
||||
]
|
||||
}
|
||||
21
vendor/timer/LICENSE
vendored
Normal file
21
vendor/timer/LICENSE
vendored
Normal 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.
|
||||
36
vendor/timer/README.md
vendored
Normal file
36
vendor/timer/README.md
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# @cordisjs/plugin-timer
|
||||
|
||||
Disposal-aware timer service for Cordis.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { Context } from 'cordis'
|
||||
import Timer from '@cordisjs/plugin-timer'
|
||||
|
||||
const root = new Context()
|
||||
await root.plugin(Timer)
|
||||
|
||||
const dispose = root.timeout(() => {
|
||||
root.logger.info('done')
|
||||
}, 1000)
|
||||
|
||||
dispose()
|
||||
```
|
||||
|
||||
Timer handles are registered on the current fiber, so they are cleared
|
||||
automatically when the plugin that created them is disposed.
|
||||
|
||||
## API
|
||||
|
||||
| API | Description |
|
||||
| --- | --- |
|
||||
| `ctx.timeout(callback, delay)` | Run once and return a disposer. |
|
||||
| `ctx.timeout(delay)` | Return a promise that resolves after `delay`. |
|
||||
| `ctx.interval(callback, delay)` | Run repeatedly and return a disposer. |
|
||||
| `ctx.interval(delay)` | Return an async iterator that yields on each interval. |
|
||||
| `ctx.throttle(callback, delay, noTrailing?)` | Return a throttled function with `.dispose()`. |
|
||||
| `ctx.debounce(callback, delay)` | Return a debounced function with `.dispose()`. |
|
||||
|
||||
`ctx.setTimeout()` and `ctx.setInterval()` are kept as deprecated aliases for
|
||||
`ctx.timeout()` and `ctx.interval()`.
|
||||
29
vendor/timer/package.json
vendored
Normal file
29
vendor/timer/package.json
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@cordisjs/plugin-timer",
|
||||
"description": "Timer service for cordis",
|
||||
"version": "1.1.2",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
147
vendor/timer/src/index.ts
vendored
Normal file
147
vendor/timer/src/index.ts
vendored
Normal file
@@ -0,0 +1,147 @@
|
||||
import { Context, Service } from 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context extends Pick<TimerService, 'interval' | 'timeout' | 'throttle' | 'debounce' | 'setTimeout' | 'setInterval'> {
|
||||
timer: TimerService
|
||||
}
|
||||
}
|
||||
|
||||
type WithDispose<T> = T & { dispose: () => void }
|
||||
|
||||
/** Disposable timer helpers mixed into Cordis contexts. */
|
||||
export class TimerService extends Service {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'timer')
|
||||
ctx.mixin('timer', ['timeout', 'interval', 'throttle', 'debounce', 'setTimeout', 'setInterval'])
|
||||
}
|
||||
|
||||
/** @deprecated use `ctx.timeout()` instead */
|
||||
setTimeout(callback: () => void, delay: number) {
|
||||
return this.timeout(callback, delay)
|
||||
}
|
||||
|
||||
/** @deprecated use `ctx.interval()` instead */
|
||||
setInterval(callback: () => void, delay: number) {
|
||||
return this.interval(callback, delay)
|
||||
}
|
||||
|
||||
/** Run a callback once, or return a promise that resolves after `delay`. */
|
||||
timeout(callback: () => void, delay: number): () => void
|
||||
timeout(delay: number): Promise<void>
|
||||
timeout(...args: any[]): any {
|
||||
const callback = typeof args[0] === 'function' ? args.shift() : undefined
|
||||
const delay = args[0] as number
|
||||
if (callback) {
|
||||
const dispose = this.ctx.effect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
dispose()
|
||||
callback()
|
||||
}, delay)
|
||||
return () => clearTimeout(timer)
|
||||
}, 'ctx.timeout()')
|
||||
return dispose
|
||||
} else {
|
||||
const { promise, resolve, reject } = Promise.withResolvers<void>()
|
||||
const dispose = this.ctx.effect(() => {
|
||||
const timer = setTimeout(resolve, delay)
|
||||
return () => {
|
||||
clearTimeout(timer)
|
||||
reject(new Error('Context has been disposed'))
|
||||
}
|
||||
}, 'ctx.timeout()')
|
||||
return promise.finally(dispose)
|
||||
}
|
||||
}
|
||||
|
||||
/** Run a callback repeatedly, or return an async iterator of ticks. */
|
||||
interval(callback: () => void, delay: number): () => void
|
||||
interval<R = any>(delay: number): AsyncIterableIterator<void, R, void>
|
||||
interval(...args: any[]): any {
|
||||
const callback = typeof args[0] === 'function' ? args.shift() : undefined
|
||||
const delay = args[0] as number
|
||||
if (callback) {
|
||||
return this.ctx.effect(() => {
|
||||
const timer = setInterval(callback, delay)
|
||||
return () => clearInterval(timer)
|
||||
}, 'ctx.interval()')
|
||||
} else {
|
||||
let done: { kind: 'return'; value: any } | { kind: 'throw'; reason: any } | undefined
|
||||
let nextTask: PromiseWithResolvers<IteratorResult<void>> | undefined
|
||||
const dispose = this.ctx.effect(() => {
|
||||
const timer = setInterval(() => {
|
||||
nextTask?.resolve({ done: false, value: undefined })
|
||||
}, delay)
|
||||
return () => {
|
||||
clearInterval(timer)
|
||||
if (done) return
|
||||
done = { kind: 'throw', reason: new Error('Context has been disposed') }
|
||||
nextTask?.reject(done.reason)
|
||||
}
|
||||
}, 'ctx.interval()')
|
||||
return {
|
||||
next: () => {
|
||||
if (!done) return (nextTask = Promise.withResolvers()).promise
|
||||
if (done.kind === 'return') return Promise.resolve({ done: true, value: done.value })
|
||||
return Promise.reject(done.reason)
|
||||
},
|
||||
return: (value) => {
|
||||
if (!done) done = { kind: 'return', value }
|
||||
nextTask?.resolve({ done: true, value })
|
||||
dispose()
|
||||
return Promise.resolve({ done: true, value })
|
||||
},
|
||||
throw: (reason) => {
|
||||
if (!done) done = { kind: 'throw', reason }
|
||||
nextTask?.reject(reason)
|
||||
dispose()
|
||||
return Promise.resolve({ done: true, value: undefined })
|
||||
},
|
||||
[Symbol.asyncIterator]() {
|
||||
return this
|
||||
},
|
||||
} satisfies AsyncIterableIterator<void>
|
||||
}
|
||||
}
|
||||
|
||||
private _schedule(label: string, trigger: (args: any[], isDisposed: boolean) => any, isDisposed = false) {
|
||||
let timer: number | NodeJS.Timeout | undefined
|
||||
const dispose = this.ctx.effect(() => () => {
|
||||
isDisposed = true
|
||||
clearTimeout(timer)
|
||||
}, label)
|
||||
const wrapper: any = (...args: any[]) => {
|
||||
clearTimeout(timer)
|
||||
timer = trigger(args, isDisposed)
|
||||
}
|
||||
wrapper.dispose = dispose
|
||||
return wrapper
|
||||
}
|
||||
|
||||
/** Return a throttled function whose timer is disposed with the current fiber. */
|
||||
throttle<F extends (...args: any[]) => void>(callback: F, delay: number, noTrailing?: boolean): WithDispose<F> {
|
||||
let lastCall = -Infinity
|
||||
const execute = (...args: any[]) => {
|
||||
lastCall = Date.now()
|
||||
callback(...args)
|
||||
}
|
||||
return this._schedule('ctx.throttle()', (args, isDisposed) => {
|
||||
const now = Date.now()
|
||||
const remaining = delay - now + lastCall
|
||||
if (remaining <= 0) {
|
||||
execute(...args)
|
||||
} else if (!isDisposed) {
|
||||
return setTimeout(execute, remaining, ...args)
|
||||
}
|
||||
}, noTrailing)
|
||||
}
|
||||
|
||||
/** Return a debounced function whose timer is disposed with the current fiber. */
|
||||
debounce<F extends (...args: any[]) => void>(callback: F, delay: number): WithDispose<F> {
|
||||
return this._schedule('ctx.debounce()', (args, isDisposed) => {
|
||||
if (isDisposed) return
|
||||
return setTimeout(callback, delay, ...args)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default TimerService
|
||||
12
vendor/timer/tsconfig.json
vendored
Normal file
12
vendor/timer/tsconfig.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{ "path": "../cosmokit" },
|
||||
{ "path": "../cordis" }
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user