From 20381082d97692d342bd2bd13139332edd7a4fc7 Mon Sep 17 00:00:00 2001 From: LastLeaf Date: Fri, 6 Dec 2024 19:35:35 +0800 Subject: [PATCH] feat: initial coding --- README.md | 13 +- eslint.config.js | 11 +- package-lock.json | 9 ++ package.json | 3 + src/base.ts | 288 ++++++++++++++++++++++++++++++++++++++++++ src/behavior.ts | 44 +++++++ src/component.ts | 36 ++++++ src/index.ts | 7 +- src/trait_behavior.ts | 45 +++++++ tsconfig.json | 3 +- 10 files changed, 447 insertions(+), 12 deletions(-) create mode 100644 src/base.ts create mode 100644 src/behavior.ts create mode 100644 src/component.ts create mode 100644 src/trait_behavior.ts diff --git a/README.md b/README.md index 2624da5..e300c7b 100644 --- a/README.md +++ b/README.md @@ -40,16 +40,19 @@ npm install 之后,需要点一下小程序开发者工具菜单中的“工 ```ts import { Component } from 'miniprogram-chaining-api-polyfill' -``` -然后就可以使用 Chaining API 了。 +// 然后就可以使用 Chaining API 了 +Component() + // ... + .register() +``` -上述方式引入时,会自动判断当前环境是否具有原生的 Chaining API 支持。如果有,就会使用原生的;反之会激活 polyfill 。 +注意:如果这个组件本身只用在 glass-easel 组件框架下,最好不要在这个组件文件中引入 polyfill 。 -如果需要强制激活 polyfill ,可以这样引入: +类似地,也有: ```ts -import { ComponentForcePolyfill as Component } from 'miniprogram-chaining-api-polyfill' +import { Behavior } from 'miniprogram-chaining-api-polyfill' ``` diff --git a/eslint.config.js b/eslint.config.js index 27af096..2240e53 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -4,13 +4,20 @@ const importPlugin = require('eslint-plugin-import') const prettierRecommended = require('eslint-plugin-prettier/recommended') const promise = require('eslint-plugin-promise') -module.exports = tseslint.config({ +const tsconfig = tseslint.config({ files: ["src/**/*.[jt]s", "dist/**/*.[jt]s"], extends: [ js.configs.recommended, - importPlugin.flatConfigs.recommended, promise.configs['flat/recommended'], prettierRecommended, tseslint.configs.recommended, ], + rules: { + "@typescript-eslint/no-explicit-any": "off", + }, }) + +module.exports = [ + // importPlugin.flatConfigs.recommended, + ...tsconfig, +] diff --git a/package-lock.json b/package-lock.json index 99c23ab..4695d56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,9 @@ "typescript": "^5.7.2", "typescript-eslint": "^8.17.0", "webpack-cli": "^5.1.4" + }, + "peerDependencies": { + "miniprogram-api-typings": "^4.0.2" } }, "node_modules/@ampproject/remapping": { @@ -5753,6 +5756,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/miniprogram-api-typings": { + "version": "4.0.2", + "resolved": "https://mirrors.tencent.com/npm/miniprogram-api-typings/-/miniprogram-api-typings-4.0.2.tgz", + "integrity": "sha512-hDrWCaJamsMB6Sg+0oMzGgSOse8EzumfN+VQFBKtJe2LbeKibGruB/ptNd4viXdcdgfahQ+Ji5Z9v9meoG9iuw==", + "peer": true + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://mirrors.tencent.com/npm/ms/-/ms-2.1.3.tgz", diff --git a/package.json b/package.json index ef3ad2f..fb3e185 100644 --- a/package.json +++ b/package.json @@ -47,5 +47,8 @@ "typescript": "^5.7.2", "typescript-eslint": "^8.17.0", "webpack-cli": "^5.1.4" + }, + "peerDependencies": { + "miniprogram-api-typings": "^4.0.2" } } diff --git a/src/base.ts b/src/base.ts new file mode 100644 index 0000000..cd65f86 --- /dev/null +++ b/src/base.ts @@ -0,0 +1,288 @@ +import { type typeUtils } from 'glass-easel' +import { TraitBehavior, TraitGroup } from './trait_behavior' + +export type Empty = typeUtils.Empty +export type DataList = WechatMiniprogram.Behavior.DataOption +export type PropertyList = WechatMiniprogram.Behavior.PropertyOption +export type MethodList = WechatMiniprogram.Behavior.MethodOption +export type ChainingFilterType = typeUtils.ChainingFilterType +export type AllData = TData & + WechatMiniprogram.Component.PropertyOptionToData +export type PropertyType = typeUtils.PropertyType +export type PropertyTypeToValueType = typeUtils.PropertyTypeToValueType +export type ComponentMethod = typeUtils.ComponentMethod +export type TaggedMethod = typeUtils.TaggedMethod +export type UnTaggedMethod> = typeUtils.UnTaggedMethod + +type Lifetimes = { + created: () => void + attached: () => void + moved: () => void + detached: () => void + ready: () => void +} + +export type GeneralComponent = Component + +export type Component< + TData extends DataList, + TProperty extends PropertyList, + TMethod extends MethodList, + TExtraThisFields extends DataList = Empty, +> = WechatMiniprogram.Component.Instance & TExtraThisFields + +export type ResolveBehaviorBuilder = + typeUtils.IsNever extends false + ? TChainingFilter extends ChainingFilterType + ? Omit & TChainingFilter['add'] + : B + : B + +class ChainingPolyfillMetadata { + traitGroup = new TraitGroup() +} + +const getChainingPolyfillMetadata = (comp: GeneralComponent) => { + return comp._$chainingPolyfill +} + +const chainingPolyfillBehavior = Behavior({ + created() { + const self = this as any + if (!self._$chainingPolyfill) { + self._$chainingPolyfill = new ChainingPolyfillMetadata() + } + }, +}) + +export class BaseBehaviorBuilder< + TPrevData extends DataList = Empty, + TData extends DataList = Empty, + TProperty extends PropertyList = Empty, + TMethod extends MethodList = Empty, + TChainingFilter extends ChainingFilterType = never, + TPendingChainingFilter extends ChainingFilterType = never, + TComponentExport = never, + TExtraThisFields extends DataList = Empty, +> { + protected _$definition: WechatMiniprogram.Component.Options = { + properties: {}, + behaviors: [chainingPolyfillBehavior], + } + private _$export: (() => any) | null = null + + /** Add external classes */ + externalClasses(list: string[]): this { + const def = this._$definition + if (def.externalClasses) def.externalClasses = def.externalClasses.concat(list) + else def.externalClasses = list + return this + } + + /** Set the export value when the component is being selected */ + export( + f: () => TNewComponentExport, + ): ResolveBehaviorBuilder< + BaseBehaviorBuilder< + TPrevData, + TData, + TProperty, + TMethod, + TChainingFilter, + TPendingChainingFilter, + TNewComponentExport, + TExtraThisFields + >, + TChainingFilter + > { + if (!this._$definition.export) this._$definition.behaviors.unshift('wx://component-export') + this._$definition.export = f as any + return this as any + } + + data( + gen: typeUtils.NewFieldList, T>, + ): ResolveBehaviorBuilder< + BaseBehaviorBuilder< + T, + TData & T, + TProperty, + TMethod, + TChainingFilter, + TPendingChainingFilter, + TComponentExport, + TExtraThisFields + >, + TChainingFilter + > { + this._$definition.data(gen) + return this as any + } + + property>( + name: N, + def: N extends keyof (TData & TProperty) ? never : typeUtils.PropertyListItem, + ): ResolveBehaviorBuilder< + BaseBehaviorBuilder< + TPrevData, + TData, + TProperty & Record>, + TMethod, + TChainingFilter, + TPendingChainingFilter, + TComponentExport, + TExtraThisFields + >, + TChainingFilter + > { + this._$definition.properties[name] = def + return this as any + } + + /** + * Add some public methods + * + * The public method can be used as an event handler, and can be visited in component instance. + */ + methods( + funcs: T & ThisType>, + ): ResolveBehaviorBuilder< + BaseBehaviorBuilder< + TPrevData, + TData, + TProperty, + TMethod & T, + TChainingFilter, + TPendingChainingFilter, + TComponentExport, + TExtraThisFields + >, + TChainingFilter + > { + Object.assign(this._$definition.methods, funcs) + return this as any + } + + /** + * Add a data observer + */ + observer< + P extends typeUtils.ObserverDataPathStrings>, + V = typeUtils.GetFromObserverPathString, P>, + >( + paths: P, + func: (this: Component, newValue: V) => void, + once?: boolean, + ): ResolveBehaviorBuilder + observer< + P extends typeUtils.ObserverDataPathStrings>[], + V = { + [K in keyof P]: typeUtils.GetFromObserverPathString, P[K]> + }, + >( + paths: readonly [...P], + func: ( + this: Component, + ...newValues: V extends any[] ? V : never + ) => void, + once?: boolean, + ): ResolveBehaviorBuilder + observer( + paths: string | readonly string[], + func: (this: Component, ...args: any[]) => any, + once = false, + ): ResolveBehaviorBuilder { + // TODO + return this as any + } + + /** + * Add a lifetime callback + */ + lifetime( + name: L, + func: ( + this: Component, + ...args: Parameters + ) => ReturnType, + once = false, + ): ResolveBehaviorBuilder { + // TODO + return this as any + } + + /** + * Add a page-lifetime callback + */ + pageLifetime( + name: string, + func: (this: Component, ...args: any[]) => any, + once = false, + ): ResolveBehaviorBuilder { + // TODO + return this as any + } + + init any>> | void>( + func: ( + this: Component, + builderContext: BuilderContext< + TPrevData, + TProperty, + Component + >, + ) => TExport, + ): ResolveBehaviorBuilder< + BaseBehaviorBuilder< + TPrevData, + TData, + TProperty, + TMethod & + (TExport extends void + ? Empty + : { + [K in keyof TExport]: UnTaggedMethod + }), + TChainingFilter, + TPendingChainingFilter, + TComponentExport, + TExtraThisFields + >, + TChainingFilter + > { + // TODO + return this as any + } + + definition< + TNewData extends DataList = Empty, + TNewProperty extends PropertyList = Empty, + TNewMethod extends MethodList = Empty, + TNewComponentExport = never, + >( + definition: WechatMiniprogram.Component.Options & + ThisType< + Component< + TData & TNewData, + TProperty & TNewProperty, + TMethod & TNewMethod, + TExtraThisFields + > + >, + ): ResolveBehaviorBuilder< + BaseBehaviorBuilder< + TPrevData, + TData & TNewData, + TProperty & TNewProperty, + TMethod & TNewMethod, + TChainingFilter, + TPendingChainingFilter, + TNewComponentExport, + TExtraThisFields + >, + TChainingFilter + > { + // TODO + return this as any + } +} diff --git a/src/behavior.ts b/src/behavior.ts new file mode 100644 index 0000000..0244793 --- /dev/null +++ b/src/behavior.ts @@ -0,0 +1,44 @@ +import 'miniprogram-api-typings' +import { TraitBehavior } from './trait_behavior' +import { BaseBehaviorBuilder } from './base' + +declare const Behavior: WechatMiniprogram.Behavior.Constructor + +export interface IBehaviorWithPolyfill { + (): BehaviorBuilder + < + TData extends WechatMiniprogram.Behavior.DataOption, + TProperty extends WechatMiniprogram.Behavior.PropertyOption, + TMethod extends WechatMiniprogram.Behavior.MethodOption, + TBehavior extends WechatMiniprogram.Behavior.BehaviorOption, + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + TCustomInstanceProperty extends WechatMiniprogram.IAnyObject = {}, + >( + options: WechatMiniprogram.Behavior.Options< + TData, + TProperty, + TMethod, + TBehavior, + TCustomInstanceProperty + >, + ): string + trait(): TraitBehavior + trait( + trans: (impl: TIn) => TOut, + ): TraitBehavior +} + +export const BehaviorWithPolyfill = ((options: any) => { + if (typeof options === 'undefined') { + return new BehaviorBuilder() + } + return Behavior(options) +}) as IBehaviorWithPolyfill + +BehaviorWithPolyfill.trait = (trans?: any) => { + return new TraitBehavior(trans) +} + +export class BehaviorBuilder extends BaseBehaviorBuilder { + // TODO +} diff --git a/src/component.ts b/src/component.ts new file mode 100644 index 0000000..6ef3058 --- /dev/null +++ b/src/component.ts @@ -0,0 +1,36 @@ +import { BaseBehaviorBuilder } from './base' + +declare const Component: WechatMiniprogram.Component.Constructor + +export interface IComponentWithPolyfill { + (): ComponentBuilder + < + TData extends WechatMiniprogram.Component.DataOption, + TProperty extends WechatMiniprogram.Component.PropertyOption, + TMethod extends WechatMiniprogram.Component.MethodOption, + TBehavior extends WechatMiniprogram.Component.BehaviorOption, + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + TCustomInstanceProperty extends WechatMiniprogram.IAnyObject = {}, + TIsPage extends boolean = false, + >( + options: WechatMiniprogram.Component.Options< + TData, + TProperty, + TMethod, + TBehavior, + TCustomInstanceProperty, + TIsPage + >, + ): string +} + +export const ComponentWithPolyfill = ((options?: any) => { + if (typeof options === 'undefined') { + return new ComponentBuilder() + } + return Component(options) +}) as IComponentWithPolyfill + +export class ComponentBuilder extends BaseBehaviorBuilder { + // TODO +} diff --git a/src/index.ts b/src/index.ts index 9406113..e4c5cd3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ -export const hello = (a: number, b: number) => { - return a + b -} +import 'miniprogram-api-typings' + +export { ComponentWithPolyfill as Component } from './component' +export { BehaviorWithPolyfill as Behavior } from './behavior' diff --git a/src/trait_behavior.ts b/src/trait_behavior.ts new file mode 100644 index 0000000..be86007 --- /dev/null +++ b/src/trait_behavior.ts @@ -0,0 +1,45 @@ +/** + * Interface that can be implement dynamically + * + * A `TraitBehavior` is like a TypeScript interface, but can be implemented dynamically. + * It requires the implementors to implement `TIn` . + * Also, it can provide do a transform from `TIn` to `TOut` as common logic of the trait. + */ +export class TraitBehavior< + TIn extends { [key: string]: any }, + TOut extends { [key: string]: any } = TIn, +> { + /** @internal */ + private _$trans?: (impl: TIn) => TOut + + /** @internal */ + constructor(trans?: (impl: TIn) => TOut) { + this._$trans = trans + } + + /** @internal */ + _$implement(impl: TIn): TOut { + return this._$trans?.(impl) || (impl as unknown as TOut) + } +} + +/** + * A manager that can implement multiple different trait behaviors + */ +export class TraitGroup { + private _$traits: WeakMap, unknown> = new WeakMap() + + implement( + traitBehavior: TraitBehavior, + impl: TIn, + ) { + const traitImpl = traitBehavior._$implement(impl) + this._$traits.set(traitBehavior, traitImpl) + } + + get( + traitBehavior: TraitBehavior, + ): TOut | undefined { + return this._$traits.get(traitBehavior) as TOut | undefined + } +} diff --git a/tsconfig.json b/tsconfig.json index 40f84bc..4b8c2dc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,8 +5,7 @@ "target": "es5", "esModuleInterop": true, "moduleResolution": "node", - "lib": ["ES6", "ES7", "DOM", "ESNext"], - "types": ["jest", "node"] + "lib": ["ES6", "ES7", "DOM", "ESNext"] }, "include": [ "src/**/*.ts"