Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions apps/lapikit.dev/src/routes/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
let { data } = $props();
let { data } = $props();
</script>

<h1>Welcome to SvelteKit</h1>
Expand All @@ -15,4 +15,10 @@
<kit:repl>demo</kit:repl>

<h3>Labs merge Class and Styles Core</h3>
<kit:btn id="demo-btn" class="my-custom-class" style="color: red;" s-class="additional-class" s-class_new-additional-class={true}>Demo</kit:btn>
<kit:sheet
id="demo-sheet"
class="my-custom-class"
style="color: red;"
s-class="additional-class"
s-class_new-additional-class={true}>Demo</kit:sheet
>
8 changes: 8 additions & 0 deletions packages/lapikit/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.3.8] - 2026-02-01

### Updated

- Optmize preprocess lili with new function
- Rewrite all mapping code
- Add test for critical function in lili core

## [0.3.7] - 2026-01-25

### Added
Expand Down
2 changes: 1 addition & 1 deletion packages/lapikit/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lapikit",
"version": "0.3.7",
"version": "0.3.8",
"license": "MIT",
"publishConfig": {
"access": "public"
Expand Down
133 changes: 3 additions & 130 deletions packages/lapikit/src/lib/entry-bundler.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>();

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(`<kit:${comp}`));

if (!hasComponent) return;

let processedContent = content;
const importedComponents = new Map<string, string>();

let hasChanges = true;
while (hasChanges) {
hasChanges = false;

for (const shortName of allComponents) {
const componentNameStr = componentName(shortName);

const attrPattern = `(?:[^>"']|"[^"]*"|'[^']*')*?`;

const selfClosingRegex = new RegExp(`<kit:${shortName}(${attrPattern})\\s*/>`, 'g');

const pairRegex = new RegExp(
`<kit:${shortName}(${attrPattern})>([\\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}</${componentNameStr}>`;
});

if (newContent !== processedContent) {
processedContent = newContent;
}
}
}

if (processedContent === content) return;

if (importedComponents.size > 0) {
const importsByRef = new Map<string, string[]>();
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 = /<script(?![^>]*\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(/<script[^>]*\bmodule\b[^>]*>/);

if (moduleScriptMatch && moduleScriptMatch.index !== undefined) {
const moduleScriptEnd =
processedContent.indexOf('</script>', moduleScriptMatch.index) + '</script>'.length;
processedContent =
processedContent.slice(0, moduleScriptEnd) +
`\n\n<script>${importLines}\n</script>` +
processedContent.slice(moduleScriptEnd);
} else {
processedContent = `<script>${importLines}\n</script>\n\n` + processedContent;
}
}
}

return {
code: processedContent
};
}
};
return liliCore(options);
}

export { lapikitPreprocess };
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import { describe, it, expect } from 'vitest';
import { computeSClasses } from '../mapped-code.js';
import type { SClassProp } from '../types/index.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');
});
});
});
Loading