From 91e5a13e6b7f26ea8a84305286c29b67ba9e4c48 Mon Sep 17 00:00:00 2001 From: skirtle <65301168+skirtles-code@users.noreply.github.com> Date: Tue, 8 Oct 2024 15:33:23 +0100 Subject: [PATCH] Refactor into multiple files --- .../src/__tests__/base.spec.ts | 245 ++++++++ ...-vnode-utils.spec.ts => iterators.spec.ts} | 243 +------- packages/vue-vnode-utils/src/base.ts | 95 ++++ packages/vue-vnode-utils/src/index.ts | 524 +----------------- packages/vue-vnode-utils/src/iterators.ts | 414 ++++++++++++++ 5 files changed, 791 insertions(+), 730 deletions(-) create mode 100644 packages/vue-vnode-utils/src/__tests__/base.spec.ts rename packages/vue-vnode-utils/src/__tests__/{vue-vnode-utils.spec.ts => iterators.spec.ts} (80%) create mode 100644 packages/vue-vnode-utils/src/base.ts create mode 100644 packages/vue-vnode-utils/src/iterators.ts diff --git a/packages/vue-vnode-utils/src/__tests__/base.spec.ts b/packages/vue-vnode-utils/src/__tests__/base.spec.ts new file mode 100644 index 0000000..cb4f850 --- /dev/null +++ b/packages/vue-vnode-utils/src/__tests__/base.spec.ts @@ -0,0 +1,245 @@ +import { describe, expect, it } from 'vitest' +import { + createCommentVNode, + createStaticVNode, + createVNode, + createTextVNode, + defineAsyncComponent, + Fragment, + h, + Text +} from 'vue' +import { + getText, + getType, + isComment, + isComponent, + isElement, + isFragment, + isFunctionalComponent, + isStatefulComponent, + isStatic, + isText +} from '../base' + +describe('isComment', () => { + it('isComment - 194a', () => { + expect(isComment(undefined)).toBe(true) + expect(isComment(null)).toBe(true) + expect(isComment(false)).toBe(true) + expect(isComment(true)).toBe(true) + expect(isComment(createCommentVNode('Text'))).toBe(true) + + expect(isComment('')).toBe(false) + expect(isComment(0)).toBe(false) + expect(isComment(NaN)).toBe(false) + expect(isComment(0n)).toBe(false) + expect(isComment(createTextVNode('Text'))).toBe(false) + expect(isComment({})).toBe(false) + expect(isComment([])).toBe(false) + }) +}) + +describe('isComponent', () => { + it('isComponent - b049', () => { + expect(isComponent(h({}))).toBe(true) + expect(isComponent(h(() => null))).toBe(true) + expect(isComponent(h(defineAsyncComponent(() => Promise.resolve({}))))).toBe(true) + + expect(isComponent(h('div'))).toBe(false) + expect(isComponent(createTextVNode('Text'))).toBe(false) + expect(isComponent(createCommentVNode('Text'))).toBe(false) + expect(isComponent('')).toBe(false) + expect(isComponent({})).toBe(false) + expect(isComponent([])).toBe(false) + expect(isComponent(null)).toBe(false) + expect(isComponent(undefined)).toBe(false) + expect(isComponent(false)).toBe(false) + expect(isComponent(true)).toBe(false) + expect(isComponent(0)).toBe(false) + expect(isComponent(7)).toBe(false) + }) +}) + +describe('isFunctionalComponent', () => { + it('isFunctionalComponent - 1af7', () => { + expect(isFunctionalComponent(h(() => null))).toBe(true) + + expect(isFunctionalComponent(h({}))).toBe(false) + expect(isFunctionalComponent(h(defineAsyncComponent(() => Promise.resolve({}))))).toBe(false) + expect(isFunctionalComponent(h('div'))).toBe(false) + expect(isFunctionalComponent(createTextVNode('Text'))).toBe(false) + expect(isFunctionalComponent(createCommentVNode('Text'))).toBe(false) + expect(isFunctionalComponent('')).toBe(false) + expect(isFunctionalComponent({})).toBe(false) + expect(isFunctionalComponent([])).toBe(false) + expect(isFunctionalComponent(null)).toBe(false) + expect(isFunctionalComponent(undefined)).toBe(false) + expect(isFunctionalComponent(false)).toBe(false) + expect(isFunctionalComponent(true)).toBe(false) + expect(isFunctionalComponent(0)).toBe(false) + expect(isFunctionalComponent(7)).toBe(false) + }) +}) + +describe('isStatefulComponent', () => { + it('isStatefulComponent - ecee', () => { + expect(isStatefulComponent(h({}))).toBe(true) + expect(isStatefulComponent(h(defineAsyncComponent(() => Promise.resolve({}))))).toBe(true) + + expect(isStatefulComponent(h(() => null))).toBe(false) + expect(isStatefulComponent(h('div'))).toBe(false) + expect(isStatefulComponent(createTextVNode('Text'))).toBe(false) + expect(isStatefulComponent(createCommentVNode('Text'))).toBe(false) + expect(isStatefulComponent('')).toBe(false) + expect(isStatefulComponent({})).toBe(false) + expect(isStatefulComponent([])).toBe(false) + expect(isStatefulComponent(null)).toBe(false) + expect(isStatefulComponent(undefined)).toBe(false) + expect(isStatefulComponent(false)).toBe(false) + expect(isStatefulComponent(true)).toBe(false) + expect(isStatefulComponent(0)).toBe(false) + expect(isStatefulComponent(7)).toBe(false) + }) +}) + +describe('isElement', () => { + it('isElement - aa0d', () => { + expect(isElement(h('div'))).toBe(true) + + expect(isElement(h({}))).toBe(false) + expect(isElement(h(() => null))).toBe(false) + expect(isElement(h(defineAsyncComponent(() => Promise.resolve({}))))).toBe(false) + expect(isElement(createTextVNode('Text'))).toBe(false) + expect(isElement(createCommentVNode('Text'))).toBe(false) + expect(isElement(createVNode(Fragment, null, [h('div')]))).toBe(false) + expect(isElement('')).toBe(false) + expect(isElement('string')).toBe(false) + expect(isElement({})).toBe(false) + expect(isElement([])).toBe(false) + expect(isElement(null)).toBe(false) + expect(isElement(undefined)).toBe(false) + expect(isElement(false)).toBe(false) + expect(isElement(true)).toBe(false) + expect(isElement(0)).toBe(false) + expect(isElement(7)).toBe(false) + }) +}) + +describe('isFragment', () => { + it('isFragment - d88b', () => { + expect(isFragment([])).toBe(true) + expect(isFragment(createVNode(Fragment, null, []))).toBe(true) + + expect(isFragment(h({}))).toBe(false) + expect(isFragment(h(() => null))).toBe(false) + expect(isFragment(h(defineAsyncComponent(() => Promise.resolve({}))))).toBe(false) + expect(isFragment(h('div'))).toBe(false) + expect(isFragment(h('div', null, []))).toBe(false) + expect(isFragment(createTextVNode('Text'))).toBe(false) + expect(isFragment(createCommentVNode('Text'))).toBe(false) + expect(isFragment('')).toBe(false) + expect(isFragment('string')).toBe(false) + expect(isFragment({})).toBe(false) + expect(isFragment(null)).toBe(false) + expect(isFragment(undefined)).toBe(false) + expect(isFragment(false)).toBe(false) + expect(isFragment(true)).toBe(false) + expect(isFragment(0)).toBe(false) + expect(isFragment(7)).toBe(false) + }) +}) + +describe('isText', () => { + it('isText - 7952', () => { + expect(isText('')).toBe(true) + expect(isText('string')).toBe(true) + expect(isText(0)).toBe(true) + expect(isText(7)).toBe(true) + expect(isText(createTextVNode('Text'))).toBe(true) + + expect(isText(h({}))).toBe(false) + expect(isText(h(() => null))).toBe(false) + expect(isText(h(defineAsyncComponent(() => Promise.resolve({}))))).toBe(false) + expect(isText(h('div'))).toBe(false) + expect(isText(h('div', null, []))).toBe(false) + expect(isText(createCommentVNode('Text'))).toBe(false) + expect(isText(createVNode(Fragment, null, []))).toBe(false) + expect(isText({})).toBe(false) + expect(isText([])).toBe(false) + expect(isText(null)).toBe(false) + expect(isText(undefined)).toBe(false) + expect(isText(false)).toBe(false) + expect(isText(true)).toBe(false) + }) +}) + +describe('isStatic', () => { + it('isStatic - aabf', () => { + expect(isStatic(createStaticVNode('
', 1))).toBe(true) + + expect(isStatic(h({}))).toBe(false) + expect(isStatic(h(() => null))).toBe(false) + expect(isStatic(h(defineAsyncComponent(() => Promise.resolve({}))))).toBe(false) + expect(isStatic(h('div'))).toBe(false) + expect(isStatic(h('div', null, []))).toBe(false) + expect(isStatic(createTextVNode('Text'))).toBe(false) + expect(isStatic(createCommentVNode('Text'))).toBe(false) + expect(isStatic(createVNode(Fragment, null, []))).toBe(false) + expect(isStatic('')).toBe(false) + expect(isStatic('string')).toBe(false) + expect(isStatic({})).toBe(false) + expect(isStatic([])).toBe(false) + expect(isStatic(null)).toBe(false) + expect(isStatic(undefined)).toBe(false) + expect(isStatic(false)).toBe(false) + expect(isStatic(true)).toBe(false) + expect(isStatic(0)).toBe(false) + expect(isStatic(7)).toBe(false) + }) +}) + +describe('getText', () => { + it('getText - 50eb', () => { + expect(getText('')).toBe('') + expect(getText('Text')).toBe('Text') + expect(getText(0)).toBe('0') + expect(getText(7)).toBe('7') + expect(getText(createTextVNode('Text'))).toBe('Text') + + expect(getText(null as any)).toBe(undefined) + expect(getText(undefined as any)).toBe(undefined) + expect(getText({} as any)).toBe(undefined) + expect(getText([] as any)).toBe(undefined) + expect(getText(true as any)).toBe(undefined) + expect(getText(false as any)).toBe(undefined) + expect(getText(h('div'))).toBe(undefined) + expect(getText(h({}))).toBe(undefined) + }) +}) + +describe('getType', () => { + it('getType - ba43', () => { + expect(getType(h({}))).toBe('component') + expect(getType(h(() => null))).toBe('component') + expect(getType(h(defineAsyncComponent(() => Promise.resolve({}))))).toBe('component') + expect(getType(h('div'))).toBe('element') + expect(getType(h('div', null, []))).toBe('element') + expect(getType(createTextVNode('Text'))).toBe('text') + expect(getType(createCommentVNode('Text'))).toBe('comment') + expect(getType(createVNode(Fragment, null, []))).toBe('fragment') + expect(getType(createStaticVNode('
', 1))).toBe('static') + expect(getType('')).toBe('text') + expect(getType('string')).toBe('text') + expect(getType({})).toBe(undefined) + expect(getType([])).toBe('fragment') + expect(getType(null)).toBe('comment') + expect(getType(undefined)).toBe('comment') + expect(getType(false)).toBe('comment') + expect(getType(true)).toBe('comment') + expect(getType(0)).toBe('text') + expect(getType(7)).toBe('text') + expect(getType(Fragment)).toBe(undefined) + expect(getType(Text)).toBe(undefined) + }) +}) diff --git a/packages/vue-vnode-utils/src/__tests__/vue-vnode-utils.spec.ts b/packages/vue-vnode-utils/src/__tests__/iterators.spec.ts similarity index 80% rename from packages/vue-vnode-utils/src/__tests__/vue-vnode-utils.spec.ts rename to packages/vue-vnode-utils/src/__tests__/iterators.spec.ts index 37fa37d..57ca3fa 100644 --- a/packages/vue-vnode-utils/src/__tests__/vue-vnode-utils.spec.ts +++ b/packages/vue-vnode-utils/src/__tests__/iterators.spec.ts @@ -6,7 +6,6 @@ import { createStaticVNode, createVNode, createTextVNode, - defineAsyncComponent, Fragment, h, isVNode, @@ -16,6 +15,14 @@ import { type VNodeArrayChildren, type VNodeChild } from 'vue' +import { + getText, + isComment, + isComponent, + isElement, + isFragment, + isText +} from '../base' import { addProps, ALL_VNODES, @@ -25,21 +32,11 @@ import { everyChild, extractSingleChild, findChild, - getText, - getType, - isComment, - isComponent, - isElement, isEmpty, - isFragment, - isFunctionalComponent, - isStatefulComponent, - isStatic, - isText, replaceChildren, SKIP_COMMENTS, someChild -} from '../index' +} from '../iterators' type TreeNode = string | number | null | undefined | boolean | [string | typeof Fragment | Component, (Record | null)?, TreeNode[]?] @@ -1583,225 +1580,3 @@ describe('extractSingleChild', () => { expect(out).toBe(node) }) }) - -describe('isComment', () => { - it('isComment - 194a', () => { - expect(isComment(undefined)).toBe(true) - expect(isComment(null)).toBe(true) - expect(isComment(false)).toBe(true) - expect(isComment(true)).toBe(true) - expect(isComment(createCommentVNode('Text'))).toBe(true) - - expect(isComment('')).toBe(false) - expect(isComment(0)).toBe(false) - expect(isComment(NaN)).toBe(false) - expect(isComment(0n)).toBe(false) - expect(isComment(createTextVNode('Text'))).toBe(false) - expect(isComment({})).toBe(false) - expect(isComment([])).toBe(false) - }) -}) - -describe('isComponent', () => { - it('isComponent - b049', () => { - expect(isComponent(h({}))).toBe(true) - expect(isComponent(h(() => null))).toBe(true) - expect(isComponent(h(defineAsyncComponent(() => Promise.resolve({}))))).toBe(true) - - expect(isComponent(h('div'))).toBe(false) - expect(isComponent(createTextVNode('Text'))).toBe(false) - expect(isComponent(createCommentVNode('Text'))).toBe(false) - expect(isComponent('')).toBe(false) - expect(isComponent({})).toBe(false) - expect(isComponent([])).toBe(false) - expect(isComponent(null)).toBe(false) - expect(isComponent(undefined)).toBe(false) - expect(isComponent(false)).toBe(false) - expect(isComponent(true)).toBe(false) - expect(isComponent(0)).toBe(false) - expect(isComponent(7)).toBe(false) - }) -}) - -describe('isFunctionalComponent', () => { - it('isFunctionalComponent - 1af7', () => { - expect(isFunctionalComponent(h(() => null))).toBe(true) - - expect(isFunctionalComponent(h({}))).toBe(false) - expect(isFunctionalComponent(h(defineAsyncComponent(() => Promise.resolve({}))))).toBe(false) - expect(isFunctionalComponent(h('div'))).toBe(false) - expect(isFunctionalComponent(createTextVNode('Text'))).toBe(false) - expect(isFunctionalComponent(createCommentVNode('Text'))).toBe(false) - expect(isFunctionalComponent('')).toBe(false) - expect(isFunctionalComponent({})).toBe(false) - expect(isFunctionalComponent([])).toBe(false) - expect(isFunctionalComponent(null)).toBe(false) - expect(isFunctionalComponent(undefined)).toBe(false) - expect(isFunctionalComponent(false)).toBe(false) - expect(isFunctionalComponent(true)).toBe(false) - expect(isFunctionalComponent(0)).toBe(false) - expect(isFunctionalComponent(7)).toBe(false) - }) -}) - -describe('isStatefulComponent', () => { - it('isStatefulComponent - ecee', () => { - expect(isStatefulComponent(h({}))).toBe(true) - expect(isStatefulComponent(h(defineAsyncComponent(() => Promise.resolve({}))))).toBe(true) - - expect(isStatefulComponent(h(() => null))).toBe(false) - expect(isStatefulComponent(h('div'))).toBe(false) - expect(isStatefulComponent(createTextVNode('Text'))).toBe(false) - expect(isStatefulComponent(createCommentVNode('Text'))).toBe(false) - expect(isStatefulComponent('')).toBe(false) - expect(isStatefulComponent({})).toBe(false) - expect(isStatefulComponent([])).toBe(false) - expect(isStatefulComponent(null)).toBe(false) - expect(isStatefulComponent(undefined)).toBe(false) - expect(isStatefulComponent(false)).toBe(false) - expect(isStatefulComponent(true)).toBe(false) - expect(isStatefulComponent(0)).toBe(false) - expect(isStatefulComponent(7)).toBe(false) - }) -}) - -describe('isElement', () => { - it('isElement - aa0d', () => { - expect(isElement(h('div'))).toBe(true) - - expect(isElement(h({}))).toBe(false) - expect(isElement(h(() => null))).toBe(false) - expect(isElement(h(defineAsyncComponent(() => Promise.resolve({}))))).toBe(false) - expect(isElement(createTextVNode('Text'))).toBe(false) - expect(isElement(createCommentVNode('Text'))).toBe(false) - expect(isElement(createVNode(Fragment, null, [h('div')]))).toBe(false) - expect(isElement('')).toBe(false) - expect(isElement('string')).toBe(false) - expect(isElement({})).toBe(false) - expect(isElement([])).toBe(false) - expect(isElement(null)).toBe(false) - expect(isElement(undefined)).toBe(false) - expect(isElement(false)).toBe(false) - expect(isElement(true)).toBe(false) - expect(isElement(0)).toBe(false) - expect(isElement(7)).toBe(false) - }) -}) - -describe('isFragment', () => { - it('isFragment - d88b', () => { - expect(isFragment([])).toBe(true) - expect(isFragment(createVNode(Fragment, null, []))).toBe(true) - - expect(isFragment(h({}))).toBe(false) - expect(isFragment(h(() => null))).toBe(false) - expect(isFragment(h(defineAsyncComponent(() => Promise.resolve({}))))).toBe(false) - expect(isFragment(h('div'))).toBe(false) - expect(isFragment(h('div', null, []))).toBe(false) - expect(isFragment(createTextVNode('Text'))).toBe(false) - expect(isFragment(createCommentVNode('Text'))).toBe(false) - expect(isFragment('')).toBe(false) - expect(isFragment('string')).toBe(false) - expect(isFragment({})).toBe(false) - expect(isFragment(null)).toBe(false) - expect(isFragment(undefined)).toBe(false) - expect(isFragment(false)).toBe(false) - expect(isFragment(true)).toBe(false) - expect(isFragment(0)).toBe(false) - expect(isFragment(7)).toBe(false) - }) -}) - -describe('isText', () => { - it('isText - 7952', () => { - expect(isText('')).toBe(true) - expect(isText('string')).toBe(true) - expect(isText(0)).toBe(true) - expect(isText(7)).toBe(true) - expect(isText(createTextVNode('Text'))).toBe(true) - - expect(isText(h({}))).toBe(false) - expect(isText(h(() => null))).toBe(false) - expect(isText(h(defineAsyncComponent(() => Promise.resolve({}))))).toBe(false) - expect(isText(h('div'))).toBe(false) - expect(isText(h('div', null, []))).toBe(false) - expect(isText(createCommentVNode('Text'))).toBe(false) - expect(isText(createVNode(Fragment, null, []))).toBe(false) - expect(isText({})).toBe(false) - expect(isText([])).toBe(false) - expect(isText(null)).toBe(false) - expect(isText(undefined)).toBe(false) - expect(isText(false)).toBe(false) - expect(isText(true)).toBe(false) - }) -}) - -describe('isStatic', () => { - it('isStatic - aabf', () => { - expect(isStatic(createStaticVNode('
', 1))).toBe(true) - - expect(isStatic(h({}))).toBe(false) - expect(isStatic(h(() => null))).toBe(false) - expect(isStatic(h(defineAsyncComponent(() => Promise.resolve({}))))).toBe(false) - expect(isStatic(h('div'))).toBe(false) - expect(isStatic(h('div', null, []))).toBe(false) - expect(isStatic(createTextVNode('Text'))).toBe(false) - expect(isStatic(createCommentVNode('Text'))).toBe(false) - expect(isStatic(createVNode(Fragment, null, []))).toBe(false) - expect(isStatic('')).toBe(false) - expect(isStatic('string')).toBe(false) - expect(isStatic({})).toBe(false) - expect(isStatic([])).toBe(false) - expect(isStatic(null)).toBe(false) - expect(isStatic(undefined)).toBe(false) - expect(isStatic(false)).toBe(false) - expect(isStatic(true)).toBe(false) - expect(isStatic(0)).toBe(false) - expect(isStatic(7)).toBe(false) - }) -}) - -describe('getText', () => { - it('getText - 50eb', () => { - expect(getText('')).toBe('') - expect(getText('Text')).toBe('Text') - expect(getText(0)).toBe('0') - expect(getText(7)).toBe('7') - expect(getText(createTextVNode('Text'))).toBe('Text') - - expect(getText(null as any)).toBe(undefined) - expect(getText(undefined as any)).toBe(undefined) - expect(getText({} as any)).toBe(undefined) - expect(getText([] as any)).toBe(undefined) - expect(getText(true as any)).toBe(undefined) - expect(getText(false as any)).toBe(undefined) - expect(getText(h('div'))).toBe(undefined) - expect(getText(h({}))).toBe(undefined) - }) -}) - -describe('getType', () => { - it('getType - ba43', () => { - expect(getType(h({}))).toBe('component') - expect(getType(h(() => null))).toBe('component') - expect(getType(h(defineAsyncComponent(() => Promise.resolve({}))))).toBe('component') - expect(getType(h('div'))).toBe('element') - expect(getType(h('div', null, []))).toBe('element') - expect(getType(createTextVNode('Text'))).toBe('text') - expect(getType(createCommentVNode('Text'))).toBe('comment') - expect(getType(createVNode(Fragment, null, []))).toBe('fragment') - expect(getType(createStaticVNode('
', 1))).toBe('static') - expect(getType('')).toBe('text') - expect(getType('string')).toBe('text') - expect(getType({})).toBe(undefined) - expect(getType([])).toBe('fragment') - expect(getType(null)).toBe('comment') - expect(getType(undefined)).toBe('comment') - expect(getType(false)).toBe('comment') - expect(getType(true)).toBe('comment') - expect(getType(0)).toBe('text') - expect(getType(7)).toBe('text') - expect(getType(Fragment)).toBe(undefined) - expect(getType(Text)).toBe(undefined) - }) -}) diff --git a/packages/vue-vnode-utils/src/base.ts b/packages/vue-vnode-utils/src/base.ts new file mode 100644 index 0000000..90690e9 --- /dev/null +++ b/packages/vue-vnode-utils/src/base.ts @@ -0,0 +1,95 @@ +import { + Comment as CommentVNode, + type Component, + type ComponentOptions, + Fragment as FragmentVNode, + type FunctionalComponent, + isVNode, + Static as StaticVNode, + Text as TextVNode, + type VNode, + type VNodeArrayChildren +} from 'vue' + +export const isComment = (vnode: unknown): vnode is (null | undefined | boolean | (VNode & { type: typeof CommentVNode })) => { + return getType(vnode) === 'comment' +} + +export const isComponent = (vnode: unknown): vnode is (VNode & { type: Component }) => { + return getType(vnode) === 'component' +} + +export const isElement = (vnode: unknown): vnode is (VNode & { type: string }) => { + return getType(vnode) === 'element' +} + +export const isFragment = (vnode: unknown): vnode is ((VNode & { type: typeof FragmentVNode }) | VNodeArrayChildren) => { + return getType(vnode) === 'fragment' +} + +export const isFunctionalComponent = (vnode: unknown): vnode is (VNode & { type: FunctionalComponent }) => { + return isComponent(vnode) && typeof vnode.type === 'function' +} + +export const isStatefulComponent = (vnode: unknown): vnode is (VNode & { type: ComponentOptions }) => { + return isComponent(vnode) && typeof vnode.type === 'object' +} + +export const isStatic = (vnode: unknown): vnode is (VNode & { type: typeof StaticVNode }) => { + return getType(vnode) === 'static' +} + +export const isText = (vnode: unknown): vnode is (string | number | (VNode & { type: typeof TextVNode })) => { + return getType(vnode) === 'text' +} + +export const getText = (vnode: VNode | string | number): string | undefined => { + if (typeof vnode === 'string') { + return vnode + } + + if (typeof vnode === 'number') { + return String(vnode) + } + + if (isVNode(vnode) && vnode.type === TextVNode) { + return String(vnode.children) + } + + return undefined +} + +export const getType = (vnode: unknown) => { + const typeofVNode = typeof vnode + + if (vnode == null || typeofVNode === 'boolean') { + return 'comment' + } else if (typeofVNode === 'string' || typeofVNode === 'number') { + return 'text' + } else if (Array.isArray(vnode)) { + return 'fragment' + } + + if (isVNode(vnode)) { + const { type } = vnode + const typeofType = typeof type + + if (typeofType === 'symbol') { + if (type === FragmentVNode) { + return 'fragment' + } else if (type === TextVNode) { + return 'text' + } else if (type === CommentVNode) { + return 'comment' + } else if (type === StaticVNode) { + return 'static' + } + } else if (typeofType === 'string') { + return 'element' + } else if (typeofType === 'object' || typeofType === 'function') { + return 'component' + } + } + + return undefined +} diff --git a/packages/vue-vnode-utils/src/index.ts b/packages/vue-vnode-utils/src/index.ts index d55fb8b..e243a73 100644 --- a/packages/vue-vnode-utils/src/index.ts +++ b/packages/vue-vnode-utils/src/index.ts @@ -1,496 +1,28 @@ -import { - cloneVNode, - Comment as CommentVNode, - type Component, - type ComponentOptions, - createCommentVNode, - createTextVNode, - Fragment as FragmentVNode, - type FunctionalComponent, - isVNode, - Static as StaticVNode, - Text as TextVNode, - type VNode, - type VNodeArrayChildren, - type VNodeChild -} from 'vue' - -export const isComment = (vnode: unknown): vnode is (null | undefined | boolean | (VNode & { type: typeof CommentVNode })) => { - return getType(vnode) === 'comment' -} - -export const isComponent = (vnode: unknown): vnode is (VNode & { type: Component }) => { - return getType(vnode) === 'component' -} - -export const isElement = (vnode: unknown): vnode is (VNode & { type: string }) => { - return getType(vnode) === 'element' -} - -export const isFragment = (vnode: unknown): vnode is ((VNode & { type: typeof FragmentVNode }) | VNodeArrayChildren) => { - return getType(vnode) === 'fragment' -} - -export const isFunctionalComponent = (vnode: unknown): vnode is (VNode & { type: FunctionalComponent }) => { - return isComponent(vnode) && typeof vnode.type === 'function' -} - -export const isStatefulComponent = (vnode: unknown): vnode is (VNode & { type: ComponentOptions }) => { - return isComponent(vnode) && typeof vnode.type === 'object' -} - -export const isStatic = (vnode: unknown): vnode is (VNode & { type: typeof StaticVNode }) => { - return getType(vnode) === 'static' -} - -export const isText = (vnode: unknown): vnode is (string | number | (VNode & { type: typeof TextVNode })) => { - return getType(vnode) === 'text' -} - -export const getText = (vnode: VNode | string | number): string | undefined => { - if (typeof vnode === 'string') { - return vnode - } - - if (typeof vnode === 'number') { - return String(vnode) - } - - if (isVNode(vnode) && vnode.type === TextVNode) { - return String(vnode.children) - } - - return undefined -} - -type ValueTypes = 'string' | 'number' | 'boolean' | 'undefined' | 'symbol' | 'bigint' | 'object' | 'function' | 'array' | 'date' | 'regexp' | 'vnode' | 'null' - -const typeOf = (value: unknown) => { - let t: ValueTypes = typeof value - - if (t === 'object') { - if (value === null) { - t = 'null' - } else if (Array.isArray(value)) { - t = 'array' - } else if (isVNode(value)) { - t = 'vnode' - } else if (value instanceof Date) { - t = 'date' - } else if (value instanceof RegExp) { - t = 'regexp' - } - } - - return t -} - -const warn = (method: string, msg: string) => { - console.warn(`[${method}] ${msg}`) -} - -const checkArguments = (method: string, passed: unknown[], expected: string[]) => { - for (let index = 0; index < passed.length; ++index) { - const t = typeOf(passed[index]) - const expect = expected[index] - - if (expect !== t) { - warn(method, `Argument ${index + 1} was ${t}, should be ${expect}`) - } - } -} - -export const getType = (vnode: unknown) => { - const typeofVNode = typeof vnode - - if (vnode == null || typeofVNode === 'boolean') { - return 'comment' - } else if (typeofVNode === 'string' || typeofVNode === 'number') { - return 'text' - } else if (Array.isArray(vnode)) { - return 'fragment' - } - - if (isVNode(vnode)) { - const { type } = vnode - const typeofType = typeof type - - if (typeofType === 'symbol') { - if (type === FragmentVNode) { - return 'fragment' - } else if (type === TextVNode) { - return 'text' - } else if (type === CommentVNode) { - return 'comment' - } else if (type === StaticVNode) { - return 'static' - } - } else if (typeofType === 'string') { - return 'element' - } else if (typeofType === 'object' || typeofType === 'function') { - return 'component' - } - } - - return undefined -} - -const isEmptyObject = (obj: Record) => { - for (const prop in obj) { - return false - } - - return true -} - -const getFragmentChildren = (fragmentVNode: VNode | VNodeArrayChildren): VNodeArrayChildren => { - if (Array.isArray(fragmentVNode)) { - return fragmentVNode - } - - const { children } = fragmentVNode - - if (Array.isArray(children)) { - return children - } - - if (__DEV__) { - warn('getFragmentChildren', `Unknown children for fragment: ${typeOf(children)}`) - } - - return [] -} - -export type IterationOptions = { - element?: boolean - component?: boolean - comment?: boolean - text?: boolean - static?: boolean -} - -// esbuild can remove an identity function, so long as it uses a function declaration -function freeze(obj: T): T { - if (__DEV__) { - return Object.freeze(obj) - } - - return obj -} - -export const COMPONENTS_AND_ELEMENTS: IterationOptions = /*#__PURE__*/ freeze({ - element: true, - component: true -}) - -export const SKIP_COMMENTS: IterationOptions = /*#__PURE__*/ freeze({ - element: true, - component: true, - text: true, - static: true -}) - -export const ALL_VNODES: IterationOptions = /*#__PURE__*/ freeze({ - element: true, - component: true, - text: true, - static: true, - comment: true -}) - -const promoteToVNode = (node: VNode | string | number | boolean | null | undefined | void, options: IterationOptions): VNode | null => { - const type = getType(node) - - // In practice, we don't call this function for fragments, but TS gets unhappy if we don't handle it - if (!type || type === 'fragment' || !options[type]) { - return null - } - - if (isVNode(node)) { - return node - } - - if (type === 'text') { - return createTextVNode(getText(node as (string | number))) - } - - return createCommentVNode() -} - -export const addProps = ( - children: VNodeArrayChildren, - callback: (vnode: VNode) => (Record | null | void), - options: IterationOptions = COMPONENTS_AND_ELEMENTS -): VNodeArrayChildren => { - if (__DEV__) { - checkArguments('addProps', [children, callback, options], ['array', 'function', 'object']) - } - - return replaceChildrenInternal(children, (vnode) => { - const props = callback(vnode) - - if (__DEV__) { - const typeofProps = typeOf(props) - - if (!['object', 'null', 'undefined'].includes(typeofProps)) { - warn('addProps', `Callback returned unexpected ${typeofProps}: ${String(props)}`) - } - } - - if (props && !isEmptyObject(props)) { - return cloneVNode(vnode, props, true) - } - }, options) -} - -export const replaceChildren = ( - children: VNodeArrayChildren, - callback: (vnode: VNode) => (VNode | VNodeArrayChildren | string | number | void), - options: IterationOptions = SKIP_COMMENTS -): VNodeArrayChildren => { - if (__DEV__) { - checkArguments('replaceChildren', [children, callback, options], ['array', 'function', 'object']) - } - - return replaceChildrenInternal(children, callback, options) -} - -const replaceChildrenInternal = ( - children: VNodeArrayChildren, - callback: (vnode: VNode) => (VNode | VNodeArrayChildren | string | number | void), - options: IterationOptions -): VNodeArrayChildren => { - let nc: VNodeArrayChildren | null = null - - for (let index = 0; index < children.length; ++index) { - const child = children[index] - - if (isFragment(child)) { - const oldFragmentChildren = getFragmentChildren(child) - const newFragmentChildren = replaceChildrenInternal(oldFragmentChildren, callback, options) - - let newChild: VNodeChild = child - - if (oldFragmentChildren !== newFragmentChildren) { - nc ??= children.slice(0, index) - - if (Array.isArray(child)) { - newChild = newFragmentChildren - } else { - newChild = cloneVNode(child) - - newChild.children = newFragmentChildren - } - } - - nc && nc.push(newChild) - } else { - const vnode = promoteToVNode(child, options) - - if (vnode) { - const newNodes = callback(vnode) ?? vnode - - if (__DEV__) { - const typeOfNewNodes = typeOf(newNodes) - - if (!['array', 'vnode', 'string', 'number', 'undefined'].includes(typeOfNewNodes)) { - warn('replaceChildren', `Callback returned unexpected ${typeOfNewNodes} ${String(newNodes)}`) - } - } - - if (newNodes !== child) { - nc ??= children.slice(0, index) - } - - if (Array.isArray(newNodes)) { - nc && nc.push(...newNodes) - } else { - nc && nc.push(newNodes) - } - } else { - nc && nc.push(child) - } - } - } - - return nc ?? children -} - -export const betweenChildren = ( - children: VNodeArrayChildren, - callback: (previousVNode: VNode, nextVNode: VNode) => (VNode | VNodeArrayChildren | string | number | void), - options: IterationOptions = SKIP_COMMENTS -): VNodeArrayChildren => { - if (__DEV__) { - checkArguments('betweenChildren', [children, callback, options], ['array', 'function', 'object']) - } - - let previousVNode: VNode | null = null - - return replaceChildrenInternal(children, vnode => { - let insertedNodes: VNode | VNodeArrayChildren | string | number | void = undefined - - if (previousVNode) { - insertedNodes = callback(previousVNode, vnode) - - if (__DEV__) { - const typeOfInsertedNodes = typeOf(insertedNodes) - - if (!['array', 'vnode', 'string', 'number', 'undefined'].includes(typeOfInsertedNodes)) { - warn('betweenChildren', `Callback returned unexpected ${typeOfInsertedNodes} ${String(insertedNodes)}`) - } - } - } - - previousVNode = vnode - - if (insertedNodes == null || (Array.isArray(insertedNodes) && insertedNodes.length === 0)) { - return - } - - if (Array.isArray(insertedNodes)) { - return [...insertedNodes, vnode] - } - - return [insertedNodes, vnode] - }, options) -} - -export const someChild = ( - children: VNodeArrayChildren, - callback: (vnode: VNode) => unknown, - options: IterationOptions = ALL_VNODES -): boolean => { - if (__DEV__) { - checkArguments('someChild', [children, callback, options], ['array', 'function', 'object']) - } - - return someChildInternal(children, callback, options) -} - -const someChildInternal = ( - children: VNodeArrayChildren, - callback: (vnode: VNode) => unknown, - options: IterationOptions -): boolean => { - for (const child of children) { - if (isFragment(child)) { - if (someChild(getFragmentChildren(child), callback, options)) { - return true - } - } else { - const vnode = promoteToVNode(child, options) - - if (vnode && callback(vnode)) { - return true - } - } - } - - return false -} - -export const everyChild = ( - children: VNodeArrayChildren, - callback: (vnode: VNode) => unknown, - options: IterationOptions = ALL_VNODES -): boolean => { - if (__DEV__) { - checkArguments('everyChild', [children, callback, options], ['array', 'function', 'object']) - } - - return !someChildInternal(children, (vnode) => !callback(vnode), options) -} - -export const eachChild = ( - children: VNodeArrayChildren, - callback: (vnode: VNode) => void, - options: IterationOptions = ALL_VNODES -): void => { - if (__DEV__) { - checkArguments('eachChild', [children, callback, options], ['array', 'function', 'object']) - } - - someChildInternal(children, (vnode) => { - callback(vnode) - }, options) -} - -export const findChild = ( - children: VNodeArrayChildren, - callback: (vnode: VNode) => unknown, - options: IterationOptions = ALL_VNODES -): (VNode | undefined) => { - if (__DEV__) { - checkArguments('findChild', [children, callback, options], ['array', 'function', 'object']) - } - - let node: VNode | undefined = undefined - - someChildInternal(children, (vnode) => { - if (callback(vnode)) { - node = vnode - return true - } - }, options) - - return node -} - -const COLLAPSIBLE_WHITESPACE_RE = /\S|\u00a0/ - -export const isEmpty = (children: VNodeArrayChildren): boolean => { - if (__DEV__) { - checkArguments('isEmpty', [children], ['array']) - } - - return !someChildInternal(children, (vnode) => { - if (isText(vnode)) { - const text = getText(vnode) || '' - - return COLLAPSIBLE_WHITESPACE_RE.test(text) - } - - return true - }, SKIP_COMMENTS) -} - -export const extractSingleChild = (children: VNodeArrayChildren): VNode | undefined => { - if (__DEV__) { - checkArguments('extractSingleChild', [children], ['array']) - } - - const node = findChild(children, () => { - return true - }, COMPONENTS_AND_ELEMENTS) - - if (__DEV__) { - someChildInternal(children, (vnode) => { - let warning = '' - - // The equality check is valid here as matching nodes can't come from promotions - if (vnode === node) { - return false - } - - if (isElement(vnode) || isComponent(vnode)) { - warning = 'Multiple root nodes found, only one expected' - } else if (isText(vnode)) { - const text = getText(vnode) || '' - - if (COLLAPSIBLE_WHITESPACE_RE.test(text)) { - warning = `Non-empty text node:\n'${text}'` - } - } else { - warning = `Encountered unexpected ${getType(vnode)} VNode` - } - - if (warning) { - warn('extractSingleChild', warning) - return true - } - }, SKIP_COMMENTS) - } - - return node -} +export { + getText, + getType, + isComment, + isComponent, + isElement, + isFragment, + isFunctionalComponent, + isStatefulComponent, + isStatic, + isText +} from './base' + +export { + addProps, + ALL_VNODES, + betweenChildren, + COMPONENTS_AND_ELEMENTS, + eachChild, + everyChild, + extractSingleChild, + findChild, + isEmpty, + type IterationOptions, + replaceChildren, + SKIP_COMMENTS, + someChild +} from './iterators' diff --git a/packages/vue-vnode-utils/src/iterators.ts b/packages/vue-vnode-utils/src/iterators.ts new file mode 100644 index 0000000..7ebecc0 --- /dev/null +++ b/packages/vue-vnode-utils/src/iterators.ts @@ -0,0 +1,414 @@ +import { + cloneVNode, + createCommentVNode, + createTextVNode, + isVNode, + type VNode, + type VNodeArrayChildren, + type VNodeChild +} from 'vue' +import { + getText, + getType, + isComponent, + isElement, + isFragment, + isText +} from './base' + +const warn = (method: string, msg: string) => { + console.warn(`[${method}] ${msg}`) +} + +const checkArguments = (method: string, passed: unknown[], expected: string[]) => { + for (let index = 0; index < passed.length; ++index) { + const t = typeOf(passed[index]) + const expect = expected[index] + + if (expect !== t) { + warn(method, `Argument ${index + 1} was ${t}, should be ${expect}`) + } + } +} + +const isEmptyObject = (obj: Record) => { + for (const prop in obj) { + return false + } + + return true +} + +type ValueTypes = 'string' | 'number' | 'boolean' | 'undefined' | 'symbol' | 'bigint' | 'object' | 'function' | 'array' | 'date' | 'regexp' | 'vnode' | 'null' + +const typeOf = (value: unknown) => { + let t: ValueTypes = typeof value + + if (t === 'object') { + if (value === null) { + t = 'null' + } else if (Array.isArray(value)) { + t = 'array' + } else if (isVNode(value)) { + t = 'vnode' + } else if (value instanceof Date) { + t = 'date' + } else if (value instanceof RegExp) { + t = 'regexp' + } + } + + return t +} + +const getFragmentChildren = (fragmentVNode: VNode | VNodeArrayChildren): VNodeArrayChildren => { + if (Array.isArray(fragmentVNode)) { + return fragmentVNode + } + + const { children } = fragmentVNode + + if (Array.isArray(children)) { + return children + } + + if (__DEV__) { + warn('getFragmentChildren', `Unknown children for fragment: ${typeOf(children)}`) + } + + return [] +} + +export type IterationOptions = { + element?: boolean + component?: boolean + comment?: boolean + text?: boolean + static?: boolean +} + +// esbuild can remove an identity function, so long as it uses a function declaration +function freeze(obj: T): T { + if (__DEV__) { + return Object.freeze(obj) + } + + return obj +} + +export const COMPONENTS_AND_ELEMENTS: IterationOptions = /*#__PURE__*/ freeze({ + element: true, + component: true +}) + +export const SKIP_COMMENTS: IterationOptions = /*#__PURE__*/ freeze({ + element: true, + component: true, + text: true, + static: true +}) + +export const ALL_VNODES: IterationOptions = /*#__PURE__*/ freeze({ + element: true, + component: true, + text: true, + static: true, + comment: true +}) + +const promoteToVNode = (node: VNode | string | number | boolean | null | undefined | void, options: IterationOptions): VNode | null => { + const type = getType(node) + + // In practice, we don't call this function for fragments, but TS gets unhappy if we don't handle it + if (!type || type === 'fragment' || !options[type]) { + return null + } + + if (isVNode(node)) { + return node + } + + if (type === 'text') { + return createTextVNode(getText(node as (string | number))) + } + + return createCommentVNode() +} + +export const addProps = ( + children: VNodeArrayChildren, + callback: (vnode: VNode) => (Record | null | void), + options: IterationOptions = COMPONENTS_AND_ELEMENTS +): VNodeArrayChildren => { + if (__DEV__) { + checkArguments('addProps', [children, callback, options], ['array', 'function', 'object']) + } + + return replaceChildrenInternal(children, (vnode) => { + const props = callback(vnode) + + if (__DEV__) { + const typeofProps = typeOf(props) + + if (!['object', 'null', 'undefined'].includes(typeofProps)) { + warn('addProps', `Callback returned unexpected ${typeofProps}: ${String(props)}`) + } + } + + if (props && !isEmptyObject(props)) { + return cloneVNode(vnode, props, true) + } + }, options) +} + +export const replaceChildren = ( + children: VNodeArrayChildren, + callback: (vnode: VNode) => (VNode | VNodeArrayChildren | string | number | void), + options: IterationOptions = SKIP_COMMENTS +): VNodeArrayChildren => { + if (__DEV__) { + checkArguments('replaceChildren', [children, callback, options], ['array', 'function', 'object']) + } + + return replaceChildrenInternal(children, callback, options) +} + +const replaceChildrenInternal = ( + children: VNodeArrayChildren, + callback: (vnode: VNode) => (VNode | VNodeArrayChildren | string | number | void), + options: IterationOptions +): VNodeArrayChildren => { + let nc: VNodeArrayChildren | null = null + + for (let index = 0; index < children.length; ++index) { + const child = children[index] + + if (isFragment(child)) { + const oldFragmentChildren = getFragmentChildren(child) + const newFragmentChildren = replaceChildrenInternal(oldFragmentChildren, callback, options) + + let newChild: VNodeChild = child + + if (oldFragmentChildren !== newFragmentChildren) { + nc ??= children.slice(0, index) + + if (Array.isArray(child)) { + newChild = newFragmentChildren + } else { + newChild = cloneVNode(child) + + newChild.children = newFragmentChildren + } + } + + nc && nc.push(newChild) + } else { + const vnode = promoteToVNode(child, options) + + if (vnode) { + const newNodes = callback(vnode) ?? vnode + + if (__DEV__) { + const typeOfNewNodes = typeOf(newNodes) + + if (!['array', 'vnode', 'string', 'number', 'undefined'].includes(typeOfNewNodes)) { + warn('replaceChildren', `Callback returned unexpected ${typeOfNewNodes} ${String(newNodes)}`) + } + } + + if (newNodes !== child) { + nc ??= children.slice(0, index) + } + + if (Array.isArray(newNodes)) { + nc && nc.push(...newNodes) + } else { + nc && nc.push(newNodes) + } + } else { + nc && nc.push(child) + } + } + } + + return nc ?? children +} + +export const betweenChildren = ( + children: VNodeArrayChildren, + callback: (previousVNode: VNode, nextVNode: VNode) => (VNode | VNodeArrayChildren | string | number | void), + options: IterationOptions = SKIP_COMMENTS +): VNodeArrayChildren => { + if (__DEV__) { + checkArguments('betweenChildren', [children, callback, options], ['array', 'function', 'object']) + } + + let previousVNode: VNode | null = null + + return replaceChildrenInternal(children, vnode => { + let insertedNodes: VNode | VNodeArrayChildren | string | number | void = undefined + + if (previousVNode) { + insertedNodes = callback(previousVNode, vnode) + + if (__DEV__) { + const typeOfInsertedNodes = typeOf(insertedNodes) + + if (!['array', 'vnode', 'string', 'number', 'undefined'].includes(typeOfInsertedNodes)) { + warn('betweenChildren', `Callback returned unexpected ${typeOfInsertedNodes} ${String(insertedNodes)}`) + } + } + } + + previousVNode = vnode + + if (insertedNodes == null || (Array.isArray(insertedNodes) && insertedNodes.length === 0)) { + return + } + + if (Array.isArray(insertedNodes)) { + return [...insertedNodes, vnode] + } + + return [insertedNodes, vnode] + }, options) +} + +export const someChild = ( + children: VNodeArrayChildren, + callback: (vnode: VNode) => unknown, + options: IterationOptions = ALL_VNODES +): boolean => { + if (__DEV__) { + checkArguments('someChild', [children, callback, options], ['array', 'function', 'object']) + } + + return someChildInternal(children, callback, options) +} + +const someChildInternal = ( + children: VNodeArrayChildren, + callback: (vnode: VNode) => unknown, + options: IterationOptions +): boolean => { + for (const child of children) { + if (isFragment(child)) { + if (someChild(getFragmentChildren(child), callback, options)) { + return true + } + } else { + const vnode = promoteToVNode(child, options) + + if (vnode && callback(vnode)) { + return true + } + } + } + + return false +} + +export const everyChild = ( + children: VNodeArrayChildren, + callback: (vnode: VNode) => unknown, + options: IterationOptions = ALL_VNODES +): boolean => { + if (__DEV__) { + checkArguments('everyChild', [children, callback, options], ['array', 'function', 'object']) + } + + return !someChildInternal(children, (vnode) => !callback(vnode), options) +} + +export const eachChild = ( + children: VNodeArrayChildren, + callback: (vnode: VNode) => void, + options: IterationOptions = ALL_VNODES +): void => { + if (__DEV__) { + checkArguments('eachChild', [children, callback, options], ['array', 'function', 'object']) + } + + someChildInternal(children, (vnode) => { + callback(vnode) + }, options) +} + +export const findChild = ( + children: VNodeArrayChildren, + callback: (vnode: VNode) => unknown, + options: IterationOptions = ALL_VNODES +): (VNode | undefined) => { + if (__DEV__) { + checkArguments('findChild', [children, callback, options], ['array', 'function', 'object']) + } + + let node: VNode | undefined = undefined + + someChildInternal(children, (vnode) => { + if (callback(vnode)) { + node = vnode + return true + } + }, options) + + return node +} + +const COLLAPSIBLE_WHITESPACE_RE = /\S|\u00a0/ + +export const isEmpty = (children: VNodeArrayChildren): boolean => { + if (__DEV__) { + checkArguments('isEmpty', [children], ['array']) + } + + return !someChildInternal(children, (vnode) => { + if (isText(vnode)) { + const text = getText(vnode) || '' + + return COLLAPSIBLE_WHITESPACE_RE.test(text) + } + + return true + }, SKIP_COMMENTS) +} + +export const extractSingleChild = (children: VNodeArrayChildren): VNode | undefined => { + if (__DEV__) { + checkArguments('extractSingleChild', [children], ['array']) + } + + const node = findChild(children, () => { + return true + }, COMPONENTS_AND_ELEMENTS) + + if (__DEV__) { + someChildInternal(children, (vnode) => { + let warning = '' + + // The equality check is valid here as matching nodes can't come from promotions + if (vnode === node) { + return false + } + + if (isElement(vnode) || isComponent(vnode)) { + warning = 'Multiple root nodes found, only one expected' + } else if (isText(vnode)) { + const text = getText(vnode) || '' + + if (COLLAPSIBLE_WHITESPACE_RE.test(text)) { + warning = `Non-empty text node:\n'${text}'` + } + } else { + warning = `Encountered unexpected ${getType(vnode)} VNode` + } + + if (warning) { + warn('extractSingleChild', warning) + return true + } + }, SKIP_COMMENTS) + } + + return node +}