snice 3.7.0 → 3.8.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.
Files changed (51) hide show
  1. package/README.md +2 -2
  2. package/bin/snice.js +3 -5
  3. package/bin/templates/CLAUDE.md +5 -5
  4. package/bin/templates/pwa/README.md +31 -16
  5. package/bin/templates/pwa/index.html +0 -1
  6. package/bin/templates/pwa/package.json +9 -2
  7. package/bin/templates/pwa/src/fetcher.ts +15 -0
  8. package/bin/templates/pwa/src/guards/auth.ts +6 -4
  9. package/bin/templates/pwa/src/middleware/auth.ts +7 -6
  10. package/bin/templates/pwa/src/middleware/error.ts +16 -5
  11. package/bin/templates/pwa/src/middleware/retry.ts +7 -3
  12. package/bin/templates/pwa/src/pages/dashboard.ts +4 -3
  13. package/bin/templates/pwa/src/pages/login.ts +7 -7
  14. package/bin/templates/pwa/src/pages/notifications.ts +2 -2
  15. package/bin/templates/pwa/src/pages/profile.ts +9 -8
  16. package/bin/templates/pwa/src/router.ts +8 -4
  17. package/bin/templates/pwa/src/types/auth.ts +1 -1
  18. package/bin/templates/pwa/tests/helpers/test-utils.ts +84 -0
  19. package/bin/templates/pwa/tests/middleware/auth.test.ts +67 -0
  20. package/bin/templates/pwa/tests/middleware/error.test.ts +105 -0
  21. package/bin/templates/pwa/tests/middleware/retry.test.ts +103 -0
  22. package/bin/templates/pwa/tests/services/auth.test.ts +89 -0
  23. package/bin/templates/pwa/tests/services/jwt.test.ts +76 -0
  24. package/bin/templates/pwa/tests/services/storage.test.ts +69 -0
  25. package/bin/templates/{social/vite.config.ts → pwa/vitest.config.ts} +12 -17
  26. package/dist/index.cjs +1 -1
  27. package/dist/index.esm.js +1 -1
  28. package/dist/index.iife.js +1 -1
  29. package/dist/symbols.cjs +1 -1
  30. package/dist/symbols.esm.js +1 -1
  31. package/dist/transitions.cjs +1 -1
  32. package/dist/transitions.esm.js +1 -1
  33. package/docs/ai/patterns.md +1 -1
  34. package/docs/routing.md +9 -8
  35. package/package.json +1 -1
  36. package/bin/templates/pwa/public/manifest.json +0 -24
  37. package/bin/templates/pwa/src/utils/fetch.ts +0 -39
  38. package/bin/templates/social/README.md +0 -42
  39. package/bin/templates/social/global.d.ts +0 -14
  40. package/bin/templates/social/index.html +0 -13
  41. package/bin/templates/social/package.json +0 -21
  42. package/bin/templates/social/public/vite.svg +0 -1
  43. package/bin/templates/social/src/main.ts +0 -33
  44. package/bin/templates/social/src/pages/feed-page.ts +0 -111
  45. package/bin/templates/social/src/pages/messages-page.ts +0 -102
  46. package/bin/templates/social/src/pages/not-found-page.ts +0 -46
  47. package/bin/templates/social/src/pages/profile-page.ts +0 -99
  48. package/bin/templates/social/src/pages/settings-page.ts +0 -119
  49. package/bin/templates/social/src/router.ts +0 -9
  50. package/bin/templates/social/src/styles/global.css +0 -156
  51. package/bin/templates/social/tsconfig.json +0 -22
