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/olova.ts DELETED
@@ -1,619 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
4
- import { fileURLToPath, pathToFileURL } from "node:url";
5
- import { olovaRouter } from "./src/plugin";
6
- import { build, type Plugin, type ResolvedConfig } from "vite";
7
- import logger from "./src/logger";
8
- import type { PerformanceOptions } from "./src/performance";
9
- import {
10
- createManualChunks,
11
- generatePreloadHints,
12
- generatePreloadTags,
13
- olovaPerformance,
14
- } from "./src/performance";
15
-
16
- // =============================================================================
17
- // FLIGHT HYDRATION - Import from shared module (no duplication)
18
- // =============================================================================
19
-
20
- import {
21
- generateBuildId,
22
- generateJsonLd,
23
- generateOlovaHydration,
24
- generateResourceHints,
25
- type OlovaHydrationData,
26
- } from './src/hydration';
27
-
28
- // =============================================================================
29
- // PLUGIN INTERFACE
30
- // =============================================================================
31
-
32
- export interface OlovaOptions {
33
- /**
34
- * Static paths to pre-render at build time
35
- */
36
- staticPaths?: string[];
37
-
38
- /**
39
- * Performance optimization options
40
- */
41
- performance?: PerformanceOptions & {
42
- /**
43
- * Enable all performance optimizations
44
- * @default true
45
- */
46
- enabled?: boolean;
47
- };
48
-
49
- /**
50
- * Package name for router imports in generated route.tree.ts
51
- * @default 'olovastart'
52
- */
53
- packageName?: string;
54
- }
55
-
56
- export function olova(options: OlovaOptions = {}): any {
57
- const virtualModuleId = "virtual:olova-entry";
58
- const serverVirtualModuleId = "virtual:olova-server-entry";
59
- const workerVirtualModuleId = "virtual:olova-worker-entry";
60
- const appVirtualModuleId = "virtual:olova-app";
61
- const resolvedAppVirtualModuleId = "\0" + appVirtualModuleId;
62
- let config: ResolvedConfig;
63
-
64
- // Minimal shell - just the placeholder.
65
- // We'll prepend DOCTYPE manually to avoid duplication if app renders it (though app usually shouldn't).
66
- const htmlContent = `<!--app-html-->`;
67
-
68
- // Simple HTML minifier: removes whitespace between tags, trimming, and newlines
69
- const minifyHtml = (html: string) => {
70
- return html
71
- .replace(/>\s+</g, "><")
72
- .replace(/\s{2,}/g, " ")
73
- .replace(/<!--[\s\S]*?-->/g, "") // remove comments
74
- .trim();
75
- };
76
-
77
- return [
78
- olovaRouter({ packageName: options.packageName }),
79
- {
80
- name: "vite-plugin-olova",
81
- config(userConfig, { isSsrBuild }) {
82
- // All optimizations are enabled by default
83
- const perfEnabled = options.performance?.enabled !== false;
84
-
85
- // Skip performance optimizations for SSR builds
86
- if (isSsrBuild) {
87
- return {
88
- build: {
89
- rollupOptions: {
90
- input: userConfig.build?.rollupOptions?.input || virtualModuleId,
91
- output: {
92
- // Simple output for SSR
93
- entryFileNames: 'index.js',
94
- chunkFileNames: '[name].js',
95
- assetFileNames: '[name].[ext]',
96
- format: 'esm',
97
- manualChunks: undefined,
98
- },
99
- },
100
- },
101
- };
102
- }
103
-
104
- // Full optimization config for client builds (all automatic!)
105
- const buildConfig: any = {
106
- // Output directory for assets
107
- assetsDir: 'assets/_olova',
108
- // Report compressed size in build output
109
- reportCompressedSize: true,
110
- rollupOptions: {
111
- input: userConfig.build?.rollupOptions?.input || virtualModuleId,
112
- output: {
113
- // Optimized chunk naming for caching
114
- chunkFileNames: 'assets/_olova/[name]-[hash].js',
115
- entryFileNames: 'assets/_olova/[name]-[hash].js',
116
- assetFileNames: 'assets/_olova/[name]-[hash].[ext]',
117
- // Enable smart code splitting
118
- ...(perfEnabled && {
119
- manualChunks: createManualChunks(),
120
- }),
121
- },
122
- },
123
- // Increase warning limit since we're doing smart chunking
124
- chunkSizeWarningLimit: 500,
125
- // Inline small assets (< 4KB)
126
- assetsInlineLimit: 4096,
127
- // Enable terser minification for smaller bundles
128
- minify: 'terser',
129
- terserOptions: {
130
- compress: {
131
- // Aggressive optimizations for production
132
- drop_console: false, // Keep console for debugging
133
- drop_debugger: true,
134
- pure_funcs: ['console.debug'],
135
- passes: 2,
136
- },
137
- mangle: {
138
- properties: false,
139
- },
140
- format: {
141
- comments: false,
142
- },
143
- },
144
- // Disable source maps for smaller builds
145
- sourcemap: false,
146
- // CSS code splitting
147
- cssCodeSplit: true,
148
- // Target modern browsers (smaller bundles)
149
- target: 'es2020',
150
- };
151
-
152
- return {
153
- build: buildConfig,
154
- // Optimize deps for faster dev startup
155
- optimizeDeps: {
156
- include: ['react', 'react-dom'],
157
- exclude: ['olova'],
158
- },
159
- // SSR options
160
- ssr: {
161
- noExternal: ['olova'],
162
- },
163
- // esbuild optimizations
164
- esbuild: {
165
- treeShaking: true,
166
- legalComments: 'none',
167
- },
168
- // Preview server configuration (for testing production builds)
169
- preview: {
170
- headers: {
171
- // Long-term caching for static assets
172
- 'Cache-Control': 'public, max-age=31536000',
173
- },
174
- },
175
- };
176
- },
177
- async configResolved(resolvedConfig) {
178
- config = resolvedConfig;
179
- },
180
- async resolveId(id) {
181
- if (id === virtualModuleId || id === serverVirtualModuleId || id === workerVirtualModuleId) {
182
- const isServer = id === serverVirtualModuleId;
183
- const isWorker = id === workerVirtualModuleId;
184
- const fileName = isWorker ? "entry-worker" : (isServer ? "entry-server" : "main");
185
-
186
- // 1. Try to find local project override first
187
- const possibleLocalPaths = [
188
- path.resolve(config.root, `src/${fileName}.tsx`),
189
- path.resolve(config.root, `src/${fileName}.ts`),
190
- path.resolve(config.root, `${fileName}.tsx`),
191
- path.resolve(config.root, `plugins/${fileName}.tsx`),
192
- ];
193
-
194
- for (const p of possibleLocalPaths) {
195
- if (existsSync(p)) return p;
196
- }
197
-
198
- // 2. Try to resolve via standard module resolution (ensures deduplication)
199
- const exportName = isWorker ? 'olova/entry-worker' : (isServer ? 'olova/entry-server' : 'olova/main');
200
- try {
201
- const resolved = await this.resolve(exportName, undefined, { skipSelf: true });
202
- if (resolved && !resolved.external) {
203
- return resolved.id;
204
- }
205
- } catch (e) {
206
- // Resolution failed, continue to fallback
207
- }
208
-
209
- // 3. Fallback to package's own entry (bundled or source) - mostly for local dev of the plugin
210
- const pkgDir = path.dirname(fileURLToPath(import.meta.url));
211
- const possiblePkgPaths = [
212
- path.resolve(pkgDir, `dist/${fileName}.js`),
213
- path.resolve(pkgDir, `${fileName}.js`),
214
- path.resolve(pkgDir, `${fileName}.mjs`),
215
- path.resolve(pkgDir, `${fileName}.ts`),
216
- path.resolve(pkgDir, `${fileName}.tsx`),
217
- path.resolve(pkgDir, `src/${fileName}.tsx`),
218
- path.resolve(pkgDir, `src/${fileName}.ts`),
219
- // If running from dist, check parent directories
220
- path.resolve(pkgDir, '..', `dist/${fileName}.js`),
221
- path.resolve(pkgDir, '..', `${fileName}.js`),
222
- path.resolve(pkgDir, '..', `${fileName}.tsx`),
223
- path.resolve(pkgDir, '..', `src/${fileName}.tsx`),
224
- ];
225
-
226
- for (const p of possiblePkgPaths) {
227
- if (existsSync(p)) return p;
228
- }
229
- }
230
- if (id === appVirtualModuleId) {
231
- return resolvedAppVirtualModuleId;
232
- }
233
- return null;
234
- },
235
- load(id) {
236
- if (id === resolvedAppVirtualModuleId) {
237
- // Generate virtual module that re-exports from the user's route.tree
238
- return `
239
- export { OlovaRouter, Outlet, routes, layouts, notFoundPages } from '@/route.tree';
240
- import '@/index.css';
241
- `;
242
- }
243
- return null;
244
- },
245
- configureServer(server) {
246
- // Generate a dev build ID that stays consistent during the dev session
247
- const devBuildId = generateBuildId();
248
-
249
- // Print startup banner
250
- logger.printBanner();
251
-
252
- // Print dev server info once ready
253
-
254
-
255
- // Delay printing to let Vite finish its own output first if needed,
256
- // or print immediately if we want to override.
257
- // Since Vite 7 prints its own banner, we might want to just print our info.
258
- // But user complained about the "terminal looks bad", so let's try to be clean.
259
-
260
- server.middlewares.use(async (req, res, next) => {
261
- const url = req.url?.split("?")[0];
262
- if (
263
- url === "/" ||
264
- url === "/index.html" ||
265
- (req.headers.accept?.includes("text/html") &&
266
- !url?.match(/\.[a-z]+$/))
267
- ) {
268
- try {
269
- logger.printSSRRender(url || "/");
270
- let template = htmlContent;
271
-
272
- const { render } = await server.ssrLoadModule(
273
- serverVirtualModuleId,
274
- );
275
- const renderResult = await render(url || "/");
276
- // Handle both string return (legacy) and object return (new Flight format)
277
- const appHtml = typeof renderResult === 'string' ? renderResult : renderResult.html;
278
- const routeMetadata = typeof renderResult === 'string' ? {} : renderResult.metadata || {};
279
- const loaderData = typeof renderResult === 'string' ? undefined : renderResult.loaderData;
280
- const queryState = typeof renderResult === 'string' ? undefined : renderResult.queryState;
281
-
282
- let fullHtml = template.replace("<!--app-html-->", appHtml);
283
-
284
- // =================================================================
285
- // Flight Hydration for Dev Server
286
- // =================================================================
287
- const hydrationData: OlovaHydrationData = {
288
- route: url || "/",
289
- params: {},
290
- metadata: routeMetadata,
291
- chunks: [], // Dev mode doesn't have pre-built chunks
292
- isStatic: false,
293
- loaderData: loaderData,
294
- queryState: queryState,
295
- };
296
-
297
- // Generate Flight hydration scripts
298
- const flightScripts = generateOlovaHydration(hydrationData, devBuildId);
299
-
300
- // Generate JSON-LD for SEO
301
- const jsonLdScript = generateJsonLd(hydrationData);
302
-
303
- // Generate resource hints (minimal in dev)
304
- const resourceHints = `<link rel="dns-prefetch" href="//fonts.googleapis.com"><link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>`;
305
-
306
- // Inject JSON-LD and resource hints in head
307
- if (fullHtml.includes("</head>")) {
308
- fullHtml = fullHtml.replace("</head>", `${jsonLdScript}${resourceHints}</head>`);
309
- }
310
-
311
- // Inject Flight scripts and entry script for hydration
312
- if (fullHtml.includes("</body>")) {
313
- fullHtml = fullHtml.replace(
314
- "</body>",
315
- `${flightScripts}<script type="module" src="/@id/${virtualModuleId}"></script></body>`,
316
- );
317
- } else {
318
- fullHtml += `${flightScripts}<script type="module" src="/@id/${virtualModuleId}"></script>`;
319
- }
320
-
321
- // Now apply Vite transforms.
322
- fullHtml = await server.transformIndexHtml(url || "/", fullHtml);
323
-
324
- // Prepend DOCTYPE if missing
325
- if (!fullHtml.trim().toLowerCase().startsWith("<!doctype html>")) {
326
- fullHtml = `<!DOCTYPE html>${fullHtml}`;
327
- }
328
-
329
- res.statusCode = 200;
330
- res.setHeader("Content-Type", "text/html");
331
- res.end(minifyHtml(fullHtml));
332
- return;
333
- } catch (e) {
334
- server.ssrFixStacktrace(e as Error);
335
- console.error(e);
336
- res.statusCode = 500;
337
- res.end((e as Error).stack);
338
- return;
339
- }
340
- }
341
- next();
342
- });
343
- },
344
- async writeBundle(_options, bundle) {
345
- if (config.command === "serve" || config.build.ssr) return;
346
-
347
- const buildStartTime = Date.now();
348
- logger.printBuildStart();
349
- const outDir = config.build.outDir; // dist/client
350
- const serverOutDir = path.resolve(config.root, "dist/server"); // Always dist/server
351
-
352
- // Find client entry chunk
353
- const clientEntry = Object.values(bundle).find(
354
- (chunk) =>
355
- chunk.type === "chunk" &&
356
- chunk.isEntry &&
357
- (chunk.facadeModuleId?.includes("main") ||
358
- chunk.name === "main"),
359
- );
360
- const clientEntryFileName = clientEntry
361
- ? clientEntry.fileName
362
- : "assets/index.js";
363
-
364
- // Find CSS assets
365
- const cssAssets = Object.values(bundle).filter(
366
- (chunk) => chunk.type === "asset" && chunk.fileName.endsWith(".css"),
367
- );
368
- const cssLinks = cssAssets
369
- .map(
370
- (css) =>
371
- `<link rel="stylesheet" crossorigin href="/${css.fileName}">`,
372
- )
373
- .join("");
374
-
375
- // 1. Build Server Bundle
376
- // We use configFile to get MDX and other plugins, but override build settings for SSR
377
- await build({
378
- configFile: config.configFile,
379
- root: config.root,
380
- build: {
381
- ssr: true,
382
- emptyOutDir: false,
383
- outDir: serverOutDir,
384
- minify: false, // Keep readable for debugging
385
- rollupOptions: {
386
- input: { index: serverVirtualModuleId },
387
- output: {
388
- entryFileNames: "index.js",
389
- chunkFileNames: "[name].js",
390
- assetFileNames: "[name].[ext]",
391
- format: "esm",
392
- // Explicitly set to undefined to prevent client-side chunking config
393
- manualChunks: undefined,
394
- },
395
- },
396
- },
397
- logLevel: "error",
398
- });
399
-
400
- // 2. Load Server Entry to get routes
401
- const serverEntryPath = path.resolve(serverOutDir, "index.js");
402
- const serverEntryUrl = pathToFileURL(serverEntryPath).toString();
403
- // Initial import to get routes
404
- const { routes } = await import(serverEntryUrl);
405
-
406
- // 3. Generate Pages
407
- const paths = ["/"];
408
-
409
- function extractPaths(routesArr: any[]) {
410
- routesArr.forEach((r) => {
411
- if (r.path && !r.path.includes("*") && !r.path.includes(":")) {
412
- paths.push(r.path);
413
- }
414
- });
415
- }
416
- if (Array.isArray(routes)) extractPaths(routes);
417
-
418
- // 3b. Expand dynamic routes via getStaticPaths()
419
- if (Array.isArray(routes)) {
420
- for (const r of routes) {
421
- const isDynamic = r.path.includes(":") || r.path.includes("*");
422
- if (r.getStaticPaths && isDynamic) {
423
- try {
424
- const staticPaths = await r.getStaticPaths();
425
- if (Array.isArray(staticPaths)) {
426
- for (const entry of staticPaths) {
427
- // entry = { params: { id: '1' } } or { params: { slug: 'hello/world' } }
428
- let expandedPath = r.path;
429
- if (entry.params) {
430
- for (const [key, value] of Object.entries(entry.params)) {
431
- expandedPath = expandedPath.replace(`:${key}`, value as string);
432
- expandedPath = expandedPath.replace('*', value as string);
433
- }
434
- }
435
- paths.push(expandedPath);
436
- }
437
- logger.info(`getStaticPaths for ${r.path}: ${staticPaths.length} paths expanded`);
438
- }
439
- } catch (e) {
440
- logger.warn(`getStaticPaths() failed for ${r.path}: ${(e as Error).message}`);
441
- }
442
- }
443
- }
444
- }
445
-
446
- // Add user-provided static paths
447
- if (options.staticPaths) {
448
- options.staticPaths.forEach((p) => paths.push(p));
449
- }
450
-
451
- const uniquePaths = [...new Set(paths)];
452
-
453
- // Generate a unique build ID for this build
454
- const buildId = generateBuildId();
455
- logger.printSSGStart(buildId);
456
-
457
- // Print route table
458
- const routeInfo = uniquePaths.map(p => ({
459
- path: p,
460
- type: (p.includes(':') || p.includes('*') ? 'dynamic' : 'static') as 'static' | 'dynamic'
461
- }));
462
- logger.printRoutes(routeInfo);
463
-
464
- logger.printFlightInfo();
465
-
466
- // Collect all JS chunks for prefetching
467
- const jsChunks = Object.values(bundle)
468
- .filter((chunk) => chunk.type === "chunk" && chunk.fileName.endsWith(".js"))
469
- .map((chunk) => chunk.fileName);
470
-
471
- let successCount = 0;
472
- let failCount = 0;
473
-
474
- for (const p of uniquePaths) {
475
- try {
476
- // Update window location for the fresh import to pick up
477
- if (typeof (globalThis as any).window !== "undefined") {
478
- (globalThis as any).window.location.pathname = p;
479
- }
480
-
481
- // FORCE RE-IMPORT for every page to ensure clean state (fresh Router/History instance)
482
- const cacheBuster = `?t=${Date.now()}-${Math.random()}`;
483
- // We destructured render from the fresh module
484
- const { render } = await import(serverEntryUrl + cacheBuster);
485
-
486
- // Render returns { html, metadata, route, loaderData, queryState }
487
- const renderResult = await render(p);
488
- const appHtml = typeof renderResult === 'string' ? renderResult : renderResult.html;
489
- const routeMetadata = typeof renderResult === 'string' ? {} : renderResult.metadata || {};
490
- const loaderData = typeof renderResult === 'string' ? undefined : renderResult.loaderData;
491
- const queryState = typeof renderResult === 'string' ? undefined : renderResult.queryState;
492
-
493
- let html = htmlContent.replace("<!--app-html-->", appHtml);
494
-
495
- // Generate smart preload hints based on chunk priority
496
- const preloadHints = generatePreloadHints(jsChunks, clientEntryFileName);
497
- const preloadTags = generatePreloadTags(preloadHints);
498
-
499
- // Prepare hydration data for this page
500
- const hydrationData: OlovaHydrationData = {
501
- route: p,
502
- params: {},
503
- metadata: routeMetadata,
504
- chunks: jsChunks,
505
- isStatic: true,
506
- loaderData: loaderData,
507
- queryState: queryState,
508
- };
509
-
510
- // Generate Flight hydration scripts
511
- const flightScripts = generateOlovaHydration(hydrationData, buildId);
512
-
513
- // Generate JSON-LD for SEO
514
- const jsonLdScript = generateJsonLd(hydrationData);
515
-
516
- // Generate resource hints (DNS prefetch, preconnect)
517
- const resourceHints = generateResourceHints(hydrationData);
518
-
519
- // Inject JSON-LD, resource hints, CSS, and smart preload tags at end of head
520
- if (html.includes("</head>")) {
521
- html = html.replace("</head>", `${jsonLdScript}${resourceHints}${cssLinks}${preloadTags}</head>`);
522
- }
523
-
524
- // Inject Flight scripts and main script at end of body
525
- if (html.includes("</body>")) {
526
- html = html.replace(
527
- "</body>",
528
- `${flightScripts}<script type="module" src="/${clientEntryFileName}"></script></body>`,
529
- );
530
- } else {
531
- // Fallback if no body tag found
532
- html += `${flightScripts}<script type="module" src="/${clientEntryFileName}"></script>`;
533
- }
534
-
535
- // Prepend DOCTYPE if missing
536
- if (!html.trim().toLowerCase().startsWith("<!doctype html>")) {
537
- html = `<!DOCTYPE html>${html}`;
538
- }
539
-
540
- const itemPath =
541
- p === "/" ? "index.html" : `${p.substring(1)}/index.html`;
542
- const finalPath = path.resolve(outDir, itemPath);
543
- await fs.mkdir(path.dirname(finalPath), { recursive: true });
544
- await fs.writeFile(finalPath, minifyHtml(html));
545
- logger.printPageGenerated(itemPath, true);
546
- successCount++;
547
- } catch (e) {
548
- logger.printPageError(p, (e as Error).message);
549
- failCount++;
550
- }
551
- }
552
-
553
- // =====================================================================
554
- // Generate 404.html for CDN compatibility (Cloudflare, Vercel, Netlify)
555
- // =====================================================================
556
- try {
557
- const notFoundPath = '/__olova_404__';
558
- if (typeof (globalThis as any).window !== "undefined") {
559
- (globalThis as any).window.location.pathname = notFoundPath;
560
- }
561
- const cacheBuster404 = `?t=${Date.now()}-${Math.random()}`;
562
- const { render: render404 } = await import(serverEntryUrl + cacheBuster404);
563
- const result404 = await render404(notFoundPath);
564
- const html404Content = typeof result404 === 'string' ? result404 : result404.html;
565
-
566
- let html404 = htmlContent.replace("<!--app-html-->", html404Content);
567
-
568
- // Add basic CSS and entry script
569
- const preloadHints404 = generatePreloadHints(jsChunks, clientEntryFileName);
570
- const preloadTags404 = generatePreloadTags(preloadHints404);
571
-
572
- const hydrationData404: OlovaHydrationData = {
573
- route: '/404',
574
- params: {},
575
- metadata: { title: '404 - Page Not Found' },
576
- chunks: jsChunks,
577
- isStatic: true,
578
- };
579
-
580
- const flightScripts404 = generateOlovaHydration(hydrationData404, buildId);
581
- const jsonLd404 = generateJsonLd(hydrationData404);
582
- const resourceHints404 = generateResourceHints(hydrationData404);
583
-
584
- if (html404.includes("</head>")) {
585
- html404 = html404.replace("</head>", `${jsonLd404}${resourceHints404}${cssLinks}${preloadTags404}</head>`);
586
- }
587
- if (html404.includes("</body>")) {
588
- html404 = html404.replace("</body>", `${flightScripts404}<script type="module" src="/${clientEntryFileName}"></script></body>`);
589
- } else {
590
- html404 += `${flightScripts404}<script type="module" src="/${clientEntryFileName}"></script>`;
591
- }
592
- if (!html404.trim().toLowerCase().startsWith("<!doctype html>")) {
593
- html404 = `<!DOCTYPE html>${html404}`;
594
- }
595
-
596
- const notFoundFinalPath = path.resolve(outDir, '404.html');
597
- await fs.writeFile(notFoundFinalPath, minifyHtml(html404));
598
- logger.printPageGenerated('404.html', true);
599
- successCount++;
600
- } catch (e) {
601
- logger.warn(`Failed to generate 404.html: ${(e as Error).message}`);
602
- }
603
-
604
- // Cleanup server bundle (not needed for static serving)
605
- await fs.rm(serverOutDir, { recursive: true, force: true });
606
-
607
- const buildTime = Date.now() - buildStartTime;
608
- logger.printSSGComplete({
609
- totalPages: successCount,
610
- successPages: successCount,
611
- failedPages: failCount,
612
- buildTime
613
- });
614
- },
615
- } as Plugin,
616
- // Add performance optimization plugins
617
- ...(options.performance?.enabled !== false ? olovaPerformance(options.performance) : []),
618
- ];
619
- }