@cordisjs/core
Version:
Meta-Framework for Modern JavaScript Applications
203 lines (184 loc) • 7.9 kB
text/typescript
import { defineProperty } from 'cosmokit'
import type { Context, Service } from './index.ts'
export interface Tracker {
associate?: string
property?: string
}
export const symbols = {
// internal symbols
shadow: Symbol.for('cordis.shadow'),
receiver: Symbol.for('cordis.receiver'),
original: Symbol.for('cordis.original'),
// context symbols
store: Symbol.for('cordis.store') as typeof Context.store,
events: Symbol.for('cordis.events') as typeof Context.events,
static: Symbol.for('cordis.static') as typeof Context.static,
filter: Symbol.for('cordis.filter') as typeof Context.filter,
expose: Symbol.for('cordis.expose') as typeof Context.expose,
isolate: Symbol.for('cordis.isolate') as typeof Context.isolate,
internal: Symbol.for('cordis.internal') as typeof Context.internal,
intercept: Symbol.for('cordis.intercept') as typeof Context.intercept,
// service symbols
setup: Symbol.for('cordis.setup') as typeof Service.setup,
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,
provide: Symbol.for('cordis.provide') as typeof Service.provide,
immediate: Symbol.for('cordis.immediate') as typeof Service.immediate,
}
const GeneratorFunction = function* () {}.constructor
const AsyncGeneratorFunction = async function* () {}.constructor
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 proxified
// 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
}
export function resolveConfig(plugin: any, config: any) {
const schema = plugin['Config'] || plugin['schema']
if (schema && plugin['schema'] !== false) config = schema(config)
return config ?? {}
}
export function isUnproxyable(value: any) {
return [Map, Set, Date, Promise].some(constructor => value instanceof constructor)
}
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
}
export function isObject(value: any): value is {} {
return value && (typeof value === 'object' || typeof value === 'function')
}
export function getTraceable<T>(ctx: Context, value: T, noTrap?: boolean): 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, noTrap)
}
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) => {
// contravariant
if (thisArg === outer) thisArg = shadow
// contravariant
args = args.map((arg) => {
if (typeof arg !== 'function' || arg[symbols.original]) return arg
return new Proxy(arg, {
get: (target, prop, receiver) => {
if (prop === symbols.original) return target
const value = Reflect.get(target, prop, receiver)
// https://github.com/cordiverse/cordis/issues/14
if (prop === 'toString' && value === Function.prototype.toString) {
return function (...args: any[]) {
return Reflect.apply(value, this === receiver ? target : this, args)
}
}
return value
},
apply: (target: Function, thisArg, args) => {
// covariant
return Reflect.apply(target, getTraceable(ctx, thisArg), args.map(arg => getTraceable(ctx, arg)))
},
construct: (target: Function, args, newTarget) => {
// covariant
return Reflect.construct(target, args.map(arg => getTraceable(ctx, arg)), newTarget)
},
})
})
// covariant
return getTraceable(ctx, Reflect.apply(target, thisArg, args))
},
})
}
function createTraceable(ctx: Context, value: any, tracker: Tracker, noTrap?: boolean) {
if (ctx[symbols.shadow]) {
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[symbols.internal][`${tracker.associate}.${prop}`]) {
return Reflect.get(ctx, `${tracker.associate}.${prop}`, withProp(ctx, symbols.receiver, receiver))
}
const shadow = createShadow(ctx, target, tracker.property, receiver)
const innerValue = Reflect.get(target, prop, shadow)
const innerTracker = innerValue?.[symbols.tracker]
if (innerTracker) {
return createTraceable(ctx, innerValue, innerTracker)
} else if (!noTrap && typeof innerValue === 'function') {
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[symbols.internal][`${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)
}
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)
}