From e2f55bcbc6b948068aee98f35bfff16e33dceed3 Mon Sep 17 00:00:00 2001 From: Nycolaide Date: Tue, 27 Jan 2026 18:45:13 +0100 Subject: [PATCH 01/10] refactorisation preprocess for file compiler --- .../compiler/__test__/computeSClasses.test.ts | 210 +++++++++++++ .../compiler/__test__/computeSStyles.test.ts | 218 ++++++++++++++ .../__test__/makeComponentProps.test.ts | 279 ++++++++++++++++++ .../src/lib/labs/compiler/mapped-code.ts | 105 +++++++ .../src/lib/labs/compiler/types/dom.ts | 2 + .../src/lib/labs/compiler/types/index.ts | 1 + 6 files changed, 815 insertions(+) create mode 100644 packages/lapikit/src/lib/labs/compiler/__test__/computeSClasses.test.ts create mode 100644 packages/lapikit/src/lib/labs/compiler/__test__/computeSStyles.test.ts create mode 100644 packages/lapikit/src/lib/labs/compiler/__test__/makeComponentProps.test.ts create mode 100644 packages/lapikit/src/lib/labs/compiler/mapped-code.ts create mode 100644 packages/lapikit/src/lib/labs/compiler/types/dom.ts create mode 100644 packages/lapikit/src/lib/labs/compiler/types/index.ts diff --git a/packages/lapikit/src/lib/labs/compiler/__test__/computeSClasses.test.ts b/packages/lapikit/src/lib/labs/compiler/__test__/computeSClasses.test.ts new file mode 100644 index 0000000..3a8eb17 --- /dev/null +++ b/packages/lapikit/src/lib/labs/compiler/__test__/computeSClasses.test.ts @@ -0,0 +1,210 @@ +import { describe, it, expect } from 'vitest'; +import { computeSClasses } from '../mapped-code.js'; +import type { SClassProp } from '../types/dom.js'; + +describe('computeSClasses', () => { + describe('sClass as string', () => { + it('should return the string when sClass is a non-empty string', () => { + const result = computeSClasses('btn btn-primary', {}); + expect(result).toBe('btn btn-primary'); + }); + + it('should return empty string when sClass is an empty string', () => { + const result = computeSClasses('', {}); + expect(result).toBe(''); + }); + }); + + describe('sClass as array', () => { + it('should join array elements with spaces', () => { + const result = computeSClasses(['btn', 'btn-primary'], {}); + expect(result).toBe('btn btn-primary'); + }); + + it('should filter out empty strings from array', () => { + const result = computeSClasses(['btn', '', 'btn-primary'], {}); + expect(result).toBe('btn btn-primary'); + }); + + it('should filter out non-string values from array', () => { + const result = computeSClasses( + ['btn', null, undefined, 'btn-primary'] as unknown as SClassProp, + {} + ); + expect(result).toBe('btn btn-primary'); + }); + + it('should return empty string for empty array', () => { + const result = computeSClasses([], {}); + expect(result).toBe(''); + }); + }); + + describe('sClass as object', () => { + it('should add keys where value is true', () => { + const result = computeSClasses( + { + btn: true, + 'btn-primary': true, + 'btn-disabled': false + }, + {} + ); + expect(result).toBe('btn btn-primary'); + }); + + it('should use string values as class names', () => { + const result = computeSClasses( + { + btn: 'custom-button', + size: 'large-size' + }, + {} + ); + expect(result).toBe('custom-button large-size'); + }); + + it('should handle mixed boolean and string values', () => { + const result = computeSClasses( + { + btn: true, + color: 'text-primary', + disabled: false + }, + {} + ); + expect(result).toBe('btn text-primary'); + }); + + it('should filter out empty string values', () => { + const result = computeSClasses( + { + btn: true, + color: '' + }, + {} + ); + expect(result).toBe('btn'); + }); + + it('should return empty string for empty object', () => { + const result = computeSClasses({}, {}); + expect(result).toBe(''); + }); + }); + + describe('classDirectiveProps (s-class_xxx)', () => { + it('should add class name when value is true', () => { + const result = computeSClasses('', { + 's-class_btn': true, + 's-class_primary': true + }); + expect(result).toBe('btn primary'); + }); + + it('should concatenate base with string value', () => { + const result = computeSClasses('', { + 's-class_btn': '-primary', + 's-class_size': '-large' + }); + expect(result).toBe('btn-primary size-large'); + }); + + it('should ignore non-true and non-string values', () => { + const result = computeSClasses('', { + 's-class_btn': true, + 's-class_disabled': false, + 's-class_color': '' + }); + expect(result).toBe('btn'); + }); + + it('should handle mixed boolean and string values', () => { + const result = computeSClasses('', { + 's-class_btn': true, + 's-class_variant': '-outlined' + }); + expect(result).toBe('btn variant-outlined'); + }); + }); + + describe('combined scenarios', () => { + it('should combine sClass string and classDirectiveProps', () => { + const result = computeSClasses('base-class', { + 's-class_modifier': '-active' + }); + expect(result).toBe('base-class modifier-active'); + }); + + it('should combine sClass array and classDirectiveProps', () => { + const result = computeSClasses(['btn', 'rounded'], { + 's-class_variant': '-primary' + }); + expect(result).toBe('btn rounded variant-primary'); + }); + + it('should combine sClass object and classDirectiveProps', () => { + const result = computeSClasses( + { + btn: true, + outlined: 'border-solid' + }, + { + 's-class_color': '-blue' + } + ); + expect(result).toBe('btn border-solid color-blue'); + }); + + it('should handle all types combined', () => { + const result = computeSClasses( + { + btn: true, + size: 'text-large' + }, + { + 's-class_variant': '-outlined', + 's-class_active': true + } + ); + expect(result).toBe('btn text-large variant-outlined active'); + }); + + it('should return empty string when everything is empty', () => { + const result = computeSClasses('', {}); + expect(result).toBe(''); + }); + + it('should handle null and undefined sClass', () => { + const result = computeSClasses(undefined, {}); + expect(result).toBe(''); + }); + }); + + describe('edge cases', () => { + it('should handle multiple spaces in string sClass', () => { + const result = computeSClasses('btn btn-primary', {}); + expect(result).toBe('btn btn-primary'); + }); + + it('should preserve order of classes', () => { + const result = computeSClasses(['z-class', 'a-class'], { + 's-class_m': true + }); + expect(result).toBe('z-class a-class m'); + }); + + it('should handle special characters in class names', () => { + const result = computeSClasses( + { + 'btn--primary': true, + btn__icon: 'icon--hover' + }, + { + 's-class_state': ':active' + } + ); + expect(result).toBe('btn--primary icon--hover state:active'); + }); + }); +}); diff --git a/packages/lapikit/src/lib/labs/compiler/__test__/computeSStyles.test.ts b/packages/lapikit/src/lib/labs/compiler/__test__/computeSStyles.test.ts new file mode 100644 index 0000000..6144ffb --- /dev/null +++ b/packages/lapikit/src/lib/labs/compiler/__test__/computeSStyles.test.ts @@ -0,0 +1,218 @@ +import { describe, it, expect } from 'vitest'; +import { computeSStyles } from '../mapped-code.js'; + +describe('computeSStyles', () => { + describe('sStyle as object', () => { + it('should convert object to CSS style string', () => { + const result = computeSStyles( + { + color: 'red', + 'font-size': '16px' + }, + {} + ); + expect(result).toBe('color: red; font-size: 16px'); + }); + + it('should filter out falsy values', () => { + const result = computeSStyles( + { + color: 'red', + display: false, + margin: '' + }, + {} + ); + expect(result).toBe('color: red'); + }); + + it('should handle boolean true values', () => { + const result = computeSStyles( + { + color: 'blue', + visible: true + }, + {} + ); + expect(result).toBe('color: blue; visible: true'); + }); + + it('should return empty string for empty object', () => { + const result = computeSStyles({}, {}); + expect(result).toBe(''); + }); + + it('should handle undefined sStyle', () => { + const result = computeSStyles(undefined, {}); + expect(result).toBe(''); + }); + + it('should handle CSS properties with units', () => { + const result = computeSStyles( + { + width: '100px', + height: '50%', + padding: '1rem' + }, + {} + ); + expect(result).toBe('width: 100px; height: 50%; padding: 1rem'); + }); + }); + + describe('styleDirectiveProps (s-style_xxx)', () => { + it('should add styles from directives', () => { + const result = computeSStyles( + {}, + { + 's-style_color': 'red', + 's-style_background': 'blue' + } + ); + expect(result).toBe('color: red; background: blue'); + }); + + it('should filter out falsy directive values', () => { + const result = computeSStyles( + {}, + { + 's-style_color': 'red', + 's-style_display': false, + 's-style_margin': '' + } + ); + expect(result).toBe('color: red'); + }); + + it('should handle boolean true values in directives', () => { + const result = computeSStyles( + {}, + { + 's-style_visible': true, + 's-style_color': 'green' + } + ); + expect(result).toBe('visible: true; color: green'); + }); + + it('should handle hyphenated CSS properties', () => { + const result = computeSStyles( + {}, + { + 's-style_font-size': '14px', + 's-style_text-align': 'center' + } + ); + expect(result).toBe('font-size: 14px; text-align: center'); + }); + }); + + describe('combined scenarios', () => { + it('should combine sStyle object and styleDirectiveProps', () => { + const result = computeSStyles( + { + color: 'red', + 'font-size': '16px' + }, + { + 's-style_margin': '10px', + 's-style_padding': '5px' + } + ); + expect(result).toBe('color: red; font-size: 16px; margin: 10px; padding: 5px'); + }); + + it('should filter falsy values from both sources', () => { + const result = computeSStyles( + { + color: 'red', + display: false + }, + { + 's-style_margin': '10px', + 's-style_padding': '' + } + ); + expect(result).toBe('color: red; margin: 10px'); + }); + + it('should return empty string when everything is empty', () => { + const result = computeSStyles({}, {}); + expect(result).toBe(''); + }); + + it('should handle mixed boolean and string values', () => { + const result = computeSStyles( + { + color: 'blue', + visible: true + }, + { + 's-style_display': 'block', + 's-style_active': true + } + ); + expect(result).toBe('color: blue; visible: true; display: block; active: true'); + }); + }); + + describe('edge cases', () => { + it('should preserve order of styles', () => { + const result = computeSStyles( + { + 'z-index': '999', + 'a-property': 'value' + }, + { + 's-style_m-property': 'test' + } + ); + expect(result).toBe('z-index: 999; a-property: value; m-property: test'); + }); + + it('should handle CSS custom properties (variables)', () => { + const result = computeSStyles( + { + '--primary-color': '#ff0000', + '--spacing': '1rem' + }, + {} + ); + expect(result).toBe('--primary-color: #ff0000; --spacing: 1rem'); + }); + + it('should handle calc and other CSS functions', () => { + const result = computeSStyles( + { + width: 'calc(100% - 20px)', + transform: 'rotate(45deg)' + }, + {} + ); + expect(result).toBe('width: calc(100% - 20px); transform: rotate(45deg)'); + }); + + it('should handle numeric values', () => { + const result = computeSStyles( + { + opacity: '0.5', + 'z-index': '10' + }, + {} + ); + expect(result).toBe('opacity: 0.5; z-index: 10'); + }); + + it('should handle color formats', () => { + const result = computeSStyles( + { + color: 'rgb(255, 0, 0)', + background: '#ff0000', + border: 'rgba(0, 0, 0, 0.5)' + }, + {} + ); + expect(result).toBe('color: rgb(255, 0, 0); background: #ff0000; border: rgba(0, 0, 0, 0.5)'); + }); + }); +}); diff --git a/packages/lapikit/src/lib/labs/compiler/__test__/makeComponentProps.test.ts b/packages/lapikit/src/lib/labs/compiler/__test__/makeComponentProps.test.ts new file mode 100644 index 0000000..8567578 --- /dev/null +++ b/packages/lapikit/src/lib/labs/compiler/__test__/makeComponentProps.test.ts @@ -0,0 +1,279 @@ +import { describe, it, expect } from 'vitest'; +import { makeComponentProps } from '../mapped-code.js'; + +describe('makeComponentProps', () => { + describe('basic separation', () => { + it('should separate s-class_ directives into classProps', () => { + const props = { + 's-class_btn': true, + 's-class_variant': '-primary', + id: 'test', + disabled: false + }; + + const result = makeComponentProps(props); + + expect(result.classProps).toEqual({ + 's-class_btn': true, + 's-class_variant': '-primary' + }); + }); + + it('should separate s-style_ directives into styleProps', () => { + const props = { + 's-style_color': 'red', + 's-style_font-size': '16px', + id: 'test', + disabled: false + }; + + const result = makeComponentProps(props); + + expect(result.styleProps).toEqual({ + 's-style_color': 'red', + 's-style_font-size': '16px' + }); + }); + + it('should separate remaining props into restProps', () => { + const props = { + 's-class_btn': true, + 's-style_color': 'red', + id: 'test', + disabled: false, + onClick: () => {} + }; + + const result = makeComponentProps(props); + + expect(result.restProps).toEqual({ + id: 'test', + disabled: false, + onClick: expect.any(Function) + }); + }); + }); + + describe('s-class and s-style base properties', () => { + it('should exclude s-class base property from all groups', () => { + const props = { + 's-class': 'btn btn-primary', + 's-class_active': true, + id: 'test' + }; + + const result = makeComponentProps(props); + + expect(result.classProps).toEqual({ + 's-class_active': true + }); + expect(result.styleProps).toEqual({}); + expect(result.restProps).toEqual({ + id: 'test' + }); + }); + + it('should exclude s-style base property from all groups', () => { + const props = { + 's-style': { color: 'red' }, + 's-style_margin': '10px', + id: 'test' + }; + + const result = makeComponentProps(props); + + expect(result.classProps).toEqual({}); + expect(result.styleProps).toEqual({ + 's-style_margin': '10px' + }); + expect(result.restProps).toEqual({ + id: 'test' + }); + }); + }); + + describe('all types combined', () => { + it('should properly separate all three types of props', () => { + const props = { + 's-class': 'base-class', + 's-class_btn': true, + 's-class_variant': '-outlined', + 's-style': { display: 'flex' }, + 's-style_color': 'blue', + 's-style_padding': '20px', + id: 'component', + disabled: false, + onClick: () => {}, + 'data-testid': 'test' + }; + + const result = makeComponentProps(props); + + expect(result.classProps).toEqual({ + 's-class_btn': true, + 's-class_variant': '-outlined' + }); + + expect(result.styleProps).toEqual({ + 's-style_color': 'blue', + 's-style_padding': '20px' + }); + + expect(result.restProps).toEqual({ + id: 'component', + disabled: false, + onClick: expect.any(Function), + 'data-testid': 'test' + }); + }); + }); + + describe('edge cases', () => { + it('should handle empty props object', () => { + const result = makeComponentProps({}); + + expect(result.classProps).toEqual({}); + expect(result.styleProps).toEqual({}); + expect(result.restProps).toEqual({}); + }); + + it('should handle props with only s-class directives', () => { + const props = { + 's-class_btn': true, + 's-class_size': '-large' + }; + + const result = makeComponentProps(props); + + expect(result.classProps).toEqual({ + 's-class_btn': true, + 's-class_size': '-large' + }); + expect(result.styleProps).toEqual({}); + expect(result.restProps).toEqual({}); + }); + + it('should handle props with only s-style directives', () => { + const props = { + 's-style_color': 'red', + 's-style_margin': '10px' + }; + + const result = makeComponentProps(props); + + expect(result.classProps).toEqual({}); + expect(result.styleProps).toEqual({ + 's-style_color': 'red', + 's-style_margin': '10px' + }); + expect(result.restProps).toEqual({}); + }); + + it('should handle props with only regular props', () => { + const props = { + id: 'test', + disabled: true, + 'aria-label': 'Button' + }; + + const result = makeComponentProps(props); + + expect(result.classProps).toEqual({}); + expect(result.styleProps).toEqual({}); + expect(result.restProps).toEqual({ + id: 'test', + disabled: true, + 'aria-label': 'Button' + }); + }); + + it('should handle hyphenated s-class directives', () => { + const props = { + 's-class_btn-primary': true, + 's-class_font-size': '-large' + }; + + const result = makeComponentProps(props); + + expect(result.classProps).toEqual({ + 's-class_btn-primary': true, + 's-class_font-size': '-large' + }); + }); + + it('should handle hyphenated s-style directives', () => { + const props = { + 's-style_font-size': '14px', + 's-style_background-color': 'white' + }; + + const result = makeComponentProps(props); + + expect(result.styleProps).toEqual({ + 's-style_font-size': '14px', + 's-style_background-color': 'white' + }); + }); + + it('should preserve values of different types', () => { + const fn = () => {}; + const obj = { nested: 'value' }; + + const props = { + 's-class_active': true, + 's-style_display': 'block', + string: 'text', + number: 42, + boolean: true, + null: null, + undefined: undefined, + function: fn, + object: obj, + array: [1, 2, 3] + }; + + const result = makeComponentProps(props); + + expect(result.restProps).toEqual({ + string: 'text', + number: 42, + boolean: true, + null: null, + undefined: undefined, + function: fn, + object: obj, + array: [1, 2, 3] + }); + }); + }); + + describe('return object structure', () => { + it('should always return an object with classProps, styleProps, and restProps keys', () => { + const result = makeComponentProps({ test: 'value' }); + + expect(result).toHaveProperty('classProps'); + expect(result).toHaveProperty('styleProps'); + expect(result).toHaveProperty('restProps'); + }); + + it('should return separate objects that do not reference the original props', () => { + const props = { + 's-class_btn': true, + 's-style_color': 'red', + id: 'test' + }; + + const result = makeComponentProps(props); + + // Modify result objects + result.classProps['s-class_new'] = true; + result.styleProps['s-style_new'] = 'blue'; + result.restProps['newProp'] = 'value'; + + // Original should be unchanged + expect(props).not.toHaveProperty('s-class_new'); + expect(props).not.toHaveProperty('s-style_new'); + expect(props).not.toHaveProperty('newProp'); + }); + }); +}); diff --git a/packages/lapikit/src/lib/labs/compiler/mapped-code.ts b/packages/lapikit/src/lib/labs/compiler/mapped-code.ts new file mode 100644 index 0000000..798dccc --- /dev/null +++ b/packages/lapikit/src/lib/labs/compiler/mapped-code.ts @@ -0,0 +1,105 @@ +import type { SClassProp, SStyleProp } from '$lib/labs/compiler/types/dom.js'; + +/** + * Computes a string of class names based on the provided sClass and classDirectiveProps. + * @param sClass - The s-class property which can be a string, array, or object. + * @param classDirectiveProps - An object containing s-class_xxx directives. + * @returns A string of class names. + */ +export function computeSClasses( + sClass: SClassProp, + classDirectiveProps: Record +): string { + const classes: string[] = []; + + // s-class string + if (typeof sClass === 'string' && sClass) { + classes.push(sClass); + } + + // s-class array + if (Array.isArray(sClass)) { + for (const value of sClass) { + if (typeof value === 'string' && value) { + classes.push(value); + } + } + } + + // s-class object + if (sClass && typeof sClass === 'object' && !Array.isArray(sClass)) { + Object.entries(sClass).forEach(([key, value]) => { + if (value === true) { + classes.push(key); + } else if (typeof value === 'string' && value) { + classes.push(value); + } + }); + } + + // s-class_xxx + Object.entries(classDirectiveProps).forEach(([key, value]) => { + const base = key.replace('s-class_', ''); + + if (value === true) { + classes.push(base); + } else if (typeof value === 'string' && value) { + classes.push(`${base}${value}`); + } + }); + + return classes.join(' '); +} + +/** + * Computes a string of style declarations based on the provided sStyle and styleDirectiveProps. + * @param sStyle - The s-style property which is an object of style key-value pairs. + * @param styleDirectiveProps - An object containing s-style_xxx directives. + * @returns A string of style declarations. + */ +export function computeSStyles( + sStyle: SStyleProp, + styleDirectiveProps: Record +): string { + const styles: string[] = []; + + if (sStyle && typeof sStyle === 'object') { + Object.entries(sStyle).forEach(([key, value]) => { + if (value) { + styles.push(`${key}: ${value}`); + } + }); + } + + Object.entries(styleDirectiveProps).forEach(([key, value]) => { + const base = key.replace('s-style_', ''); + if (value) { + styles.push(`${base}: ${value}`); + } + }); + + return styles.join('; '); +} + +/** + * Makes component props by separating s-class and s-style directives from other props. + * @param props The original props object containing all props. + * @returns An object containing separated classProps, styleProps, and restProps. + */ +export function makeComponentProps(props: Record) { + const classProps = Object.fromEntries( + Object.entries(props).filter(([key]) => key.startsWith('s-class_')) + ); + + const styleProps = Object.fromEntries( + Object.entries(props).filter(([key]) => key.startsWith('s-style_')) + ); + + const restProps = Object.fromEntries( + Object.entries(props).filter( + ([key]) => !key.startsWith('s-class') && !key.startsWith('s-style') + ) + ); + + return { classProps, styleProps, restProps }; +} diff --git a/packages/lapikit/src/lib/labs/compiler/types/dom.ts b/packages/lapikit/src/lib/labs/compiler/types/dom.ts new file mode 100644 index 0000000..3676b7f --- /dev/null +++ b/packages/lapikit/src/lib/labs/compiler/types/dom.ts @@ -0,0 +1,2 @@ +export type SClassProp = string | string[] | Record | undefined; +export type SStyleProp = Record | undefined; diff --git a/packages/lapikit/src/lib/labs/compiler/types/index.ts b/packages/lapikit/src/lib/labs/compiler/types/index.ts new file mode 100644 index 0000000..351ea2d --- /dev/null +++ b/packages/lapikit/src/lib/labs/compiler/types/index.ts @@ -0,0 +1 @@ +export type * from './dom.js'; From 09c4e216eb2ab04b80082d0ef84abb5e21dbd24f Mon Sep 17 00:00:00 2001 From: Nycolaide Date: Tue, 27 Jan 2026 19:11:14 +0100 Subject: [PATCH 02/10] add global useClassName function and useStyles --- .../compiler/__test__/computeSClasses.test.ts | 2 +- .../src/lib/labs/compiler/mapped-code.ts | 13 ++- .../src/lib/labs/components/btn/btn.svelte | 16 ++-- .../btn/btn.svelte.ts => utils/components.ts} | 44 ++++----- packages/lapikit/src/lib/labs/utils/index.ts | 2 +- packages/lapikit/src/lib/labs/utils/props.ts | 89 ------------------- .../lapikit/src/lib/labs/utils/types/index.ts | 16 ++++ 7 files changed, 58 insertions(+), 124 deletions(-) rename packages/lapikit/src/lib/labs/{components/btn/btn.svelte.ts => utils/components.ts} (59%) delete mode 100644 packages/lapikit/src/lib/labs/utils/props.ts create mode 100644 packages/lapikit/src/lib/labs/utils/types/index.ts diff --git a/packages/lapikit/src/lib/labs/compiler/__test__/computeSClasses.test.ts b/packages/lapikit/src/lib/labs/compiler/__test__/computeSClasses.test.ts index 3a8eb17..c5379cf 100644 --- a/packages/lapikit/src/lib/labs/compiler/__test__/computeSClasses.test.ts +++ b/packages/lapikit/src/lib/labs/compiler/__test__/computeSClasses.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; import { computeSClasses } from '../mapped-code.js'; -import type { SClassProp } from '../types/dom.js'; +import type { SClassProp } from '../types/index.js'; describe('computeSClasses', () => { describe('sClass as string', () => { diff --git a/packages/lapikit/src/lib/labs/compiler/mapped-code.ts b/packages/lapikit/src/lib/labs/compiler/mapped-code.ts index 798dccc..8ef6f93 100644 --- a/packages/lapikit/src/lib/labs/compiler/mapped-code.ts +++ b/packages/lapikit/src/lib/labs/compiler/mapped-code.ts @@ -1,4 +1,5 @@ -import type { SClassProp, SStyleProp } from '$lib/labs/compiler/types/dom.js'; +import type { SClassProp, SStyleProp } from '$lib/labs/compiler/types/index.js'; +import type { PropValue } from '$lib/labs/utils/types/index.js'; /** * Computes a string of class names based on the provided sClass and classDirectiveProps. @@ -86,14 +87,18 @@ export function computeSStyles( * @param props The original props object containing all props. * @returns An object containing separated classProps, styleProps, and restProps. */ -export function makeComponentProps(props: Record) { +export function makeComponentProps(props: Record): { + classProps: Record; + styleProps: Record; + restProps: Record; +} { const classProps = Object.fromEntries( Object.entries(props).filter(([key]) => key.startsWith('s-class_')) - ); + ) as Record; const styleProps = Object.fromEntries( Object.entries(props).filter(([key]) => key.startsWith('s-style_')) - ); + ) as Record; const restProps = Object.fromEntries( Object.entries(props).filter( diff --git a/packages/lapikit/src/lib/labs/components/btn/btn.svelte b/packages/lapikit/src/lib/labs/components/btn/btn.svelte index 3319b5e..1bf0c00 100644 --- a/packages/lapikit/src/lib/labs/components/btn/btn.svelte +++ b/packages/lapikit/src/lib/labs/components/btn/btn.svelte @@ -1,6 +1,6 @@ - diff --git a/packages/lapikit/src/lib/labs/components/btn/btn.svelte.ts b/packages/lapikit/src/lib/labs/utils/components.ts similarity index 59% rename from packages/lapikit/src/lib/labs/components/btn/btn.svelte.ts rename to packages/lapikit/src/lib/labs/utils/components.ts index 00f70a0..7ea15db 100644 --- a/packages/lapikit/src/lib/labs/components/btn/btn.svelte.ts +++ b/packages/lapikit/src/lib/labs/utils/components.ts @@ -1,16 +1,19 @@ -import type { SClassProp, SStyleProp } from '../../utils/index.js'; +import type { useClassNameProps, useStylesProps } from '$lib/labs/utils/types/index.js'; +/** + * useClassName - Utility to compute class names for a component. + * @param baseClass - The base class name for the component. + * @param className - Additional class names as a string. + * @param sClass - The s-class property which can be a string, array, or object. + * @param classProps - An object containing s-class_xxx directives. + * @returns + */ export function useClassName({ baseClass = '', className, sClass, - classDirectiveProps -}: { - baseClass?: string; - className?: string; - sClass?: SClassProp; - classDirectiveProps?: Record; -} = {}) { + classProps +}: useClassNameProps = {}) { return { get value() { const classes: string[] = []; @@ -41,8 +44,8 @@ export function useClassName({ }); } - if (classDirectiveProps) { - Object.entries(classDirectiveProps).forEach(([key, value]) => { + if (classProps) { + Object.entries(classProps).forEach(([key, value]) => { const base = key.replace('s-class_', ''); if (value === true) { @@ -62,15 +65,14 @@ export function useClassName({ }; } -export function useStyles({ - styleAttr, - sStyle, - styleDirectiveProps -}: { - styleAttr?: string; - sStyle?: SStyleProp; - styleDirectiveProps?: Record; -} = {}) { +/** + * useStyles - Utility to compute style declarations for a component. + * @param styleAttr - Inline style attribute as a string. + * @param sStyle - The s-style property which is an object of style key-value pairs. + * @param styleProps - An object containing s-style_xxx directives. + * @returns + */ +export function useStyles({ styleAttr, sStyle, styleProps }: useStylesProps = {}) { return { get value() { const styles: string[] = []; @@ -83,8 +85,8 @@ export function useStyles({ }); } - if (styleDirectiveProps) { - Object.entries(styleDirectiveProps).forEach(([key, value]) => { + if (styleProps) { + Object.entries(styleProps).forEach(([key, value]) => { const base = key.replace('s-style_', ''); if (value) { styles.push(`${base}: ${value}`); diff --git a/packages/lapikit/src/lib/labs/utils/index.ts b/packages/lapikit/src/lib/labs/utils/index.ts index 1b692d7..dcd948a 100644 --- a/packages/lapikit/src/lib/labs/utils/index.ts +++ b/packages/lapikit/src/lib/labs/utils/index.ts @@ -1 +1 @@ -export * from './props.js'; +export * from './components.js'; diff --git a/packages/lapikit/src/lib/labs/utils/props.ts b/packages/lapikit/src/lib/labs/utils/props.ts deleted file mode 100644 index c3cf22a..0000000 --- a/packages/lapikit/src/lib/labs/utils/props.ts +++ /dev/null @@ -1,89 +0,0 @@ -export type SClassProp = string | string[] | Record | undefined; -export type SStyleProp = Record | undefined; - -export function splitSyntheticProps(allProps: Record) { - const classDirectiveProps = Object.fromEntries( - Object.entries(allProps).filter(([key]) => key.startsWith('s-class_')) - ); - - const styleDirectiveProps = Object.fromEntries( - Object.entries(allProps).filter(([key]) => key.startsWith('s-style_')) - ); - - const regularProps = Object.fromEntries( - Object.entries(allProps).filter( - ([key]) => !key.startsWith('s-class') && !key.startsWith('s-style') - ) - ); - - return { classDirectiveProps, styleDirectiveProps, regularProps }; -} - -export function computeSClasses( - sClass: SClassProp, - classDirectiveProps: Record -): string { - const classes: string[] = []; - - // s-class string - if (typeof sClass === 'string' && sClass) { - classes.push(sClass); - } - - // s-class array - if (Array.isArray(sClass)) { - for (const value of sClass) { - if (typeof value === 'string' && value) { - classes.push(value); - } - } - } - - // s-class object - if (sClass && typeof sClass === 'object' && !Array.isArray(sClass)) { - Object.entries(sClass).forEach(([key, value]) => { - if (value === true) { - classes.push(key); - } else if (typeof value === 'string' && value) { - classes.push(value); - } - }); - } - - // s-class_xxx - Object.entries(classDirectiveProps).forEach(([key, value]) => { - const base = key.replace('s-class_', ''); - - if (value === true) { - classes.push(base); - } else if (typeof value === 'string' && value) { - classes.push(`${base}${value}`); - } - }); - - return classes.join(' '); -} - -export function computeSStyles( - sStyle: SStyleProp, - styleDirectiveProps: Record -): string { - const styles: string[] = []; - - if (sStyle && typeof sStyle === 'object') { - Object.entries(sStyle).forEach(([key, value]) => { - if (value) { - styles.push(`${key}: ${value}`); - } - }); - } - - Object.entries(styleDirectiveProps).forEach(([key, value]) => { - const base = key.replace('s-style_', ''); - if (value) { - styles.push(`${base}: ${value}`); - } - }); - - return styles.join('; '); -} diff --git a/packages/lapikit/src/lib/labs/utils/types/index.ts b/packages/lapikit/src/lib/labs/utils/types/index.ts new file mode 100644 index 0000000..bee8e51 --- /dev/null +++ b/packages/lapikit/src/lib/labs/utils/types/index.ts @@ -0,0 +1,16 @@ +import type { SClassProp, SStyleProp } from '$lib/labs/compiler/types/index.js'; + +export type PropValue = string | boolean | number | null | undefined; + +export interface useClassNameProps { + baseClass?: string; + className?: string; + sClass?: SClassProp; + classProps?: Record; +} + +export interface useStylesProps { + styleAttr?: string; + sStyle?: SStyleProp; + styleProps?: Record; +} From c1b27c27769dac7e1b6cddefe0321317a8c07f42 Mon Sep 17 00:00:00 2001 From: Nycolaide Date: Tue, 27 Jan 2026 19:22:39 +0100 Subject: [PATCH 03/10] optimization for compiler and utils functions components --- .../src/lib/labs/compiler/mapped-code.ts | 83 ++++++----- .../src/lib/labs/components/btn/btn.svelte | 10 +- .../lapikit/src/lib/labs/utils/components.ts | 136 +++++++++--------- 3 files changed, 125 insertions(+), 104 deletions(-) diff --git a/packages/lapikit/src/lib/labs/compiler/mapped-code.ts b/packages/lapikit/src/lib/labs/compiler/mapped-code.ts index 8ef6f93..a9ce106 100644 --- a/packages/lapikit/src/lib/labs/compiler/mapped-code.ts +++ b/packages/lapikit/src/lib/labs/compiler/mapped-code.ts @@ -29,25 +29,32 @@ export function computeSClasses( // s-class object if (sClass && typeof sClass === 'object' && !Array.isArray(sClass)) { - Object.entries(sClass).forEach(([key, value]) => { - if (value === true) { - classes.push(key); - } else if (typeof value === 'string' && value) { - classes.push(value); + const entries = Object.entries(sClass); + if (entries.length > 0) { + for (const [key, value] of entries) { + if (value === true) { + classes.push(key); + } else if (typeof value === 'string' && value) { + classes.push(value); + } } - }); + } } // s-class_xxx - Object.entries(classDirectiveProps).forEach(([key, value]) => { - const base = key.replace('s-class_', ''); + const classEntries = Object.entries(classDirectiveProps); + if (classEntries.length > 0) { + for (const [key, value] of classEntries) { + // Use slice instead of replace for better performance (8 = 's-class_'.length) + const base = key.slice(8); - if (value === true) { - classes.push(base); - } else if (typeof value === 'string' && value) { - classes.push(`${base}${value}`); + if (value === true) { + classes.push(base); + } else if (typeof value === 'string' && value) { + classes.push(`${base}${value}`); + } } - }); + } return classes.join(' '); } @@ -65,25 +72,33 @@ export function computeSStyles( const styles: string[] = []; if (sStyle && typeof sStyle === 'object') { - Object.entries(sStyle).forEach(([key, value]) => { - if (value) { - styles.push(`${key}: ${value}`); + const entries = Object.entries(sStyle); + if (entries.length > 0) { + for (const [key, value] of entries) { + if (value) { + styles.push(`${key}: ${value}`); + } } - }); + } } - Object.entries(styleDirectiveProps).forEach(([key, value]) => { - const base = key.replace('s-style_', ''); - if (value) { - styles.push(`${base}: ${value}`); + const styleEntries = Object.entries(styleDirectiveProps); + if (styleEntries.length > 0) { + for (const [key, value] of styleEntries) { + // Use slice instead of replace for better performance (8 = 's-style_'.length) + const base = key.slice(8); + if (value) { + styles.push(`${base}: ${value}`); + } } - }); + } return styles.join('; '); } /** * Makes component props by separating s-class and s-style directives from other props. + * Optimized to use a single pass instead of three separate iterations. * @param props The original props object containing all props. * @returns An object containing separated classProps, styleProps, and restProps. */ @@ -92,19 +107,19 @@ export function makeComponentProps(props: Record): { styleProps: Record; restProps: Record; } { - const classProps = Object.fromEntries( - Object.entries(props).filter(([key]) => key.startsWith('s-class_')) - ) as Record; - - const styleProps = Object.fromEntries( - Object.entries(props).filter(([key]) => key.startsWith('s-style_')) - ) as Record; + const classProps: Record = {}; + const styleProps: Record = {}; + const restProps: Record = {}; - const restProps = Object.fromEntries( - Object.entries(props).filter( - ([key]) => !key.startsWith('s-class') && !key.startsWith('s-style') - ) - ); + for (const [key, value] of Object.entries(props)) { + if (key.startsWith('s-class_')) { + classProps[key] = value as PropValue; + } else if (key.startsWith('s-style_')) { + styleProps[key] = value as PropValue; + } else if (!key.startsWith('s-class') && !key.startsWith('s-style')) { + restProps[key] = value; + } + } return { classProps, styleProps, restProps }; } diff --git a/packages/lapikit/src/lib/labs/components/btn/btn.svelte b/packages/lapikit/src/lib/labs/components/btn/btn.svelte index 1bf0c00..6437e01 100644 --- a/packages/lapikit/src/lib/labs/components/btn/btn.svelte +++ b/packages/lapikit/src/lib/labs/components/btn/btn.svelte @@ -15,25 +15,25 @@ makeComponentProps(rest as Record) ); - let finalClass = $derived( + let componentClass = $derived( useClassName({ baseClass: 'kit-btn', className, sClass, classProps - }).value + }) ); - let finalStyle = $derived( + let componentStyle = $derived( useStyles({ styleAttr, sStyle, styleProps - }).value + }) ); - diff --git a/packages/lapikit/src/lib/labs/utils/components.ts b/packages/lapikit/src/lib/labs/utils/components.ts index 7ea15db..a4cbefe 100644 --- a/packages/lapikit/src/lib/labs/utils/components.ts +++ b/packages/lapikit/src/lib/labs/utils/components.ts @@ -6,99 +6,105 @@ import type { useClassNameProps, useStylesProps } from '$lib/labs/utils/types/in * @param className - Additional class names as a string. * @param sClass - The s-class property which can be a string, array, or object. * @param classProps - An object containing s-class_xxx directives. - * @returns + * @returns A computed class string. */ export function useClassName({ baseClass = '', className, sClass, classProps -}: useClassNameProps = {}) { - return { - get value() { - const classes: string[] = []; +}: useClassNameProps = {}): string { + const classes: string[] = []; - if (baseClass) { - classes.push(baseClass); - } + if (baseClass) { + classes.push(baseClass); + } - if (typeof sClass === 'string' && sClass) { - classes.push(sClass); - } + if (typeof sClass === 'string' && sClass) { + classes.push(sClass); + } - if (Array.isArray(sClass)) { - for (const value of sClass) { - if (typeof value === 'string' && value) { - classes.push(value); - } - } + if (Array.isArray(sClass)) { + for (const value of sClass) { + if (typeof value === 'string' && value) { + classes.push(value); } + } + } - if (sClass && typeof sClass === 'object' && !Array.isArray(sClass)) { - Object.entries(sClass).forEach(([key, value]) => { - if (value === true) { - classes.push(key); - } else if (typeof value === 'string' && value) { - classes.push(value); - } - }); + if (sClass && typeof sClass === 'object' && !Array.isArray(sClass)) { + const entries = Object.entries(sClass); + if (entries.length > 0) { + for (const [key, value] of entries) { + if (value === true) { + classes.push(key); + } else if (typeof value === 'string' && value) { + classes.push(value); + } } + } + } - if (classProps) { - Object.entries(classProps).forEach(([key, value]) => { - const base = key.replace('s-class_', ''); + if (classProps) { + const entries = Object.entries(classProps); + if (entries.length > 0) { + for (const [key, value] of entries) { + // Use slice instead of replace for better performance (8 = 's-class_'.length) + const base = key.slice(8); - if (value === true) { - classes.push(base); - } else if (typeof value === 'string' && value) { - classes.push(`${base}${value}`); - } - }); + if (value === true) { + classes.push(base); + } else if (typeof value === 'string' && value) { + classes.push(`${base}${value}`); + } } + } + } - if (className) { - classes.push(className); - } + if (className) { + classes.push(className); + } - return classes.filter(Boolean).join(' '); - } - }; + return classes.filter(Boolean).join(' '); } /** - * useStyles - Utility to compute style declarations for a component. + * useStyles - Utility to compute style declarations for a component (optimized pure function). * @param styleAttr - Inline style attribute as a string. * @param sStyle - The s-style property which is an object of style key-value pairs. * @param styleProps - An object containing s-style_xxx directives. - * @returns + * @returns A computed style string. */ -export function useStyles({ styleAttr, sStyle, styleProps }: useStylesProps = {}) { - return { - get value() { - const styles: string[] = []; +export function useStyles({ styleAttr, sStyle, styleProps }: useStylesProps = {}): string { + const styles: string[] = []; - if (sStyle && typeof sStyle === 'object') { - Object.entries(sStyle).forEach(([key, value]) => { - if (value) { - styles.push(`${key}: ${value}`); - } - }); + if (sStyle && typeof sStyle === 'object') { + const entries = Object.entries(sStyle); + if (entries.length > 0) { + for (const [key, value] of entries) { + if (value) { + styles.push(`${key}: ${value}`); + } } + } + } - if (styleProps) { - Object.entries(styleProps).forEach(([key, value]) => { - const base = key.replace('s-style_', ''); - if (value) { - styles.push(`${base}: ${value}`); - } - }); + if (styleProps) { + const entries = Object.entries(styleProps); + if (entries.length > 0) { + for (const [key, value] of entries) { + // Use slice instead of replace for better performance (8 = 's-style_'.length) + const base = key.slice(8); + if (value) { + styles.push(`${base}: ${value}`); + } } + } + } - if (styleAttr) { - styles.push(styleAttr); - } + if (styleAttr) { + styles.push(styleAttr); + } - return styles.filter(Boolean).join('; '); - } - }; + return styles.filter(Boolean).join('; '); } From 405d0e02feaa74007dc57323dc6eb7beb5ae497d Mon Sep 17 00:00:00 2001 From: Nycolaide Date: Tue, 27 Jan 2026 19:38:00 +0100 Subject: [PATCH 04/10] add test for components utils --- ...lasses.test.ts => compute-classes.test.ts} | 0 ...SStyles.test.ts => compute-styles.test.ts} | 0 ...s.test.ts => make-component-props.test.ts} | 0 .../utils/__test__/use-class-name.test.ts | 241 ++++++++++++++++ .../labs/utils/__test__/use-styles.test.ts | 260 ++++++++++++++++++ 5 files changed, 501 insertions(+) rename packages/lapikit/src/lib/labs/compiler/__test__/{computeSClasses.test.ts => compute-classes.test.ts} (100%) rename packages/lapikit/src/lib/labs/compiler/__test__/{computeSStyles.test.ts => compute-styles.test.ts} (100%) rename packages/lapikit/src/lib/labs/compiler/__test__/{makeComponentProps.test.ts => make-component-props.test.ts} (100%) create mode 100644 packages/lapikit/src/lib/labs/utils/__test__/use-class-name.test.ts create mode 100644 packages/lapikit/src/lib/labs/utils/__test__/use-styles.test.ts diff --git a/packages/lapikit/src/lib/labs/compiler/__test__/computeSClasses.test.ts b/packages/lapikit/src/lib/labs/compiler/__test__/compute-classes.test.ts similarity index 100% rename from packages/lapikit/src/lib/labs/compiler/__test__/computeSClasses.test.ts rename to packages/lapikit/src/lib/labs/compiler/__test__/compute-classes.test.ts diff --git a/packages/lapikit/src/lib/labs/compiler/__test__/computeSStyles.test.ts b/packages/lapikit/src/lib/labs/compiler/__test__/compute-styles.test.ts similarity index 100% rename from packages/lapikit/src/lib/labs/compiler/__test__/computeSStyles.test.ts rename to packages/lapikit/src/lib/labs/compiler/__test__/compute-styles.test.ts diff --git a/packages/lapikit/src/lib/labs/compiler/__test__/makeComponentProps.test.ts b/packages/lapikit/src/lib/labs/compiler/__test__/make-component-props.test.ts similarity index 100% rename from packages/lapikit/src/lib/labs/compiler/__test__/makeComponentProps.test.ts rename to packages/lapikit/src/lib/labs/compiler/__test__/make-component-props.test.ts diff --git a/packages/lapikit/src/lib/labs/utils/__test__/use-class-name.test.ts b/packages/lapikit/src/lib/labs/utils/__test__/use-class-name.test.ts new file mode 100644 index 0000000..9b3f1bf --- /dev/null +++ b/packages/lapikit/src/lib/labs/utils/__test__/use-class-name.test.ts @@ -0,0 +1,241 @@ +import { describe, expect, it } from 'vitest'; +import { useClassName } from '../components.js'; + +describe('useClassName', () => { + describe('baseClass', () => { + it('should return baseClass when only baseClass is provided', () => { + const result = useClassName({ baseClass: 'btn' }); + expect(result).toBe('btn'); + }); + + it('should return empty string when no arguments are provided', () => { + const result = useClassName(); + expect(result).toBe(''); + }); + + it('should ignore empty baseClass', () => { + const result = useClassName({ baseClass: '' }); + expect(result).toBe(''); + }); + }); + + describe('className', () => { + it('should return className when only className is provided', () => { + const result = useClassName({ className: 'custom-class' }); + expect(result).toBe('custom-class'); + }); + + it('should combine baseClass and className', () => { + const result = useClassName({ baseClass: 'btn', className: 'custom' }); + expect(result).toBe('btn custom'); + }); + + it('should ignore empty className', () => { + const result = useClassName({ baseClass: 'btn', className: '' }); + expect(result).toBe('btn'); + }); + }); + + describe('sClass - string', () => { + it('should handle sClass as a string', () => { + const result = useClassName({ sClass: 'primary' }); + expect(result).toBe('primary'); + }); + + it('should combine baseClass and sClass string', () => { + const result = useClassName({ baseClass: 'btn', sClass: 'primary' }); + expect(result).toBe('btn primary'); + }); + + it('should ignore empty sClass string', () => { + const result = useClassName({ baseClass: 'btn', sClass: '' }); + expect(result).toBe('btn'); + }); + }); + + describe('sClass - array', () => { + it('should handle sClass as an array of strings', () => { + const result = useClassName({ sClass: ['primary', 'large'] }); + expect(result).toBe('primary large'); + }); + + it('should combine baseClass and sClass array', () => { + const result = useClassName({ baseClass: 'btn', sClass: ['primary', 'large'] }); + expect(result).toBe('btn primary large'); + }); + + it('should filter out empty strings in array', () => { + const result = useClassName({ sClass: ['primary', '', 'large', ''] }); + expect(result).toBe('primary large'); + }); + + it('should handle empty array', () => { + const result = useClassName({ baseClass: 'btn', sClass: [] }); + expect(result).toBe('btn'); + }); + + it('should ignore non-string values in array', () => { + // Testing runtime behavior with invalid types + const result = useClassName({ + sClass: ['primary', null, undefined, 123, true] as unknown as string[] + }); + expect(result).toBe('primary'); + }); + }); + + describe('sClass - object', () => { + it('should handle sClass object with boolean true values', () => { + const result = useClassName({ sClass: { primary: true, disabled: true } }); + expect(result).toBe('primary disabled'); + }); + + it('should ignore sClass object with boolean false values', () => { + const result = useClassName({ sClass: { primary: true, disabled: false } }); + expect(result).toBe('primary'); + }); + + it('should handle sClass object with string values', () => { + const result = useClassName({ sClass: { variant: 'primary', size: 'lg' } }); + expect(result).toBe('primary lg'); + }); + + it('should ignore empty string values in object', () => { + const result = useClassName({ sClass: { variant: 'primary', size: '' } }); + expect(result).toBe('primary'); + }); + + it('should combine baseClass and sClass object', () => { + const result = useClassName({ baseClass: 'btn', sClass: { primary: true, large: true } }); + expect(result).toBe('btn primary large'); + }); + + it('should handle empty object', () => { + const result = useClassName({ baseClass: 'btn', sClass: {} }); + expect(result).toBe('btn'); + }); + + it('should handle mixed boolean and string values in object', () => { + const result = useClassName({ + sClass: { primary: true, variant: 'outlined', disabled: false, size: 'md' } + }); + expect(result).toBe('primary outlined md'); + }); + }); + + describe('classProps (s-class_xxx directives)', () => { + it('should handle classProps with boolean true', () => { + const result = useClassName({ classProps: { 's-class_active': true } }); + expect(result).toBe('active'); + }); + + it('should handle classProps with string values', () => { + const result = useClassName({ classProps: { 's-class_variant': '-primary' } }); + expect(result).toBe('variant-primary'); + }); + + it('should ignore classProps with boolean false', () => { + const result = useClassName({ + classProps: { 's-class_active': true, 's-class_disabled': false } + }); + expect(result).toBe('active'); + }); + + it('should handle multiple classProps', () => { + const result = useClassName({ + classProps: { + 's-class_active': true, + 's-class_variant': '-primary', + 's-class_size': '-lg' + } + }); + expect(result).toBe('active variant-primary size-lg'); + }); + + it('should handle empty classProps object', () => { + const result = useClassName({ baseClass: 'btn', classProps: {} }); + expect(result).toBe('btn'); + }); + + it('should ignore empty string values in classProps', () => { + const result = useClassName({ + classProps: { 's-class_variant': '-primary', 's-class_size': '' } + }); + expect(result).toBe('variant-primary'); + }); + }); + + describe('combination of all props', () => { + it('should combine all props in correct order', () => { + const result = useClassName({ + baseClass: 'btn', + sClass: 'primary', + className: 'custom', + classProps: { 's-class_active': true } + }); + expect(result).toBe('btn primary active custom'); + }); + + it('should handle complex combination with sClass array', () => { + const result = useClassName({ + baseClass: 'btn', + sClass: ['primary', 'large'], + className: 'my-btn', + classProps: { 's-class_rounded': true, 's-class_shadow': '-md' } + }); + expect(result).toBe('btn primary large rounded shadow-md my-btn'); + }); + + it('should handle complex combination with sClass object', () => { + const result = useClassName({ + baseClass: 'btn', + sClass: { primary: true, disabled: false, variant: 'outlined' }, + className: 'custom', + classProps: { 's-class_active': true } + }); + expect(result).toBe('btn primary outlined active custom'); + }); + + it('should filter out all falsy values', () => { + const result = useClassName({ + baseClass: '', + sClass: { primary: false, active: true, size: '' }, + className: '', + classProps: { 's-class_disabled': false, 's-class_rounded': true } + }); + expect(result).toBe('active rounded'); + }); + }); + + describe('edge cases', () => { + it('should handle undefined values gracefully', () => { + const result = useClassName({ + baseClass: undefined, + sClass: undefined, + className: undefined, + classProps: undefined + }); + expect(result).toBe(''); + }); + + it('should handle null values in sClass object', () => { + // Testing runtime behavior with invalid types + const result = useClassName({ + sClass: { primary: null } as unknown as Record + }); + expect(result).toBe(''); + }); + + it('should handle number values in classProps', () => { + // Testing runtime behavior with invalid types + const result = useClassName({ + classProps: { 's-class_count': 5 } as unknown as Record + }); + expect(result).toBe(''); + }); + + it('should preserve whitespace in class names', () => { + const result = useClassName({ baseClass: 'btn', className: 'my custom' }); + expect(result).toBe('btn my custom'); + }); + }); +}); diff --git a/packages/lapikit/src/lib/labs/utils/__test__/use-styles.test.ts b/packages/lapikit/src/lib/labs/utils/__test__/use-styles.test.ts new file mode 100644 index 0000000..aa0461d --- /dev/null +++ b/packages/lapikit/src/lib/labs/utils/__test__/use-styles.test.ts @@ -0,0 +1,260 @@ +import { describe, expect, it } from 'vitest'; +import { useStyles } from '../components.js'; + +describe('useStyles', () => { + describe('no arguments', () => { + it('should return empty string when no arguments are provided', () => { + const result = useStyles(); + expect(result).toBe(''); + }); + + it('should return empty string when all arguments are undefined', () => { + const result = useStyles({ + styleAttr: undefined, + sStyle: undefined, + styleProps: undefined + }); + expect(result).toBe(''); + }); + }); + + describe('styleAttr', () => { + it('should return styleAttr when only styleAttr is provided', () => { + const result = useStyles({ styleAttr: 'color: red' }); + expect(result).toBe('color: red'); + }); + + it('should ignore empty styleAttr', () => { + const result = useStyles({ styleAttr: '' }); + expect(result).toBe(''); + }); + + it('should handle multiple style declarations in styleAttr', () => { + const result = useStyles({ styleAttr: 'color: red; font-size: 16px' }); + expect(result).toBe('color: red; font-size: 16px'); + }); + }); + + describe('sStyle - object', () => { + it('should handle sStyle object with string values', () => { + const result = useStyles({ sStyle: { color: 'red', fontSize: '16px' } }); + expect(result).toBe('color: red; fontSize: 16px'); + }); + + it('should handle sStyle object with single property', () => { + const result = useStyles({ sStyle: { color: 'blue' } }); + expect(result).toBe('color: blue'); + }); + + it('should ignore falsy values in sStyle object', () => { + const result = useStyles({ + sStyle: { color: 'red', fontSize: '', display: false as unknown as string } + }); + expect(result).toBe('color: red'); + }); + + it('should handle empty sStyle object', () => { + const result = useStyles({ sStyle: {} }); + expect(result).toBe(''); + }); + + it('should handle sStyle with boolean true values', () => { + const result = useStyles({ + sStyle: { color: 'red', display: true as unknown as string } + }); + expect(result).toBe('color: red; display: true'); + }); + + it('should handle CSS custom properties (variables)', () => { + const result = useStyles({ + sStyle: { '--primary-color': '#3b82f6', '--spacing': '1rem' } + }); + expect(result).toBe('--primary-color: #3b82f6; --spacing: 1rem'); + }); + + it('should handle numeric string values', () => { + const result = useStyles({ sStyle: { width: '100px', height: '50px' } }); + expect(result).toBe('width: 100px; height: 50px'); + }); + }); + + describe('styleProps (s-style_xxx directives)', () => { + it('should handle styleProps with string values', () => { + const result = useStyles({ styleProps: { 's-style_color': 'red' } }); + expect(result).toBe('color: red'); + }); + + it('should handle multiple styleProps', () => { + const result = useStyles({ + styleProps: { + 's-style_color': 'red', + 's-style_fontSize': '16px', + 's-style_display': 'flex' + } + }); + expect(result).toBe('color: red; fontSize: 16px; display: flex'); + }); + + it('should ignore falsy values in styleProps', () => { + const result = useStyles({ + styleProps: { + 's-style_color': 'red', + 's-style_fontSize': '', + 's-style_display': false as unknown as string + } + }); + expect(result).toBe('color: red'); + }); + + it('should handle empty styleProps object', () => { + const result = useStyles({ styleProps: {} }); + expect(result).toBe(''); + }); + + it('should handle styleProps with CSS custom properties', () => { + const result = useStyles({ + styleProps: { + 's-style_--primary': '#3b82f6', + 's-style_--spacing': '1rem' + } + }); + expect(result).toBe('--primary: #3b82f6; --spacing: 1rem'); + }); + + it('should handle styleProps with boolean true', () => { + const result = useStyles({ + styleProps: { 's-style_display': true as unknown as string } + }); + expect(result).toBe('display: true'); + }); + }); + + describe('combination of props', () => { + it('should combine sStyle and styleAttr', () => { + const result = useStyles({ + sStyle: { color: 'red' }, + styleAttr: 'font-size: 16px' + }); + expect(result).toBe('color: red; font-size: 16px'); + }); + + it('should combine styleProps and styleAttr', () => { + const result = useStyles({ + styleProps: { 's-style_color': 'red' }, + styleAttr: 'font-size: 16px' + }); + expect(result).toBe('color: red; font-size: 16px'); + }); + + it('should combine all props in correct order (sStyle, styleProps, styleAttr)', () => { + const result = useStyles({ + sStyle: { color: 'red' }, + styleProps: { 's-style_fontSize': '16px' }, + styleAttr: 'display: flex' + }); + expect(result).toBe('color: red; fontSize: 16px; display: flex'); + }); + + it('should handle complex combination with multiple properties', () => { + const result = useStyles({ + sStyle: { color: 'red', backgroundColor: 'white' }, + styleProps: { + 's-style_fontSize': '16px', + 's-style_padding': '10px' + }, + styleAttr: 'display: flex; align-items: center' + }); + expect(result).toBe( + 'color: red; backgroundColor: white; fontSize: 16px; padding: 10px; display: flex; align-items: center' + ); + }); + + it('should filter out all falsy values in combination', () => { + const result = useStyles({ + sStyle: { color: 'red', fontSize: '' }, + styleProps: { 's-style_display': 'flex', 's-style_margin': '' }, + styleAttr: '' + }); + expect(result).toBe('color: red; display: flex'); + }); + }); + + describe('edge cases', () => { + it('should handle null sStyle', () => { + const result = useStyles({ sStyle: null as unknown as Record }); + expect(result).toBe(''); + }); + + it('should handle undefined styleProps', () => { + const result = useStyles({ styleProps: undefined }); + expect(result).toBe(''); + }); + + it('should handle number values in sStyle', () => { + // Testing runtime behavior with invalid types + const result = useStyles({ + sStyle: { width: 100 as unknown as string, color: 'red' } + }); + expect(result).toBe('width: 100; color: red'); + }); + + it('should handle null values in sStyle object', () => { + // Testing runtime behavior with invalid types + const result = useStyles({ + sStyle: { color: null as unknown as string, fontSize: '16px' } + }); + expect(result).toBe('fontSize: 16px'); + }); + + it('should preserve semicolons in styleAttr', () => { + const result = useStyles({ + sStyle: { color: 'red' }, + styleAttr: 'font-size: 16px;' + }); + expect(result).toBe('color: red; font-size: 16px;'); + }); + + it('should handle styleAttr with leading/trailing spaces', () => { + const result = useStyles({ styleAttr: ' color: red ' }); + expect(result).toBe(' color: red '); + }); + + it('should handle empty string values correctly', () => { + const result = useStyles({ + sStyle: { color: '', fontSize: '16px' }, + styleProps: { 's-style_display': '' } + }); + expect(result).toBe('fontSize: 16px'); + }); + + it('should handle zero values in sStyle', () => { + const result = useStyles({ + sStyle: { margin: '0', padding: '0px', opacity: '0' } + }); + expect(result).toBe('margin: 0; padding: 0px; opacity: 0'); + }); + }); + + describe('CSS properties formatting', () => { + it('should handle kebab-case properties', () => { + const result = useStyles({ + sStyle: { 'background-color': 'red', 'font-size': '16px' } + }); + expect(result).toBe('background-color: red; font-size: 16px'); + }); + + it('should handle camelCase properties', () => { + const result = useStyles({ + sStyle: { backgroundColor: 'red', fontSize: '16px' } + }); + expect(result).toBe('backgroundColor: red; fontSize: 16px'); + }); + + it('should handle mixed kebab-case and camelCase', () => { + const result = useStyles({ + sStyle: { 'background-color': 'red', fontSize: '16px' } + }); + expect(result).toBe('background-color: red; fontSize: 16px'); + }); + }); +}); From 97640f4b45a77c1a418eaee5b40565854363b699 Mon Sep 17 00:00:00 2001 From: Nycolaide Date: Wed, 28 Jan 2026 20:51:35 +0100 Subject: [PATCH 05/10] plugins --- packages/lapikit/src/lib/labs/compiler/plugins.ts | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 packages/lapikit/src/lib/labs/compiler/plugins.ts diff --git a/packages/lapikit/src/lib/labs/compiler/plugins.ts b/packages/lapikit/src/lib/labs/compiler/plugins.ts new file mode 100644 index 0000000..c66f6a1 --- /dev/null +++ b/packages/lapikit/src/lib/labs/compiler/plugins.ts @@ -0,0 +1,6 @@ +export const lapikitPlugins = { + repl: { + components: ['repl'], + ref: '@lapikit/repl' + } +} as const; From 07a3048c6c75bde2eb0f3b2ed1ec3f3907a1c918 Mon Sep 17 00:00:00 2001 From: Nycolaide Date: Thu, 29 Jan 2026 20:57:56 +0100 Subject: [PATCH 06/10] refacto core lili --- packages/lapikit/src/lib/entry-bundler.ts | 133 +----------------- .../src/lib/labs/compiler/components.ts | 3 + .../lapikit/src/lib/labs/compiler/index.ts | 1 + .../lapikit/src/lib/labs/compiler/plugins.ts | 4 +- .../src/lib/labs/compiler/preprocess/index.ts | 1 + .../src/lib/labs/compiler/preprocess/lili.ts | 122 ++++++++++++++++ .../labs/compiler/preprocess/source-import.ts | 10 ++ .../src/lib/labs/compiler/types/index.ts | 1 + .../src/lib/labs/compiler/types/options.ts | 3 + packages/lapikit/test-debug.js | 7 + 10 files changed, 154 insertions(+), 131 deletions(-) create mode 100644 packages/lapikit/src/lib/labs/compiler/components.ts create mode 100644 packages/lapikit/src/lib/labs/compiler/index.ts create mode 100644 packages/lapikit/src/lib/labs/compiler/preprocess/index.ts create mode 100644 packages/lapikit/src/lib/labs/compiler/preprocess/lili.ts create mode 100644 packages/lapikit/src/lib/labs/compiler/preprocess/source-import.ts create mode 100644 packages/lapikit/src/lib/labs/compiler/types/options.ts create mode 100644 packages/lapikit/test-debug.js diff --git a/packages/lapikit/src/lib/entry-bundler.ts b/packages/lapikit/src/lib/entry-bundler.ts index 8341dd4..f99b0f9 100644 --- a/packages/lapikit/src/lib/entry-bundler.ts +++ b/packages/lapikit/src/lib/entry-bundler.ts @@ -1,135 +1,8 @@ -const components: readonly string[] = ['btn'] as const; - -function componentName(shortName: string): string { - return 'Kit' + shortName.charAt(0).toUpperCase() + shortName.slice(1); -} - -// plugins lapikit -const lapikitPlugins = { - repl: { - components: ['repl'], - ref: '@lapikit/repl' - } -} as const; - -type LapikitPreprocessOptions = { - plugins?: string[]; -}; +import { liliCore } from '$lib/labs/compiler/preprocess/index.js'; +import type { LapikitPreprocessOptions } from '$lib/labs/compiler/types/options.js'; function lapikitPreprocess(options?: LapikitPreprocessOptions) { - return { - markup({ content }: { content: string; filename?: string }) { - const allComponents = [...components]; - const componentToRef = new Map(); - - components.forEach((comp) => { - componentToRef.set(comp, 'lapikit/labs/components'); - }); - - // plugins - if (options?.plugins) { - options.plugins.forEach((pluginKey) => { - const plugin = lapikitPlugins[pluginKey as keyof typeof lapikitPlugins]; - if (plugin) { - plugin.components.forEach((comp) => { - if (!allComponents.includes(comp)) { - allComponents.push(comp); - } - componentToRef.set(comp, plugin.ref); - }); - } - }); - } - - const hasComponent = allComponents.some((comp) => content.includes(`(); - - let hasChanges = true; - while (hasChanges) { - hasChanges = false; - - for (const shortName of allComponents) { - const componentNameStr = componentName(shortName); - - const attrPattern = `(?:[^>"']|"[^"]*"|'[^']*')*?`; - - const selfClosingRegex = new RegExp(``, 'g'); - - const pairRegex = new RegExp( - `([\\s\\S]*?)<\\/kit:${shortName}\\s*>`, - 'g' - ); - - let newContent = processedContent.replace(selfClosingRegex, (fullMatch, attrs) => { - hasChanges = true; - const ref = componentToRef.get(shortName) || 'lapikit/labs/components'; - importedComponents.set(componentNameStr, ref); - return `<${componentNameStr}${attrs} />`; - }); - - newContent = newContent.replace(pairRegex, (fullMatch, attrs, children) => { - hasChanges = true; - const ref = componentToRef.get(shortName) || 'lapikit/labs/components'; - importedComponents.set(componentNameStr, ref); - return `<${componentNameStr}${attrs}>${children}`; - }); - - if (newContent !== processedContent) { - processedContent = newContent; - } - } - } - - if (processedContent === content) return; - - if (importedComponents.size > 0) { - const importsByRef = new Map(); - importedComponents.forEach((ref, component) => { - if (!importsByRef.has(ref)) { - importsByRef.set(ref, []); - } - importsByRef.get(ref)!.push(component); - }); - - const importLines = Array.from(importsByRef.entries()) - .map(([ref, components]) => { - const imports = components.join(', '); - return `\n\timport { ${imports} } from '${ref}';`; - }) - .join(''); - - const scriptRegex = /]*\bmodule\b)([^>]*)>/; - const scriptMatch = processedContent.match(scriptRegex); - - if (scriptMatch && scriptMatch.index !== undefined) { - const insertPos = scriptMatch.index + scriptMatch[0].length; - processedContent = - processedContent.slice(0, insertPos) + importLines + processedContent.slice(insertPos); - } else { - const moduleScriptMatch = processedContent.match(/]*\bmodule\b[^>]*>/); - - if (moduleScriptMatch && moduleScriptMatch.index !== undefined) { - const moduleScriptEnd = - processedContent.indexOf('', moduleScriptMatch.index) + ''.length; - processedContent = - processedContent.slice(0, moduleScriptEnd) + - `\n\n` + - processedContent.slice(moduleScriptEnd); - } else { - processedContent = `\n\n` + processedContent; - } - } - } - - return { - code: processedContent - }; - } - }; + return liliCore(options); } export { lapikitPreprocess }; diff --git a/packages/lapikit/src/lib/labs/compiler/components.ts b/packages/lapikit/src/lib/labs/compiler/components.ts new file mode 100644 index 0000000..db32946 --- /dev/null +++ b/packages/lapikit/src/lib/labs/compiler/components.ts @@ -0,0 +1,3 @@ +const lapikitComponents: readonly string[] = ['btn'] as const; + +export default lapikitComponents; diff --git a/packages/lapikit/src/lib/labs/compiler/index.ts b/packages/lapikit/src/lib/labs/compiler/index.ts new file mode 100644 index 0000000..7a5df06 --- /dev/null +++ b/packages/lapikit/src/lib/labs/compiler/index.ts @@ -0,0 +1 @@ +export * from './preprocess/index.js'; diff --git a/packages/lapikit/src/lib/labs/compiler/plugins.ts b/packages/lapikit/src/lib/labs/compiler/plugins.ts index c66f6a1..9b5e1d0 100644 --- a/packages/lapikit/src/lib/labs/compiler/plugins.ts +++ b/packages/lapikit/src/lib/labs/compiler/plugins.ts @@ -1,6 +1,8 @@ -export const lapikitPlugins = { +const lapikitPlugins = { repl: { components: ['repl'], ref: '@lapikit/repl' } } as const; + +export default lapikitPlugins; diff --git a/packages/lapikit/src/lib/labs/compiler/preprocess/index.ts b/packages/lapikit/src/lib/labs/compiler/preprocess/index.ts new file mode 100644 index 0000000..91e8097 --- /dev/null +++ b/packages/lapikit/src/lib/labs/compiler/preprocess/index.ts @@ -0,0 +1 @@ +export * from './lili.js'; diff --git a/packages/lapikit/src/lib/labs/compiler/preprocess/lili.ts b/packages/lapikit/src/lib/labs/compiler/preprocess/lili.ts new file mode 100644 index 0000000..23a6283 --- /dev/null +++ b/packages/lapikit/src/lib/labs/compiler/preprocess/lili.ts @@ -0,0 +1,122 @@ +import type { LapikitPreprocessOptions } from '$lib/labs/compiler/types/index.js'; +import { componentName, lapikitImportsRef } from '$lib/labs/compiler/preprocess/source-import.js'; + +// imports components and plugins +import lapikitComponents from '$lib/labs/compiler/components.js'; +import lapikitPlugins from '$lib/labs/compiler/plugins.js'; + +export function liliCore(options?: LapikitPreprocessOptions) { + return { + markup({ content }: { content: string; filename?: string }) { + const allComponents = [...lapikitComponents]; + const componentToRef = new Map(); + + lapikitComponents.forEach((comp) => { + componentToRef.set(comp, lapikitImportsRef); + }); + + // plugins + if (options?.plugins) { + options.plugins.forEach((pluginKey) => { + const plugin = lapikitPlugins[pluginKey as keyof typeof lapikitPlugins]; + if (plugin) { + plugin.components.forEach((comp) => { + if (!allComponents.includes(comp)) { + allComponents.push(comp); + } + componentToRef.set(comp, plugin.ref); + }); + } + }); + } + + const hasComponent = allComponents.some((comp) => content.includes(`(); + + let hasChanges = true; + while (hasChanges) { + hasChanges = false; + + for (const shortName of allComponents) { + const componentNameStr = componentName(shortName); + + const attrPattern = `(?:[^>"']|"[^"]*"|'[^']*')*?`; + + const selfClosingRegex = new RegExp(``, 'g'); + + const pairRegex = new RegExp( + `([\\s\\S]*?)<\\/kit:${shortName}\\s*>`, + 'g' + ); + + let newContent = processedContent.replace(selfClosingRegex, (fullMatch, attrs) => { + hasChanges = true; + const ref = componentToRef.get(shortName) || lapikitImportsRef; + importedComponents.set(componentNameStr, ref); + return `<${componentNameStr}${attrs} />`; + }); + + newContent = newContent.replace(pairRegex, (fullMatch, attrs, children) => { + hasChanges = true; + const ref = componentToRef.get(shortName) || lapikitImportsRef; + importedComponents.set(componentNameStr, ref); + return `<${componentNameStr}${attrs}>${children}`; + }); + + if (newContent !== processedContent) { + processedContent = newContent; + } + } + } + + if (processedContent === content) return; + + if (importedComponents.size > 0) { + const importsByRef = new Map(); + importedComponents.forEach((ref, component) => { + if (!importsByRef.has(ref)) { + importsByRef.set(ref, []); + } + importsByRef.get(ref)!.push(component); + }); + + const importLines = Array.from(importsByRef.entries()) + .map(([ref, components]) => { + const imports = components.join(', '); + return `\n\timport { ${imports} } from '${ref}';`; + }) + .join(''); + + const scriptRegex = /]*\bmodule\b)([^>]*)>/; + const scriptMatch = processedContent.match(scriptRegex); + + if (scriptMatch && scriptMatch.index !== undefined) { + const insertPos = scriptMatch.index + scriptMatch[0].length; + processedContent = + processedContent.slice(0, insertPos) + importLines + processedContent.slice(insertPos); + } else { + const moduleScriptMatch = processedContent.match(/]*\bmodule\b[^>]*>/); + + if (moduleScriptMatch && moduleScriptMatch.index !== undefined) { + const moduleScriptEnd = + processedContent.indexOf('', moduleScriptMatch.index) + ''.length; + processedContent = + processedContent.slice(0, moduleScriptEnd) + + `\n\n` + + processedContent.slice(moduleScriptEnd); + } else { + processedContent = `\n\n` + processedContent; + } + } + } + + return { + code: processedContent + }; + } + }; +} diff --git a/packages/lapikit/src/lib/labs/compiler/preprocess/source-import.ts b/packages/lapikit/src/lib/labs/compiler/preprocess/source-import.ts new file mode 100644 index 0000000..204150a --- /dev/null +++ b/packages/lapikit/src/lib/labs/compiler/preprocess/source-import.ts @@ -0,0 +1,10 @@ +/** + * componentName generates the component name used in imports + * @param shortName The short name of the component + * @returns The component name with "Kit" prefix and the first letter capitalized + */ +export function componentName(shortName: string): string { + return 'Kit' + shortName.charAt(0).toUpperCase() + shortName.slice(1); +} + +export const lapikitImportsRef = 'lapikit/labs/components'; diff --git a/packages/lapikit/src/lib/labs/compiler/types/index.ts b/packages/lapikit/src/lib/labs/compiler/types/index.ts index 351ea2d..f9436cd 100644 --- a/packages/lapikit/src/lib/labs/compiler/types/index.ts +++ b/packages/lapikit/src/lib/labs/compiler/types/index.ts @@ -1 +1,2 @@ export type * from './dom.js'; +export * from './options.js'; diff --git a/packages/lapikit/src/lib/labs/compiler/types/options.ts b/packages/lapikit/src/lib/labs/compiler/types/options.ts new file mode 100644 index 0000000..04360db --- /dev/null +++ b/packages/lapikit/src/lib/labs/compiler/types/options.ts @@ -0,0 +1,3 @@ +export type LapikitPreprocessOptions = { + plugins?: string[]; +}; diff --git a/packages/lapikit/test-debug.js b/packages/lapikit/test-debug.js new file mode 100644 index 0000000..a48c01e --- /dev/null +++ b/packages/lapikit/test-debug.js @@ -0,0 +1,7 @@ +import { liliCore } from './src/lib/labs/compiler/preprocess/lili.ts'; + +const input = '
'; +const result = liliCore().markup({ content: input }); + +console.log('Result:', result); +console.log('Code:', result?.code); From 580e157acc44b11b25d24e282802eae7270d0a80 Mon Sep 17 00:00:00 2001 From: Nycolaide Date: Sat, 31 Jan 2026 15:21:02 +0100 Subject: [PATCH 07/10] remove legacy btn component and create sanbdox component named sheet --- apps/lapikit.dev/src/routes/+page.svelte | 10 ++++++++-- .../lapikit/src/lib/labs/compiler/components.ts | 2 +- packages/lapikit/src/lib/labs/components/index.ts | 2 +- .../{btn/btn.svelte => sheet/sheet.svelte} | 13 +++++++++---- 4 files changed, 19 insertions(+), 8 deletions(-) rename packages/lapikit/src/lib/labs/components/{btn/btn.svelte => sheet/sheet.svelte} (71%) diff --git a/apps/lapikit.dev/src/routes/+page.svelte b/apps/lapikit.dev/src/routes/+page.svelte index 854486e..3f117ed 100644 --- a/apps/lapikit.dev/src/routes/+page.svelte +++ b/apps/lapikit.dev/src/routes/+page.svelte @@ -1,5 +1,5 @@

Welcome to SvelteKit

@@ -15,4 +15,10 @@ demo

Labs merge Class and Styles Core

-Demo +Demo diff --git a/packages/lapikit/src/lib/labs/compiler/components.ts b/packages/lapikit/src/lib/labs/compiler/components.ts index db32946..90e2eb9 100644 --- a/packages/lapikit/src/lib/labs/compiler/components.ts +++ b/packages/lapikit/src/lib/labs/compiler/components.ts @@ -1,3 +1,3 @@ -const lapikitComponents: readonly string[] = ['btn'] as const; +const lapikitComponents: readonly string[] = ['sheet'] as const; export default lapikitComponents; diff --git a/packages/lapikit/src/lib/labs/components/index.ts b/packages/lapikit/src/lib/labs/components/index.ts index 5139ff9..f82ea47 100644 --- a/packages/lapikit/src/lib/labs/components/index.ts +++ b/packages/lapikit/src/lib/labs/components/index.ts @@ -1,2 +1,2 @@ // components -export { default as KitBtn } from './btn/btn.svelte'; +export { default as KitSheet } from './sheet/sheet.svelte'; diff --git a/packages/lapikit/src/lib/labs/components/btn/btn.svelte b/packages/lapikit/src/lib/labs/components/sheet/sheet.svelte similarity index 71% rename from packages/lapikit/src/lib/labs/components/btn/btn.svelte rename to packages/lapikit/src/lib/labs/components/sheet/sheet.svelte index 6437e01..298bad5 100644 --- a/packages/lapikit/src/lib/labs/components/btn/btn.svelte +++ b/packages/lapikit/src/lib/labs/components/sheet/sheet.svelte @@ -1,4 +1,9 @@ - + From ef3b5169673fe6ffcff82c848ea172546bb378d8 Mon Sep 17 00:00:00 2001 From: Nycolaide Date: Sat, 31 Jan 2026 15:39:08 +0100 Subject: [PATCH 08/10] add test for preprocess lili --- .../compiler/preprocess/__test__/lili.test.ts | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 packages/lapikit/src/lib/labs/compiler/preprocess/__test__/lili.test.ts diff --git a/packages/lapikit/src/lib/labs/compiler/preprocess/__test__/lili.test.ts b/packages/lapikit/src/lib/labs/compiler/preprocess/__test__/lili.test.ts new file mode 100644 index 0000000..5c0ab29 --- /dev/null +++ b/packages/lapikit/src/lib/labs/compiler/preprocess/__test__/lili.test.ts @@ -0,0 +1,160 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { liliCore } from '../lili.js'; + +// mocks +vi.mock('$lib/labs/compiler/components.js', () => ({ + default: ['sheet'] +})); + +vi.mock('$lib/labs/compiler/plugins.js', () => ({ + default: {} +})); + +vi.mock('$lib/labs/compiler/preprocess/source-import.js', () => ({ + componentName: (name: string) => name.charAt(0).toUpperCase() + name.slice(1), + lapikitImportsRef: 'lapikit/labs/components' +})); + +describe('liliCore preprocess', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('replaces self-closing components', () => { + const content = ``; + + const result = liliCore().markup({ content }); + + expect(result?.code).toContain(''); + expect(result?.code).toContain(`import { Sheet } from 'lapikit/labs/components';`); + }); + + it('replaces components with children ...', () => { + const content = ` + +
hello
+
+ `; + + const result = liliCore().markup({ content }); + + expect(result?.code).toContain( + ` +
hello
+
` + ); + expect(result?.code).toContain(`import { Sheet } from 'lapikit/labs/components';`); + }); + + it('injects imports into an existing + + + `; + + const result = liliCore().markup({ content }); + + expect(result?.code).toMatch( + / + + + `; + + const result = liliCore().markup({ content }); + + expect(result?.code).toMatch(/\n\n a > b}\n/>`; -vi.mock('$lib/labs/compiler/plugins.js', () => ({ - default: {} -})); + const result = preprocess.markup({ content: input }); -vi.mock('$lib/labs/compiler/preprocess/source-import.js', () => ({ - componentName: (name: string) => name.charAt(0).toUpperCase() + name.slice(1), - lapikitImportsRef: 'lapikit/labs/components' -})); - -describe('liliCore preprocess', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('replaces self-closing components', () => { - const content = ``; - - const result = liliCore().markup({ content }); - - expect(result?.code).toContain(''); - expect(result?.code).toContain(`import { Sheet } from 'lapikit/labs/components';`); - }); - - it('replaces components with children ...', () => { - const content = ` - -
hello
-
- `; - - const result = liliCore().markup({ content }); - - expect(result?.code).toContain( - ` -
hello
-
` - ); - expect(result?.code).toContain(`import { Sheet } from 'lapikit/labs/components';`); - }); - - it('injects imports into an existing - - - `; - - const result = liliCore().markup({ content }); - - expect(result?.code).toMatch( - /\n\n\n\ncontent`; - const result = liliCore().markup({ content }); + const result = preprocess.markup({ content: input }); - expect(result?.code.startsWith('\n\n`; - const result = liliCore().markup({ content }); + const result = preprocess.markup({ content: input }); expect(result).toBeUndefined(); }); - - it('preserves component attributes', () => { - const content = ``; - - const result = liliCore().markup({ content }); - - expect(result?.code).toContain(``); - }); - - it('supports single-quoted attributes and expressions', () => { - const content = ``; - - const result = liliCore().markup({ content }); - - expect(result?.code).toContain(``); - }); - - it('imports Sheet only once even when used multiple times', () => { - const content = ` - -
- - `; - - const result = liliCore().markup({ content }); - - const matches = result?.code.match(/import \{ Sheet \} from 'lapikit\/labs\/components';/g); - - expect(matches?.length).toBe(1); - }); - - it('handles nested components correctly', () => { - const content = ` - - - nested - - - `; - - const result = liliCore().markup({ content }); - - expect(result?.code).toContain(''); - expect(result?.code).toContain(''); - }); - - it('adds a regular - - - `; - - const result = liliCore().markup({ content }); - - expect(result?.code).toMatch(/