@react-spa-scaffold/mcp 0.3.0
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/README.md +423 -0
- package/dist/features/index.d.ts +5 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/index.js +3 -0
- package/dist/features/index.js.map +1 -0
- package/dist/features/registry.d.ts +10 -0
- package/dist/features/registry.d.ts.map +1 -0
- package/dist/features/registry.js +508 -0
- package/dist/features/registry.js.map +1 -0
- package/dist/features/types.d.ts +45 -0
- package/dist/features/types.d.ts.map +1 -0
- package/dist/features/types.js +5 -0
- package/dist/features/types.js.map +1 -0
- package/dist/features/versions.d.ts +16 -0
- package/dist/features/versions.d.ts.map +1 -0
- package/dist/features/versions.js +46 -0
- package/dist/features/versions.js.map +1 -0
- package/dist/features/versions.json +5 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/resources/docs.d.ts +29 -0
- package/dist/resources/docs.d.ts.map +1 -0
- package/dist/resources/docs.js +105 -0
- package/dist/resources/docs.js.map +1 -0
- package/dist/resources/index.d.ts +2 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +2 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +115 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/get-example.d.ts +51 -0
- package/dist/tools/get-example.d.ts.map +1 -0
- package/dist/tools/get-example.js +90 -0
- package/dist/tools/get-example.js.map +1 -0
- package/dist/tools/get-features.d.ts +30 -0
- package/dist/tools/get-features.d.ts.map +1 -0
- package/dist/tools/get-features.js +46 -0
- package/dist/tools/get-features.js.map +1 -0
- package/dist/tools/get-scaffold.d.ts +77 -0
- package/dist/tools/get-scaffold.d.ts.map +1 -0
- package/dist/tools/get-scaffold.js +153 -0
- package/dist/tools/get-scaffold.js.map +1 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +4 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/utils/docs.d.ts +14 -0
- package/dist/utils/docs.d.ts.map +1 -0
- package/dist/utils/docs.js +64 -0
- package/dist/utils/docs.js.map +1 -0
- package/dist/utils/examples.d.ts +27 -0
- package/dist/utils/examples.d.ts.map +1 -0
- package/dist/utils/examples.js +399 -0
- package/dist/utils/examples.js.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/paths.d.ts +28 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +40 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/scaffold.d.ts +50 -0
- package/dist/utils/scaffold.d.ts.map +1 -0
- package/dist/utils/scaffold.js +500 -0
- package/dist/utils/scaffold.js.map +1 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +19 -0
- package/dist/version.js.map +1 -0
- package/package.json +63 -0
- package/templates/.bundled +0 -0
- package/templates/CLAUDE.md +145 -0
- package/templates/docs/API_REFERENCE.md +58 -0
- package/templates/docs/ARCHITECTURE.md +185 -0
- package/templates/docs/CODING_STANDARDS.md +53 -0
- package/templates/docs/COMPONENT_GUIDELINES.md +301 -0
- package/templates/docs/E2E_TESTING.md +116 -0
- package/templates/docs/INTERNATIONALIZATION.md +67 -0
- package/templates/docs/TESTING.md +259 -0
- package/templates/docs/WORKFLOW.md +170 -0
- package/templates/src/App.tsx +42 -0
- package/templates/src/components/layout/Header.tsx +19 -0
- package/templates/src/components/layout/index.ts +1 -0
- package/templates/src/components/shared/ErrorBoundary/ErrorBoundary.tsx +104 -0
- package/templates/src/components/shared/ErrorBoundary/index.ts +1 -0
- package/templates/src/components/shared/LanguageSwitcher/LanguageSwitcher.tsx +45 -0
- package/templates/src/components/shared/LanguageSwitcher/index.ts +1 -0
- package/templates/src/components/shared/SEO/SEO.tsx +55 -0
- package/templates/src/components/shared/SEO/index.ts +1 -0
- package/templates/src/components/shared/ThemeToggle/ThemeToggle.tsx +41 -0
- package/templates/src/components/shared/ThemeToggle/index.ts +1 -0
- package/templates/src/components/shared/index.ts +4 -0
- package/templates/src/components/ui/button.tsx +48 -0
- package/templates/src/components/ui/dropdown-menu.tsx +228 -0
- package/templates/src/components/ui/form-error.tsx +95 -0
- package/templates/src/components/ui/loading.tsx +58 -0
- package/templates/src/components/ui/skeleton.tsx +52 -0
- package/templates/src/components/ui/sonner.tsx +34 -0
- package/templates/src/components/ui/spinner.tsx +40 -0
- package/templates/src/components/ui/visually-hidden.tsx +51 -0
- package/templates/src/contexts/mobileContext.tsx +66 -0
- package/templates/src/contexts/queryContext.tsx +28 -0
- package/templates/src/hooks/index.ts +7 -0
- package/templates/src/hooks/useContactForm.ts +33 -0
- package/templates/src/hooks/useExampleQuery.ts +20 -0
- package/templates/src/hooks/useLanguage.ts +23 -0
- package/templates/src/hooks/useMediaQuery.ts +53 -0
- package/templates/src/hooks/useThemeEffect.ts +31 -0
- package/templates/src/hooks/useTouchSizes.ts +16 -0
- package/templates/src/i18n/config.ts +11 -0
- package/templates/src/i18n/detectLanguage.ts +57 -0
- package/templates/src/i18n/index.ts +20 -0
- package/templates/src/i18n/loadCatalog.ts +30 -0
- package/templates/src/index.css +98 -0
- package/templates/src/lib/api.ts +142 -0
- package/templates/src/lib/config.ts +15 -0
- package/templates/src/lib/constants.ts +8 -0
- package/templates/src/lib/env.ts +53 -0
- package/templates/src/lib/format.ts +119 -0
- package/templates/src/lib/index.ts +24 -0
- package/templates/src/lib/routes.ts +11 -0
- package/templates/src/lib/storage.ts +91 -0
- package/templates/src/lib/storageKeys.ts +10 -0
- package/templates/src/lib/utils.ts +6 -0
- package/templates/src/lib/validations.ts +39 -0
- package/templates/src/locales/de.po +65 -0
- package/templates/src/locales/en.po +65 -0
- package/templates/src/locales/es.po +65 -0
- package/templates/src/main.tsx +107 -0
- package/templates/src/mocks/fixtures/index.ts +1 -0
- package/templates/src/mocks/fixtures/todos.ts +40 -0
- package/templates/src/mocks/handlers/index.ts +7 -0
- package/templates/src/mocks/handlers/todos.ts +59 -0
- package/templates/src/mocks/index.ts +3 -0
- package/templates/src/mocks/node.ts +9 -0
- package/templates/src/pages/Home.tsx +27 -0
- package/templates/src/pages/NotFound.tsx +28 -0
- package/templates/src/pages/index.ts +2 -0
- package/templates/src/stores/index.ts +2 -0
- package/templates/src/stores/preferencesStore.ts +85 -0
- package/templates/src/test/index.ts +8 -0
- package/templates/src/test/mocks.ts +17 -0
- package/templates/src/test/providers.tsx +54 -0
- package/templates/src/test-setup.ts +54 -0
- package/templates/src/types/api.ts +31 -0
- package/templates/src/types/index.ts +2 -0
- package/templates/src/types/preferences.ts +5 -0
- package/templates/src/vite-env.d.ts +10 -0
- package/templates/tests/unit/components/ErrorBoundary.test.tsx +193 -0
- package/templates/tests/unit/components/Header.test.tsx +33 -0
- package/templates/tests/unit/components/LanguageSwitcher.test.tsx +40 -0
- package/templates/tests/unit/components/Loading.test.tsx +76 -0
- package/templates/tests/unit/components/SEO.test.tsx +80 -0
- package/templates/tests/unit/components/ThemeToggle.test.tsx +62 -0
- package/templates/tests/unit/contexts/mobileContext.test.tsx +54 -0
- package/templates/tests/unit/hooks/useContactForm.test.ts +60 -0
- package/templates/tests/unit/hooks/useExampleQuery.test.tsx +94 -0
- package/templates/tests/unit/hooks/useLanguage.test.tsx +75 -0
- package/templates/tests/unit/hooks/useMediaQuery.test.ts +57 -0
- package/templates/tests/unit/hooks/useThemeEffect.test.ts +42 -0
- package/templates/tests/unit/i18n/detectLanguage.test.ts +40 -0
- package/templates/tests/unit/i18n/loadCatalog.test.ts +70 -0
- package/templates/tests/unit/lib/api.test.ts +142 -0
- package/templates/tests/unit/lib/format.test.ts +100 -0
- package/templates/tests/unit/lib/storage.test.ts +90 -0
- package/templates/tests/unit/lib/utils.test.ts +19 -0
- package/templates/tests/unit/lib/validations.test.ts +56 -0
- package/templates/tests/unit/stores/preferencesStore.test.ts +75 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { clearAppStorage, getStorageItem, removeStorageItem, setStorageItem } from '@/lib/storage';
|
|
4
|
+
import { STORAGE_KEYS } from '@/lib/storageKeys';
|
|
5
|
+
|
|
6
|
+
describe('storage utilities', () => {
|
|
7
|
+
beforeEach(() => localStorage.clear());
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
localStorage.clear();
|
|
10
|
+
vi.restoreAllMocks();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('setStorageItem', () => {
|
|
14
|
+
it('stores values and returns true', () => {
|
|
15
|
+
expect(setStorageItem(STORAGE_KEYS.preferences, { theme: 'dark' })).toBe(true);
|
|
16
|
+
expect(localStorage.getItem(STORAGE_KEYS.preferences)).toBe('{"theme":"dark"}');
|
|
17
|
+
|
|
18
|
+
expect(setStorageItem(STORAGE_KEYS.locale, 'en')).toBe(true);
|
|
19
|
+
expect(localStorage.getItem(STORAGE_KEYS.locale)).toBe('"en"');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('returns false on storage failure', () => {
|
|
23
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
24
|
+
vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
|
|
25
|
+
throw new Error('QuotaExceeded');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
expect(setStorageItem(STORAGE_KEYS.preferences, {})).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('getStorageItem', () => {
|
|
33
|
+
it.each([
|
|
34
|
+
{ stored: '{"theme":"dark"}', defaultVal: { theme: 'light' }, expected: { theme: 'dark' } },
|
|
35
|
+
{ stored: null, defaultVal: { theme: 'light' }, expected: { theme: 'light' } },
|
|
36
|
+
{ stored: 'not-json', defaultVal: 'en', expected: 'not-json' },
|
|
37
|
+
])('retrieves stored value or default', ({ stored, defaultVal, expected }) => {
|
|
38
|
+
if (stored) localStorage.setItem(STORAGE_KEYS.preferences, stored);
|
|
39
|
+
expect(getStorageItem(STORAGE_KEYS.preferences, defaultVal)).toEqual(expected);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns default when storage cleared during parse', () => {
|
|
43
|
+
let callCount = 0;
|
|
44
|
+
vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
|
|
45
|
+
callCount++;
|
|
46
|
+
return callCount === 1 ? 'invalid{' : null;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(getStorageItem(STORAGE_KEYS.locale, 'default')).toBe('default');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('removeStorageItem', () => {
|
|
54
|
+
it('removes value and returns true', () => {
|
|
55
|
+
localStorage.setItem(STORAGE_KEYS.preferences, '{}');
|
|
56
|
+
expect(removeStorageItem(STORAGE_KEYS.preferences)).toBe(true);
|
|
57
|
+
expect(localStorage.getItem(STORAGE_KEYS.preferences)).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('returns false on failure', () => {
|
|
61
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
62
|
+
vi.spyOn(Storage.prototype, 'removeItem').mockImplementation(() => {
|
|
63
|
+
throw new Error();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(removeStorageItem(STORAGE_KEYS.preferences)).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('clearAppStorage', () => {
|
|
71
|
+
it('clears only app keys', () => {
|
|
72
|
+
localStorage.setItem(STORAGE_KEYS.preferences, '{}');
|
|
73
|
+
localStorage.setItem(STORAGE_KEYS.locale, '"en"');
|
|
74
|
+
localStorage.setItem('other-key', 'value');
|
|
75
|
+
|
|
76
|
+
expect(clearAppStorage()).toBe(true);
|
|
77
|
+
expect(localStorage.getItem(STORAGE_KEYS.preferences)).toBeNull();
|
|
78
|
+
expect(localStorage.getItem('other-key')).toBe('value');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('returns false on failure', () => {
|
|
82
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
83
|
+
vi.spyOn(Storage.prototype, 'removeItem').mockImplementation(() => {
|
|
84
|
+
throw new Error();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(clearAppStorage()).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@/lib/utils';
|
|
4
|
+
|
|
5
|
+
describe('cn utility', () => {
|
|
6
|
+
it('should merge class names', () => {
|
|
7
|
+
expect(cn('foo', 'bar')).toBe('foo bar');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should handle conditional classes', () => {
|
|
11
|
+
const isHidden = false;
|
|
12
|
+
const isVisible = true;
|
|
13
|
+
expect(cn('base', isHidden && 'hidden', isVisible && 'visible')).toBe('base visible');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should merge tailwind classes correctly', () => {
|
|
17
|
+
expect(cn('px-2 py-1', 'px-4')).toBe('py-1 px-4');
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { contactFormSchema, registerFormSchema } from '@/lib/validations';
|
|
4
|
+
|
|
5
|
+
describe('contactFormSchema', () => {
|
|
6
|
+
const validContact = {
|
|
7
|
+
name: 'John Doe',
|
|
8
|
+
email: 'john@example.com',
|
|
9
|
+
message: 'This is a valid message that is long enough.',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
it('accepts valid data', () => {
|
|
13
|
+
expect(contactFormSchema.safeParse(validContact).success).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it.each([
|
|
17
|
+
{ field: 'name', value: 'J', errorContains: 'at least 2 characters' },
|
|
18
|
+
{ field: 'email', value: 'not-an-email', errorContains: 'valid email' },
|
|
19
|
+
{ field: 'message', value: 'Short', errorContains: 'at least 10 characters' },
|
|
20
|
+
])('rejects invalid $field', ({ field, value, errorContains }) => {
|
|
21
|
+
const data = { ...validContact, [field]: value };
|
|
22
|
+
const result = contactFormSchema.safeParse(data);
|
|
23
|
+
|
|
24
|
+
expect(result.success).toBe(false);
|
|
25
|
+
if (!result.success) {
|
|
26
|
+
expect(result.error.issues[0].message).toContain(errorContains);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('registerFormSchema', () => {
|
|
32
|
+
const validRegister = {
|
|
33
|
+
username: 'john_doe',
|
|
34
|
+
email: 'john@example.com',
|
|
35
|
+
password: 'SecurePass123',
|
|
36
|
+
confirmPassword: 'SecurePass123',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
it('accepts valid data', () => {
|
|
40
|
+
expect(registerFormSchema.safeParse(validRegister).success).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it.each([
|
|
44
|
+
{ field: 'username', value: 'john@doe', errorContains: 'letters, numbers, and underscores' },
|
|
45
|
+
{ field: 'password', value: 'weakpass', errorContains: null },
|
|
46
|
+
{ fields: { confirmPassword: 'DifferentPass123' }, errorContains: "don't match" },
|
|
47
|
+
])('rejects invalid input', ({ field, fields, value, errorContains }) => {
|
|
48
|
+
const data = field ? { ...validRegister, [field]: value } : { ...validRegister, ...fields };
|
|
49
|
+
const result = registerFormSchema.safeParse(data);
|
|
50
|
+
|
|
51
|
+
expect(result.success).toBe(false);
|
|
52
|
+
if (!result.success && errorContains) {
|
|
53
|
+
expect(result.error.issues[0].message).toContain(errorContains);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { act } from '@testing-library/react';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { initPreferencesSync, usePreferencesStore } from '@/stores/preferencesStore';
|
|
5
|
+
import { mockMatchMedia } from '@/test';
|
|
6
|
+
|
|
7
|
+
describe('preferencesStore', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
usePreferencesStore.setState({ theme: 'light' });
|
|
10
|
+
window.matchMedia = mockMatchMedia(false);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
vi.restoreAllMocks();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('setTheme', () => {
|
|
18
|
+
it.each(['light', 'dark', 'system'] as const)('sets theme to %s', (theme) => {
|
|
19
|
+
act(() => usePreferencesStore.getState().setTheme(theme));
|
|
20
|
+
expect(usePreferencesStore.getState().theme).toBe(theme);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('toggleTheme', () => {
|
|
25
|
+
it.each([
|
|
26
|
+
{ initial: 'light', systemDark: false, expected: 'dark' },
|
|
27
|
+
{ initial: 'dark', systemDark: false, expected: 'light' },
|
|
28
|
+
{ initial: 'system', systemDark: false, expected: 'dark' },
|
|
29
|
+
{ initial: 'system', systemDark: true, expected: 'light' },
|
|
30
|
+
] as const)('toggles from $initial to $expected', ({ initial, systemDark, expected }) => {
|
|
31
|
+
window.matchMedia = mockMatchMedia(systemDark);
|
|
32
|
+
usePreferencesStore.setState({ theme: initial });
|
|
33
|
+
|
|
34
|
+
act(() => usePreferencesStore.getState().toggleTheme());
|
|
35
|
+
|
|
36
|
+
expect(usePreferencesStore.getState().theme).toBe(expected);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('getResolvedTheme', () => {
|
|
41
|
+
it.each([
|
|
42
|
+
{ theme: 'light', systemDark: false, expected: 'light' },
|
|
43
|
+
{ theme: 'dark', systemDark: false, expected: 'dark' },
|
|
44
|
+
{ theme: 'system', systemDark: false, expected: 'light' },
|
|
45
|
+
{ theme: 'system', systemDark: true, expected: 'dark' },
|
|
46
|
+
] as const)('resolves $theme to $expected', ({ theme, systemDark, expected }) => {
|
|
47
|
+
window.matchMedia = mockMatchMedia(systemDark);
|
|
48
|
+
usePreferencesStore.setState({ theme });
|
|
49
|
+
expect(usePreferencesStore.getState().getResolvedTheme()).toBe(expected);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('reset', () => {
|
|
54
|
+
it('resets to initial state', () => {
|
|
55
|
+
usePreferencesStore.setState({ theme: 'dark' });
|
|
56
|
+
act(() => usePreferencesStore.getState().reset());
|
|
57
|
+
expect(usePreferencesStore.getState().theme).toBe('system');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('initPreferencesSync', () => {
|
|
62
|
+
it('adds and removes storage event listener', () => {
|
|
63
|
+
const addSpy = vi.spyOn(window, 'addEventListener');
|
|
64
|
+
const removeSpy = vi.spyOn(window, 'removeEventListener');
|
|
65
|
+
|
|
66
|
+
const cleanup = initPreferencesSync();
|
|
67
|
+
|
|
68
|
+
expect(addSpy).toHaveBeenCalledWith('storage', expect.any(Function));
|
|
69
|
+
|
|
70
|
+
cleanup();
|
|
71
|
+
|
|
72
|
+
expect(removeSpy).toHaveBeenCalledWith('storage', expect.any(Function));
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|