yaxa-svelte 1.5.1 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,26 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { tokenizeCode } from './CodeBlock.svelte';
3
+ describe('tokenizeCode', () => {
4
+ it('tokenizes markdown headings correctly', () => {
5
+ const tokens = tokenizeCode('# Hello World', 'markdown');
6
+ expect(tokens).toHaveLength(1);
7
+ expect(tokens[0]).toEqual([
8
+ { text: '# ', type: 'punctuation' },
9
+ { text: 'Hello World', type: 'heading' }
10
+ ]);
11
+ });
12
+ it('tokenizes typescript keywords and identifiers', () => {
13
+ const tokens = tokenizeCode('const x = 42;', 'typescript');
14
+ expect(tokens).toHaveLength(1);
15
+ const texts = tokens[0].map((t) => t.text);
16
+ expect(texts).toContain('const');
17
+ expect(texts).toContain('x');
18
+ expect(texts).toContain('42');
19
+ });
20
+ it('handles multiline code and empty lines', () => {
21
+ const code = 'line 1\n\nline 3';
22
+ const tokens = tokenizeCode(code, 'plain');
23
+ expect(tokens).toHaveLength(3);
24
+ expect(tokens[1]).toEqual([{ text: '', type: 'plain' }]);
25
+ });
26
+ });
@@ -3,8 +3,15 @@
3
3
  import type { ZodSchema } from 'zod';
4
4
  import Alert from '../overlays/Alert.svelte';
5
5
 
