1
0
mirror of https://github.com/robonen/tools.git synced 2026-03-20 10:54:44 +00:00

feat(monorepo): migrate vue packages and apply oxlint refactors

This commit is contained in:
2026-03-07 18:07:22 +07:00
parent abd6605db3
commit 41d5e18f6b
286 changed files with 10295 additions and 5028 deletions

1
vue/primitives/src/env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare const __DEV__: boolean;

View File

@@ -0,0 +1 @@
export * from './primitive';

View File

@@ -0,0 +1,22 @@
import type { Component, IntrinsicElementAttributes, SetupContext } from 'vue';
import { h } from 'vue';
import { Slot } from './Slot';
type FunctionalComponentContext = Omit<SetupContext, 'expose'>;
export interface PrimitiveProps {
as?: keyof IntrinsicElementAttributes | Component;
}
export function Primitive(props: PrimitiveProps, ctx: FunctionalComponentContext) {
return props.as === 'template'
? h(Slot, ctx.attrs, ctx.slots)
: h(props.as!, ctx.attrs, ctx.slots);
}
Primitive.props = {
as: {
type: [String, Object],
default: 'div' as const,
},
};

View File

@@ -0,0 +1,29 @@
import type { SetupContext } from 'vue';
import { cloneVNode, warn } from 'vue';
import { getRawChildren } from '../utils/getRawChildren';
type FunctionalComponentContext = Omit<SetupContext, 'expose'>;
/**
* A component that renders a single child from its default slot,
* applying the provided attributes to it.
*
* @param _ - Props (unused)
* @param context - Setup context containing slots and attrs
* @returns Cloned VNode with merged attrs or null
*/
export function Slot(_: Record<string, unknown>, { slots, attrs }: FunctionalComponentContext) {
if (!slots.default) return null;
const children = getRawChildren(slots.default());
if (!children.length) return null;
if (__DEV__ && children.length > 1) {
warn('<Slot> can only be used on a single element or component.');
}
return cloneVNode(children[0]!, attrs, true);
}
Slot.inheritAttrs = false;

View File

@@ -0,0 +1,116 @@
import { bench, describe } from 'vitest';
import { cloneVNode, Comment, createVNode, h } from 'vue';
import { Primitive, Slot } from '..';
// -- Attribute sets of increasing size --
const attrs1 = { class: 'a' };
const attrs5 = { class: 'a', id: 'b', role: 'button', tabindex: '0', title: 'tip' };
const attrs15 = {
'class': 'a',
'id': 'b',
'style': { color: 'red' },
'onClick': () => {},
'role': 'button',
'tabindex': '0',
'title': 'tip',
'data-a': '1',
'data-b': '2',
'data-c': '3',
'data-d': '4',
'data-e': '5',
'data-f': '6',
'data-g': '7',
'data-h': '8',
};
const defaultSlot = { default: () => [h('span', 'content')] };
const noop = () => {};
// ---- Baselines (raw Vue calls) ----
describe('baseline: raw h()', () => {
bench('h() — 1 attr', () => {
h('div', attrs1, defaultSlot);
});
bench('h() — 5 attrs', () => {
h('div', attrs5, defaultSlot);
});
bench('h() — 15 attrs', () => {
h('div', attrs15, defaultSlot);
});
});
describe('baseline: raw cloneVNode()', () => {
const child = h('div', 'content');
bench('cloneVNode — 1 attr', () => {
cloneVNode(child, attrs1, true);
});
bench('cloneVNode — 5 attrs', () => {
cloneVNode(child, attrs5, true);
});
bench('cloneVNode — 15 attrs', () => {
cloneVNode(child, attrs15, true);
});
});
// ---- Primitive overhead vs raw h() ----
describe('Primitive vs h()', () => {
bench('h("div") — baseline', () => {
h('div', attrs5, defaultSlot);
});
bench('Primitive({ as: "div" })', () => {
Primitive({ as: 'div' }, { attrs: attrs5, slots: defaultSlot, emit: noop });
});
bench('Primitive({ as: "template" }) — Slot mode', () => {
Primitive({ as: 'template' }, { attrs: attrs5, slots: defaultSlot, emit: noop });
});
});
// ---- Slot scaling by attribute count ----
describe('Slot — scaling by attrs', () => {
bench('1 attr', () => {
Slot({} as never, { attrs: attrs1, slots: defaultSlot, emit: noop });
});
bench('5 attrs', () => {
Slot({} as never, { attrs: attrs5, slots: defaultSlot, emit: noop });
});
bench('15 attrs (mixed types)', () => {
Slot({} as never, { attrs: attrs15, slots: defaultSlot, emit: noop });
});
});
// ---- Slot edge cases ----
describe('Slot — edge cases', () => {
bench('child with comments to skip', () => {
Slot({} as never, {
attrs: attrs5,
slots: {
default: () => [
createVNode(Comment, null, 'skip'),
createVNode(Comment, null, 'skip'),
h('span', 'content'),
],
},
emit: noop,
});
});
bench('no default slot', () => {
Slot({} as never, { attrs: attrs5, slots: {}, emit: noop });
});
});

