yaxa-svelte 1.5.0 → 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.
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/index.js +3 -0
- package/dist/components/elements/CodeBlock.test.d.ts +1 -0
- package/dist/components/elements/CodeBlock.test.js +26 -0
- package/dist/components/forms/Form.svelte +29 -3
- package/dist/components/forms/Form.svelte.d.ts +11 -1
- package/dist/components/saas/UserMenu.svelte +17 -22
- package/dist/components/saas/UserMenu.svelte.d.ts +3 -9
- package/dist/composables/useGate.test.d.ts +1 -0
- package/dist/composables/useGate.test.js +58 -0
- package/dist/index.d.ts +1 -3
- package/dist/index.js +1 -3
- package/dist/server/auth/hook.test.d.ts +1 -0
- package/dist/server/auth/hook.test.js +82 -0
- package/dist/server/polar/webhook.js +25 -9
- package/dist/server/polar/webhook.test.d.ts +1 -0
- package/dist/server/polar/webhook.test.js +90 -0
- package/dist/site/config.js +3 -3
- package/dist/site/config.test.d.ts +1 -0
- package/dist/site/config.test.js +39 -0
- package/dist/site/og.test.d.ts +1 -0
- package/dist/site/og.test.js +41 -0
- package/dist/site/robots.test.d.ts +1 -0
- package/dist/site/robots.test.js +53 -0
- package/dist/site/schema.test.d.ts +1 -0
- package/dist/site/schema.test.js +95 -0
- package/dist/site/seo-helpers.test.d.ts +1 -0
- package/dist/site/seo-helpers.test.js +51 -0
- package/dist/theme/theme.test.d.ts +1 -0
- package/dist/theme/theme.test.js +53 -0
- package/dist/utils/cn.test.d.ts +1 -0
- package/dist/utils/cn.test.js +37 -0
- package/package.json +16 -10
|
@@ -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.
|
|
34
|
-
|
|
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;
|
|
@@ -3,24 +3,16 @@
|
|
|
3
3
|
import Badge from '../elements/Badge.svelte';
|
|
4
4
|
import Icon from '../elements/Icon.svelte';
|
|
5
5
|
import DropdownMenu, { type MenuItem } from '../navigation/DropdownMenu.svelte';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
interface UserInfo {
|
|
9
|
-
name?: string | null;
|
|
10
|
-
email?: string | null;
|
|
11
|
-
image?: string | null;
|
|
12
|
-
role?: string | null;
|
|
13
|
-
tier?: string | null;
|
|
14
|
-
}
|
|
6
|
+
import { getAuthUserContext, type AuthUserContext } from '../../site/context';
|
|
15
7
|
|
|
16
8
|
interface Props {
|
|
17
|
-
user?:
|
|
9
|
+
user?: AuthUserContext | null;
|
|
18
10
|
tier?: string;
|
|
19
11
|
dashboardUrl?: string;
|
|
20
12
|
settingsUrl?: string;
|
|
21
13
|
billingUrl?: string;
|
|
22
14
|
class?: string;
|
|
23
|
-
onsignout?: () => void
|
|
15
|
+
onsignout?: () => void | Promise<void>;
|
|
24
16
|
}
|
|
25
17
|
|
|
26
18
|
let {
|
|
@@ -33,16 +25,17 @@
|
|
|
33
25
|
onsignout
|
|
34
26
|
}: Props = $props();
|
|
35
27
|
|
|
36
|
-
const
|
|
28
|
+
const contextUser = $derived(getAuthUserContext());
|
|
37
29
|
|
|
38
30
|
const currentUser = $derived(
|
|
39
|
-
propUser
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
31
|
+
propUser !== undefined
|
|
32
|
+
? propUser
|
|
33
|
+
: contextUser || {
|
|
34
|
+
name: 'Solo Developer',
|
|
35
|
+
email: 'dev@yaxa.dev',
|
|
36
|
+
image: null,
|
|
37
|
+
tier
|
|
38
|
+
}
|
|
46
39
|
);
|
|
47
40
|
|
|
48
41
|
const initials = $derived.by(() => {
|
|
@@ -91,9 +84,11 @@
|
|
|
91
84
|
icon: 'log-out',
|
|
92
85
|
destructive: true,
|
|
93
86
|
onSelect: async () => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (typeof window !== 'undefined')
|
|
87
|
+
if (onsignout) {
|
|
88
|
+
await onsignout();
|
|
89
|
+
} else if (typeof window !== 'undefined') {
|
|
90
|
+
window.location.href = '/login';
|
|
91
|
+
}
|
|
97
92
|
}
|
|
98
93
|
}
|
|
99
94
|
]);
|
|
@@ -1,18 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
name?: string | null;
|
|
3
|
-
email?: string | null;
|
|
4
|
-
image?: string | null;
|
|
5
|
-
role?: string | null;
|
|
6
|
-
tier?: string | null;
|
|
7
|
-
}
|
|
1
|
+
import { type AuthUserContext } from '../../site/context';
|
|
8
2
|
interface Props {
|
|
9
|
-
user?:
|
|
3
|
+
user?: AuthUserContext | null;
|
|
10
4
|
tier?: string;
|
|
11
5
|
dashboardUrl?: string;
|
|
12
6
|
settingsUrl?: string;
|
|
13
7
|
billingUrl?: string;
|
|
14
8
|
class?: string;
|
|
15
|
-
onsignout?: () => void
|
|
9
|
+
onsignout?: () => void | Promise<void>;
|
|
16
10
|
}
|
|
17
11
|
declare const UserMenu: import("svelte").Component<Props, {}, "">;
|
|
18
12
|
type UserMenu = ReturnType<typeof UserMenu>;
|
|
@@ -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';
|
|
@@ -123,13 +124,10 @@ export { yaxa } from './vite/index';
|
|
|
123
124
|
export type { YaxaPluginOptions } from './vite/index';
|
|
124
125
|
export { default as Gate } from './components/saas/Gate.svelte';
|
|
125
126
|
export type { GateProps } from './components/saas/Gate.svelte';
|
|
126
|
-
export { default as AuthCard } from './components/saas/AuthCard.svelte';
|
|
127
127
|
export { default as UserMenu } from './components/saas/UserMenu.svelte';
|
|
128
128
|
export { default as PricingCard } from './components/saas/PricingCard.svelte';
|
|
129
129
|
export { default as PricingTable } from './components/saas/PricingTable.svelte';
|
|
130
130
|
export { default as SubscriptionCard } from './components/saas/SubscriptionCard.svelte';
|
|
131
|
-
export { useAuth } from './composables/useAuth.svelte';
|
|
132
|
-
export type { UseAuthOptions, SocialProvider } from './composables/useAuth.svelte';
|
|
133
131
|
export { useGate } from './composables/useGate.svelte';
|
|
134
132
|
export type { UseGateOptions } from './composables/useGate.svelte';
|
|
135
133
|
export { useUpload } from './composables/useUpload.svelte';
|
package/dist/index.js
CHANGED
|
@@ -89,14 +89,12 @@ export { createManifestHandler } from './site/manifest';
|
|
|
89
89
|
export { generateWebSiteSchema, generateOrganizationSchema, generateSoftwareApplicationSchema, generateSoftwareSourceCodeSchema, generatePersonSchema, generateArticleSchema, generateBreadcrumbSchema } from './site/schema';
|
|
90
90
|
export { definePageSeo } from './site/seo-helpers';
|
|
91
91
|
export { yaxa } from './vite/index';
|
|
92
|
-
// SaaS Suite: UI Components & Composables
|
|
92
|
+
// SaaS Suite: UI Components & Composables (Pure UI & Context Runes)
|
|
93
93
|
export { default as Gate } from './components/saas/Gate.svelte';
|
|
94
|
-
export { default as AuthCard } from './components/saas/AuthCard.svelte';
|
|
95
94
|
export { default as UserMenu } from './components/saas/UserMenu.svelte';
|
|
96
95
|
export { default as PricingCard } from './components/saas/PricingCard.svelte';
|
|
97
96
|
export { default as PricingTable } from './components/saas/PricingTable.svelte';
|
|
98
97
|
export { default as SubscriptionCard } from './components/saas/SubscriptionCard.svelte';
|
|
99
|
-
export { useAuth } from './composables/useAuth.svelte';
|
|
100
98
|
export { useGate } from './composables/useGate.svelte';
|
|
101
99
|
export { useUpload } from './composables/useUpload.svelte';
|
|
102
100
|
// Utilities
|
|
@@ -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('
|
|
27
|
+
return new Response('Failed to read request body', { status: 400 });
|
|
29
28
|
}
|
|
30
|
-
|
|
29
|
+
let payload;
|
|
30
|
+
// Cryptographically verify signature if secret provided
|
|
31
31
|
if (secret) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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(
|
|
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
|
+
});
|
package/dist/site/config.js
CHANGED
|
@@ -56,7 +56,8 @@ export function defineSiteConfig(config) {
|
|
|
56
56
|
...config.project
|
|
57
57
|
};
|
|
58
58
|
return {
|
|
59
|
-
|
|
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('<Script>');
|
|
25
|
+
expect(svg).toContain('&');
|
|
26
|
+
expect(svg).toContain('"');
|
|
27
|
+
expect(svg).toContain(''');
|
|
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.
|
|
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",
|
|
@@ -54,8 +54,9 @@
|
|
|
54
54
|
"default": "./dist/server/index.js"
|
|
55
55
|
},
|
|
56
56
|
"./auth": {
|
|
57
|
-
"types": "./dist/
|
|
58
|
-
"
|
|
57
|
+
"types": "./dist/auth/index.d.ts",
|
|
58
|
+
"svelte": "./dist/auth/index.js",
|
|
59
|
+
"default": "./dist/auth/index.js"
|
|
59
60
|
},
|
|
60
61
|
"./db": {
|
|
61
62
|
"types": "./dist/server/db/index.d.ts",
|
|
@@ -114,17 +115,16 @@
|
|
|
114
115
|
"typescript": "^6.0.3",
|
|
115
116
|
"typescript-eslint": "^8.60.1",
|
|
116
117
|
"unplugin-icons": "^23.0.1",
|
|
117
|
-
"vite": "^8.0.16"
|
|
118
|
+
"vite": "^8.0.16",
|
|
119
|
+
"vitest": "^5.0.0",
|
|
120
|
+
"zod": "^4.5.4"
|
|
118
121
|
},
|
|
119
122
|
"dependencies": {
|
|
120
123
|
"bits-ui": "^2.19.0",
|
|
121
|
-
"formsnap": "^2.0.1",
|
|
122
124
|
"mode-watcher": "^1.1.0",
|
|
123
125
|
"runed": "^0.37.1",
|
|
124
126
|
"svelte-sonner": "^1.2.1",
|
|
125
|
-
"
|
|
126
|
-
"tailwind-variants": "^3.3.1",
|
|
127
|
-
"zod": "^4.5.4"
|
|
127
|
+
"tailwind-variants": "^3.3.1"
|
|
128
128
|
},
|
|
129
129
|
"peerDependencies": {
|
|
130
130
|
"@aws-sdk/client-s3": ">=3.0.0",
|
|
@@ -137,7 +137,8 @@
|
|
|
137
137
|
"better-auth": ">=1.7.0",
|
|
138
138
|
"drizzle-orm": ">=0.45.0",
|
|
139
139
|
"resend": ">=6.27.0",
|
|
140
|
-
"svelte": "^5.0.0"
|
|
140
|
+
"svelte": "^5.0.0",
|
|
141
|
+
"zod": ">=4.0.0"
|
|
141
142
|
},
|
|
142
143
|
"peerDependenciesMeta": {
|
|
143
144
|
"@aws-sdk/client-s3": {
|
|
@@ -166,6 +167,9 @@
|
|
|
166
167
|
},
|
|
167
168
|
"resend": {
|
|
168
169
|
"optional": true
|
|
170
|
+
},
|
|
171
|
+
"zod": {
|
|
172
|
+
"optional": true
|
|
169
173
|
}
|
|
170
174
|
},
|
|
171
175
|
"scripts": {
|
|
@@ -175,7 +179,9 @@
|
|
|
175
179
|
"preview": "vite preview",
|
|
176
180
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
177
181
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
182
|
+
"test": "vitest run",
|
|
183
|
+
"test:watch": "vitest",
|
|
178
184
|
"lint": "prettier --check . && eslint .",
|
|
179
|
-
"format": "prettier --write ."
|
|
185
|
+
"format": "eslint --fix . && prettier --write ."
|
|
180
186
|
}
|
|
181
187
|
}
|