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.
- package/CHANGELOG.md +5 -0
- package/README.md +42 -61
- package/dist/compiler.d.ts +44 -0
- package/dist/compiler.js +2139 -0
- package/dist/compiler.js.map +1 -0
- package/dist/core.d.ts +4 -0
- package/dist/core.js +859 -0
- package/dist/core.js.map +1 -0
- package/dist/global.d.ts +15 -0
- package/dist/global.js +226 -0
- package/dist/global.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2302 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime.d.ts +89 -0
- package/dist/runtime.js +633 -0
- package/dist/runtime.js.map +1 -0
- package/dist/signals-core-BdfWh1Yt.d.ts +43 -0
- package/dist/vite.d.ts +5 -0
- package/dist/vite.js +2302 -0
- package/dist/vite.js.map +1 -0
- package/package.json +83 -65
- package/dist/chunk-D7SIC5TC.js +0 -367
- package/dist/chunk-D7SIC5TC.js.map +0 -1
- package/dist/entry-server.cjs +0 -120
- package/dist/entry-server.cjs.map +0 -1
- package/dist/entry-server.js +0 -115
- package/dist/entry-server.js.map +0 -1
- package/dist/entry-worker.cjs +0 -133
- package/dist/entry-worker.cjs.map +0 -1
- package/dist/entry-worker.js +0 -127
- package/dist/entry-worker.js.map +0 -1
- package/dist/main.cjs +0 -18
- package/dist/main.cjs.map +0 -1
- package/dist/main.js +0 -16
- package/dist/main.js.map +0 -1
- package/dist/olova.cjs +0 -1680
- package/dist/olova.cjs.map +0 -1
- package/dist/olova.d.cts +0 -72
- package/dist/olova.d.ts +0 -72
- package/dist/olova.js +0 -1321
- package/dist/olova.js.map +0 -1
- package/dist/performance.cjs +0 -386
- package/dist/performance.cjs.map +0 -1
- package/dist/performance.js +0 -3
- package/dist/performance.js.map +0 -1
- package/dist/router.cjs +0 -646
- package/dist/router.cjs.map +0 -1
- package/dist/router.d.cts +0 -113
- package/dist/router.d.ts +0 -113
- package/dist/router.js +0 -632
- package/dist/router.js.map +0 -1
- package/main.tsx +0 -76
- package/olova.ts +0 -619
- package/src/entry-server.tsx +0 -165
- package/src/entry-worker.tsx +0 -201
- package/src/generator/index.ts +0 -409
- package/src/hydration/flight.ts +0 -320
- package/src/hydration/index.ts +0 -12
- package/src/hydration/types.ts +0 -225
- package/src/logger.ts +0 -182
- package/src/main.tsx +0 -24
- package/src/performance.ts +0 -488
- package/src/plugin/index.ts +0 -204
- package/src/router/ErrorBoundary.tsx +0 -145
- package/src/router/Link.tsx +0 -117
- package/src/router/OlovaRouter.tsx +0 -354
- package/src/router/Outlet.tsx +0 -8
- package/src/router/context.ts +0 -117
- package/src/router/index.ts +0 -29
- package/src/router/matching.ts +0 -63
- package/src/router/router.tsx +0 -23
- package/src/router/search-params.ts +0 -29
- package/src/scanner/index.ts +0 -114
- package/src/types/index.ts +0 -190
- package/src/utils/export.ts +0 -85
- package/src/utils/index.ts +0 -4
- package/src/utils/naming.ts +0 -54
- package/src/utils/path.ts +0 -45
- 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
|
-
}
|
package/src/performance.ts
DELETED
|
@@ -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;
|