View File

@@ -0,0 +1,482 @@
import type { PrimitiveProps } from '..';
import { describe, it, expect, vi } from 'vitest';
import { createVNode, Comment, h, defineComponent, markRaw, nextTick, ref, shallowRef } from 'vue';
import { mount } from '@vue/test-utils';
import { Primitive, Slot } from '..';
// --- Slot ---
describe(Slot, () => {
it('returns null when no default slot is provided', () => {
const wrapper = mount(
defineComponent({
setup() {
return () => h(Slot);
},
}),
);
expect(wrapper.html()).toBe('');
wrapper.unmount();
});
it('renders the first valid child from the slot', () => {
const wrapper = mount(
defineComponent({
setup() {
return () => h(Slot, null, { default: () => [h('span', 'hello')] });
},
}),
);
expect(wrapper.html()).toBe('<span>hello</span>');
wrapper.unmount();
});
it('applies attrs to the slotted child', () => {
const wrapper = mount(
defineComponent({
setup() {
return () =>
h(Slot, { class: 'custom', id: 'test' }, { default: () => [h('div')] });
},
}),
);
expect(wrapper.find('div').classes()).toContain('custom');
expect(wrapper.find('div').attributes('id')).toBe('test');
wrapper.unmount();
});
it('skips Comment nodes and picks the first element', () => {
const wrapper = mount(
defineComponent({
setup() {
return () =>
h(Slot, null, {
default: () => [createVNode(Comment, null, 'skip'), h('em', 'content')],
});
},
}),
);
expect(wrapper.html()).toBe('<em>content</em>');
wrapper.unmount();
});
it('warns in DEV mode when multiple valid children are provided', () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const wrapper = mount(
defineComponent({
setup() {
return () =>
h(Slot, null, {
default: () => [h('div', 'a'), h('span', 'b')],
});
},
}),
);
expect(warnSpy).toHaveBeenCalled();
expect(warnSpy.mock.calls.some(args =>
args.some(arg => typeof arg === 'string' && arg.includes('<Slot>')),
)).toBe(true);
warnSpy.mockRestore();
wrapper.unmount();
});
it('renders null when slot has only comments', () => {
const wrapper = mount(
defineComponent({
setup() {
return () =>
h(Slot, null, {
default: () => [
createVNode(Comment, null, 'a'),
createVNode(Comment, null, 'b'),
],
});
},
}),
);
expect(wrapper.html()).toBe('');
wrapper.unmount();
});
});
// --- Primitive ---
describe(Primitive, () => {
it('renders a div by default', () => {
const wrapper = mount(Primitive, {
slots: { default: () => 'content' },
});
expect(wrapper.element.tagName).toBe('DIV');
expect(wrapper.text()).toBe('content');
wrapper.unmount();
});
it('renders the element specified by "as" prop', () => {
const wrapper = mount(Primitive, {
props: { as: 'button' },
slots: { default: () => 'click me' },
});
expect(wrapper.element.tagName).toBe('BUTTON');
expect(wrapper.text()).toBe('click me');
wrapper.unmount();
});
it('renders a span element', () => {
const wrapper = mount(Primitive, {
props: { as: 'span' },
slots: { default: () => 'text' },
});
expect(wrapper.element.tagName).toBe('SPAN');
wrapper.unmount();
});
it('passes attributes to the rendered element', () => {
const wrapper = mount(Primitive, {
props: { as: 'input' },
attrs: { type: 'text', placeholder: 'enter' },
});
expect(wrapper.attributes('type')).toBe('text');
expect(wrapper.attributes('placeholder')).toBe('enter');
wrapper.unmount();
});
it('passes class and style attributes', () => {
const wrapper = mount(Primitive, {
props: { as: 'div' },
attrs: { class: 'my-class', style: 'color: red' },
slots: { default: () => 'styled' },
});
expect(wrapper.classes()).toContain('my-class');
expect(wrapper.attributes('style')).toBe('color: red;');
wrapper.unmount();
});
it('forwards event listeners', async () => {
const onClick = vi.fn();
const wrapper = mount(Primitive, {
props: { as: 'button' },
attrs: { onClick },
slots: { default: () => 'click' },
});
await wrapper.trigger('click');
expect(onClick).toHaveBeenCalledOnce();
wrapper.unmount();
});
it('renders a custom Vue component via "as"', () => {
const Custom = markRaw(defineComponent({
props: { label: String },
setup(props) {
return () => h('span', { class: 'custom' }, props.label);
},
}));
const wrapper = mount(Primitive, {
props: { as: Custom },
attrs: { label: 'hello' },
});
expect(wrapper.find('.custom').exists()).toBe(true);
expect(wrapper.text()).toBe('hello');
wrapper.unmount();
});
it('renders in Slot mode when as="template"', () => {
const wrapper = mount(Primitive, {
props: { as: 'template' },
slots: { default: () => h('section', 'slot content') },
});
expect(wrapper.element.tagName).toBe('SECTION');
expect(wrapper.text()).toBe('slot content');
wrapper.unmount();
});
it('merges attrs onto the slotted child in template mode', () => {
const wrapper = mount(Primitive, {
props: { as: 'template' },
attrs: { 'class': 'merged', 'data-testid': 'slot' },
slots: { default: () => h('div', 'child') },
});
expect(wrapper.classes()).toContain('merged');
expect(wrapper.attributes('data-testid')).toBe('slot');
wrapper.unmount();
});
it('forwards event listeners in template mode', async () => {
const onClick = vi.fn();
const wrapper = mount(Primitive, {
props: { as: 'template' },
attrs: { onClick },
slots: { default: () => h('button', 'click me') },
});
await wrapper.trigger('click');
expect(onClick).toHaveBeenCalledOnce();
wrapper.unmount();
});
it('renders empty when template mode has no slot', () => {
const wrapper = mount(Primitive, {
props: { as: 'template' },
});
expect(wrapper.html()).toBe('');
wrapper.unmount();
});
it('can switch element via reactive "as" prop', async () => {
const Wrapper = defineComponent({
props: { tag: { type: String, default: 'div' } },
setup(props) {
return () => h(Primitive, { as: props.tag as PrimitiveProps['as'] }, { default: () => 'test' });
},
});
const wrapper = mount(Wrapper, { props: { tag: 'div' } });
expect(wrapper.element.tagName).toBe('DIV');
await wrapper.setProps({ tag: 'span' });
await nextTick();
expect(wrapper.element.tagName).toBe('SPAN');
wrapper.unmount();
});
it('exposes root element via template ref', async () => {
const primitiveRef = shallowRef<Element | null>(null);
const wrapper = mount(
defineComponent({
setup() {
return () =>
h(Primitive, { ref: primitiveRef, as: 'button' }, { default: () => 'click' });
},
}),
);
await nextTick();
expect(primitiveRef.value).toBeInstanceOf(HTMLButtonElement);
wrapper.unmount();
});
it('exposes slotted element via template ref in template mode', async () => {
const primitiveRef = shallowRef<Element | null>(null);
const wrapper = mount(
defineComponent({
setup() {
return () =>
h(
Primitive,
{ ref: primitiveRef, as: 'template' },
{ default: () => h('section', 'content') },
);
},
}),
);
await nextTick();
expect(primitiveRef.value).toBeInstanceOf(HTMLElement);
expect((primitiveRef.value as HTMLElement).tagName).toBe('SECTION');
wrapper.unmount();
});
it('updates template ref when element changes', async () => {
const primitiveRef = shallowRef<Element | null>(null);
const tag = ref<PrimitiveProps['as']>('div');
const wrapper = mount(
defineComponent({
setup() {
return () =>
// @ts-expect-error — h() struggles with ref + broad PrimitiveProps['as'] union type
h(Primitive, { ref: primitiveRef, as: tag.value }, { default: () => 'test' });
},
}),
);
await nextTick();
expect(primitiveRef.value).toBeInstanceOf(HTMLDivElement);
tag.value = 'span';
await nextTick();
expect(primitiveRef.value).toBeInstanceOf(HTMLSpanElement);
wrapper.unmount();
});
});
// --- Nested as="template" ---
describe.each([1, 2, 3])('Primitive nested as="template" (depth=%i)', (depth) => {
function wrapInTemplate(attrs: Array<Record<string, unknown>>, slot: () => ReturnType<typeof h>) {
let current = slot;
for (let i = attrs.length - 1; i >= 0; i--) {
const inner = current;
current = () => h(Primitive, { as: 'template', ...attrs[i] }, { default: inner });
}
return current();
}
function makeAttrsPerLevel(base: string, depth: number) {
return Array.from({ length: depth }, (_, i) => ({ [`data-level-${i}`]: `${base}-${i}` }));
}
it('renders the inner child element', () => {
const attrs = makeAttrsPerLevel('v', depth);
const wrapper = mount(
defineComponent({
setup() {
return () => wrapInTemplate(attrs, () => h('section', 'leaf'));
},
}),
);
expect(wrapper.element.tagName).toBe('SECTION');
expect(wrapper.text()).toBe('leaf');
wrapper.unmount();
});
it('merges data attrs from all levels onto the leaf', () => {
const attrs = makeAttrsPerLevel('v', depth);
const wrapper = mount(
defineComponent({
setup() {
return () => wrapInTemplate(attrs, () => h('div', 'child'));
},
}),
);
for (let i = 0; i < depth; i++) {
expect(wrapper.attributes(`data-level-${i}`)).toBe(`v-${i}`);
}
wrapper.unmount();
});
it('merges classes from all levels', () => {
const attrs = Array.from({ length: depth }, (_, i) => ({ class: `level-${i}` }));
const wrapper = mount(
defineComponent({
setup() {
return () => wrapInTemplate(attrs, () => h('div', { class: 'leaf' }, 'child'));
},
}),
);
for (let i = 0; i < depth; i++) {
expect(wrapper.classes()).toContain(`level-${i}`);
}
expect(wrapper.classes()).toContain('leaf');
wrapper.unmount();
});
it('forwards event listeners from all levels', async () => {
const handlers = Array.from({ length: depth }, () => vi.fn());
const attrs = handlers.map(fn => ({ onClick: fn }));
const wrapper = mount(
defineComponent({
setup() {
return () => wrapInTemplate(attrs, () => h('button', 'click'));
},
}),
);
await wrapper.trigger('click');
for (const handler of handlers) {
expect(handler).toHaveBeenCalledOnce();
}
wrapper.unmount();
});
it('exposes inner element via template ref', async () => {
const primitiveRef = shallowRef<Element | null>(null);
const attrs = makeAttrsPerLevel('v', depth);
attrs[0] = { ...attrs[0], ref: primitiveRef };
const wrapper = mount(
defineComponent({
setup() {
return () => wrapInTemplate(attrs, () => h('section', 'content'));
},
}),
);
await nextTick();
expect(primitiveRef.value).toBeInstanceOf(HTMLElement);
expect((primitiveRef.value as HTMLElement).tagName).toBe('SECTION');
wrapper.unmount();
});
it('renders empty when innermost slot is missing', () => {
const attrs = makeAttrsPerLevel('v', depth);
const wrapper = mount(
defineComponent({
setup() {
return () => wrapInTemplate(attrs, () => h(Slot));
},
}),
);
expect(wrapper.html()).toBe('');
wrapper.unmount();
});
});