6
+ export interface FormSchema<T = any> {
7
+ parse?: (data: unknown) => T;
8
+ safeParse?: (data: unknown) => { success: boolean; data?: T; error?: any };
9
+ validate?: (data: unknown) => T;
10
+ [key: string]: any;
11
+ }
12
+
6
13
  interface Props {
7
- schema?: ZodSchema;
14
+ schema?: FormSchema | ZodSchema | any;
8
15
  values?: Record<string, any>;
9
16
  loading?: boolean;
10
17
  errorSummary?: boolean;
@@ -30,8 +37,27 @@
30
37
  function validate() {
31
38
  if (!schema) return true;
32
39
  try {
33
- schema.parse(values);
34
- errors = {};
40
+ if (typeof schema.safeParse === 'function') {
41
+ const res = schema.safeParse(values);
42
+ if (!res.success) {
43
+ const fieldErrors: Record<string, string> = {};
44
+ const issues = res.error?.issues || res.error?.errors || [];
45
+ for (const e of issues) {
46
+ const field = Array.isArray(e.path) ? e.path.join('.') : String(e.path || 'form');
47
+ if (!fieldErrors[field]) {
48
+ fieldErrors[field] = e.message;
49
+ }
50
+ }
51
+ errors = fieldErrors;
52
+ return false;
53
+ }
54
+ errors = {};
55
+ return true;
56
+ } else if (typeof schema.parse === 'function') {
57
+ schema.parse(values);
58
+ errors = {};
59
+ return true;
60
+ }
35
61
  return true;
36
62
  } catch (err: any) {
37
63
  const fieldErrors: Record<string, string> = {};
@@ -1,7 +1,17 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { ZodSchema } from 'zod';
3
+ export interface FormSchema<T = any> {
4
+ parse?: (data: unknown) => T;
5
+ safeParse?: (data: unknown) => {
6
+ success: boolean;
7
+ data?: T;
8
+ error?: any;
9
+ };
10
+ validate?: (data: unknown) => T;
11
+ [key: string]: any;
12
+ }
3
13
  interface Props {
4
- schema?: ZodSchema;
14
+ schema?: FormSchema | ZodSchema | any;
5
15
  values?: Record<string, any>;
6
16
  loading?: boolean;
7
17
  errorSummary?: boolean;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { useGate } from './useGate.svelte';
3
+ describe('useGate', () => {
4
+ it('evaluates plan hierarchy correctly', () => {
5
+ const gateStarter = useGate({
6
+ user: { id: '1', role: 'user', plan: 'starter' }
7
+ });
8
+ expect(gateStarter.allowsPlan('free')).toBe(true);
9
+ expect(gateStarter.allowsPlan('starter')).toBe(true);
10
+ expect(gateStarter.allowsPlan('pro')).toBe(false);
11
+ expect(gateStarter.allowsPlan('enterprise')).toBe(false);
12
+ const gatePro = useGate({
13
+ user: { id: '2', role: 'user', plan: 'pro' }
14
+ });
15
+ expect(gatePro.allowsPlan('starter')).toBe(true);
16
+ expect(gatePro.allowsPlan('pro')).toBe(true);
17
+ expect(gatePro.allowsPlan('enterprise')).toBe(false);
18
+ });
19
+ it('allows plan arrays (exact match within allowed array)', () => {
20
+ const gate = useGate({
21
+ user: { id: '1', role: 'user', plan: 'pro' }
22
+ });
23
+ expect(gate.allowsPlan(['starter', 'pro'])).toBe(true);
24
+ expect(gate.allowsPlan(['starter', 'enterprise'])).toBe(false);
25
+ });
26
+ it('checks user roles correctly', () => {
27
+ const gateUser = useGate({
28
+ user: { id: '1', role: 'member', plan: 'free' }
29
+ });
30
+ expect(gateUser.hasRole('member')).toBe(true);
31
+ expect(gateUser.hasRole('editor')).toBe(false);
32
+ expect(gateUser.hasRole(['editor', 'member'])).toBe(true);
33
+ });
34
+ it('allows admin bypass by default', () => {
35
+ const gateAdmin = useGate({
36
+ user: { id: 'admin-1', role: 'admin', plan: 'free' }
37
+ });
38
+ expect(gateAdmin.isAdmin).toBe(true);
39
+ expect(gateAdmin.allowsPlan('enterprise')).toBe(true);
40
+ expect(gateAdmin.hasRole('billing_manager')).toBe(true);
41
+ expect(gateAdmin.can(() => false)).toBe(true);
42
+ });
43
+ it('respects allowAdminBypass = false', () => {
44
+ const gateAdminRestricted = useGate({
45
+ user: { id: 'admin-1', role: 'admin', plan: 'free' },
46
+ allowAdminBypass: false
47
+ });
48
+ expect(gateAdminRestricted.isAdmin).toBe(false);
49
+ expect(gateAdminRestricted.allowsPlan('pro')).toBe(false);
50
+ });
51
+ it('evaluates custom can() predicate correctly', () => {
52
+ const gate = useGate({
53
+ user: { id: 'custom-1', role: 'user', plan: 'pro' }
54
+ });
55
+ expect(gate.can((u) => u?.id === 'custom-1')).toBe(true);
56
+ expect(gate.can((u) => u?.id === 'other')).toBe(false);
57
+ });
58
+ });
package/dist/index.d.ts CHANGED
@@ -24,6 +24,7 @@ export type { CodeBlockProps } from './components/elements/CodeBlock.svelte';
24
24
  export { default as SortableList } from './components/elements/SortableList.svelte';
25
25
  export type { SortableListProps } from './components/elements/SortableList.svelte';
26
26
  export { default as Form } from './components/forms/Form.svelte';
27
+ export type { FormSchema } from './components/forms/Form.svelte';
27
28
  export { default as FormField } from './components/forms/FormField.svelte';
28
29
  export { default as Input } from './components/forms/Input.svelte';
29
30
  export type { InputProps } from './components/forms/Input.svelte';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,82 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { createYaxaAuthHook } from './hook';
3
+ describe('createYaxaAuthHook', () => {
4
+ const mockAuth = {
5
+ handler: vi.fn(() => new Response('auth handler response')),
6
+ api: {
7
+ getSession: vi.fn()
8
+ }
9
+ };
10
+ it('routes /api/auth/* requests directly to Better-Auth handler', async () => {
11
+ const hook = createYaxaAuthHook({ auth: mockAuth });
12
+ const event = {
13
+ url: new URL('https://example.com/api/auth/sign-in'),
14
+ request: new Request('https://example.com/api/auth/sign-in'),
15
+ locals: {}
16
+ };
17
+ const resolve = vi.fn();
18
+ const response = await hook({ event: event, resolve });
19
+ expect(mockAuth.handler).toHaveBeenCalledWith(event.request);
20
+ expect(await response.text()).toBe('auth handler response');
21
+ expect(resolve).not.toHaveBeenCalled();
22
+ });
23
+ it('allows public routes and resolves successfully', async () => {
24
+ mockAuth.api.getSession.mockResolvedValueOnce(null);
25
+ const hook = createYaxaAuthHook({ auth: mockAuth });
26
+ const event = {
27
+ url: new URL('https://example.com/'),
28
+ request: new Request('https://example.com/'),
29
+ locals: {}
30
+ };
31
+ const resolve = vi.fn().mockResolvedValue(new Response('home page'));
32
+ const response = await hook({ event: event, resolve });
33
+ expect(resolve).toHaveBeenCalledWith(event);
34
+ expect(event.locals.user).toBeNull();
35
+ expect(await response.text()).toBe('home page');
36
+ });
37
+ it('throws 303 redirect when unauthenticated user accesses protected path', async () => {
38
+ mockAuth.api.getSession.mockResolvedValueOnce(null);
39
+ const hook = createYaxaAuthHook({
40
+ auth: mockAuth,
41
+ protectedPaths: ['/dashboard'],
42
+ loginPath: '/auth/login'
43
+ });
44
+ const event = {
45
+ url: new URL('https://example.com/dashboard/billing?tab=invoices'),
46
+ request: new Request('https://example.com/dashboard/billing?tab=invoices'),
47
+ locals: {}
48
+ };
49
+ const resolve = vi.fn();
50
+ try {
51
+ await hook({ event: event, resolve });
52
+ expect.unreachable('Should have thrown redirect');
53
+ }
54
+ catch (err) {
55
+ expect(err.status).toBe(303);
56
+ expect(err.location).toBe('/auth/login?returnTo=%2Fdashboard%2Fbilling%3Ftab%3Dinvoices');
57
+ }
58
+ });
59
+ it('allows access to protected path when user is authenticated', async () => {
60
+ const mockUser = { id: 'user-123', email: 'user@example.com' };
61
+ const mockSession = { id: 'session-456' };
62
+ mockAuth.api.getSession.mockResolvedValueOnce({
63
+ user: mockUser,
64
+ session: mockSession
65
+ });
66
+ const hook = createYaxaAuthHook({
67
+ auth: mockAuth,
68
+ protectedPaths: ['/dashboard']
69
+ });
70
+ const event = {
71
+ url: new URL('https://example.com/dashboard'),
72
+ request: new Request('https://example.com/dashboard'),
73
+ locals: {}
74
+ };
75
+ const resolve = vi.fn().mockResolvedValue(new Response('dashboard content'));
76
+ const response = await hook({ event: event, resolve });
77
+ expect(event.locals.user).toEqual(mockUser);
78
+ expect(event.locals.session).toEqual(mockSession);
79
+ expect(resolve).toHaveBeenCalled();
80
+ expect(await response.text()).toBe('dashboard content');
81
+ });
82
+ });
@@ -1,4 +1,5 @@
1
1
  import { json } from '@sveltejs/kit';
2
+ import { validateEvent } from '@polar-sh/sdk/webhooks';
2
3
  import { eq } from 'drizzle-orm';
3
4
  import { getDb, schemaPg, schemaSqlite } from '../db';
4
5
  /**
@@ -19,20 +20,35 @@ export function createPolarWebhookHandler(options = {}) {
19
20
  return new Response('Method Not Allowed', { status: 405 });
20
21
  }
21
22
  let rawBody;
22
- let payload;
23
23
  try {
24
24
  rawBody = await event.request.text();
25
- payload = JSON.parse(rawBody);
26
25
  }
27
26
  catch {
28
- return new Response('Invalid JSON payload', { status: 400 });
27
+ return new Response('Failed to read request body', { status: 400 });
29
28
  }
30
- // Verify signature if secret provided
29
+ let payload;
30
+ // Cryptographically verify signature if secret provided
31
31
  if (secret) {
32
- const signature = event.request.headers.get('webhook-signature') ||
33
- event.request.headers.get('polar-signature');
34
- if (!signature) {
35
- return new Response('Missing webhook signature', { status: 401 });
32
+ try {
33
+ const headersObj = {};
34
+ event.request.headers.forEach((val, key) => {
35
+ headersObj[key.toLowerCase()] = val;
36
+ });
37
+ if (headersObj['polar-signature'] && !headersObj['webhook-signature']) {
38
+ headersObj['webhook-signature'] = headersObj['polar-signature'];
39
+ }
40
+ payload = validateEvent(rawBody, headersObj, secret);
41
+ }
42
+ catch {
43
+ return new Response('Invalid or missing webhook signature', { status: 401 });
44
+ }
45
+ }
46
+ else {
47
+ try {
48
+ payload = JSON.parse(rawBody);
49
+ }
50
+ catch {
51
+ return new Response('Invalid JSON payload', { status: 400 });
36
52
  }
37
53
  }
38
54
  const eventType = payload.type;
@@ -112,7 +128,7 @@ export function createPolarWebhookHandler(options = {}) {
112
128
  }
113
129
  catch (err) {
114
130
  console.error('[Polar Webhook Error]', err);
115
- return new Response(`Webhook handling error: ${err.message}`, { status: 500 });
131
+ return new Response('Internal Server Error', { status: 500 });
116
132
  }
117
133
  };
118
134
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,90 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { createPolarWebhookHandler } from './webhook';
3
+ describe('createPolarWebhookHandler', () => {
4
+ it('rejects non-POST requests with 405 Method Not Allowed', async () => {
5
+ const handler = createPolarWebhookHandler();
6
+ const response = await handler({
7
+ request: new Request('https://example.com/api/webhooks/polar', {
8
+ method: 'GET'
9
+ })
10
+ });
11
+ expect(response.status).toBe(405);
12
+ expect(await response.text()).toBe('Method Not Allowed');
13
+ });
14
+ it('rejects invalid JSON payloads with 400 when unauthenticated', async () => {
15
+ const handler = createPolarWebhookHandler();
16
+ const response = await handler({
17
+ request: new Request('https://example.com/api/webhooks/polar', {
18
+ method: 'POST',
19
+ body: 'not-json-content'
20
+ })
21
+ });
22
+ expect(response.status).toBe(400);
23
+ expect(await response.text()).toBe('Invalid JSON payload');
24
+ });
25
+ it('rejects requests with missing or forged signature when secret is configured', async () => {
26
+ const handler = createPolarWebhookHandler({
27
+ webhookSecret: 'whsec_test_secret_key_12345'
28
+ });
29
+ // 1. Missing signature
30
+ const resMissing = await handler({
31
+ request: new Request('https://example.com/api/webhooks/polar', {
32
+ method: 'POST',
33
+ body: JSON.stringify({ type: 'subscription.created', data: {} })
34
+ })
35
+ });
36
+ expect(resMissing.status).toBe(401);
37
+ expect(await resMissing.text()).toBe('Invalid or missing webhook signature');
38
+ // 2. Forged/invalid signature
39
+ const resForged = await handler({
40
+ request: new Request('https://example.com/api/webhooks/polar', {
41
+ method: 'POST',
42
+ headers: {
43
+ 'webhook-id': 'msg_123',
44
+ 'webhook-timestamp': `${Math.floor(Date.now() / 1000)}`,
45
+ 'webhook-signature': 'v1,forged_invalid_signature'
46
+ },
47
+ body: JSON.stringify({ type: 'subscription.created', data: {} })
48
+ })
49
+ });
50
+ expect(resForged.status).toBe(401);
51
+ expect(await resForged.text()).toBe('Invalid or missing webhook signature');
52
+ });
53
+ it('invokes onOrderCreated callback and responds with received: true', async () => {
54
+ const onOrderCreated = vi.fn();
55
+ const handler = createPolarWebhookHandler({
56
+ onOrderCreated
57
+ });
58
+ const payload = {
59
+ type: 'order.created',
60
+ data: { id: 'order_123', amount: 4900 }
61
+ };
62
+ const response = await handler({
63
+ request: new Request('https://example.com/api/webhooks/polar', {
64
+ method: 'POST',
65
+ body: JSON.stringify(payload)
66
+ })
67
+ });
68
+ expect(response.status).toBe(200);
69
+ const json = await response.json();
70
+ expect(json).toEqual({ received: true });
71
+ expect(onOrderCreated).toHaveBeenCalledWith(payload);
72
+ });
73
+ it('sanitizes internal errors and returns 500 without leaking exception messages', async () => {
74
+ const handler = createPolarWebhookHandler({
75
+ onSubscriptionCreated: () => {
76
+ throw new Error('Sensitive DB connection string exposed: postgres://user:pass@host/db');
77
+ }
78
+ });
79
+ const response = await handler({
80
+ request: new Request('https://example.com/api/webhooks/polar', {
81
+ method: 'POST',
82
+ body: JSON.stringify({ type: 'subscription.created', data: {} })
83
+ })
84
+ });
85
+ expect(response.status).toBe(500);
86
+ const text = await response.text();
87
+ expect(text).toBe('Internal Server Error');
88
+ expect(text).not.toContain('postgres://');
89
+ });
90
+ });
@@ -56,7 +56,8 @@ export function defineSiteConfig(config) {
56
56
  ...config.project
57
57
  };
58
58
  return {
59
- defaultLocale: 'en',
59
+ ...config,
60
+ defaultLocale: config.defaultLocale || 'en',
60
61
  email: primaryEmail,
61
62
  theme: {
62
63
  defaultMode: 'system',
@@ -93,8 +94,7 @@ export function defineSiteConfig(config) {
93
94
  priority: 0.8,
94
95
  exclude: ['/api/*'],
95
96
  ...config.sitemap
96
- },
97
- ...config
97
+ }
98
98
  };
99
99
  }
100
100
  export const DEFAULT_SITE_CONFIG = defineSiteConfig({
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,39 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { defineSiteConfig, computeProjectBadge } from './config';
3
+ describe('computeProjectBadge', () => {
4
+ it('returns fallback badge when no project is provided', () => {
5
+ expect(computeProjectBadge(undefined)).toBe('Web Application');
6
+ });
7
+ it('respects explicit custom badge', () => {
8
+ expect(computeProjectBadge({ badge: 'Custom Pro SaaS' })).toBe('Custom Pro SaaS');
9
+ });
10
+ it('computes open-source badge with license', () => {
11
+ expect(computeProjectBadge({ type: 'open-source', license: 'MIT' })).toBe('MIT Open Source');
12
+ expect(computeProjectBadge({ type: 'open-source' })).toBe('Open Source');
13
+ });
14
+ it('computes commercial-saas and freemium badges', () => {
15
+ expect(computeProjectBadge({ type: 'commercial-saas' })).toBe('Commercial SaaS');
16
+ expect(computeProjectBadge({ type: 'freemium' })).toBe('Freemium SaaS');
17
+ });
18
+ });
19
+ describe('defineSiteConfig', () => {
20
+ it('computes primary email and fallback notice', () => {
21
+ const config = defineSiteConfig({
22
+ name: 'Test App',
23
+ title: 'Test App Title',
24
+ description: 'Test Description',
25
+ url: 'https://testapp.com',
26
+ legal: {
27
+ paymentProcessor: 'polar'
28
+ },
29
+ project: {
30
+ type: 'open-source',
31
+ license: 'MIT'
32
+ }
33
+ });
34
+ expect(config.name).toBe('Test App');
35
+ expect(config.email).toBe('contact@testapp.com');
36
+ expect(config.legal?.morNotice).toBe('Payments securely processed by Polar.sh (Merchant of Record)');
37
+ expect(config.project?.badge).toBe('MIT Open Source');
38
+ });
39
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,41 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateOgSvg } from './og';
3
+ describe('generateOgSvg', () => {
4
+ it('generates a valid 1200x630 SVG element', () => {
5
+ const svg = generateOgSvg({
6
+ title: 'Welcome to Yaxa',
7
+ description: 'A Next-Gen UI library for Svelte 5',
8
+ siteName: 'Yaxa',
9
+ badge: 'v1.5.1'
10
+ });
11
+ expect(svg).toContain('<svg width="1200" height="630" viewBox="0 0 1200 630"');
12
+ expect(svg).toContain('Welcome to Yaxa');
13
+ expect(svg).toContain('A Next-Gen UI library for Svelte 5');
14
+ expect(svg).toContain('v1.5.1');
15
+ expect(svg).toContain('</svg>');
16
+ });
17
+ it('escapes dangerous XML characters in titles and descriptions', () => {
18
+ const svg = generateOgSvg({
19
+ title: 'Tom & Jerry <Script> "Quotes" \'Single\'',
20
+ description: 'Symbols: < > & " \'',
21
+ siteName: 'Yaxa & Co'
22
+ });
23
+ expect(svg).not.toContain('<Script>');
24
+ expect(svg).toContain('&lt;Script&gt;');
25
+ expect(svg).toContain('&amp;');
26
+ expect(svg).toContain('&quot;');
27
+ expect(svg).toContain('&apos;');
28
+ });
29
+ it('handles light mode and dark mode color palettes', () => {
30
+ const darkSvg = generateOgSvg({
31
+ title: 'Dark Mode',
32
+ theme: 'dark'
33
+ });
34
+ const lightSvg = generateOgSvg({
35
+ title: 'Light Mode',
36
+ theme: 'light'
37
+ });
38
+ expect(darkSvg).toContain('#09090b');
39
+ expect(lightSvg).toContain('#fafafa');
40
+ });
41
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,53 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { createRobotsHandler } from './robots';
3
+ import { defineSiteConfig } from './config';
4
+ const config = defineSiteConfig({
5
+ name: 'Yaxa',
6
+ title: 'Yaxa UI',
7
+ description: 'UI toolkit',
8
+ url: 'https://yaxa.dev'
9
+ });
10
+ describe('createRobotsHandler', () => {
11
+ it('disallows indexing in non-production environments', async () => {
12
+ const handler = createRobotsHandler({
13
+ config,
14
+ isProduction: false
15
+ });
16
+ const response = (await handler({
17
+ url: new URL('http://localhost:5173/robots.txt')
18
+ }));
19
+ const body = await response.text();
20
+ expect(response.headers.get('Content-Type')).toContain('text/plain');
21
+ expect(body).toContain('User-agent: *');
22
+ expect(body).toContain('Disallow: /');
23
+ expect(body).toContain('# Non-production environment');
24
+ });
25
+ it('allows indexing and provides sitemap in production', async () => {
26
+ const handler = createRobotsHandler({
27
+ config,
28
+ isProduction: true
29
+ });
30
+ const response = (await handler({
31
+ url: new URL('https://yaxa.dev/robots.txt')
32
+ }));
33
+ const body = await response.text();
34
+ expect(body).toContain('User-agent: *');
35
+ expect(body).toContain('Allow: /');
36
+ expect(body).toContain('Disallow: /admin/');
37
+ expect(body).toContain('Sitemap: https://yaxa.dev/sitemap.xml');
38
+ });
39
+ it('blocks AI web crawlers when aiCrawlers: disallow', async () => {
40
+ const handler = createRobotsHandler({
41
+ config,
42
+ isProduction: true,
43
+ aiCrawlers: 'disallow'
44
+ });
45
+ const response = (await handler({
46
+ url: new URL('https://yaxa.dev/robots.txt')
47
+ }));
48
+ const body = await response.text();
49
+ expect(body).toContain('User-agent: GPTBot');
50
+ expect(body).toContain('User-agent: ClaudeBot');
51
+ expect(body).toContain('User-agent: PerplexityBot');
52
+ });
53
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,95 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateWebSiteSchema, generatePersonSchema, generateOrganizationSchema, generateSoftwareApplicationSchema, generateSoftwareSourceCodeSchema, generateArticleSchema, generateBreadcrumbSchema } from './schema';
3
+ import { defineSiteConfig } from './config';
4
+ const testConfig = defineSiteConfig({
5
+ name: 'Yaxa App',
6
+ title: 'Yaxa App Title',
7
+ description: 'Test application description',
8
+ url: 'https://yaxa.test',
9
+ author: {
10
+ name: 'John Doe',
11
+ url: 'https://johndoe.me',
12
+ twitter: 'johndoe',
13
+ github: 'johndoe'
14
+ },
15
+ company: {
16
+ legalName: 'Yaxa Technologies Inc',
17
+ contactEmail: 'support@yaxa.test',
18
+ country: 'US'
19
+ },
20
+ project: {
21
+ license: 'MIT',
22
+ licenseUrl: 'https://yaxa.test/license',
23
+ type: 'open-source',
24
+ pricingModel: 'free',
25
+ repositoryUrl: 'https://github.com/yaxa/app',
26
+ isAccessibleForFree: true
27
+ }
28
+ });
29
+ describe('SEO Schema Generators', () => {
30
+ it('generates valid WebSite schema', () => {
31
+ const schema = generateWebSiteSchema(testConfig);
32
+ expect(schema['@context']).toBe('https://schema.org');
33
+ expect(schema['@type']).toBe('WebSite');
34
+ expect(schema.name).toBe('Yaxa App');
35
+ expect(schema.url).toBe('https://yaxa.test');
36
+ expect(schema.license).toBe('https://yaxa.test/license');
37
+ expect(schema.isAccessibleForFree).toBe(true);
38
+ expect(schema.author?.name).toBe('John Doe');
39
+ });
40
+ it('generates valid Person schema', () => {
41
+ const schema = generatePersonSchema(testConfig);
42
+ expect(schema['@type']).toBe('Person');
43
+ expect(schema.name).toBe('John Doe');
44
+ expect(schema.sameAs).toContain('https://twitter.com/johndoe');
45
+ expect(schema.sameAs).toContain('https://github.com/johndoe');
46
+ });
47
+ it('generates valid Organization schema', () => {
48
+ const schema = generateOrganizationSchema(testConfig);
49
+ expect(schema['@type']).toBe('Organization');
50
+ expect(schema.name).toBe('Yaxa App');
51
+ expect(schema.legalName).toBe('Yaxa Technologies Inc');
52
+ expect(schema.email).toBe('support@yaxa.test');
53
+ });
54
+ it('generates valid SoftwareApplication schema', () => {
55
+ const schema = generateSoftwareApplicationSchema(testConfig);
56
+ expect(schema['@type']).toBe('SoftwareApplication');
57
+ expect(schema.applicationCategory).toBe('DeveloperApplication');
58
+ expect(schema.offers?.price).toBe('0');
59
+ });
60
+ it('generates valid SoftwareSourceCode schema', () => {
61
+ const schema = generateSoftwareSourceCodeSchema(testConfig);
62
+ expect(schema['@type']).toBe('SoftwareSourceCode');
63
+ expect(schema.codeRepository).toBe('https://github.com/yaxa/app');
64
+ expect(schema.programmingLanguage).toBe('Svelte');
65
+ });
66
+ it('generates valid Article schema', () => {
67
+ const schema = generateArticleSchema(testConfig, {
68
+ title: 'Building with Svelte 5',
69
+ description: 'Guide to runes in Svelte 5',
70
+ url: 'https://yaxa.test/blog/svelte-5',
71
+ datePublished: '2026-01-01T00:00:00Z',
72
+ authorName: 'John Doe'
73
+ });
74
+ expect(schema['@type']).toBe('Article');
75
+ expect(schema.headline).toBe('Building with Svelte 5');
76
+ expect(schema.author?.name).toBe('John Doe');
77
+ expect(schema.publisher?.name).toBe('Yaxa App');
78
+ });
79
+ it('generates valid BreadcrumbList schema', () => {
80
+ const schema = generateBreadcrumbSchema(testConfig, [
81
+ { name: 'Home', url: '/' },
82
+ { name: 'Docs', url: '/docs' },
83
+ { name: 'Components', url: '/docs/components' }
84
+ ]);
85
+ expect(schema['@type']).toBe('BreadcrumbList');
86
+ expect(schema.itemListElement).toHaveLength(3);
87
+ expect(schema.itemListElement[0]).toEqual({
88
+ '@type': 'ListItem',
89
+ position: 1,
90
+ name: 'Home',
91
+ item: 'https://yaxa.test/'
92
+ });
93
+ expect(schema.itemListElement[2].position).toBe(3);
94
+ });
95
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { definePageSeo } from './seo-helpers';
3
+ describe('definePageSeo', () => {
4
+ it('returns the exact SEO configuration object passed to it', () => {
5
+ const seo = definePageSeo({
6
+ title: 'Test Page',
7
+ description: 'A test page description',
8
+ badge: 'Documentation'
9
+ });
10
+ expect(seo).toEqual({
11
+ title: 'Test Page',
12
+ description: 'A test page description',
13
+ badge: 'Documentation'
14
+ });
15
+ });
16
+ it('supports structured OpenGraph configuration', () => {
17
+ const seo = definePageSeo({
18
+ title: 'Analytics',
19
+ ogImage: {
20
+ title: 'Custom OG Title',
21
+ description: 'Custom OG Description',
22
+ badge: 'SaaS',
23
+ theme: 'dark'
24
+ },
25
+ twitterCard: 'summary_large_image'
26
+ });
27
+ expect(seo.ogImage).toEqual({
28
+ title: 'Custom OG Title',
29
+ description: 'Custom OG Description',
30
+ badge: 'SaaS',
31
+ theme: 'dark'
32
+ });
33
+ expect(seo.twitterCard).toBe('summary_large_image');
34
+ });
35
+ it('supports robots directives and schema markup', () => {
36
+ const schema = {
37
+ '@context': 'https://schema.org',
38
+ '@type': 'WebSite',
39
+ name: 'Yaxa'
40
+ };
41
+ const seo = definePageSeo({
42
+ title: 'Secret Page',
43
+ noindex: true,
44
+ nofollow: true,
45
+ schema
46
+ });
47
+ expect(seo.noindex).toBe(true);
48
+ expect(seo.nofollow).toBe(true);
49
+ expect(seo.schema).toBe(schema);
50
+ });
51
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,53 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ACCENT_PALETTES, NEUTRAL_PALETTES, FONT_PRESETS, RADIUS_PRESETS, theme } from './theme.svelte';
3
+ describe('Theme Palettes & Engine', () => {
4
+ const requiredShades = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
5
+ it('includes all 7 standard accent palettes with complete shades and ring tokens', () => {
6
+ const expectedAccents = [
7
+ 'svelte',
8
+ 'amber',
9
+ 'emerald',
10
+ 'sky',
11
+ 'violet',
12
+ 'rose',
13
+ 'indigo'
14
+ ];
15
+ for (const accent of expectedAccents) {
16
+ const palette = ACCENT_PALETTES[accent];
17
+ expect(palette).toBeDefined();
18
+ expect(palette.id).toBe(accent);
19
+ expect(palette.color).toMatch(/^#[0-9a-fA-F]{6}$/);
20
+ expect(palette.shades.ring).toBeDefined();
21
+ for (const shade of requiredShades) {
22
+ expect(palette.shades[shade]).toBeDefined();
23
+ }
24
+ }
25
+ });
26
+ it('includes all 4 standard neutral palettes', () => {
27
+ const expectedNeutrals = ['zinc', 'slate', 'stone', 'neutral'];
28
+ for (const neutral of expectedNeutrals) {
29
+ const palette = NEUTRAL_PALETTES[neutral];
30
+ expect(palette).toBeDefined();
31
+ expect(palette.name).toBeDefined();
32
+ for (const shade of requiredShades) {
33
+ expect(palette.shades[shade]).toBeDefined();
34
+ }
35
+ }
36
+ });
37
+ it('provides font presets and radius presets', () => {
38
+ expect(FONT_PRESETS.sans.value).toBeDefined();
39
+ expect(FONT_PRESETS.serif.value).toBeDefined();
40
+ expect(FONT_PRESETS.mono.value).toBeDefined();
41
+ expect(RADIUS_PRESETS.sharp.value).toBe('0px');
42
+ expect(RADIUS_PRESETS.rounded.value).toBe('0.75rem');
43
+ expect(RADIUS_PRESETS.pill.value).toBe('1.25rem');
44
+ });
45
+ it('updates theme state reactively via theme methods', () => {
46
+ theme.setAccent('emerald');
47
+ expect(theme.accent).toBe('emerald');
48
+ theme.setNeutral('stone');
49
+ expect(theme.neutral).toBe('stone');
50
+ theme.setMode('dark');
51
+ expect(theme.mode).toBe('dark');
52
+ });
53
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { cn, tv } from './cn';
3
+ describe('cn utility', () => {
4
+ it('merges class names and handles conditional classes', () => {
5
+ const isEnabled = true;
6
+ const isDisabled = false;
7
+ const result = cn('px-4 py-2', isEnabled && 'bg-red-500', isDisabled && 'text-white', null, undefined);
8
+ expect(result).toBe('px-4 py-2 bg-red-500');
9
+ });
10
+ it('resolves conflicting Tailwind CSS classes correctly', () => {
11
+ const result = cn('px-4 px-8 text-sm text-lg');
12
+ expect(result).toBe('px-8 text-lg');
13
+ });
14
+ });
15
+ describe('tv utility', () => {
16
+ it('generates variant classes accurately', () => {
17
+ const button = tv({
18
+ base: 'rounded font-bold',
19
+ variants: {
20
+ color: {
21
+ primary: 'bg-blue-500 text-white',
22
+ secondary: 'bg-gray-500 text-black'
23
+ },
24
+ size: {
25
+ sm: 'text-sm p-1',
26
+ md: 'text-base p-2'
27
+ }
28
+ },
29
+ defaultVariants: {
30
+ color: 'primary',
31
+ size: 'md'
32
+ }
33
+ });
34
+ expect(button()).toContain('rounded font-bold bg-blue-500 text-white text-base p-2');
35
+ expect(button({ color: 'secondary', size: 'sm' })).toContain('rounded font-bold bg-gray-500 text-black text-sm p-1');
36
+ });
37
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yaxa-svelte",
3
- "version": "1.5.1",
3
+ "version": "1.5.2",
4
4
  "description": "The Intuitive Svelte UI Library — Nuxt UI v4 equivalent for SvelteKit 2.7+ & Svelte 5 with automated SEO, dynamic OpenGraph cards, sitemaps, robots.txt, and Tailwind CSS v4.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://yaxa.vercel.app",
@@ -116,6 +116,7 @@
116
116
  "typescript-eslint": "^8.60.1",
117
117
  "unplugin-icons": "^23.0.1",
118
118
  "vite": "^8.0.16",
119
+ "vitest": "^5.0.0",
119
120
  "zod": "^4.5.4"
120
121
  },
121
122
  "dependencies": {
@@ -137,7 +138,7 @@
137
138
  "drizzle-orm": ">=0.45.0",
138
139
  "resend": ">=6.27.0",
139
140
  "svelte": "^5.0.0",
140
- "zod": ">=3.0.0 || >=4.0.0"
141
+ "zod": ">=4.0.0"
141
142
  },
142
143
  "peerDependenciesMeta": {
143
144
  "@aws-sdk/client-s3": {
@@ -178,7 +179,9 @@
178
179
  "preview": "vite preview",
179
180
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
180
181
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
182
+ "test": "vitest run",
183
+ "test:watch": "vitest",
181
184
  "lint": "prettier --check . && eslint .",
182
- "format": "prettier --write ."
185
+ "format": "eslint --fix . && prettier --write ."
183
186
  }
184
187
  }