@@ -0,0 +1,105 @@
1
+ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
2
+ import { Context } from 'snice';
3
+ import { errorMiddleware } from '../../src/middleware/error';
4
+ import * as storage from '../../src/services/storage';
5
+ import type { Principal } from '../../src/types/auth';
6
+
7
+ describe('Error Middleware', () => {
8
+ let mockContext: Context;
9
+ let mockNext: ReturnType<typeof vi.fn>;
10
+ let originalLocation: Location;
11
+
12
+ beforeEach(() => {
13
+ localStorage.clear();
14
+ vi.clearAllMocks();
15
+
16
+ // Save original location
17
+ originalLocation = window.location;
18
+
19
+ // Mock window.location
20
+ delete (window as any).location;
21
+ window.location = { href: '' } as Location;
22
+
23
+ // Create mock context
24
+ mockContext = {
25
+ application: {
26
+ principal: {
27
+ user: { id: '1', name: 'Test', email: 'test@example.com' },
28
+ isAuthenticated: true,
29
+ } as Principal,
30
+ },
31
+ navigation: {
32
+ route: '/',
33
+ params: {},
34
+ },
35
+ update: vi.fn(),
36
+ } as unknown as Context;
37
+
38
+ // Create mock next function
39
+ const mockResponse = new Response('{}', { status: 200 });
40
+ mockNext = vi.fn(() => Promise.resolve(mockResponse));
41
+ });
42
+
43
+ afterEach(() => {
44
+ window.location = originalLocation;
45
+ });
46
+
47
+ it('should pass through successful responses', async () => {
48
+ const mockResponse = new Response('{"data":"ok"}', { status: 200 });
49
+ const response = await errorMiddleware.call(mockContext, mockResponse, mockNext);
50
+
51
+ expect(mockNext).toHaveBeenCalled();
52
+ expect(response).toBeInstanceOf(Response);
53
+ });
54
+
55
+ it('should handle 401 unauthorized by clearing token and redirecting', async () => {
56
+ storage.setToken('test-token');
57
+ const mockResponse = new Response('Unauthorized', { status: 401 });
58
+
59
+ await expect(
60
+ errorMiddleware.call(mockContext, mockResponse, mockNext)
61
+ ).rejects.toThrow('Unauthorized - redirecting to login');
62
+
63
+ expect(storage.getToken()).toBeNull();
64
+ const principal = mockContext.application.principal as Principal;
65
+ expect(principal.user).toBeNull();
66
+ expect(principal.isAuthenticated).toBe(false);
67
+ expect(window.location.href).toBe('#/login');
68
+ });
69
+
70
+ it('should throw error for failed requests with JSON error message', async () => {
71
+ const mockResponse = new Response(
72
+ JSON.stringify({ message: 'Custom error message' }),
73
+ {
74
+ status: 400,
75
+ headers: { 'content-type': 'application/json' },
76
+ }
77
+ );
78
+
79
+ await expect(
80
+ errorMiddleware.call(mockContext, mockResponse, mockNext)
81
+ ).rejects.toThrow('Custom error message');
82
+ });
83
+
84
+ it('should throw error for failed requests without JSON body', async () => {
85
+ const mockResponse = new Response('Internal Server Error', {
86
+ status: 500,
87
+ headers: { 'content-type': 'text/plain' },
88
+ });
89
+
90
+ await expect(
91
+ errorMiddleware.call(mockContext, mockResponse, mockNext)
92
+ ).rejects.toThrow('Request failed with status 500');
93
+ });
94
+
95
+ it('should throw generic error for failed JSON requests without message', async () => {
96
+ const mockResponse = new Response(JSON.stringify({}), {
97
+ status: 400,
98
+ headers: { 'content-type': 'application/json' },
99
+ });
100
+
101
+ await expect(
102
+ errorMiddleware.call(mockContext, mockResponse, mockNext)
103
+ ).rejects.toThrow('Request failed with status 400');
104
+ });
105
+ });
@@ -0,0 +1,103 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { Context } from 'snice';
3
+ import { createRetryMiddleware } from '../../src/middleware/retry';
4
+
5
+ describe('Retry Middleware', () => {
6
+ let mockContext: Context;
7
+ let mockRequest: Request;
8
+
9
+ beforeEach(() => {
10
+ vi.clearAllMocks();
11
+
12
+ mockContext = {
13
+ application: {
14
+ principal: {
15
+ user: null,
16
+ isAuthenticated: false,
17
+ },
18
+ },
19
+ navigation: {
20
+ route: '/',
21
+ params: {},
22
+ },
23
+ update: vi.fn(),
24
+ } as unknown as Context;
25
+
26
+ mockRequest = new Request('https://api.example.com/data');
27
+ });
28
+
29
+ it('should return response on first successful attempt', async () => {
30
+ const retryMiddleware = createRetryMiddleware(3, 10);
31
+ const mockResponse = new Response('{}', { status: 200 });
32
+ const mockNext = vi.fn(() => Promise.resolve(mockResponse));
33
+
34
+ const response = await retryMiddleware.call(mockContext, mockRequest, mockNext);
35
+
36
+ expect(mockNext).toHaveBeenCalledTimes(1);
37
+ expect(response).toBe(mockResponse);
38
+ });
39
+
40
+ it('should retry on failure and eventually succeed', async () => {
41
+ const retryMiddleware = createRetryMiddleware(3, 10);
42
+ const mockResponse = new Response('{}', { status: 200 });
43
+ const mockNext = vi
44
+ .fn()
45
+ .mockRejectedValueOnce(new Error('Network error'))
46
+ .mockRejectedValueOnce(new Error('Network error'))
47
+ .mockResolvedValueOnce(mockResponse);
48
+
49
+ const response = await retryMiddleware.call(mockContext, mockRequest, mockNext);
50
+
51
+ expect(mockNext).toHaveBeenCalledTimes(3);
52
+ expect(response).toBe(mockResponse);
53
+ });
54
+
55
+ it('should throw error after max retries', async () => {
56
+ const retryMiddleware = createRetryMiddleware(3, 10);
57
+ const error = new Error('Network error');
58
+ const mockNext = vi.fn(() => Promise.reject(error));
59
+
60
+ await expect(
61
+ retryMiddleware.call(mockContext, mockRequest, mockNext)
62
+ ).rejects.toThrow('Network error');
63
+
64
+ expect(mockNext).toHaveBeenCalledTimes(3);
65
+ });
66
+
67
+ it('should not retry on Unauthorized errors', async () => {
68
+ const retryMiddleware = createRetryMiddleware(3, 10);
69
+ const error = new Error('Unauthorized');
70
+ const mockNext = vi.fn(() => Promise.reject(error));
71
+
72
+ await expect(
73
+ retryMiddleware.call(mockContext, mockRequest, mockNext)
74
+ ).rejects.toThrow('Unauthorized');
75
+
76
+ // Should only try once, no retries
77
+ expect(mockNext).toHaveBeenCalledTimes(1);
78
+ });
79
+
80
+ it('should use exponential backoff', async () => {
81
+ vi.useFakeTimers();
82
+
83
+ try {
84
+ const retryMiddleware = createRetryMiddleware(3, 100);
85
+ const mockNext = vi.fn(() => Promise.reject(new Error('Network error')));
86
+
87
+ const promise = retryMiddleware.call(mockContext, mockRequest, mockNext).catch(err => err);
88
+
89
+ // Run all timers to completion
90
+ await vi.runAllTimersAsync();
91
+
92
+ // All 3 attempts should have been made
93
+ expect(mockNext).toHaveBeenCalledTimes(3);
94
+
95
+ // Promise should have rejected with network error
96
+ const result = await promise;
97
+ expect(result).toBeInstanceOf(Error);
98
+ expect(result.message).toBe('Network error');
99
+ } finally {
100
+ vi.useRealTimers();
101
+ }
102
+ });
103
+ });
@@ -0,0 +1,89 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { login, logout, isAuthenticated, refreshToken } from '../../src/services/auth';
3
+ import * as storage from '../../src/services/storage';
4
+
5
+ describe('Auth Service', () => {
6
+ beforeEach(() => {
7
+ localStorage.clear();
8
+ vi.clearAllMocks();
9
+ });
10
+
11
+ describe('login', () => {
12
+ it('should login with valid credentials', async () => {
13
+ const credentials = { email: 'demo@example.com', password: 'demo' };
14
+ const result = await login(credentials);
15
+
16
+ expect(result).toHaveProperty('token');
17
+ expect(result).toHaveProperty('user');
18
+ expect(result.user.email).toBe('demo@example.com');
19
+ expect(result.user.name).toBe('Demo User');
20
+ });
21
+
22
+ it('should store token and user on successful login', async () => {
23
+ const credentials = { email: 'demo@example.com', password: 'demo' };
24
+ await login(credentials);
25
+
26
+ expect(storage.getToken()).toBeTruthy();
27
+ expect(storage.getUser()).toBeTruthy();
28
+ });
29
+
30
+ it('should throw error for invalid credentials', async () => {
31
+ const credentials = { email: 'wrong@example.com', password: 'wrong' };
32
+
33
+ await expect(login(credentials)).rejects.toThrow('Invalid credentials');
34
+ });
35
+
36
+ it('should throw error for wrong password', async () => {
37
+ const credentials = { email: 'demo@example.com', password: 'wrong' };
38
+
39
+ await expect(login(credentials)).rejects.toThrow('Invalid credentials');
40
+ });
41
+ });
42
+
43
+ describe('logout', () => {
44
+ it('should clear token on logout', async () => {
45
+ storage.setToken('test-token');
46
+ storage.setUser({ id: '1', name: 'Test' });
47
+
48
+ await logout();
49
+
50
+ expect(storage.getToken()).toBeNull();
51
+ expect(storage.getUser()).toBeNull();
52
+ });
53
+ });
54
+
55
+ describe('isAuthenticated', () => {
56
+ it('should return true when valid token exists', () => {
57
+ // Non-expired token
58
+ const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZW1haWwiOiJkZW1vQGV4YW1wbGUuY29tIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjk5OTk5OTk5OTl9.mock';
59
+ storage.setToken(token);
60
+
61
+ expect(isAuthenticated()).toBe(true);
62
+ });
63
+
64
+ it('should return false when no token exists', () => {
65
+ expect(isAuthenticated()).toBe(false);
66
+ });
67
+
68
+ it('should return false when token is expired', () => {
69
+ // Expired token
70
+ const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZXhwIjoxNjAwMDAwMDAwfQ.mock';
71
+ storage.setToken(token);
72
+
73
+ expect(isAuthenticated()).toBe(false);
74
+ });
75
+ });
76
+
77
+ describe('refreshToken', () => {
78
+ it('should return new token', async () => {
79
+ const newToken = await refreshToken();
80
+ expect(newToken).toBeTruthy();
81
+ expect(typeof newToken).toBe('string');
82
+ });
83
+
84
+ it('should update stored token', async () => {
85
+ await refreshToken();
86
+ expect(storage.getToken()).toBeTruthy();
87
+ });
88
+ });
89
+ });
@@ -0,0 +1,76 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { decodeJWT, isTokenExpired, getTokenExpiration } from '../../src/services/jwt';
3
+
4
+ describe('JWT Service', () => {
5
+ describe('decodeJWT', () => {
6
+ it('should decode a valid JWT', () => {
7
+ // Valid JWT: {"sub":"123","name":"Test","exp":9999999999}
8
+ const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJuYW1lIjoiVGVzdCIsImV4cCI6OTk5OTk5OTk5OX0.mock';
9
+ const payload = decodeJWT(token);
10
+
11
+ expect(payload).toBeTruthy();
12
+ expect(payload?.sub).toBe('123');
13
+ expect(payload?.name).toBe('Test');
14
+ expect(payload?.exp).toBe(9999999999);
15
+ });
16
+
17
+ it('should return null for invalid JWT format', () => {
18
+ const payload = decodeJWT('invalid-token');
19
+ expect(payload).toBeNull();
20
+ });
21
+
22
+ it('should return null for malformed JWT', () => {
23
+ const payload = decodeJWT('part1.part2');
24
+ expect(payload).toBeNull();
25
+ });
26
+
27
+ it('should return null for JWT with invalid base64', () => {
28
+ const payload = decodeJWT('header.!!!invalid!!!.signature');
29
+ expect(payload).toBeNull();
30
+ });
31
+ });
32
+
33
+ describe('isTokenExpired', () => {
34
+ it('should return false for non-expired token', () => {
35
+ // Token expires in year 2286
36
+ const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJleHAiOjk5OTk5OTk5OTl9.mock';
37
+ expect(isTokenExpired(token)).toBe(false);
38
+ });
39
+
40
+ it('should return true for expired token', () => {
41
+ // Token expired in 2020
42
+ const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJleHAiOjE2MDAwMDAwMDB9.mock';
43
+ expect(isTokenExpired(token)).toBe(true);
44
+ });
45
+
46
+ it('should return true for token without exp claim', () => {
47
+ // Token without exp: {"sub":"123"}
48
+ const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMifQ.mock';
49
+ expect(isTokenExpired(token)).toBe(true);
50
+ });
51
+
52
+ it('should return true for invalid token', () => {
53
+ expect(isTokenExpired('invalid')).toBe(true);
54
+ });
55
+ });
56
+
57
+ describe('getTokenExpiration', () => {
58
+ it('should return expiration date for valid token', () => {
59
+ // Token with exp: 9999999999 (Sat Nov 20 2286)
60
+ const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJleHAiOjk5OTk5OTk5OTl9.mock';
61
+ const expiration = getTokenExpiration(token);
62
+
63
+ expect(expiration).toBeInstanceOf(Date);
64
+ expect(expiration?.getTime()).toBe(9999999999 * 1000);
65
+ });
66
+
67
+ it('should return null for token without exp', () => {
68
+ const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMifQ.mock';
69
+ expect(getTokenExpiration(token)).toBeNull();
70
+ });
71
+
72
+ it('should return null for invalid token', () => {
73
+ expect(getTokenExpiration('invalid')).toBeNull();
74
+ });
75
+ });
76
+ });
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { getToken, setToken, clearToken, getUser, setUser } from '../../src/services/storage';
3
+
4
+ describe('Storage Service', () => {
5
+ beforeEach(() => {
6
+ localStorage.clear();
7
+ vi.clearAllMocks();
8
+ });
9
+
10
+ describe('Token Management', () => {
11
+ it('should set and get token', () => {
12
+ const token = 'test-token-123';
13
+ setToken(token);
14
+ expect(getToken()).toBe(token);
15
+ });
16
+
17
+ it('should return null when no token exists', () => {
18
+ expect(getToken()).toBeNull();
19
+ });
20
+
21
+ it('should clear token and user', () => {
22
+ setToken('test-token');
23
+ setUser({ id: '1', name: 'Test' });
24
+
25
+ clearToken();
26
+
27
+ expect(getToken()).toBeNull();
28
+ expect(getUser()).toBeNull();
29
+ });
30
+ });
31
+
32
+ describe('User Management', () => {
33
+ it('should set and get user object', () => {
34
+ const user = { id: '1', name: 'Test User', email: 'test@example.com' };
35
+ setUser(user);
36
+ expect(getUser()).toEqual(user);
37
+ });
38
+
39
+ it('should return null when no user exists', () => {
40
+ expect(getUser()).toBeNull();
41
+ });
42
+
43
+ it('should handle complex user objects', () => {
44
+ const user = {
45
+ id: '1',
46
+ name: 'Test User',
47
+ email: 'test@example.com',
48
+ metadata: {
49
+ preferences: {
50
+ theme: 'dark',
51
+ notifications: true,
52
+ },
53
+ },
54
+ };
55
+
56
+ setUser(user);
57
+ expect(getUser()).toEqual(user);
58
+ });
59
+
60
+ it('should overwrite existing user', () => {
61
+ setUser({ id: '1', name: 'User 1' });
62
+ setUser({ id: '2', name: 'User 2' });
63
+
64
+ const user = getUser<{ id: string; name: string }>();
65
+ expect(user?.id).toBe('2');
66
+ expect(user?.name).toBe('User 2');
67
+ });
68
+ });
69
+ });
@@ -1,4 +1,4 @@
1
- import { defineConfig } from 'vite';
1
+ import { defineConfig } from 'vitest/config';
2
2
  import swc from 'unplugin-swc';