View File

@@ -0,0 +1,2 @@
export { Primitive, type PrimitiveProps } from './Primitive';
export { Slot } from './Slot';

View File

@@ -0,0 +1,142 @@
import { bench, describe } from 'vitest';
import { createVNode, Comment, Fragment, h, render } from 'vue';
import { PatchFlags } from '@vue/shared';
import { getRawChildren } from '../getRawChildren';
// -- Helpers --
function keyedFragment(children: Array<ReturnType<typeof h>>) {
return createVNode(Fragment, null, children, PatchFlags.KEYED_FRAGMENT);
}
const flatChildren = [h('div'), h('span'), h('p')];
const keyedChildren = Array.from({ length: 10 }, (_, i) =>
h('div', { key: i }, `child-${i}`),
);
// ---- Processing cost ----
describe('getRawChildren', () => {
bench('flat elements', () => {
getRawChildren(flatChildren);
});
bench('mixed elements and comments', () => {
getRawChildren([
createVNode(Comment, null, 'c'),
h('div'),
createVNode(Comment, null, 'c'),
h('span'),
createVNode(Comment, null, 'c'),
]);
});
bench('single fragment with children', () => {
getRawChildren([createVNode(Fragment, null, [h('a'), h('b'), h('c')])]);
});
bench('nested fragments (depth 5)', () => {
let current: ReturnType<typeof h> = h('div');
for (let i = 0; i < 5; i++) {
current = createVNode(Fragment, null, [current, h('span')]);
}
getRawChildren([current]);
});
bench('wide fragment (50 children)', () => {
const children = Array.from({ length: 50 }, (_, i) => h('div', `child-${i}`));
getRawChildren([createVNode(Fragment, null, children)]);
});
});
// ---- BAIL path cost ----
describe('getRawChildren — BAIL path', () => {
bench('1 keyed fragment (no BAIL)', () => {
getRawChildren([keyedFragment([...keyedChildren])]);
});
bench('2 keyed fragments (BAIL triggered)', () => {
getRawChildren([
keyedFragment(keyedChildren.slice(0, 5)),
keyedFragment(keyedChildren.slice(5)),
]);
});
bench('3 keyed fragments (BAIL triggered)', () => {
getRawChildren([
keyedFragment(keyedChildren.slice(0, 3)),
keyedFragment(keyedChildren.slice(3, 7)),
keyedFragment(keyedChildren.slice(7)),
]);
});
});
// ---- Render impact: optimized patchFlags vs BAIL ----
describe('patch — optimized vs BAIL patchFlag', () => {
bench('patch with TEXT patchFlag', () => {
const container = document.createElement('div');
const initial = h('div', null, [
createVNode('span', null, 'a', PatchFlags.TEXT),
createVNode('span', null, 'b', PatchFlags.TEXT),
createVNode('span', null, 'c', PatchFlags.TEXT),
]);
const updated = h('div', null, [
createVNode('span', null, 'x', PatchFlags.TEXT),
createVNode('span', null, 'y', PatchFlags.TEXT),
createVNode('span', null, 'z', PatchFlags.TEXT),
]);
render(initial, container);
render(updated, container);
});
bench('patch with BAIL patchFlag', () => {
const container = document.createElement('div');
const initial = h('div', null, [
createVNode('span', null, 'a', PatchFlags.BAIL),
createVNode('span', null, 'b', PatchFlags.BAIL),
createVNode('span', null, 'c', PatchFlags.BAIL),
]);
const updated = h('div', null, [
createVNode('span', null, 'x', PatchFlags.BAIL),
createVNode('span', null, 'y', PatchFlags.BAIL),
createVNode('span', null, 'z', PatchFlags.BAIL),
]);
render(initial, container);
render(updated, container);
});
bench('patch with CLASS patchFlag', () => {
const container = document.createElement('div');
const initial = h('div', null, [
createVNode('span', { class: 'a' }, null, PatchFlags.CLASS),
createVNode('span', { class: 'b' }, null, PatchFlags.CLASS),
createVNode('span', { class: 'c' }, null, PatchFlags.CLASS),
]);
const updated = h('div', null, [
createVNode('span', { class: 'x' }, null, PatchFlags.CLASS),
createVNode('span', { class: 'y' }, null, PatchFlags.CLASS),
createVNode('span', { class: 'z' }, null, PatchFlags.CLASS),
]);
render(initial, container);
render(updated, container);
});
bench('patch with CLASS→BAIL patchFlag', () => {
const container = document.createElement('div');
const initial = h('div', null, [
createVNode('span', { class: 'a' }, null, PatchFlags.BAIL),
createVNode('span', { class: 'b' }, null, PatchFlags.BAIL),
createVNode('span', { class: 'c' }, null, PatchFlags.BAIL),
]);
const updated = h('div', null, [
createVNode('span', { class: 'x' }, null, PatchFlags.BAIL),
createVNode('span', { class: 'y' }, null, PatchFlags.BAIL),
createVNode('span', { class: 'z' }, null, PatchFlags.BAIL),
]);
render(initial, container);
render(updated, container);
});
});

