From 44a6cf5eee44c6a898e2e551938b32aba534bb30 Mon Sep 17 00:00:00 2001 From: Xuguang Mei Date: Tue, 13 Aug 2019 19:13:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(core=20&=20build):=20=E5=B0=8F=E7=A8=8B?= =?UTF-8?q?=E5=BA=8F=20SFC=20=E4=B8=AD=E6=94=AF=E6=8C=81=20Vue.extend()=20?= =?UTF-8?q?=E5=86=99=E6=B3=95=20(#161)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * .vue 中支持 Vue.extend() 写法 * feat: 补充类型定义 --- .../compiler/script/babel-plugin-script.js | 9 +- packages/mars-core/package.json | 4 +- packages/mars-core/types/index.d.ts | 25 +++ packages/mars-core/types/mars.d.ts | 33 +++ packages/mars-core/types/swan.d.ts | 93 ++++++++ packages/mars-core/types/vue/index.d.ts | 40 ++++ packages/mars-core/types/vue/options.d.ts | 206 ++++++++++++++++++ packages/mars-core/types/vue/plugin.d.ts | 8 + packages/mars-core/types/vue/vnode.d.ts | 76 +++++++ packages/mars-core/types/vue/vue.d.ts | 128 +++++++++++ 10 files changed, 620 insertions(+), 2 deletions(-) create mode 100644 packages/mars-core/types/index.d.ts create mode 100644 packages/mars-core/types/mars.d.ts create mode 100644 packages/mars-core/types/swan.d.ts create mode 100644 packages/mars-core/types/vue/index.d.ts create mode 100644 packages/mars-core/types/vue/options.d.ts create mode 100644 packages/mars-core/types/vue/plugin.d.ts create mode 100644 packages/mars-core/types/vue/vnode.d.ts create mode 100644 packages/mars-core/types/vue/vue.d.ts diff --git a/packages/mars-build/src/compiler/script/babel-plugin-script.js b/packages/mars-build/src/compiler/script/babel-plugin-script.js index f7d13f3b..2a3debe3 100644 --- a/packages/mars-build/src/compiler/script/babel-plugin-script.js +++ b/packages/mars-build/src/compiler/script/babel-plugin-script.js @@ -140,7 +140,7 @@ const getPropertyVisitor = (t, options) => { function transfromSFCExport(t, declarationPath, options) { if (!t.isObjectExpression(declarationPath)) { - throw declarationPath.buildCodeFrameError('should export plain object in SFC'); + throw declarationPath.buildCodeFrameError('should export plain object or Vue.extend() in SFC'); } declarationPath.traverse(getPropertyVisitor(t, options)); @@ -173,6 +173,13 @@ module.exports = function getVisitor(options = {}) { visitor: { ExportDefaultDeclaration(path, state) { declarationPath = path.get('declaration'); + + // 只取 Vue.extend() 的参数部分 + if (t.isCallExpression(declarationPath)) { + const objectExpression = declarationPath.get('arguments')[0]; + declarationPath.replaceWith(objectExpression); + } + transfromSFCExport(t, declarationPath, options); exportPath = path; file.moduleType = 'esm'; diff --git a/packages/mars-core/package.json b/packages/mars-core/package.json index 68b026fd..61da10a6 100644 --- a/packages/mars-core/package.json +++ b/packages/mars-core/package.json @@ -3,5 +3,7 @@ "version": "0.2.18", "scripts": { "test": "jest --coverage" - } + }, + "main": "src/index.js", + "typings": "types/index.d.ts" } diff --git a/packages/mars-core/types/index.d.ts b/packages/mars-core/types/index.d.ts new file mode 100644 index 00000000..54f2230e --- /dev/null +++ b/packages/mars-core/types/index.d.ts @@ -0,0 +1,25 @@ +export {default as Vue} from './vue/index'; +import Vue from './vue/index'; +import {swanApp, marsApis} from './mars'; +import './swan'; + +export {swanApp} from './mars'; + +// 补充 this. 上的属性和方法 +declare module './vue/vue' { + interface Vue { + $myProperty: string; + $mpUpdated: (cb?: Function) => Promise; + $api: marsApis + } +} + +// 补充 Vue.extend() 参数对象中的 key +declare module './vue/options' { + interface ComponentOptions { + onLoad?: Function; + onReady?: Function; + onHide?: Function; + config?: any + } +} diff --git a/packages/mars-core/types/mars.d.ts b/packages/mars-core/types/mars.d.ts new file mode 100644 index 00000000..31be2d55 --- /dev/null +++ b/packages/mars-core/types/mars.d.ts @@ -0,0 +1,33 @@ +declare global { + const getApp: () => app; + const getCurrentPages: () => any[]; +} + +export interface swanApp { + $api: marsApis; +} + +export interface marsApis { + navigateToSmartProgram: (options: swan.swanApiOptionsNavigateToSmartProgram) => Promise; + getStorage: (options: swan.swanApiOptionsGetStorage) => Promise; + setStorage: (options: any) => Promise; + getBackgroundAudioManager: () => Promise; + request: (options: any) => Promise; + login: (options?: any) => Promise; + showModal: (options?: any) => Promise; + uploadFile: (options?: any) => Promise; + isLoginSync: (options?: any) => {isLogin: boolean}; + + redirectTo: (options?: any) => Promise; + navigateTo: (options?: any) => Promise; + navigateBack: (options?: any) => Promise; + + chooseImage: (options?: any) => Promise; + + createSelectorQuery: () => { + select: (options?: any) => { + boundingClientRect: () => void; + }, + exec: (options?: any) => void; + }; +} \ No newline at end of file diff --git a/packages/mars-core/types/swan.d.ts b/packages/mars-core/types/swan.d.ts new file mode 100644 index 00000000..dc16f0a3 --- /dev/null +++ b/packages/mars-core/types/swan.d.ts @@ -0,0 +1,93 @@ +declare namespace swan { + const setPageInfo: (options: swanApiOptionsSetPageInfo) => void; + const showLoading: (options: swanApiOptionsShowLoading) => void; + const hideLoading: () => void; + const showToast: (options: swanApiOptionsShowToast) => void; + const getSystemInfo: (options: swanApiOptionsGetSystemInfo) => void; + const showFavoriteGuide: () => void; + const navigateToSmartProgram: (options: swanApiOptionsNavigateToSmartProgram) => void; + const getStorage: (options: swanApiOptionsGetStorage) => void; + + interface swanApiOptionsBase { + success?: (res: any) => any; + fail?: () => {}; + complete?: () => {}; + } + + interface swanApiOptionsSetPageInfo extends swanApiOptionsBase { + title: string; + keywords: string; + description: string; + releaseDate?: string; + articleTitle?: string; + image?: string | string[]; + video?: any; + visit?: any; + likes?: string; + comments?: string; + collects?: string; + shares?: string; + followers?: string; + } + + interface swanApiOptionsShowLoading extends swanApiOptionsBase { + title: string; + mask?: boolean; + } + + interface swanApiOptionsShowToast extends swanApiOptionsBase { + title: string; + icon?: string; + image?: string; + duration?: number; + mask?: boolean; + } + + type swanApiOptionsGetSystemInfo = { + success: (res: systemInfo) => void; + fail?: () => {}; + complete?: () => {}; + } + + interface swanApiOptionsNavigateToSmartProgram extends swanApiOptionsBase { + appKey: string; + path: string; + extraData: { + [key: string]: any + } + } + interface swanApiOptionsGetStorage extends swanApiOptionsBase { + key: string; + } + + interface systemInfo { + brand: string; + model: string; + pixelRatio: string; + screenWidth: string; + screenHeight: string; + windowWidth: string; + windowHeight: string; + statusBarHeight: string; + navigationBarHeight: string; + language: string; + version: string; + system: string; + platform: string; + fontSizeSetting: string; + SDKVersion: string; + host: string; + cacheLocation: string; + swanNativeVersion: string; + devicePixelRatio: string + } + + interface backgroundAudioManager { + onEnded: (res: any) => void; + onError: (res: any) => void; + play: () => void; + pause: () => void; + src: string; + } +} + diff --git a/packages/mars-core/types/vue/index.d.ts b/packages/mars-core/types/vue/index.d.ts new file mode 100644 index 00000000..b0e24d76 --- /dev/null +++ b/packages/mars-core/types/vue/index.d.ts @@ -0,0 +1,40 @@ +import { Vue } from "./vue"; + +export default Vue; + +export as namespace Vue; + +export { + CreateElement, + VueConstructor +} from "./vue"; + +export { + Component, + AsyncComponent, + ComponentOptions, + FunctionalComponentOptions, + RenderContext, + PropType, + PropOptions, + ComputedOptions, + WatchHandler, + WatchOptions, + WatchOptionsWithHandler, + DirectiveFunction, + DirectiveOptions +} from "./options"; + +export { + PluginFunction, + PluginObject +} from "./plugin"; + +export { + VNodeChildren, + VNodeChildrenArrayContents, + VNode, + VNodeComponentOptions, + VNodeData, + VNodeDirective +} from "./vnode"; diff --git a/packages/mars-core/types/vue/options.d.ts b/packages/mars-core/types/vue/options.d.ts new file mode 100644 index 00000000..3fe7a9b7 --- /dev/null +++ b/packages/mars-core/types/vue/options.d.ts @@ -0,0 +1,206 @@ +import { Vue, CreateElement, CombinedVueInstance } from "./vue"; +import { VNode, VNodeData, VNodeDirective, NormalizedScopedSlot } from "./vnode"; + +type Constructor = { + new (...args: any[]): any; +} + +// we don't support infer props in async component +// N.B. ComponentOptions is contravariant, the default generic should be bottom type +export type Component, Methods=DefaultMethods, Computed=DefaultComputed, Props=DefaultProps> = + | typeof Vue + | FunctionalComponentOptions + | ComponentOptions + +interface EsModuleComponent { + default: Component +} + +export type AsyncComponent, Methods=DefaultMethods, Computed=DefaultComputed, Props=DefaultProps> + = AsyncComponentPromise + | AsyncComponentFactory + +export type AsyncComponentPromise, Methods=DefaultMethods, Computed=DefaultComputed, Props=DefaultProps> = ( + resolve: (component: Component) => void, + reject: (reason?: any) => void +) => Promise | void; + +export type AsyncComponentFactory, Methods=DefaultMethods, Computed=DefaultComputed, Props=DefaultProps> = () => { + component: AsyncComponentPromise; + loading?: Component | EsModuleComponent; + error?: Component | EsModuleComponent; + delay?: number; + timeout?: number; +} + +/** + * When the `Computed` type parameter on `ComponentOptions` is inferred, + * it should have a property with the return type of every get-accessor. + * Since there isn't a way to query for the return type of a function, we allow TypeScript + * to infer from the shape of `Accessors` and work backwards. + */ +export type Accessors = { + [K in keyof T]: (() => T[K]) | ComputedOptions +} + +type DataDef = Data | ((this: Readonly & V) => Data) +/** + * This type should be used when an array of strings is used for a component's `props` value. + */ +export type ThisTypedComponentOptionsWithArrayProps = + object & + ComponentOptions, V>, Methods, Computed, PropNames[], Record> & + ThisType>>>; + +/** + * This type should be used when an object mapped to `PropOptions` is used for a component's `props` value. + */ +export type ThisTypedComponentOptionsWithRecordProps = + object & + ComponentOptions, Methods, Computed, RecordPropsDefinition, Props> & + ThisType>>; + +type DefaultData = object | ((this: V) => object); +type DefaultProps = Record; +type DefaultMethods = { [key: string]: (this: V, ...args: any[]) => any }; +type DefaultComputed = { [key: string]: any }; +export interface ComponentOptions< + V extends Vue, + Data=DefaultData, + Methods=DefaultMethods, + Computed=DefaultComputed, + PropsDef=PropsDefinition, + Props=DefaultProps> { + data?: Data; + props?: PropsDef; + propsData?: object; + computed?: Accessors; + methods?: Methods; + watch?: Record | WatchHandler | string>; + + el?: Element | string; + template?: string; + // hack is for functional component type inference, should not be used in user code + render?(createElement: CreateElement, hack: RenderContext): VNode; + renderError?(createElement: CreateElement, err: Error): VNode; + staticRenderFns?: ((createElement: CreateElement) => VNode)[]; + + beforeCreate?(this: V): void; + created?(): void; + beforeDestroy?(): void; + destroyed?(): void; + beforeMount?(): void; + mounted?(): void; + beforeUpdate?(): void; + updated?(): void; + activated?(): void; + deactivated?(): void; + errorCaptured?(err: Error, vm: Vue, info: string): boolean | void; + serverPrefetch?(this: V): Promise; + + directives?: { [key: string]: DirectiveFunction | DirectiveOptions }; + components?: { [key: string]: Component | AsyncComponent }; + transitions?: { [key: string]: object }; + filters?: { [key: string]: Function }; + + provide?: object | (() => object); + inject?: InjectOptions; + + model?: { + prop?: string; + event?: string; + }; + + parent?: Vue; + mixins?: (ComponentOptions | typeof Vue)[]; + name?: string; + // TODO: support properly inferred 'extends' + extends?: ComponentOptions | typeof Vue; + delimiters?: [string, string]; + comments?: boolean; + inheritAttrs?: boolean; +} + +export interface FunctionalComponentOptions> { + name?: string; + props?: PropDefs; + model?: { + prop?: string; + event?: string; + }; + inject?: InjectOptions; + functional: boolean; + render?(this: undefined, createElement: CreateElement, context: RenderContext): VNode | VNode[]; +} + +export interface RenderContext { + props: Props; + children: VNode[]; + slots(): any; + data: VNodeData; + parent: Vue; + listeners: { [key: string]: Function | Function[] }; + scopedSlots: { [key: string]: NormalizedScopedSlot }; + injections: any +} + +export type Prop = { (): T } | { new(...args: any[]): T & object } | { new(...args: string[]): Function } + +export type PropType = Prop | Prop[]; + +export type PropValidator = PropOptions | PropType; + +export interface PropOptions { + type?: PropType; + required?: boolean; + default?: T | null | undefined | (() => T | null | undefined); + validator?(value: T): boolean; +} + +export type RecordPropsDefinition = { + [K in keyof T]: PropValidator +} +export type ArrayPropsDefinition = (keyof T)[]; +export type PropsDefinition = ArrayPropsDefinition | RecordPropsDefinition; + +export interface ComputedOptions { + get?(): T; + set?(value: T): void; + cache?: boolean; +} + +export type WatchHandler = (val: T, oldVal: T) => void; + +export interface WatchOptions { + deep?: boolean; + immediate?: boolean; +} + +export interface WatchOptionsWithHandler extends WatchOptions { + handler: WatchHandler; +} + +export interface DirectiveBinding extends Readonly { + readonly modifiers: { [key: string]: boolean }; +} + +export type DirectiveFunction = ( + el: HTMLElement, + binding: DirectiveBinding, + vnode: VNode, + oldVnode: VNode +) => void; + +export interface DirectiveOptions { + bind?: DirectiveFunction; + inserted?: DirectiveFunction; + update?: DirectiveFunction; + componentUpdated?: DirectiveFunction; + unbind?: DirectiveFunction; +} + +export type InjectKey = string | symbol; + +export type InjectOptions = { + [key: string]: InjectKey | { from?: InjectKey, default?: any } +} | string[]; diff --git a/packages/mars-core/types/vue/plugin.d.ts b/packages/mars-core/types/vue/plugin.d.ts new file mode 100644 index 00000000..5741f862 --- /dev/null +++ b/packages/mars-core/types/vue/plugin.d.ts @@ -0,0 +1,8 @@ +import { Vue as _Vue } from "./vue"; + +export type PluginFunction = (Vue: typeof _Vue, options?: T) => void; + +export interface PluginObject { + install: PluginFunction; + [key: string]: any; +} diff --git a/packages/mars-core/types/vue/vnode.d.ts b/packages/mars-core/types/vue/vnode.d.ts new file mode 100644 index 00000000..dc4470ff --- /dev/null +++ b/packages/mars-core/types/vue/vnode.d.ts @@ -0,0 +1,76 @@ +import { Vue } from "./vue"; + +export type ScopedSlot = (props: any) => ScopedSlotReturnValue; +type ScopedSlotReturnValue = VNode | string | boolean | null | undefined | ScopedSlotReturnArray; +interface ScopedSlotReturnArray extends Array {} + +// Scoped slots are guaranteed to return Array of VNodes starting in 2.6 +export type NormalizedScopedSlot = (props: any) => ScopedSlotChildren; +export type ScopedSlotChildren = VNode[] | undefined; + +// Relaxed type compatible with $createElement +export type VNodeChildren = VNodeChildrenArrayContents | [ScopedSlot] | string | boolean | null | undefined; +export interface VNodeChildrenArrayContents extends Array {} + +export interface VNode { + tag?: string; + data?: VNodeData; + children?: VNode[]; + text?: string; + elm?: Node; + ns?: string; + context?: Vue; + key?: string | number; + componentOptions?: VNodeComponentOptions; + componentInstance?: Vue; + parent?: VNode; + raw?: boolean; + isStatic?: boolean; + isRootInsert: boolean; + isComment: boolean; +} + +export interface VNodeComponentOptions { + Ctor: typeof Vue; + propsData?: object; + listeners?: object; + children?: VNode[]; + tag?: string; +} + +export interface VNodeData { + key?: string | number; + slot?: string; + scopedSlots?: { [key: string]: ScopedSlot | undefined }; + ref?: string; + refInFor?: boolean; + tag?: string; + staticClass?: string; + class?: any; + staticStyle?: { [key: string]: any }; + style?: string | object[] | object; + props?: { [key: string]: any }; + attrs?: { [key: string]: any }; + domProps?: { [key: string]: any }; + hook?: { [key: string]: Function }; + on?: { [key: string]: Function | Function[] }; + nativeOn?: { [key: string]: Function | Function[] }; + transition?: object; + show?: boolean; + inlineTemplate?: { + render: Function; + staticRenderFns: Function[]; + }; + directives?: VNodeDirective[]; + keepAlive?: boolean; +} + +export interface VNodeDirective { + name: string; + value?: any; + oldValue?: any; + expression?: any; + arg?: string; + oldArg?: string; + modifiers?: { [key: string]: boolean }; +} diff --git a/packages/mars-core/types/vue/vue.d.ts b/packages/mars-core/types/vue/vue.d.ts new file mode 100644 index 00000000..204f9cca --- /dev/null +++ b/packages/mars-core/types/vue/vue.d.ts @@ -0,0 +1,128 @@ +import { + Component, + AsyncComponent, + ComponentOptions, + FunctionalComponentOptions, + WatchOptionsWithHandler, + WatchHandler, + DirectiveOptions, + DirectiveFunction, + RecordPropsDefinition, + ThisTypedComponentOptionsWithArrayProps, + ThisTypedComponentOptionsWithRecordProps, + WatchOptions, +} from "./options"; +import { VNode, VNodeData, VNodeChildren, NormalizedScopedSlot } from "./vnode"; +import { PluginFunction, PluginObject } from "./plugin"; + +export interface CreateElement { + (tag?: string | Component | AsyncComponent | (() => Component), children?: VNodeChildren): VNode; + (tag?: string | Component | AsyncComponent | (() => Component), data?: VNodeData, children?: VNodeChildren): VNode; +} + +export interface Vue { + readonly $el: Element; + readonly $options: ComponentOptions; + readonly $parent: Vue; + readonly $root: Vue; + readonly $children: Vue[]; + readonly $refs: { [key: string]: Vue | Element | Vue[] | Element[] }; + readonly $slots: { [key: string]: VNode[] | undefined }; + readonly $scopedSlots: { [key: string]: NormalizedScopedSlot | undefined }; + readonly $isServer: boolean; + readonly $data: Record; + readonly $props: Record; + readonly $ssrContext: any; + readonly $vnode: VNode; + readonly $attrs: Record; + readonly $listeners: Record; + + $mount(elementOrSelector?: Element | string, hydrating?: boolean): this; + $forceUpdate(): void; + $destroy(): void; + $set: typeof Vue.set; + $delete: typeof Vue.delete; + $watch( + expOrFn: string, + callback: (this: this, n: any, o: any) => void, + options?: WatchOptions + ): (() => void); + $watch( + expOrFn: (this: this) => T, + callback: (this: this, n: T, o: T) => void, + options?: WatchOptions + ): (() => void); + $on(event: string | string[], callback: Function): this; + $once(event: string | string[], callback: Function): this; + $off(event?: string | string[], callback?: Function): this; + $emit(event: string, ...args: any[]): this; + $nextTick(callback: (this: this) => void): void; + $nextTick(): Promise; + $createElement: CreateElement; +} + +export type CombinedVueInstance = Data & Methods & Computed & Props & Instance; +export type ExtendedVue = VueConstructor & Vue>; + +export interface VueConfiguration { + silent: boolean; + optionMergeStrategies: any; + devtools: boolean; + productionTip: boolean; + performance: boolean; + errorHandler(err: Error, vm: Vue, info: string): void; + warnHandler(msg: string, vm: Vue, trace: string): void; + ignoredElements: (string | RegExp)[]; + keyCodes: { [key: string]: number | number[] }; + async: boolean; +} + +export interface VueConstructor { + new (options?: ThisTypedComponentOptionsWithArrayProps): CombinedVueInstance>; + // ideally, the return type should just contain Props, not Record. But TS requires to have Base constructors with the same return type. + new (options?: ThisTypedComponentOptionsWithRecordProps): CombinedVueInstance>; + new (options?: ComponentOptions): CombinedVueInstance>; + + extend(options?: ThisTypedComponentOptionsWithArrayProps): ExtendedVue>; + extend(options?: ThisTypedComponentOptionsWithRecordProps): ExtendedVue; + extend(definition: FunctionalComponentOptions, PropNames[]>): ExtendedVue>; + extend(definition: FunctionalComponentOptions>): ExtendedVue; + extend(options?: ComponentOptions): ExtendedVue; + + nextTick(callback: (this: T) => void, context?: T): void; + nextTick(): Promise + set(object: object, key: string | number, value: T): T; + set(array: T[], key: number, value: T): T; + delete(object: object, key: string | number): void; + delete(array: T[], key: number): void; + + directive( + id: string, + definition?: DirectiveOptions | DirectiveFunction + ): DirectiveOptions; + filter(id: string, definition?: Function): Function; + + component(id: string): VueConstructor; + component(id: string, constructor: VC): VC; + component(id: string, definition: AsyncComponent): ExtendedVue; + component(id: string, definition?: ThisTypedComponentOptionsWithArrayProps): ExtendedVue>; + component(id: string, definition?: ThisTypedComponentOptionsWithRecordProps): ExtendedVue; + component(id: string, definition: FunctionalComponentOptions, PropNames[]>): ExtendedVue>; + component(id: string, definition: FunctionalComponentOptions>): ExtendedVue; + component(id: string, definition?: ComponentOptions): ExtendedVue; + + use(plugin: PluginObject | PluginFunction, options?: T): VueConstructor; + use(plugin: PluginObject | PluginFunction, ...options: any[]): VueConstructor; + mixin(mixin: VueConstructor | ComponentOptions): VueConstructor; + compile(template: string): { + render(createElement: typeof Vue.prototype.$createElement): VNode; + staticRenderFns: (() => VNode)[]; + }; + + observable(obj: T): T; + + config: VueConfiguration; + version: string; +} + +export const Vue: VueConstructor;