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/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
|
-
}
|