3
3
 
4
4
  export default defineConfig({
@@ -18,21 +18,16 @@ export default defineConfig({
18
18
  },
19
19
  }),
20
20
  ],
21
- build: {
22
- target: 'es2015',
23
- minify: 'terser',
24
- cssMinify: true,
25
- rollupOptions: {
26
- output: {
27
- manualChunks: {
28
- vendor: ['snice']
29
- }
30
- }
21
+ test: {
22
+ environment: 'happy-dom',
23
+ globals: true,
24
+ coverage: {
25
+ reporter: ['text', 'json', 'html'],
26
+ exclude: [
27
+ 'node_modules/',
28
+ 'dist/',
29
+ 'tests/**',
30
+ ],
31
31
  },
32
- sourcemap: true,
33
- chunkSizeWarningLimit: 500
34
32
  },
35
- esbuild: {
36
- drop: process.env.NODE_ENV === 'production' ? ['debugger'] : []
37
- }
38
- });
33
+ });
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v3.6.0
2
+ * snice v3.7.0
3
3
  * Imperative TypeScript framework for building vanilla web components with decorators, differential rendering, routing, and controllers. No virtual DOM, no build complexity.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
package/dist/index.esm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v3.6.0
2
+ * snice v3.7.0
3
3
  * Imperative TypeScript framework for building vanilla web components with decorators, differential rendering, routing, and controllers. No virtual DOM, no build complexity.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v3.6.0
