olova 2.0.61 → 2.0.63

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 (80) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +42 -61
  3. package/dist/compiler.d.ts +44 -0
  4. package/dist/compiler.js +2139 -0
  5. package/dist/compiler.js.map +1 -0
  6. package/dist/core.d.ts +4 -0
  7. package/dist/core.js +859 -0
  8. package/dist/core.js.map +1 -0
  9. package/dist/global.d.ts +15 -0
  10. package/dist/global.js +226 -0
  11. package/dist/global.js.map +1 -0
  12. package/dist/index.d.ts +2 -0
  13. package/dist/index.js +2302 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/runtime.d.ts +89 -0
  16. package/dist/runtime.js +633 -0
  17. package/dist/runtime.js.map +1 -0
  18. package/dist/signals-core-BdfWh1Yt.d.ts +43 -0
  19. package/dist/vite.d.ts +5 -0
  20. package/dist/vite.js +2302 -0
  21. package/dist/vite.js.map +1 -0
  22. package/package.json +83 -65
  23. package/dist/chunk-D7SIC5TC.js +0 -367
  24. package/dist/chunk-D7SIC5TC.js.map +0 -1
  25. package/dist/entry-server.cjs +0 -120
  26. package/dist/entry-server.cjs.map +0 -1
  27. package/dist/entry-server.js +0 -115
  28. package/dist/entry-server.js.map +0 -1
  29. package/dist/entry-worker.cjs +0 -133
  30. package/dist/entry-worker.cjs.map +0 -1
  31. package/dist/entry-worker.js +0 -127
  32. package/dist/entry-worker.js.map +0 -1
  33. package/dist/main.cjs +0 -18
  34. package/dist/main.cjs.map +0 -1
  35. package/dist/main.js +0 -16
  36. package/dist/main.js.map +0 -1
  37. package/dist/olova.cjs +0 -1680
  38. package/dist/olova.cjs.map +0 -1
  39. package/dist/olova.d.cts +0 -72
  40. package/dist/olova.d.ts +0 -72
  41. package/dist/olova.js +0 -1321
  42. package/dist/olova.js.map +0 -1
  43. package/dist/performance.cjs +0 -386
  44. package/dist/performance.cjs.map +0 -1
  45. package/dist/performance.js +0 -3
  46. package/dist/performance.js.map +0 -1
  47. package/dist/router.cjs +0 -646
  48. package/dist/router.cjs.map +0 -1
  49. package/dist/router.d.cts +0 -113
  50. package/dist/router.d.ts +0 -113
  51. package/dist/router.js +0 -632
  52. package/dist/router.js.map +0 -1
  53. package/main.tsx +0 -76
  54. package/olova.ts +0 -619
  55. package/src/entry-server.tsx +0 -165
  56. package/src/entry-worker.tsx +0 -201
  57. package/src/generator/index.ts +0 -409
  58. package/src/hydration/flight.ts +0 -320
  59. package/src/hydration/index.ts +0 -12
  60. package/src/hydration/types.ts +0 -225
  61. package/src/logger.ts +0 -182
  62. package/src/main.tsx +0 -24
  63. package/src/performance.ts +0 -488
  64. package/src/plugin/index.ts +0 -204
  65. package/src/router/ErrorBoundary.tsx +0 -145
  66. package/src/router/Link.tsx +0 -117
  67. package/src/router/OlovaRouter.tsx +0 -354
  68. package/src/router/Outlet.tsx +0 -8
  69. package/src/router/context.ts +0 -117
  70. package/src/router/index.ts +0 -29
  71. package/src/router/matching.ts +0 -63
  72. package/src/router/router.tsx +0 -23
  73. package/src/router/search-params.ts +0 -29
  74. package/src/scanner/index.ts +0 -114
  75. package/src/types/index.ts +0 -190
  76. package/src/utils/export.ts +0 -85
  77. package/src/utils/index.ts +0 -4
  78. package/src/utils/naming.ts +0 -54
  79. package/src/utils/path.ts +0 -45
  80. package/tsup.config.ts +0 -35
