@singhey/spa-ssr-renderer 1.0.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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +247 -0
  3. package/dist/__tests__/setup.test.d.ts +2 -0
  4. package/dist/__tests__/setup.test.d.ts.map +1 -0
  5. package/dist/__tests__/setup.test.js +22 -0
  6. package/dist/__tests__/setup.test.js.map +1 -0
  7. package/dist/components/BotDetector.d.ts +12 -0
  8. package/dist/components/BotDetector.d.ts.map +1 -0
  9. package/dist/components/BotDetector.js +46 -0
  10. package/dist/components/BotDetector.js.map +1 -0
  11. package/dist/components/CacheManager.d.ts +13 -0
  12. package/dist/components/CacheManager.d.ts.map +1 -0
  13. package/dist/components/CacheManager.js +46 -0
  14. package/dist/components/CacheManager.js.map +1 -0
  15. package/dist/components/FileServer.d.ts +7 -0
  16. package/dist/components/FileServer.d.ts.map +1 -0
  17. package/dist/components/FileServer.js +15 -0
  18. package/dist/components/FileServer.js.map +1 -0
  19. package/dist/components/RequestRouter.d.ts +7 -0
  20. package/dist/components/RequestRouter.d.ts.map +1 -0
  21. package/dist/components/RequestRouter.js +15 -0
  22. package/dist/components/RequestRouter.js.map +1 -0
  23. package/dist/components/SSRRenderer.d.ts +9 -0
  24. package/dist/components/SSRRenderer.d.ts.map +1 -0
  25. package/dist/components/SSRRenderer.js +55 -0
  26. package/dist/components/SSRRenderer.js.map +1 -0
  27. package/dist/components/index.d.ts +6 -0
  28. package/dist/components/index.d.ts.map +1 -0
  29. package/dist/components/index.js +6 -0
  30. package/dist/components/index.js.map +1 -0
  31. package/dist/config/index.d.ts +4 -0
  32. package/dist/config/index.d.ts.map +1 -0
  33. package/dist/config/index.js +44 -0
  34. package/dist/config/index.js.map +1 -0
  35. package/dist/index.d.ts +2 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +21 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/server.d.ts +27 -0
  40. package/dist/server.d.ts.map +1 -0
  41. package/dist/server.js +245 -0
  42. package/dist/server.js.map +1 -0
  43. package/dist/types/index.d.ts +101 -0
  44. package/dist/types/index.d.ts.map +1 -0
  45. package/dist/types/index.js +2 -0
  46. package/dist/types/index.js.map +1 -0
  47. package/dist/utils/index.d.ts +2 -0
  48. package/dist/utils/index.d.ts.map +1 -0
  49. package/dist/utils/index.js +2 -0
  50. package/dist/utils/index.js.map +1 -0
  51. package/dist/utils/logger.d.ts +13 -0
  52. package/dist/utils/logger.d.ts.map +1 -0
  53. package/dist/utils/logger.js +17 -0
  54. package/dist/utils/logger.js.map +1 -0
  55. package/package.json +80 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 singhey
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,247 @@
1
+ # @singhey/spa-ssr-renderer
2
+
3
+ A Node.js server application that intelligently serves Single Page Applications (SPAs) by providing server-side rendered content to bots and crawlers while serving the original SPA files to regular users.
4
+
5
+ ## Features
6
+
7
+ - **Bot Detection**: Identifies web crawlers and search engine bots using ua-parser-js
8
+ - **Server-Side Rendering**: Uses Playwright for headless browser rendering
9
+ - **Intelligent Caching**: TTL-based cache with `@isaacs/ttlcache`
10
+ - **Static File Serving**: Efficient serving of static assets with SPA fallback
11
+ - **Sitemap Support**: Parse sitemaps to discover paths for pre-rendering
12
+ - **Exclusion Patterns**: Flexible path exclusion with wildcard support
13
+ - **Graceful Fallbacks**: Continues operation even when components fail
14
+ - **Library & CLI**: Can be used as an importable library or standalone CLI tool
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @singhey/spa-ssr-renderer
20
+ # or
21
+ pnpm add @singhey/spa-ssr-renderer
22
+ # or
23
+ yarn add @singhey/spa-ssr-renderer
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ### As a Library
29
+
30
+ Import and use the server in your Node.js application:
31
+
32
+ ```typescript
33
+ import { SPASSRServer, ServerConfig } from '@singhey/spa-ssr-renderer';
34
+
35
+ // Define your configuration
36
+ const config: ServerConfig = {
37
+ port: 3000,
38
+ staticDir: 'public',
39
+ spaEntryPoint: 'index.html',
40
+ prerender: {
41
+ // Explicit paths to pre-render
42
+ paths: ['/', '/about', '/products'],
43
+
44
+ // Sitemaps to parse (URLs or local file paths)
45
+ sitemaps: ['https://example.com/sitemap.xml', './public/sitemap.xml'],
46
+
47
+ // Paths or patterns to exclude (supports wildcards)
48
+ exclude: ['/admin/*', '/api/*', '/private']
49
+ },
50
+ cache: {
51
+ type: 'memory',
52
+ ttl: 300000, // 5 minutes
53
+ maxSize: 100,
54
+ },
55
+ renderer: {
56
+ timeout: 30000,
57
+ viewport: { width: 1280, height: 720 },
58
+ waitForNetworkIdle: false,
59
+ },
60
+ botDetection: {
61
+ customPatterns: [],
62
+ enableVerification: false,
63
+ },
64
+ };
65
+
66
+ // Create and start server
67
+ const server = new SPASSRServer({ config });
68
+
69
+ await server.start();
70
+ console.log('Server running!');
71
+
72
+ // Access underlying Fastify instance for custom routes
73
+ const fastifyInstance = server.getServer();
74
+ fastifyInstance.get('/api/custom', async () => {
75
+ return { message: 'Custom endpoint' };
76
+ });
77
+
78
+ // Graceful shutdown
79
+ process.on('SIGTERM', async () => {
80
+ await server.stop();
81
+ });
82
+ ```
83
+
84
+ ### As a CLI Tool
85
+
86
+ Run directly from the command line:
87
+
88
+ ```bash
89
+ # Using npx
90
+ npx @singhey/spa-ssr-renderer
91
+
92
+ # Or install globally
93
+ npm install -g @singhey/spa-ssr-renderer
94
+ spa-ssr-renderer
95
+ ```
96
+
97
+ ### Environment Variables
98
+
99
+ Configure the server using environment variables:
100
+
101
+ - `PORT` - Server port (default: 3000)
102
+ - `STATIC_DIR` - Static files directory (default: public)
103
+ - `SPA_ENTRY_POINT` - SPA entry file (default: index.html)
104
+ - **Pre-rendering Configuration:**
105
+ - `PRERENDER_PATHS` - Comma-separated list of paths to pre-render (e.g., "/,/about,/products")
106
+ - `PRERENDER_SITEMAPS` - Comma-separated list of sitemap URLs or file paths (e.g., "https://example.com/sitemap.xml,./public/sitemap.xml")
107
+ - `PRERENDER_EXCLUDE` - Comma-separated list of paths or patterns to exclude (supports wildcards: `*` and `?`)
108
+ - `CACHE_TYPE` - Cache type: memory|redis (default: memory)
109
+ - `CACHE_TTL` - Cache TTL in ms (default: 300000)
110
+ - `RENDER_TIMEOUT` - Render timeout in ms (default: 30000)
111
+ - `VIEWPORT_WIDTH` - Render viewport width (default: 1280)
112
+ - `VIEWPORT_HEIGHT` - Render viewport height (default: 720)
113
+
114
+ ## How It Works
115
+
116
+ 1. **Static Files**: If a file exists in the static directory, it's served directly
117
+ 2. **Pre-rendering**: On startup, specified paths are rendered using Playwright and cached
118
+ - **Explicit Paths**: Define specific paths to pre-render
119
+ - **Sitemap Support**: Parse sitemaps (URLs or local files) to discover paths
120
+ - **Exclusions**: Use patterns to exclude paths from pre-rendering (e.g., `/admin/*`, `/api/*`)
121
+ 3. **Bot Detection**: Incoming requests are analyzed for bot User-Agents using ua-parser-js
122
+ 4. **Smart Serving**:
123
+ - **Bots**: Served pre-rendered, cached HTML for instant SEO-friendly content
124
+ - **Regular Users**: Served the SPA entry point for full client-side interactivity
125
+ 5. **SPA Fallback**: For routes without file extensions, serves the SPA entry point (index.html)
126
+ 6. **Caching**: Pre-rendered content is cached using `@isaacs/ttlcache` with configurable TTL
127
+
128
+ ### Pre-rendering Configuration Examples
129
+
130
+ **Explicit paths only:**
131
+ ```typescript
132
+ prerender: {
133
+ paths: ['/', '/about', '/products', '/contact']
134
+ }
135
+ ```
136
+
137
+ **Using sitemaps:**
138
+ ```typescript
139
+ prerender: {
140
+ sitemaps: [
141
+ 'https://example.com/sitemap.xml', // Remote sitemap
142
+ './public/sitemap.xml' // Local sitemap
143
+ ]
144
+ }
145
+ ```
146
+
147
+ **With exclusions (supports wildcards):**
148
+ ```typescript
149
+ prerender: {
150
+ paths: ['/', '/about', '/products'],
151
+ sitemaps: ['./public/sitemap.xml'],
152
+ exclude: [
153
+ '/admin/*', // Exclude all admin paths
154
+ '/api/*', // Exclude all API paths
155
+ '/private', // Exclude specific path
156
+ '*/draft' // Exclude all draft pages
157
+ ]
158
+ }
159
+ ```
160
+
161
+ **Combined configuration:**
162
+ ```typescript
163
+ prerender: {
164
+ paths: ['/', '/about'], // Always pre-render these
165
+ sitemaps: ['./public/sitemap.xml'], // Plus paths from sitemap
166
+ exclude: ['/admin/*', '/api/*'] // But exclude these patterns
167
+ }
168
+ ```
169
+
170
+ ## Project Structure
171
+
172
+ ```
173
+ src/
174
+ ├── components/ # Core application components
175
+ │ ├── BotDetector.ts # Bot detection logic
176
+ │ ├── FileServer.ts # Static file serving
177
+ │ ├── RequestRouter.ts # Request routing and classification
178
+ │ ├── CacheManager.ts # Caching system
179
+ │ ├── SSRRenderer.ts # Server-side rendering
180
+ │ └── index.ts # Component exports
181
+ ├── config/ # Configuration management
182
+ │ └── index.ts # Default config and environment loading
183
+ ├── types/ # TypeScript interfaces and types
184
+ │ └── index.ts # All type definitions
185
+ ├── utils/ # Shared utilities
186
+ │ ├── logger.ts # Logging utilities
187
+ │ └── index.ts # Utility exports
188
+ ├── __tests__/ # Test files
189
+ │ └── setup.test.ts # Foundation tests
190
+ └── index.ts # Main application entry point
191
+ ```
192
+
193
+ ## Development
194
+
195
+ ### Prerequisites
196
+
197
+ - Node.js 18+
198
+ - pnpm
199
+
200
+ ### Installation
201
+
202
+ ```bash
203
+ pnpm install
204
+ npx playwright install
205
+ ```
206
+
207
+ ### Scripts
208
+
209
+ - `pnpm dev` - Start development server with hot reload
210
+ - `pnpm build` - Build for production
211
+ - `pnpm start` - Start production server
212
+ - `pnpm test` - Run tests
213
+ - `pnpm test:watch` - Run tests in watch mode
214
+ - `pnpm test:coverage` - Run tests with coverage
215
+
216
+ ### Environment Variables
217
+
218
+ - `PORT` - Server port (default: 3000)
219
+ - `STATIC_DIR` - Static files directory (default: public)
220
+ - `SPA_ENTRY_POINT` - SPA entry file (default: index.html)
221
+ - `CACHE_TYPE` - Cache type: memory|redis (default: memory)
222
+ - `CACHE_TTL` - Cache TTL in ms (default: 300000)
223
+ - `RENDER_TIMEOUT` - Render timeout in ms (default: 30000)
224
+ - `VIEWPORT_WIDTH` - Render viewport width (default: 1280)
225
+ - `VIEWPORT_HEIGHT` - Render viewport height (default: 720)
226
+
227
+ ## Implementation Status
228
+
229
+ This is the foundation setup. Core functionality will be implemented in subsequent tasks:
230
+
231
+ - [x] Task 1: Project foundation and interfaces
232
+ - [ ] Task 2: Bot Detection System
233
+ - [ ] Task 3: File Server Component
234
+ - [ ] Task 4: Request Router
235
+ - [ ] Task 5: Cache Manager
236
+ - [ ] Task 6: SSR Renderer
237
+ - [ ] Task 7: Main Server Integration
238
+ - [ ] Task 8: Configuration System
239
+ - [ ] Task 9: Final Integration
240
+
241
+ ## Testing
242
+
243
+ The project uses Jest for unit testing and fast-check for property-based testing. Each component will have comprehensive test coverage including:
244
+
245
+ - Unit tests for specific scenarios
246
+ - Property-based tests for universal behaviors
247
+ - Integration tests for component interactions
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=setup.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/setup.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,22 @@
1
+ import { loadConfig } from '../config/index.js';
2
+ import { ConsoleLogger } from '../utils/index.js';
3
+ describe('Project Foundation Setup', () => {
4
+ test('should load default configuration', () => {
5
+ const config = loadConfig();
6
+ expect(config).toBeDefined();
7
+ expect(config.port).toBe(3000);
8
+ expect(config.staticDir).toBe('public');
9
+ expect(config.spaEntryPoint).toBe('index.html');
10
+ expect(config.cache.type).toBe('memory');
11
+ expect(config.renderer.timeout).toBe(30000);
12
+ });
13
+ test('should create logger instance', () => {
14
+ const logger = new ConsoleLogger();
15
+ expect(logger).toBeDefined();
16
+ expect(typeof logger.info).toBe('function');
17
+ expect(typeof logger.warn).toBe('function');
18
+ expect(typeof logger.error).toBe('function');
19
+ expect(typeof logger.debug).toBe('function');
20
+ });
21
+ });
22
+ //# sourceMappingURL=setup.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup.test.js","sourceRoot":"","sources":["../../src/__tests__/setup.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAElD,QAAQ,CAAC,0BAA0B,EAAE,GAAG,EAAE;IACxC,IAAI,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC7C,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAE5B,MAAM,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC;QAC7B,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/B,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACxC,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAChD,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACzC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACzC,MAAM,MAAM,GAAG,IAAI,aAAa,EAAE,CAAC;QAEnC,MAAM,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC;QAC7B,MAAM,CAAC,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC5C,MAAM,CAAC,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC5C,MAAM,CAAC,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC7C,MAAM,CAAC,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,12 @@
1
+ import { BotDetector as IBotDetector, BotInfo } from '../types/index.js';
2
+ export declare class BotDetector implements IBotDetector {
3
+ private customPatterns;
4
+ private parser;
5
+ private readonly knownBots;
6
+ constructor();
7
+ isBot(userAgent: string): boolean;
8
+ addBotPattern(pattern: RegExp): void;
9
+ getBotInfo(userAgent: string): BotInfo | null;
10
+ parseUserAgent(userAgent: string): UAParser.IResult;
11
+ }
12
+ //# sourceMappingURL=BotDetector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BotDetector.d.ts","sourceRoot":"","sources":["../../src/components/BotDetector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,IAAI,YAAY,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAIzE,qBAAa,WAAY,YAAW,YAAY;IAC9C,OAAO,CAAC,cAAc,CAAgB;IACtC,OAAO,CAAC,MAAM,CAAW;IAGzB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAuBxB;;IAMF,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAIjC,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAIpC,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI;IAI7C,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,QAAQ,CAAC,OAAO;CAGpD"}
@@ -0,0 +1,46 @@
1
+ import { UAParser } from 'ua-parser-js';
2
+ import { isBot } from 'ua-parser-js/bot-detection';
3
+ export class BotDetector {
4
+ constructor() {
5
+ this.customPatterns = [];
6
+ // Known bot patterns organized by type
7
+ this.knownBots = {
8
+ search: [
9
+ { name: 'Googlebot', patterns: ['googlebot', 'google'] },
10
+ { name: 'Bingbot', patterns: ['bingbot', 'msnbot'] },
11
+ { name: 'Yahoo Slurp', patterns: ['slurp'] },
12
+ { name: 'DuckDuckBot', patterns: ['duckduckbot'] },
13
+ { name: 'Baiduspider', patterns: ['baiduspider'] },
14
+ { name: 'YandexBot', patterns: ['yandexbot'] }
15
+ ],
16
+ social: [
17
+ { name: 'Facebook', patterns: ['facebookexternalhit', 'facebookcatalog'] },
18
+ { name: 'Twitter', patterns: ['twitterbot'] },
19
+ { name: 'LinkedIn', patterns: ['linkedinbot'] },
20
+ { name: 'Pinterest', patterns: ['pinterest'] },
21
+ { name: 'WhatsApp', patterns: ['whatsapp'] },
22
+ { name: 'Telegram', patterns: ['telegrambot'] }
23
+ ],
24
+ ai: [
25
+ { name: 'ChatGPT', patterns: ['chatgpt-user', 'gptbot'] },
26
+ { name: 'Claude', patterns: ['claude-web', 'anthropic'] },
27
+ { name: 'Perplexity', patterns: ['perplexitybot'] },
28
+ { name: 'Bing AI', patterns: ['edgechat', 'sydney'] }
29
+ ]
30
+ };
31
+ this.parser = new UAParser();
32
+ }
33
+ isBot(userAgent) {
34
+ return isBot(userAgent);
35
+ }
36
+ addBotPattern(pattern) {
37
+ this.customPatterns.push(pattern);
38
+ }
39
+ getBotInfo(userAgent) {
40
+ return null;
41
+ }
42
+ parseUserAgent(userAgent) {
43
+ return this.parser.setUA(userAgent).getResult();
44
+ }
45
+ }
46
+ //# sourceMappingURL=BotDetector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BotDetector.js","sourceRoot":"","sources":["../../src/components/BotDetector.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACxC,OAAO,EAAE,KAAK,EAAE,MAAM,4BAA4B,CAAA;AAElD,MAAM,OAAO,WAAW;IA8BtB;QA7BQ,mBAAc,GAAa,EAAE,CAAC;QAGtC,uCAAuC;QACtB,cAAS,GAAG;YAC3B,MAAM,EAAE;gBACN,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC,WAAW,EAAE,QAAQ,CAAC,EAAE;gBACxD,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,EAAE;gBACpD,EAAE,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE;gBAC5C,EAAE,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,aAAa,CAAC,EAAE;gBAClD,EAAE,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,aAAa,CAAC,EAAE;gBAClD,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC,WAAW,CAAC,EAAE;aAC/C;YACD,MAAM,EAAE;gBACN,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,qBAAqB,EAAE,iBAAiB,CAAC,EAAE;gBAC1E,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,YAAY,CAAC,EAAE;gBAC7C,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,aAAa,CAAC,EAAE;gBAC/C,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC,WAAW,CAAC,EAAE;gBAC9C,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,UAAU,CAAC,EAAE;gBAC5C,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,aAAa,CAAC,EAAE;aAChD;YACD,EAAE,EAAE;gBACF,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,cAAc,EAAE,QAAQ,CAAC,EAAE;gBACzD,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,YAAY,EAAE,WAAW,CAAC,EAAE;gBACzD,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC,eAAe,CAAC,EAAE;gBACnD,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,QAAQ,CAAC,EAAE;aACtD;SACF,CAAC;QAGA,IAAI,CAAC,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;IAC/B,CAAC;IAED,KAAK,CAAC,SAAiB;QACrB,OAAO,KAAK,CAAC,SAAS,CAAC,CAAA;IACzB,CAAC;IAED,aAAa,CAAC,OAAe;QAC3B,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACpC,CAAC;IAED,UAAU,CAAC,SAAiB;QAC1B,OAAO,IAAI,CAAA;IACb,CAAC;IAED,cAAc,CAAC,SAAiB;QAC9B,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,SAAS,EAAE,CAAC;IAClD,CAAC;CACF"}
@@ -0,0 +1,13 @@
1
+ import { CacheManager as ICacheManager, CacheStats } from '../types/index.js';
2
+ export declare class CacheManager implements ICacheManager {
3
+ private cache;
4
+ private stats;
5
+ private defaultTTL;
6
+ constructor(maxSize?: number, defaultTTL?: number);
7
+ get(key: string): Promise<string | null>;
8
+ set(key: string, value: string, ttl?: number): Promise<void>;
9
+ delete(key: string): Promise<void>;
10
+ clear(): Promise<void>;
11
+ getStats(): CacheStats;
12
+ }
13
+ //# sourceMappingURL=CacheManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CacheManager.d.ts","sourceRoot":"","sources":["../../src/components/CacheManager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,IAAI,aAAa,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAG9E,qBAAa,YAAa,YAAW,aAAa;IAChD,OAAO,CAAC,KAAK,CAA2B;IACxC,OAAO,CAAC,KAAK,CAGX;IACF,OAAO,CAAC,UAAU,CAAS;gBAEf,OAAO,GAAE,MAAY,EAAE,UAAU,GAAE,MAAe;IAQxD,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAYxC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAK5D,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAM5B,QAAQ,IAAI,UAAU;CAWvB"}
@@ -0,0 +1,46 @@
1
+ import { TTLCache } from '@isaacs/ttlcache';
2
+ export class CacheManager {
3
+ constructor(maxSize = 100, defaultTTL = 300000) {
4
+ this.stats = {
5
+ hits: 0,
6
+ misses: 0,
7
+ };
8
+ this.defaultTTL = defaultTTL;
9
+ this.cache = new TTLCache({
10
+ max: maxSize,
11
+ ttl: defaultTTL,
12
+ });
13
+ }
14
+ async get(key) {
15
+ const value = this.cache.get(key);
16
+ if (value === undefined) {
17
+ this.stats.misses++;
18
+ return null;
19
+ }
20
+ this.stats.hits++;
21
+ return value;
22
+ }
23
+ async set(key, value, ttl) {
24
+ const effectiveTTL = ttl || this.defaultTTL;
25
+ this.cache.set(key, value, { ttl: effectiveTTL });
26
+ }
27
+ async delete(key) {
28
+ this.cache.delete(key);
29
+ }
30
+ async clear() {
31
+ this.cache.clear();
32
+ this.stats.hits = 0;
33
+ this.stats.misses = 0;
34
+ }
35
+ getStats() {
36
+ const totalRequests = this.stats.hits + this.stats.misses;
37
+ const hitRate = totalRequests > 0 ? this.stats.hits / totalRequests : 0;
38
+ return {
39
+ hits: this.stats.hits,
40
+ misses: this.stats.misses,
41
+ size: this.cache.size,
42
+ hitRate,
43
+ };
44
+ }
45
+ }
46
+ //# sourceMappingURL=CacheManager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CacheManager.js","sourceRoot":"","sources":["../../src/components/CacheManager.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,MAAM,OAAO,YAAY;IAQvB,YAAY,UAAkB,GAAG,EAAE,aAAqB,MAAM;QANtD,UAAK,GAAG;YACd,IAAI,EAAE,CAAC;YACP,MAAM,EAAE,CAAC;SACV,CAAC;QAIA,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,KAAK,GAAG,IAAI,QAAQ,CAAC;YACxB,GAAG,EAAE,OAAO;YACZ,GAAG,EAAE,UAAU;SAChB,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW;QACnB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAElC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;YACpB,OAAO,IAAI,CAAC;QACd,CAAC;QAED,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAClB,OAAO,KAAK,CAAC;IACf,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,KAAa,EAAE,GAAY;QAChD,MAAM,YAAY,GAAG,GAAG,IAAI,IAAI,CAAC,UAAU,CAAC;QAC5C,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,YAAY,EAAE,CAAC,CAAC;IACpD,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACzB,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACnB,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC;QACpB,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;IACxB,CAAC;IAED,QAAQ;QACN,MAAM,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;QAC1D,MAAM,OAAO,GAAG,aAAa,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;QAExE,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI;YACrB,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM;YACzB,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI;YACrB,OAAO;SACR,CAAC;IACJ,CAAC;CACF"}
@@ -0,0 +1,7 @@
1
+ import { FileServer as IFileServer, Response } from '../types/index.js';
2
+ export declare class FileServer implements IFileServer {
3
+ serveFile(filePath: string, res: Response): Promise<void>;
4
+ getMimeType(extension: string): string;
5
+ setSecurityHeaders(res: Response): void;
6
+ }
7
+ //# sourceMappingURL=FileServer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FileServer.d.ts","sourceRoot":"","sources":["../../src/components/FileServer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,IAAI,WAAW,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAExE,qBAAa,UAAW,YAAW,WAAW;IACtC,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAK/D,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAKtC,kBAAkB,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI;CAIxC"}
@@ -0,0 +1,15 @@
1
+ export class FileServer {
2
+ async serveFile(filePath, res) {
3
+ // Implementation will be added in task 3
4
+ throw new Error('Method not implemented.');
5
+ }
6
+ getMimeType(extension) {
7
+ // Implementation will be added in task 3
8
+ throw new Error('Method not implemented.');
9
+ }
10
+ setSecurityHeaders(res) {
11
+ // Implementation will be added in task 3
12
+ throw new Error('Method not implemented.');
13
+ }
14
+ }
15
+ //# sourceMappingURL=FileServer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FileServer.js","sourceRoot":"","sources":["../../src/components/FileServer.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,UAAU;IACrB,KAAK,CAAC,SAAS,CAAC,QAAgB,EAAE,GAAa;QAC7C,yCAAyC;QACzC,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC7C,CAAC;IAED,WAAW,CAAC,SAAiB;QAC3B,yCAAyC;QACzC,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC7C,CAAC;IAED,kBAAkB,CAAC,GAAa;QAC9B,yCAAyC;QACzC,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC7C,CAAC;CACF"}
@@ -0,0 +1,7 @@
1
+ import { RequestRouter as IRequestRouter, Request, Response } from '../types/index.js';
2
+ export declare class RequestRouter implements IRequestRouter {
3
+ handleRequest(req: Request, res: Response): Promise<void>;
4
+ isFileRequest(path: string): boolean;
5
+ fileExists(path: string): boolean;
6
+ }
7
+ //# sourceMappingURL=RequestRouter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RequestRouter.d.ts","sourceRoot":"","sources":["../../src/components/RequestRouter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,IAAI,cAAc,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAEvF,qBAAa,aAAc,YAAW,cAAc;IAC5C,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAK/D,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAKpC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;CAIlC"}
@@ -0,0 +1,15 @@
1
+ export class RequestRouter {
2
+ async handleRequest(req, res) {
3
+ // Implementation will be added in task 4
4
+ throw new Error('Method not implemented.');
5
+ }
6
+ isFileRequest(path) {
7
+ // Implementation will be added in task 4
8
+ throw new Error('Method not implemented.');
9
+ }
10
+ fileExists(path) {
11
+ // Implementation will be added in task 4
12
+ throw new Error('Method not implemented.');
13
+ }
14
+ }
15
+ //# sourceMappingURL=RequestRouter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RequestRouter.js","sourceRoot":"","sources":["../../src/components/RequestRouter.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,aAAa;IACxB,KAAK,CAAC,aAAa,CAAC,GAAY,EAAE,GAAa;QAC7C,yCAAyC;QACzC,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC7C,CAAC;IAED,aAAa,CAAC,IAAY;QACxB,yCAAyC;QACzC,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC7C,CAAC;IAED,UAAU,CAAC,IAAY;QACrB,yCAAyC;QACzC,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC7C,CAAC;CACF"}
@@ -0,0 +1,9 @@
1
+ import { SSRRenderer as ISSRRenderer, RenderOptions } from '../types/index.js';
2
+ export declare class SSRRenderer implements ISSRRenderer {
3
+ private browser;
4
+ private isInitialized;
5
+ initialize(): Promise<void>;
6
+ render(url: string, options?: RenderOptions): Promise<string>;
7
+ cleanup(): Promise<void>;
8
+ }
9
+ //# sourceMappingURL=SSRRenderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SSRRenderer.d.ts","sourceRoot":"","sources":["../../src/components/SSRRenderer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,IAAI,YAAY,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAG/E,qBAAa,WAAY,YAAW,YAAY;IAC9C,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,aAAa,CAAS;IAExB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAY3B,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC;IAsC7D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAO/B"}
@@ -0,0 +1,55 @@
1
+ import { chromium } from 'playwright';
2
+ export class SSRRenderer {
3
+ constructor() {
4
+ this.browser = null;
5
+ this.isInitialized = false;
6
+ }
7
+ async initialize() {
8
+ if (this.isInitialized) {
9
+ return;
10
+ }
11
+ this.browser = await chromium.launch({
12
+ headless: true,
13
+ args: ['--no-sandbox', '--disable-setuid-sandbox'],
14
+ });
15
+ this.isInitialized = true;
16
+ }
17
+ async render(url, options) {
18
+ if (!this.browser) {
19
+ await this.initialize();
20
+ }
21
+ if (!this.browser) {
22
+ throw new Error('Browser not initialized');
23
+ }
24
+ const page = await this.browser.newPage({
25
+ viewport: options?.viewport || { width: 1280, height: 720 },
26
+ });
27
+ try {
28
+ const timeout = options?.timeout || 30000;
29
+ await page.goto(url, {
30
+ waitUntil: options?.waitForNetworkIdle ? 'networkidle' : 'domcontentloaded',
31
+ timeout,
32
+ });
33
+ // Wait for specific selector if provided
34
+ if (options?.waitForSelector) {
35
+ await page.waitForSelector(options.waitForSelector, { timeout });
36
+ }
37
+ // Get the rendered HTML
38
+ const html = await page.content();
39
+ await page.close();
40
+ return html;
41
+ }
42
+ catch (error) {
43
+ await page.close();
44
+ throw error;
45
+ }
46
+ }
47
+ async cleanup() {
48
+ if (this.browser) {
49
+ await this.browser.close();
50
+ this.browser = null;
51
+ this.isInitialized = false;
52
+ }
53
+ }
54
+ }
55
+ //# sourceMappingURL=SSRRenderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SSRRenderer.js","sourceRoot":"","sources":["../../src/components/SSRRenderer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAiB,MAAM,YAAY,CAAC;AAErD,MAAM,OAAO,WAAW;IAAxB;QACU,YAAO,GAAmB,IAAI,CAAC;QAC/B,kBAAa,GAAG,KAAK,CAAC;IA2DhC,CAAC;IAzDC,KAAK,CAAC,UAAU;QACd,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACvB,OAAO;QACT,CAAC;QAED,IAAI,CAAC,OAAO,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC;YACnC,QAAQ,EAAE,IAAI;YACd,IAAI,EAAE,CAAC,cAAc,EAAE,0BAA0B,CAAC;SACnD,CAAC,CAAC;QACH,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;IAC5B,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW,EAAE,OAAuB;QAC/C,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YAClB,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC;QAC1B,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;QAC7C,CAAC;QAED,MAAM,IAAI,GAAS,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC;YAC5C,QAAQ,EAAE,OAAO,EAAE,QAAQ,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE;SAC5D,CAAC,CAAC;QAEH,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,OAAO,EAAE,OAAO,IAAI,KAAK,CAAC;YAE1C,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE;gBACnB,SAAS,EAAE,OAAO,EAAE,kBAAkB,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,kBAAkB;gBAC3E,OAAO;aACR,CAAC,CAAC;YAEH,yCAAyC;YACzC,IAAI,OAAO,EAAE,eAAe,EAAE,CAAC;gBAC7B,MAAM,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;YACnE,CAAC;YAED,wBAAwB;YACxB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;YAElC,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;YAEnB,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;YACnB,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK,CAAC,OAAO;QACX,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YAC3B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;YACpB,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;QAC7B,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,6 @@
1
+ export { BotDetector } from './BotDetector.js';
2
+ export { FileServer } from './FileServer.js';
3
+ export { RequestRouter } from './RequestRouter.js';
4
+ export { CacheManager } from './CacheManager.js';
5
+ export { SSRRenderer } from './SSRRenderer.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,6 @@
1
+ export { BotDetector } from './BotDetector.js';
2
+ export { FileServer } from './FileServer.js';
3
+ export { RequestRouter } from './RequestRouter.js';
4
+ export { CacheManager } from './CacheManager.js';
5
+ export { SSRRenderer } from './SSRRenderer.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,4 @@
1
+ import { ServerConfig } from '../types/index.js';
2
+ export declare const defaultConfig: ServerConfig;
3
+ export declare function loadConfig(): ServerConfig;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAIjD,eAAO,MAAM,aAAa,EAAE,YAqC3B,CAAC;AAEF,wBAAgB,UAAU,IAAI,YAAY,CAEzC"}