2
+ * snice v3.7.0
3
3
  * Imperative TypeScript framework for building vanilla web components with decorators, differential rendering, routing, and controllers. No virtual DOM, no build complexity.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
package/dist/symbols.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v3.6.0
2
+ * snice v3.7.0
3
3
  * Imperative TypeScript framework for building vanilla web components with decorators, differential rendering, routing, and controllers. No virtual DOM, no build complexity.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v3.6.0
2
+ * snice v3.7.0
3
3
  * Imperative TypeScript framework for building vanilla web components with decorators, differential rendering, routing, and controllers. No virtual DOM, no build complexity.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v3.6.0
2
+ * snice v3.7.0
3
3
  * Imperative TypeScript framework for building vanilla web components with decorators, differential rendering, routing, and controllers. No virtual DOM, no build complexity.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v3.6.0
2
+ * snice v3.7.0
3
3
  * Imperative TypeScript framework for building vanilla web components with decorators, differential rendering, routing, and controllers. No virtual DOM, no build complexity.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -56,7 +56,7 @@ class UserPage extends HTMLElement {
56
56
  private appContext?: AppContext;
57
57
 
58
58
  @context()
59
- handleContext(ctx: Context<AppContext>) {
59
+ handleContext(ctx: Context) {
60
60
  this.appContext = ctx.application;
61
61
  this.requestRender();
62
62
  }
package/docs/routing.md CHANGED
@@ -207,13 +207,14 @@ class DashboardPage extends HTMLElement {
207
207
  The Context object passed to `@context()` methods has the following structure:
208
208
 
209
209
  ```typescript
210
- interface Context<T = any> {
211
- application: T; // Your router context (e.g., AppContext)
210
+ interface Context {
211
+ application: AppContext; // Your router context (e.g., { user, theme, config })
212
212
  navigation: {
213
213
  placards: Placard[]; // All page placards
214
214
  route: string; // Current route name
215
215
  params: Record<string, string>; // Route parameters
216
216
  };
217
+ fetch: typeof globalThis.fetch; // Fetch function with middleware support
217
218
  update(): void; // Notify all subscribers of changes
218
219
  }
219
220
  ```
@@ -223,10 +224,10 @@ interface Context<T = any> {
223
224
  ```typescript
224
225
  @page({ tag: 'user-page', routes: ['/users/:userId'] })
225
226
  class UserPage extends HTMLElement {
226
- private ctx?: Context<AppContext>;
227
+ private ctx?: Context;
227
228
 
228
229
  @context()
229
- handleContext(ctx: Context<AppContext>) {
230
+ handleContext(ctx: Context) {
230
231
  this.ctx = ctx;
231
232
 
232
233
  // Access application state
@@ -250,10 +251,10 @@ When you modify the application context, call `update()` to notify all subscribe
250
251
  ```typescript
251
252
  @page({ tag: 'settings-page', routes: ['/settings'] })
252
253
  class SettingsPage extends HTMLElement {
253
- private ctx?: Context<AppContext>;
254
+ private ctx?: Context;
254
255
 
255
256
  @context()
256
- handleContext(ctx: Context<AppContext>) {
257
+ handleContext(ctx: Context) {
257
258
  this.ctx = ctx;
258
259
  this.requestRender();
259
260
  }
@@ -1066,7 +1067,7 @@ class DashboardPage extends HTMLElement {
1066
1067
  private appContext?: AppContext;
1067
1068
 
1068
1069
  @context()
1069
- handleContext(ctx: Context<AppContext>) {
1070
+ handleContext(ctx: Context) {
1070
1071
  this.appContext = ctx.application;
1071
1072
  this.requestRender();
1072
1073
  }
@@ -1093,7 +1094,7 @@ class LoginPage extends HTMLElement {
1093
1094
  private appContext?: AppContext;
1094
1095
 
1095
1096
  @context()
1096
- handleContext(ctx: Context<AppContext>) {
1097
+ handleContext(ctx: Context) {
1097
1098
  this.appContext = ctx.application;
1098
1099
  }
1099
1100
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snice",
3
- "version": "3.7.0",
3
+ "version": "3.8.0",
4
4
  "type": "module",
5
5
  "description": "Imperative TypeScript framework for building vanilla web components with decorators, differential rendering, routing, and controllers. No virtual DOM, no build complexity.",
6
6
  "main": "dist/index.cjs",
@@ -1,24 +0,0 @@
1
- {
2
- "name": "{{projectName}}",
3
- "short_name": "{{projectName}}",
4
- "description": "PWA built with Snice",
5
- "start_url": "/",
6
- "display": "standalone",
7
- "background_color": "#ffffff",
8
- "theme_color": "#6366f1",
9
- "orientation": "portrait-primary",
10
- "icons": [
11
- {
12
- "src": "/icons/icon-192.png",
13
- "sizes": "192x192",
14
- "type": "image/png",
15
- "purpose": "any maskable"
16
- },
17
- {
18
- "src": "/icons/icon-512.png",
19
- "sizes": "512x512",
20
- "type": "image/png",
21
- "purpose": "any maskable"
22
- }
23
- ]
24
- }
@@ -1,39 +0,0 @@
1
- export type Middleware = (
2
- url: string,
3
- options: RequestInit,
4
- next: () => Promise<Response>
5
- ) => Promise<Response>;
6
-
7
- export type FetchConfig = {
8
- baseURL?: string;
9
- middleware?: Middleware[];
10
- headers?: Record<string, string>;
11
- };
12
-
13
- export function createFetch(config: FetchConfig = {}) {
14
- return async (url: string, options: RequestInit = {}): Promise<Response> => {
15
- const fullURL = config.baseURL ? `${config.baseURL}${url}` : url;
16
-
17
- const execute = async (): Promise<Response> => {
18
- return fetch(fullURL, {
19
- ...options,
20
- headers: {
21
- ...config.headers,
22
- ...options.headers,
23
- },
24
- });
25
- };
26
-
27
- // Chain middleware (reverse order so first middleware wraps execution)
28
- let chain = execute;
29
- if (config.middleware) {
30
- for (let i = config.middleware.length - 1; i >= 0; i--) {
31
- const middleware = config.middleware[i];
32
- const next = chain;
33
- chain = () => middleware(fullURL, options, next);
34
- }
35
- }
36
-
37
- return chain();
38
- };
39
- }