View File

@@ -0,0 +1,63 @@
import { describe, it, expect } from 'vitest';
import { createVNode, Comment, Fragment, h } from 'vue';
import { getRawChildren } from '../getRawChildren';
describe(getRawChildren, () => {
it('returns empty array for empty input', () => {
expect(getRawChildren([])).toEqual([]);
});
it('returns element vnodes as-is', () => {
const div = h('div');
const span = h('span');
const result = getRawChildren([div, span]);
expect(result).toHaveLength(2);
expect(result[0]!.type).toBe('div');
expect(result[1]!.type).toBe('span');
});
it('filters out Comment vnodes', () => {
const div = h('div');
const comment = createVNode(Comment, null, 'comment');
const result = getRawChildren([comment, div, comment]);
expect(result).toHaveLength(1);
expect(result[0]!.type).toBe('div');
});
it('flattens Fragment children', () => {
const fragment = createVNode(Fragment, null, [h('a'), h('b')]);
const result = getRawChildren([fragment]);
expect(result).toHaveLength(2);
expect(result[0]!.type).toBe('a');
expect(result[1]!.type).toBe('b');
});
it('recursively flattens nested Fragment children', () => {
const innerFragment = createVNode(Fragment, null, [h('span')]);
const outerFragment = createVNode(Fragment, null, [innerFragment, h('div')]);
const result = getRawChildren([outerFragment]);
expect(result).toHaveLength(2);
expect(result[0]!.type).toBe('span');
expect(result[1]!.type).toBe('div');
});
it('filters comments inside fragments', () => {
const fragment = createVNode(Fragment, null, [
createVNode(Comment, null, 'skip'),
h('p'),
]);
const result = getRawChildren([fragment]);
expect(result).toHaveLength(1);
expect(result[0]!.type).toBe('p');
});
});

View File

@@ -0,0 +1,40 @@
import type { VNode } from 'vue';
import { Comment, Fragment } from 'vue';
import { PatchFlags } from '@vue/shared';
/**
* Recursively extracts and flattens VNodes from potentially nested Fragments
* while filtering out Comment nodes.
*
* @param children - Array of VNodes to process
* @returns Flattened array of non-Comment VNodes
*/
export function getRawChildren(children: VNode[]): VNode[] {
const result: VNode[] = [];
flatten(children, result);
return result;
}
function flatten(children: VNode[], result: VNode[]): void {
let keyedFragmentCount = 0;
const startIdx = result.length;
for (const child of children) {
if (child.type === Fragment) {
if (child.patchFlag & PatchFlags.KEYED_FRAGMENT) {
keyedFragmentCount++;
}
flatten(child.children as VNode[], result);
}
else if (child.type !== Comment) {
result.push(child);
}
}
if (keyedFragmentCount > 1) {
for (let i = startIdx; i < result.length; i++) {
result[i]!.patchFlag = PatchFlags.BAIL;
}
}
}