package/src/logger.ts DELETED
@@ -1,182 +0,0 @@
1
- /**
2
- * Olova Logger - Modern, styled terminal output
3
- * Inspired by Next.js console output
4
- */
5
-
6
- import pc from 'picocolors';
7
-
8
- // Olova version from package.json
9
- const VERSION = '0.0.14';
10
-
11
- /**
12
- * Print the startup banner (minimal, like Next.js)
13
- */
14
- export function printBanner() {
15
- console.log('');
16
- console.log(pc.cyan(` ▲ Olova`) + pc.dim(` ${VERSION}`));
17
- console.log('');
18
- }
19
-
20
- /**
21
- * Print dev server ready message
22
- */
23
- export function printDevReady(url: string, networkUrl?: string) {
24
- console.log('');
25
- console.log(` ${pc.green('✓')} ${pc.bold('Ready')} in ${pc.cyan('~1s')}`);
26
- console.log('');
27
- console.log(` ${pc.dim('Local:')} ${pc.cyan(url)}`);
28
- if (networkUrl) {
29
- console.log(` ${pc.dim('Network:')} ${pc.cyan(networkUrl)}`);
30
- }
31
- console.log('');
32
- }
33
-
34
- /**
35
- * Print SSG build start (minimal)
36
- */
37
- export function printBuildStart() {
38
- console.log('');
39
- console.log(` ${pc.dim('Creating an optimized production build...')}`);
40
- console.log('');
41
- }
42
-
43
- /**
44
- * Print SSG build header
45
- */
46
- export function printSSGStart(buildId: string) {
47
- console.log('');
48
- console.log(pc.bold(pc.cyan(` ✓ Compiled successfully`)));
49
- console.log('');
50
- console.log(` ${pc.dim('Build ID:')} ${pc.yellow(buildId)}`);
51
- console.log('');
52
- }
53
-
54
- /**
55
- * Print route table (Next.js style)
56
- */
57
- export function printRoutes(routes: { path: string; type: 'static' | 'dynamic' }[]) {
58
- // Header
59
- console.log(` ${pc.bold('Route')}${' '.repeat(32)}${pc.bold('Type')}`);
60
- console.log(` ${pc.dim('┌')}${pc.dim('─'.repeat(43))}${pc.dim('┐')}`);
61
-
62
- for (const route of routes) {
63
- const icon = route.type === 'static' ? pc.green('○') : pc.magenta('λ');
64
- const typeLabel = route.type === 'static'
65
- ? pc.green('Static')
66
- : pc.magenta('Dynamic');
67
- const pathDisplay = route.path === '/' ? '/' : route.path;
68
- const padding = ' '.repeat(Math.max(1, 35 - pathDisplay.length));
69
- console.log(` ${pc.dim('│')} ${icon} ${pc.white(pathDisplay)}${padding}${typeLabel}`);
70
- }
71
-
72
- // Footer
73
- console.log(` ${pc.dim('└')}${pc.dim('─'.repeat(43))}${pc.dim('┘')}`);
74
- console.log('');
75
- }
76
-
77
- /**
78
- * Print single page generation
79
- */
80
- export function printPageGenerated(path: string, hasFlightData = true) {
81
- const badge = hasFlightData ? pc.dim(pc.cyan(' [Flight]')) : '';
82
- console.log(` ${pc.dim('✓')} ${pc.dim('Generated')} ${pc.white(path)}${badge}`);
83
- }
84
-
85
- /**
86
- * Print page generation error
87
- */
88
- export function printPageError(path: string, error: string) {
89
- console.log(` ${pc.red('✗')} ${pc.red('Failed')} ${path}`);
90
- console.log(` ${pc.dim(error)}`);
91
- }
92
-
93
- /**
94
- * Print SSG completion summary
95
- */
96
- export function printSSGComplete(stats: {
97
- totalPages: number;
98
- successPages: number;
99
- failedPages: number;
100
- buildTime: number;
101
- }) {
102
- console.log('');
103
- console.log(` ${pc.dim('─'.repeat(45))}`);
104
- console.log('');
105
-
106
- if (stats.failedPages > 0) {
107
- console.log(` ${pc.yellow('⚠')} ${pc.yellow('Build completed with warnings')}`);
108
- } else {
109
- console.log(` ${pc.green('✓')} ${pc.green('Build completed successfully')}`);
110
- }
111
-
112
- console.log('');
113
- console.log(` ${pc.dim('Pages:')} ${pc.bold(stats.successPages.toString())} generated`);
114
- if (stats.failedPages > 0) {
115
- console.log(` ${pc.red(stats.failedPages.toString())} failed`);
116
- }
117
- console.log(` ${pc.dim('Time:')} ${pc.cyan(stats.buildTime + 'ms')}`);
118
- console.log('');
119
- }
120
-
121
- /**
122
- * Print Flight hydration info (compact)
123
- */
124
- export function printFlightInfo() {
125
- console.log(` ${pc.cyan('○')} ${pc.dim('Flight hydration enabled')}`);
126
- console.log(` ${pc.dim('• JSON-LD structured data')}`);
127
- console.log(` ${pc.dim('• Resource hints')}`);
128
- console.log(` ${pc.dim('• $OLOVA global')}`);
129
- console.log('');
130
- }
131
-
132
- /**
133
- * Print a simple info message
134
- */
135
- export function info(message: string) {
136
- console.log(` ${pc.cyan('○')} ${message}`);
137
- }
138
-
139
- /**
140
- * Print a success message
141
- */
142
- export function success(message: string) {
143
- console.log(` ${pc.green('✓')} ${pc.green(message)}`);
144
- }
145
-
146
- /**
147
- * Print a warning message
148
- */
149
- export function warn(message: string) {
150
- console.log(` ${pc.yellow('⚠')} ${pc.yellow(message)}`);
151
- }
152
-
153
- /**
154
- * Print an error message
155
- */
156
- export function error(message: string) {
157
- console.log(` ${pc.red('✗')} ${pc.red(message)}`);
158
- }
159
-
160
- /**
161
- * Print SSR render info (dev mode)
162
- */
163
- export function printSSRRender(path: string) {
164
- console.log(` ${pc.cyan('→')} ${pc.dim('SSR')} ${path}`);
165
- }
166
-
167
- export default {
168
- printBanner,
169
- printDevReady,
170
- printBuildStart,
171
- printSSGStart,
172
- printRoutes,
173
- printPageGenerated,
174
- printPageError,
175
- printSSGComplete,
176
- printFlightInfo,
177
- printSSRRender,
178
- info,
179
- success,
180
- warn,
181
- error,
182
- };
package/src/main.tsx DELETED
@@ -1,24 +0,0 @@
1
- import { StrictMode } from 'react';
2
- import { hydrateRoot } from 'react-dom/client';
3
- // @ts-expect-error - Virtual module resolved by vite-plugin-olova
4
- import { layouts, notFoundPages, OlovaRouter, Outlet, routes } from 'virtual:olova-app';
5
-
6
- // Wrapper to support "children" prop style layouts (Next.js style)
7
- const wrappedLayouts = layouts.map((item: any) => ({
8
- ...item,
9
- layout: (props: any) => (
10
- <item.layout {...props}>
11
- <Outlet />
12
- </item.layout>
13
- )
14
- }));
15
-
16
- const isBrowser = typeof window !== 'undefined' && !(import.meta as any).env.SSR;
17
-
18
- if (isBrowser) {
19
- hydrateRoot(document, (
20
- <StrictMode>
21
- <OlovaRouter routes={routes} layouts={wrappedLayouts} notFoundPages={notFoundPages} />
22
- </StrictMode>
23
- ));
24
- }
@@ -1,488 +0,0 @@
1
- /**
2
- * Olova Performance Optimizations Plugin
3
- *
4
- * Provides build-time optimizations for SSG:
5
- * - Code splitting strategies
6
- * - Asset compression (gzip/brotli)
7
- * - Chunk size analysis
8
- * - Resource prioritization
9
- */
10
-
11
- import fs from 'node:fs/promises';
12
- import path from 'node:path';
13
- import { promisify } from 'node:util';
14
- import zlib from 'node:zlib';
15
- import type { Plugin, ResolvedConfig } from 'vite';
16
- import logger from './logger';
17
-
18
- // Promisify zlib functions
19
- const gzip = promisify(zlib.gzip);
20
- const brotliCompress = promisify(zlib.brotliCompress);
21
-
22
- // =============================================================================
23
- // TYPES
24
- // =============================================================================
25
-
26
- export interface PerformanceOptions {
27
- /**
28
- * Enable gzip compression for assets
29
- * @default true
30
- */
31
- gzip?: boolean;
32
-
33
- /**
34
- * Enable brotli compression for assets
35
- * @default true
36
- */
37
- brotli?: boolean;
38
-
39
- /**
40
- * Minimum file size (in bytes) to compress
41
- * @default 1024 (1KB)
42
- */
43
- threshold?: number;
44
-
45
- /**
46
- * File extensions to compress
47
- * @default ['js', 'css', 'html', 'json', 'svg', 'xml']
48
- */
49
- extensions?: string[];
50
-
51
- /**
52
- * Enable chunk size warnings
53
- * @default true
54
- */
55
- chunkSizeWarning?: boolean;
56
-
57
- /**
58
- * Maximum chunk size before warning (in KB)
59
- * @default 250
60
- */
61
- maxChunkSize?: number;
62
-
63
- /**
64
- * Enable route-based code splitting
65
- * @default true
66
- */
67
- routeCodeSplitting?: boolean;
68
- }
69
-
70
- export interface ChunkInfo {
71
- name: string;
72
- size: number;
73
- gzipSize?: number;
74
- brotliSize?: number;
75
- isEntry: boolean;
76
- type: 'vendor' | 'framework' | 'common' | 'route' | 'other';
77
- }
78
-
79
- // =============================================================================
80
- // MANUAL CHUNKS CONFIGURATION
81
- // =============================================================================
82
-
83
- /**
84
- * Smart manual chunks configuration for optimal code splitting
85
- * Separates vendor, framework, and route-specific code
86
- * Avoids circular dependencies by grouping related routes
87
- */
88
- export function createManualChunks() {
89
- // Track modules to detect potential circular dependencies
90
- const seenModules = new Map<string, string>();
91
-
92
- return (id: string, { getModuleInfo }: { getModuleInfo: (id: string) => { importers: readonly string[] } | null }): string | undefined => {
93
- // React and React DOM - cached separately
94
- if (id.includes('node_modules/react-dom')) {
95
- return 'vendor-react-dom';
96
- }
97
- if (id.includes('node_modules/react')) {
98
- return 'vendor-react';
99
- }
100
-
101
- // Olova Router - framework code
102
- if (id.includes('olova-router') || id.includes('olovastart/dist/router')) {
103
- return 'framework-router';
104
- }
105
-
106
- // Other node_modules - vendor bundle
107
- if (id.includes('node_modules')) {
108
- // Extract package name for better caching
109
- const match = id.match(/node_modules[\\/]([^/\\]+)/);
110
- if (match) {
111
- const pkg = match[1];
112
- // Group small packages together
113
- if (['scheduler', 'object-assign', 'prop-types'].includes(pkg)) {
114
- return 'vendor-react';
115
- }
116
- // Keep large packages separate
117
- if (['lodash', 'moment', 'axios', 'date-fns'].includes(pkg)) {
118
- return `vendor-${pkg}`;
119
- }
120
- }
121
- return 'vendor';
122
- }
123
-
124
- // Shared components - group together to avoid circular deps
125
- if (id.includes('/components/')) {
126
- return 'common-components';
127
- }
128
-
129
- // Shared utilities
130
- if (id.includes('/utils/') || id.includes('/lib/') || id.includes('/helpers/')) {
131
- return 'common-utils';
132
- }
133
-
134
- // CSS - let Vite handle
135
- if (id.endsWith('.css') || id.endsWith('.scss')) {
136
- return undefined;
137
- }
138
-
139
- // Auth pages + search - group together to avoid circular deps
140
- // (login <-> register <-> search often share components/navigation)
141
- if (id.includes('/(auth)/') || id.includes('\\(auth)\\') ||
142
- id.includes('/search/') || id.includes('\\search\\')) {
143
- return 'page-auth';
144
- }
145
-
146
- // Route components - be conservative, only split truly isolated routes
147
- // Don't split if the module has cross-route imports
148
- if (id.includes('/src/') && (id.endsWith('/index.tsx') || id.endsWith('/index.mdx'))) {
149
- // Check if this module would cause circular dependencies
150
- const moduleInfo = getModuleInfo(id);
151
- if (moduleInfo) {
152
- // If imported by another route, group with common
153
- for (const importer of moduleInfo.importers) {
154
- if (importer.includes('/src/') && importer.includes('/index.') && !importer.includes(id)) {
155
- return 'common-routes';
156
- }
157
- }
158
- }
159
-
160
- const routeMatch = id.match(/[\\/]src[\\/](.+?)[\\/]index\.(tsx|mdx)$/);
161
- if (routeMatch) {
162
- const routePath = routeMatch[1]
163
- .replace(/\([^)]+\)[\\/]/g, '') // Remove route groups
164
- .replace(/\[.*?\]/g, 'dynamic') // Replace dynamic segments
165
- .replace(/[\\/]/g, '-');
166
-
167
- // Skip if we've seen a chunk with same base that would cause circular
168
- const baseRoute = routePath.split('-')[0];
169
- if (seenModules.has(baseRoute)) {
170
- const existingChunk = seenModules.get(baseRoute)!;
171
- // If related routes, group them
172
- if (existingChunk.startsWith('page-')) {
173
- return existingChunk;
174
- }
175
- }
176
-
177
- const chunkName = `page-${routePath}`;
178
- seenModules.set(baseRoute, chunkName);
179
- return chunkName;
180
- }
181
- }
182
-
183
- return undefined;
184
- };
185
- }
186
-
187
- // =============================================================================
188
- // COMPRESSION PLUGIN
189
- // =============================================================================
190
-
191
- /**
192
- * Compression plugin for generating gzip and brotli compressed assets
193
- */
194
- export function compressionPlugin(options: PerformanceOptions = {}): Plugin {
195
- const {
196
- gzip: enableGzip = true,
197
- brotli: enableBrotli = true,
198
- threshold = 1024,
199
- extensions = ['js', 'css', 'html', 'json', 'svg', 'xml'],
200
- } = options;
201
-
202
- let config: ResolvedConfig;
203
-
204
- return {
205
- name: 'olova-compression',
206
- apply: 'build',
207
-
208
- configResolved(resolvedConfig) {
209
- config = resolvedConfig;
210
- },
211
-
212
- async closeBundle() {
213
- if (config.command !== 'build' || config.build.ssr) return;
214
-
215
- const outDir = config.build.outDir;
216
- const stats = { gzip: 0, brotli: 0, skipped: 0 };
217
-
218
- // Find all files to compress
219
- const files = await findFilesToCompress(outDir, extensions);
220
-
221
- for (const file of files) {
222
- const content = await fs.readFile(file);
223
-
224
- // Skip small files
225
- if (content.length < threshold) {
226
- stats.skipped++;
227
- continue;
228
- }
229
-
230
- // Gzip compression
231
- if (enableGzip) {
232
- try {
233
- const compressed = await gzip(content, { level: 9 });
234
- await fs.writeFile(`${file}.gz`, compressed);
235
- stats.gzip++;
236
- } catch (e) {
237
- // Silently skip compression errors
238
- }
239
- }
240
-
241
- // Brotli compression
242
- if (enableBrotli) {
243
- try {
244
- const compressed = await brotliCompress(content, {
245
- params: {
246
- [zlib.constants.BROTLI_PARAM_QUALITY]: 11,
247
- },
248
- });
249
- await fs.writeFile(`${file}.br`, compressed);
250
- stats.brotli++;
251
- } catch (e) {
252
- // Silently skip compression errors
253
- }
254
- }
255
- }
256
-
257
- // Log compression stats
258
- if (stats.gzip > 0 || stats.brotli > 0) {
259
- logger.info(`Compressed ${stats.gzip} files (gzip), ${stats.brotli} files (brotli)`);
260
- }
261
- },
262
- };
263
- }
264
-
265
- // =============================================================================
266
- // CHUNK ANALYSIS PLUGIN
267
- // =============================================================================
268
-
269
- /**
270
- * Chunk analysis plugin for monitoring bundle sizes
271
- */
272
- export function chunkAnalysisPlugin(options: PerformanceOptions = {}): Plugin {
273
- const {
274
- chunkSizeWarning = true,
275
- maxChunkSize = 250,
276
- } = options;
277
-
278
- const chunks: ChunkInfo[] = [];
279
-
280
- return {
281
- name: 'olova-chunk-analysis',
282
- apply: 'build',
283
-
284
- generateBundle(_options, bundle) {
285
- for (const [fileName, chunk] of Object.entries(bundle)) {
286
- if (chunk.type === 'chunk') {
287
- const sizeKB = Buffer.byteLength(chunk.code, 'utf8') / 1024;
288
-
289
- // Determine chunk type
290
- let type: ChunkInfo['type'] = 'other';
291
- if (fileName.includes('vendor')) type = 'vendor';
292
- else if (fileName.includes('framework')) type = 'framework';
293
- else if (fileName.includes('common')) type = 'common';
294
- else if (fileName.includes('page-')) type = 'route';
295
-
296
- chunks.push({
297
- name: fileName,
298
- size: Math.round(sizeKB * 100) / 100,
299
- isEntry: chunk.isEntry,
300
- type,
301
- });
302
-
303
- // Warn about large chunks
304
- if (chunkSizeWarning && sizeKB > maxChunkSize) {
305
- logger.warn(`Chunk "${fileName}" is ${sizeKB.toFixed(2)}KB (exceeds ${maxChunkSize}KB limit)`);
306
- }
307
- }
308
- }
309
- },
310
-
311
- closeBundle() {
312
- if (chunks.length === 0) return;
313
-
314
- // Sort by size descending
315
- chunks.sort((a, b) => b.size - a.size);
316
-
317
- // Print chunk summary
318
- console.log('');
319
- logger.info('Bundle Analysis:');
320
-
321
- const typeGroups: Record<string, number> = {};
322
- let totalSize = 0;
323
-
324
- for (const chunk of chunks) {
325
- typeGroups[chunk.type] = (typeGroups[chunk.type] || 0) + chunk.size;
326
- totalSize += chunk.size;
327
- }
328
-
329
- console.log(` Vendor: ${(typeGroups.vendor || 0).toFixed(2)} KB`);
330
- console.log(` Framework: ${(typeGroups.framework || 0).toFixed(2)} KB`);
331
- console.log(` Common: ${(typeGroups.common || 0).toFixed(2)} KB`);
332
- console.log(` Routes: ${(typeGroups.route || 0).toFixed(2)} KB`);
333
- console.log(` Other: ${(typeGroups.other || 0).toFixed(2)} KB`);
334
- console.log(` ─────────────────────`);
335
- console.log(` Total: ${totalSize.toFixed(2)} KB`);
336
- console.log('');
337
- },
338
- };
339
- }
340
-
341
- // =============================================================================
342
- // PRELOAD HINTS GENERATOR
343
- // =============================================================================
344
-
345
- export interface PreloadHint {
346
- href: string;
347
- as: 'script' | 'style' | 'font' | 'image';
348
- priority: 'critical' | 'high' | 'low';
349
- crossorigin?: boolean;
350
- }
351
-
352
- /**
353
- * Generate optimized preload hints for chunks
354
- */
355
- export function generatePreloadHints(
356
- chunks: string[],
357
- entryChunk: string
358
- ): PreloadHint[] {
359
- const hints: PreloadHint[] = [];
360
-
361
- // Entry chunk - critical priority
362
- hints.push({
363
- href: `/${entryChunk}`,
364
- as: 'script',
365
- priority: 'critical',
366
- });
367
-
368
- for (const chunk of chunks) {
369
- if (chunk === entryChunk) continue;
370
-
371
- // Vendor chunks - high priority (needed for hydration)
372
- if (chunk.includes('vendor')) {
373
- hints.push({
374
- href: `/${chunk}`,
375
- as: 'script',
376
- priority: 'high',
377
- });
378
- }
379
- // Framework chunks - high priority
380
- else if (chunk.includes('framework')) {
381
- hints.push({
382
- href: `/${chunk}`,
383
- as: 'script',
384
- priority: 'high',
385
- });
386
- }
387
- // Common chunks - high priority
388
- else if (chunk.includes('common')) {
389
- hints.push({
390
- href: `/${chunk}`,
391
- as: 'script',
392
- priority: 'high',
393
- });
394
- }
395
- // Route chunks - low priority (prefetch for navigation)
396
- else if (chunk.includes('page-')) {
397
- hints.push({
398
- href: `/${chunk}`,
399
- as: 'script',
400
- priority: 'low',
401
- });
402
- }
403
- }
404
-
405
- return hints;
406
- }
407
-
408
- /**
409
- * Generate HTML preload/prefetch tags from hints
410
- */
411
- export function generatePreloadTags(hints: PreloadHint[]): string {
412
- return hints
413
- .map((hint) => {
414
- const rel = hint.priority === 'low' ? 'prefetch' : 'modulepreload';
415
- const crossorigin = hint.crossorigin ? ' crossorigin' : '';
416
- return `<link rel="${rel}" href="${hint.href}"${crossorigin}>`;
417
- })
418
- .join('');
419
- }
420
-
421
- // =============================================================================
422
- // UTILITY FUNCTIONS
423
- // =============================================================================
424
-
425
- /**
426
- * Recursively find files to compress
427
- */
428
- async function findFilesToCompress(
429
- dir: string,
430
- extensions: string[]
431
- ): Promise<string[]> {
432
- const files: string[] = [];
433
-
434
- try {
435
- const entries = await fs.readdir(dir, { withFileTypes: true });
436
-
437
- for (const entry of entries) {
438
- const fullPath = path.join(dir, entry.name);
439
-
440
- if (entry.isDirectory()) {
441
- files.push(...await findFilesToCompress(fullPath, extensions));
442
- } else if (entry.isFile()) {
443
- const ext = path.extname(entry.name).slice(1);
444
- if (extensions.includes(ext)) {
445
- files.push(fullPath);
446
- }
447
- }
448
- }
449
- } catch (e) {
450
- // Directory doesn't exist or can't be read
451
- }
452
-
453
- return files;
454
- }
455
-
456
- /**
457
- * Calculate gzip size of content
458
- */
459
- export async function getGzipSize(content: string | Buffer): Promise<number> {
460
- const buffer = typeof content === 'string' ? Buffer.from(content) : content;
461
- const compressed = await gzip(buffer);
462
- return compressed.length;
463
- }
464
-
465
- /**
466
- * Format bytes to human readable size
467
- */
468
- export function formatBytes(bytes: number): string {
469
- if (bytes < 1024) return `${bytes} B`;
470
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
471
- return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
472
- }
473
-
474
- // =============================================================================
475
- // COMBINED PERFORMANCE PLUGIN
476
- // =============================================================================
477
-
478
- /**
479
- * Main performance plugin that combines all optimizations
480
- */
481
- export function olovaPerformance(options: PerformanceOptions = {}): Plugin[] {
482
- return [
483
- chunkAnalysisPlugin(options),
484
- compressionPlugin(options),
485
- ];
486
- }
487
-
488
- export default olovaPerformance;