olovaplugin 1.0.4 → 1.0.6
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/README.md +33 -0
- package/dist/olova-plugins.d.mts +123 -0
- package/dist/olova-plugins.d.ts +123 -0
- package/dist/olova-plugins.js +1601 -0
- package/dist/olova-plugins.mjs +1540 -0
- package/package.json +24 -10
- package/dist/olovaplugin.d.ts +0 -1
- package/dist/olovaplugin.js +0 -23
|
@@ -0,0 +1,1601 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// olova-plugins.ts
|
|
31
|
+
var olova_plugins_exports = {};
|
|
32
|
+
__export(olova_plugins_exports, {
|
|
33
|
+
autoGeneratePlugin: () => autoGeneratePlugin,
|
|
34
|
+
cleanUrlPlugin: () => cleanUrlPlugin,
|
|
35
|
+
colors: () => colors3,
|
|
36
|
+
configPlugin: () => configPlugin,
|
|
37
|
+
createTimer: () => createTimer,
|
|
38
|
+
formatBytes: () => formatBytes2,
|
|
39
|
+
formatTime: () => formatTime2,
|
|
40
|
+
frameworkPlugin: () => frameworkPlugin,
|
|
41
|
+
generateBuildId: () => generateBuildId,
|
|
42
|
+
generateCriticalCSS: () => generateCriticalCSS,
|
|
43
|
+
generateJsonLd: () => generateJsonLd,
|
|
44
|
+
generateOlovaHydration: () => generateOlovaHydration,
|
|
45
|
+
generatePerformanceMeta: () => generatePerformanceMeta,
|
|
46
|
+
generateResourceHints: () => generateResourceHints,
|
|
47
|
+
generateServiceWorkerContent: () => generateServiceWorkerContent,
|
|
48
|
+
generateServiceWorkerScript: () => generateServiceWorkerScript,
|
|
49
|
+
logger: () => logger2,
|
|
50
|
+
minifyHtml: () => minifyHtml,
|
|
51
|
+
olovaPlugins: () => olovaPlugins,
|
|
52
|
+
parseFlightData: () => parseFlightData,
|
|
53
|
+
proactiveErrorPlugin: () => proactiveErrorPlugin,
|
|
54
|
+
routerPlugin: () => routerPlugin,
|
|
55
|
+
ssgPlugin: () => ssgPlugin,
|
|
56
|
+
symbols: () => symbols2,
|
|
57
|
+
virtualHtmlPlugin: () => virtualHtmlPlugin
|
|
58
|
+
});
|
|
59
|
+
module.exports = __toCommonJS(olova_plugins_exports);
|
|
60
|
+
|
|
61
|
+
// router/config.ts
|
|
62
|
+
var import_fs = __toESM(require("fs"));
|
|
63
|
+
var import_path = __toESM(require("path"));
|
|
64
|
+
var colors = {
|
|
65
|
+
reset: "\x1B[0m",
|
|
66
|
+
bold: "\x1B[1m",
|
|
67
|
+
dim: "\x1B[2m",
|
|
68
|
+
cyan: "\x1B[36m",
|
|
69
|
+
green: "\x1B[32m",
|
|
70
|
+
white: "\x1B[37m"
|
|
71
|
+
};
|
|
72
|
+
function configPlugin() {
|
|
73
|
+
const olovaDir = import_path.default.resolve(".olova");
|
|
74
|
+
return {
|
|
75
|
+
name: "olova-config",
|
|
76
|
+
config(_config, { command }) {
|
|
77
|
+
if (!import_fs.default.existsSync(olovaDir)) {
|
|
78
|
+
import_fs.default.mkdirSync(olovaDir, { recursive: true });
|
|
79
|
+
}
|
|
80
|
+
if (process.env.IS_SSG_BUILD) {
|
|
81
|
+
return {
|
|
82
|
+
appType: "custom",
|
|
83
|
+
cacheDir: "./.olova/cache"
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (command === "serve") {
|
|
87
|
+
console.log("");
|
|
88
|
+
console.log(` ${colors.bold}${colors.cyan}\u25B2 Olova${colors.reset} ${colors.dim}v1.0.0${colors.reset}`);
|
|
89
|
+
console.log("");
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
appType: "custom",
|
|
93
|
+
cacheDir: "./.olova/cache",
|
|
94
|
+
build: {
|
|
95
|
+
outDir: "./.olova/dist",
|
|
96
|
+
sourcemap: false,
|
|
97
|
+
chunkSizeWarningLimit: 500,
|
|
98
|
+
rollupOptions: {
|
|
99
|
+
onwarn(warning, warn) {
|
|
100
|
+
if (warning.code === "PLUGIN_WARNING" && warning.message?.includes("dynamic import will not move")) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (warning.message?.includes("Module level directives") && warning.message?.includes("static")) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (warning.message?.includes("sourcemap") && warning.message?.includes("Can't resolve original location")) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
warn(warning);
|
|
110
|
+
},
|
|
111
|
+
output: {
|
|
112
|
+
chunkFileNames: "pro_olova_static/chunks/[name]-[hash].js",
|
|
113
|
+
entryFileNames: "pro_olova_static/olova-[hash].js",
|
|
114
|
+
assetFileNames: "pro_olova_static/[name]-[hash][extname]",
|
|
115
|
+
manualChunks(id) {
|
|
116
|
+
if (id.includes("node_modules")) {
|
|
117
|
+
if (id.includes("react-dom")) {
|
|
118
|
+
return "vendor-react-dom";
|
|
119
|
+
}
|
|
120
|
+
if (id.includes("react")) {
|
|
121
|
+
return "vendor-react";
|
|
122
|
+
}
|
|
123
|
+
return "vendor";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
preview: {
|
|
130
|
+
port: 4173,
|
|
131
|
+
strictPort: false
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
configureServer(server) {
|
|
136
|
+
server.httpServer?.once("listening", () => {
|
|
137
|
+
const address = server.httpServer?.address();
|
|
138
|
+
const port = typeof address === "object" && address ? address.port : 5173;
|
|
139
|
+
setTimeout(() => {
|
|
140
|
+
console.log(` ${colors.green}\u2713${colors.reset} Ready in ${colors.dim}${Math.round(performance.now())}ms${colors.reset}`);
|
|
141
|
+
console.log("");
|
|
142
|
+
console.log(` ${colors.dim}\u279C${colors.reset} ${colors.bold}Local:${colors.reset} ${colors.cyan}http://localhost:${port}/${colors.reset}`);
|
|
143
|
+
console.log("");
|
|
144
|
+
}, 100);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// router/router.ts
|
|
151
|
+
var import_fs2 = __toESM(require("fs"));
|
|
152
|
+
var import_path2 = __toESM(require("path"));
|
|
153
|
+
function routerPlugin() {
|
|
154
|
+
const virtualModuleId = "olova/routes";
|
|
155
|
+
const resolvedVirtualModuleId = "\0" + virtualModuleId;
|
|
156
|
+
let server = null;
|
|
157
|
+
const invalidateRoutes = () => {
|
|
158
|
+
if (server) {
|
|
159
|
+
const mod = server.moduleGraph.getModuleById(resolvedVirtualModuleId);
|
|
160
|
+
if (mod) {
|
|
161
|
+
server.moduleGraph.invalidateModule(mod);
|
|
162
|
+
server.ws.send({
|
|
163
|
+
type: "full-reload",
|
|
164
|
+
path: "*"
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
return {
|
|
170
|
+
name: "olova-router",
|
|
171
|
+
// Configure dev server to watch for file changes
|
|
172
|
+
configureServer(devServer) {
|
|
173
|
+
server = devServer;
|
|
174
|
+
const srcDir = import_path2.default.resolve(__dirname, "..", "src");
|
|
175
|
+
const watcher = devServer.watcher;
|
|
176
|
+
watcher.on("add", (filePath) => {
|
|
177
|
+
if (filePath.startsWith(srcDir) && /\.(tsx|jsx|html|md)$/.test(filePath)) {
|
|
178
|
+
console.log("\x1B[36m[olova]\x1B[0m New route detected:", import_path2.default.relative(srcDir, filePath));
|
|
179
|
+
invalidateRoutes();
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
watcher.on("unlink", (filePath) => {
|
|
183
|
+
if (filePath.startsWith(srcDir) && /\.(tsx|jsx|html|md)$/.test(filePath)) {
|
|
184
|
+
console.log("\x1B[36m[olova]\x1B[0m Route removed:", import_path2.default.relative(srcDir, filePath));
|
|
185
|
+
invalidateRoutes();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
watcher.on("addDir", (dirPath) => {
|
|
189
|
+
if (dirPath.startsWith(srcDir) && dirPath !== srcDir) {
|
|
190
|
+
console.log("\x1B[36m[olova]\x1B[0m New route folder detected:", import_path2.default.relative(srcDir, dirPath));
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
watcher.on("unlinkDir", (dirPath) => {
|
|
194
|
+
if (dirPath.startsWith(srcDir) && dirPath !== srcDir) {
|
|
195
|
+
console.log("\x1B[36m[olova]\x1B[0m Route folder removed:", import_path2.default.relative(srcDir, dirPath));
|
|
196
|
+
invalidateRoutes();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
resolveId(id) {
|
|
201
|
+
if (id === virtualModuleId) {
|
|
202
|
+
return resolvedVirtualModuleId;
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
// Mark the virtual module as having side effects for proper HMR
|
|
206
|
+
handleHotUpdate({ file, server: devServer }) {
|
|
207
|
+
const srcDir = import_path2.default.resolve(__dirname, "..", "src");
|
|
208
|
+
if (file.startsWith(srcDir) && /\.(tsx|jsx)$/.test(file)) {
|
|
209
|
+
const mod = devServer.moduleGraph.getModuleById(resolvedVirtualModuleId);
|
|
210
|
+
if (mod) {
|
|
211
|
+
devServer.moduleGraph.invalidateModule(mod);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
load(id) {
|
|
216
|
+
if (id === resolvedVirtualModuleId) {
|
|
217
|
+
const srcDir = import_path2.default.resolve(__dirname, "..", "src");
|
|
218
|
+
const getRoutes = (dir, baseRoute = "", baseImportPath = "") => {
|
|
219
|
+
const entries = import_fs2.default.readdirSync(dir, { withFileTypes: true });
|
|
220
|
+
let routes = [];
|
|
221
|
+
for (const entry of entries) {
|
|
222
|
+
const entryName = entry.name;
|
|
223
|
+
const fullPath = import_path2.default.join(dir, entryName);
|
|
224
|
+
if (entry.isDirectory()) {
|
|
225
|
+
const isRouteGroup = /^\(.+\)$/.test(entryName);
|
|
226
|
+
if (isRouteGroup) {
|
|
227
|
+
routes = routes.concat(getRoutes(
|
|
228
|
+
fullPath,
|
|
229
|
+
baseRoute,
|
|
230
|
+
// URL path stays the same (group is ignored)
|
|
231
|
+
`${baseImportPath}/${entryName}`
|
|
232
|
+
// Import path includes the folder
|
|
233
|
+
));
|
|
234
|
+
} else {
|
|
235
|
+
routes = routes.concat(getRoutes(
|
|
236
|
+
fullPath,
|
|
237
|
+
`${baseRoute}/${entryName}`,
|
|
238
|
+
`${baseImportPath}/${entryName}`
|
|
239
|
+
));
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
const ext = import_path2.default.extname(entryName);
|
|
243
|
+
const supportedExts = [".tsx", ".jsx", ".html", ".md"];
|
|
244
|
+
if (supportedExts.includes(ext)) {
|
|
245
|
+
const nameNoExt = entryName.replace(/\.(tsx|jsx|html|md)$/, "");
|
|
246
|
+
let routePath = baseRoute;
|
|
247
|
+
if (nameNoExt !== "index" && nameNoExt !== "App") {
|
|
248
|
+
routePath = `${baseRoute}/${nameNoExt}`;
|
|
249
|
+
}
|
|
250
|
+
if (routePath === "") routePath = "/";
|
|
251
|
+
const normalizedRoutePath = routePath.replace(/\$(.+?)(?=\/|$)/g, ":$1");
|
|
252
|
+
const importPath = `/src${baseImportPath}/${entryName}`;
|
|
253
|
+
if (ext === ".html") {
|
|
254
|
+
routes.push(` "${normalizedRoutePath}": () => import("${importPath}?raw").then(m => ({ default: () => { const div = document.createElement('div'); div.innerHTML = m.default; return div.innerHTML; }, __isHtml: true, __isStatic: true, __rawHtml: m.default })),`);
|
|
255
|
+
} else if (ext === ".md") {
|
|
256
|
+
routes.push(` "${normalizedRoutePath}": () => import("${importPath}?raw").then(m => ({ default: m.default, __isMd: true, __isStatic: true })),`);
|
|
257
|
+
} else {
|
|
258
|
+
const fileContent = import_fs2.default.readFileSync(fullPath, "utf-8");
|
|
259
|
+
const firstLine = fileContent.trim().split("\n")[0].trim();
|
|
260
|
+
const hasStaticDirective = firstLine === '"static"' || firstLine === "'static'";
|
|
261
|
+
if (hasStaticDirective) {
|
|
262
|
+
routes.push(` "${normalizedRoutePath}": () => import("${importPath}").then(m => Object.assign({}, m, { __isStatic: true })),`);
|
|
263
|
+
} else {
|
|
264
|
+
routes.push(` "${normalizedRoutePath}": () => import("${importPath}"),`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return routes;
|
|
271
|
+
};
|
|
272
|
+
let routeLines = [];
|
|
273
|
+
if (import_fs2.default.existsSync(srcDir)) {
|
|
274
|
+
routeLines = getRoutes(srcDir);
|
|
275
|
+
}
|
|
276
|
+
return `export const routes = {
|
|
277
|
+
${routeLines.join("\n")}
|
|
278
|
+
};`;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// router/framework.ts
|
|
285
|
+
var import_vite = require("vite");
|
|
286
|
+
function frameworkPlugin() {
|
|
287
|
+
const virtualClientEntry = "olova/client";
|
|
288
|
+
const resolvedVirtualClientEntry = "\0" + virtualClientEntry;
|
|
289
|
+
const virtualServerEntry = "olova/server";
|
|
290
|
+
const resolvedVirtualServerEntry = "\0" + virtualServerEntry;
|
|
291
|
+
return {
|
|
292
|
+
name: "olova-framework",
|
|
293
|
+
resolveId(id) {
|
|
294
|
+
if (id === virtualClientEntry || id === "/olova/client" || id === "olova/client.tsx") return resolvedVirtualClientEntry;
|
|
295
|
+
if (id === virtualServerEntry || id === "/olova/server" || id === "olova/server.tsx") return resolvedVirtualServerEntry;
|
|
296
|
+
},
|
|
297
|
+
async load(id) {
|
|
298
|
+
if (id === resolvedVirtualClientEntry) {
|
|
299
|
+
const code = `
|
|
300
|
+
import React from 'react';
|
|
301
|
+
import { hydrateRoot, createRoot } from 'react-dom/client';
|
|
302
|
+
import Layout, { metadata as defaultMetadata } from '/src/root.tsx';
|
|
303
|
+
import { Router, loadRoute } from '/route.tsx';
|
|
304
|
+
|
|
305
|
+
// Helper to generate SEO meta tags
|
|
306
|
+
function generateSeoTags(metadata) {
|
|
307
|
+
const meta = { ...defaultMetadata, ...metadata };
|
|
308
|
+
const tags = [];
|
|
309
|
+
|
|
310
|
+
// Title
|
|
311
|
+
if (meta.title) {
|
|
312
|
+
document.title = meta.title;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Description
|
|
316
|
+
if (meta.description) {
|
|
317
|
+
let tag = document.querySelector('meta[name="description"]');
|
|
318
|
+
if (!tag) {
|
|
319
|
+
tag = document.createElement('meta');
|
|
320
|
+
tag.setAttribute('name', 'description');
|
|
321
|
+
document.head.appendChild(tag);
|
|
322
|
+
}
|
|
323
|
+
tag.setAttribute('content', meta.description);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Keywords
|
|
327
|
+
if (meta.keywords) {
|
|
328
|
+
const content = Array.isArray(meta.keywords) ? meta.keywords.join(', ') : meta.keywords;
|
|
329
|
+
let tag = document.querySelector('meta[name="keywords"]');
|
|
330
|
+
if (!tag) {
|
|
331
|
+
tag = document.createElement('meta');
|
|
332
|
+
tag.setAttribute('name', 'keywords');
|
|
333
|
+
document.head.appendChild(tag);
|
|
334
|
+
}
|
|
335
|
+
tag.setAttribute('content', content);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Open Graph
|
|
339
|
+
if (meta.openGraph) {
|
|
340
|
+
const og = meta.openGraph;
|
|
341
|
+
const ogTags = [
|
|
342
|
+
['og:title', og.title || meta.title],
|
|
343
|
+
['og:description', og.description],
|
|
344
|
+
['og:url', og.url],
|
|
345
|
+
['og:site_name', og.siteName],
|
|
346
|
+
['og:type', og.type],
|
|
347
|
+
];
|
|
348
|
+
ogTags.forEach(([prop, content]) => {
|
|
349
|
+
if (content) {
|
|
350
|
+
let tag = document.querySelector(\`meta[property="\${prop}"]\`);
|
|
351
|
+
if (!tag) {
|
|
352
|
+
tag = document.createElement('meta');
|
|
353
|
+
tag.setAttribute('property', prop);
|
|
354
|
+
document.head.appendChild(tag);
|
|
355
|
+
}
|
|
356
|
+
tag.setAttribute('content', content);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Twitter Card
|
|
362
|
+
if (meta.twitter) {
|
|
363
|
+
const tw = meta.twitter;
|
|
364
|
+
const twTags = [
|
|
365
|
+
['twitter:card', tw.card || 'summary'],
|
|
366
|
+
['twitter:site', tw.site],
|
|
367
|
+
['twitter:creator', tw.creator],
|
|
368
|
+
['twitter:title', tw.title || meta.title],
|
|
369
|
+
['twitter:description', tw.description],
|
|
370
|
+
];
|
|
371
|
+
twTags.forEach(([name, content]) => {
|
|
372
|
+
if (content) {
|
|
373
|
+
let tag = document.querySelector(\`meta[name="\${name}"]\`);
|
|
374
|
+
if (!tag) {
|
|
375
|
+
tag = document.createElement('meta');
|
|
376
|
+
tag.setAttribute('name', name);
|
|
377
|
+
document.head.appendChild(tag);
|
|
378
|
+
}
|
|
379
|
+
tag.setAttribute('content', content);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const path = window.location.pathname;
|
|
386
|
+
loadRoute(path).then((result) => {
|
|
387
|
+
const Component = result ? result.module.default : () => <div>404 Not Found</div>;
|
|
388
|
+
// @ts-ignore
|
|
389
|
+
const serverData = window.__OLOVA_DATA__ || {};
|
|
390
|
+
const params = serverData.params || (result ? result.params : {});
|
|
391
|
+
const metadata = serverData.metadata || (result ? result.metadata : undefined);
|
|
392
|
+
|
|
393
|
+
// Framework handles SEO automatically (like Next.js)
|
|
394
|
+
generateSeoTags(metadata);
|
|
395
|
+
|
|
396
|
+
const rootElement = document.getElementById('root');
|
|
397
|
+
const app = (
|
|
398
|
+
<React.StrictMode>
|
|
399
|
+
<Layout>
|
|
400
|
+
<Router url={path} initialComponent={Component} initialParams={params} onRouteChange={(newMetadata) => generateSeoTags(newMetadata)} />
|
|
401
|
+
</Layout>
|
|
402
|
+
</React.StrictMode>
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
if (rootElement && rootElement.innerHTML.trim() !== "") {
|
|
406
|
+
console.log("[Olova] Hydrating pre-rendered content");
|
|
407
|
+
hydrateRoot(document, app);
|
|
408
|
+
} else {
|
|
409
|
+
console.log("[Olova] Rendering client-side (no SSR content found)");
|
|
410
|
+
createRoot(document).render(app);
|
|
411
|
+
}
|
|
412
|
+
});`;
|
|
413
|
+
const result = await (0, import_vite.transformWithEsbuild)(code, "olova-client.tsx", {
|
|
414
|
+
loader: "tsx",
|
|
415
|
+
jsx: "automatic"
|
|
416
|
+
});
|
|
417
|
+
return result.code;
|
|
418
|
+
}
|
|
419
|
+
if (id === resolvedVirtualServerEntry) {
|
|
420
|
+
const code = `
|
|
421
|
+
import React from 'react';
|
|
422
|
+
import { renderToString } from 'react-dom/server';
|
|
423
|
+
import Layout, { metadata as defaultMetadata } from '/src/root.tsx';
|
|
424
|
+
import { Router, loadRoute } from '/route.tsx';
|
|
425
|
+
|
|
426
|
+
// Generate SEO head content
|
|
427
|
+
function generateSeoHead(metadata) {
|
|
428
|
+
const meta = { ...defaultMetadata, ...metadata };
|
|
429
|
+
let head = '';
|
|
430
|
+
|
|
431
|
+
// Title
|
|
432
|
+
if (meta.title) {
|
|
433
|
+
head += \`<title>\${meta.title}</title>\\n\`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Basic meta
|
|
437
|
+
if (meta.description) {
|
|
438
|
+
head += \`<meta name="description" content="\${meta.description}" />\\n\`;
|
|
439
|
+
}
|
|
440
|
+
if (meta.keywords) {
|
|
441
|
+
const content = Array.isArray(meta.keywords) ? meta.keywords.join(', ') : meta.keywords;
|
|
442
|
+
head += \`<meta name="keywords" content="\${content}" />\\n\`;
|
|
443
|
+
}
|
|
444
|
+
if (meta.robots) {
|
|
445
|
+
head += \`<meta name="robots" content="\${meta.robots}" />\\n\`;
|
|
446
|
+
}
|
|
447
|
+
if (meta.canonical) {
|
|
448
|
+
head += \`<link rel="canonical" href="\${meta.canonical}" />\\n\`;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Open Graph
|
|
452
|
+
if (meta.openGraph) {
|
|
453
|
+
const og = meta.openGraph;
|
|
454
|
+
head += \`<meta property="og:title" content="\${og.title || meta.title}" />\\n\`;
|
|
455
|
+
if (og.description) head += \`<meta property="og:description" content="\${og.description}" />\\n\`;
|
|
456
|
+
if (og.url) head += \`<meta property="og:url" content="\${og.url}" />\\n\`;
|
|
457
|
+
if (og.siteName) head += \`<meta property="og:site_name" content="\${og.siteName}" />\\n\`;
|
|
458
|
+
if (og.type) head += \`<meta property="og:type" content="\${og.type}" />\\n\`;
|
|
459
|
+
if (og.images) {
|
|
460
|
+
og.images.forEach(img => {
|
|
461
|
+
head += \`<meta property="og:image" content="\${img.url}" />\\n\`;
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Twitter Card
|
|
467
|
+
if (meta.twitter) {
|
|
468
|
+
const tw = meta.twitter;
|
|
469
|
+
head += \`<meta name="twitter:card" content="\${tw.card || 'summary'}" />\\n\`;
|
|
470
|
+
if (tw.site) head += \`<meta name="twitter:site" content="\${tw.site}" />\\n\`;
|
|
471
|
+
if (tw.creator) head += \`<meta name="twitter:creator" content="\${tw.creator}" />\\n\`;
|
|
472
|
+
head += \`<meta name="twitter:title" content="\${tw.title || meta.title}" />\\n\`;
|
|
473
|
+
if (tw.description) head += \`<meta name="twitter:description" content="\${tw.description}" />\\n\`;
|
|
474
|
+
if (tw.images) {
|
|
475
|
+
tw.images.forEach(img => {
|
|
476
|
+
head += \`<meta name="twitter:image" content="\${img}" />\\n\`;
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return head;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export async function render(url) {
|
|
485
|
+
const result = await loadRoute(url);
|
|
486
|
+
const Component = result ? result.module.default : () => <div>404 Not Found</div>;
|
|
487
|
+
const params = result ? result.params : {};
|
|
488
|
+
const metadata = result ? result.metadata : undefined;
|
|
489
|
+
|
|
490
|
+
// Generate the SEO head content
|
|
491
|
+
const seoHead = generateSeoHead(metadata);
|
|
492
|
+
|
|
493
|
+
let html = renderToString(
|
|
494
|
+
<Layout>
|
|
495
|
+
<Router url={url} initialComponent={Component} initialParams={params} />
|
|
496
|
+
</Layout>
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
// Inject SEO tags into head (framework handles this automatically)
|
|
500
|
+
if (html.includes('</head>')) {
|
|
501
|
+
html = html.replace('</head>', seoHead + '</head>');
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return { html, hydrationData: { params, metadata } };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Render empty shell from Layout (for client-only pages)
|
|
508
|
+
export function renderShell() {
|
|
509
|
+
const seoHead = generateSeoHead({});
|
|
510
|
+
let html = renderToString(<Layout>{null}</Layout>);
|
|
511
|
+
if (html.includes('</head>')) {
|
|
512
|
+
html = html.replace('</head>', seoHead + '</head>');
|
|
513
|
+
}
|
|
514
|
+
return html;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Render shell WITH metadata for client-only pages that have metadata exports
|
|
518
|
+
// This enables SEO pre-rendering even for pages without "static" directive
|
|
519
|
+
export function renderShellWithMetadata(metadata) {
|
|
520
|
+
const seoHead = generateSeoHead(metadata);
|
|
521
|
+
let html = renderToString(<Layout>{null}</Layout>);
|
|
522
|
+
if (html.includes('</head>')) {
|
|
523
|
+
html = html.replace('</head>', seoHead + '</head>');
|
|
524
|
+
}
|
|
525
|
+
return html;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Re-export loadRoute so it can be used during SSG to extract metadata from any route
|
|
529
|
+
export { loadRoute };`;
|
|
530
|
+
const result = await (0, import_vite.transformWithEsbuild)(code, "olova-server.tsx", {
|
|
531
|
+
loader: "tsx",
|
|
532
|
+
jsx: "automatic"
|
|
533
|
+
});
|
|
534
|
+
return result.code;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// router/virtual-html.ts
|
|
541
|
+
var import_fs3 = __toESM(require("fs"));
|
|
542
|
+
var import_path3 = __toESM(require("path"));
|
|
543
|
+
|
|
544
|
+
// router/hydration.ts
|
|
545
|
+
function generateOlovaHydration(data, buildId) {
|
|
546
|
+
const meta = data.metadata || {};
|
|
547
|
+
const scripts = [];
|
|
548
|
+
scripts.push(`<script>self.__olova_f=self.__olova_f||[];function _oF(d){self.__olova_f.push(d)}</script>`);
|
|
549
|
+
const routeChunk = {
|
|
550
|
+
type: "R",
|
|
551
|
+
data: {
|
|
552
|
+
path: data.route,
|
|
553
|
+
params: data.params || {},
|
|
554
|
+
pattern: data.route === "/" ? "/" : data.route.replace(/\/[^/]+$/, "/:slug"),
|
|
555
|
+
isStatic: data.isStatic ?? true,
|
|
556
|
+
buildId
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
scripts.push(`<script>_oF([0,"${routeChunk.type}",${JSON.stringify(routeChunk.data)}])</script>`);
|
|
560
|
+
const metadataChunk = {
|
|
561
|
+
type: "M",
|
|
562
|
+
data: {
|
|
563
|
+
title: meta.title || "Olova App",
|
|
564
|
+
description: meta.description || "",
|
|
565
|
+
keywords: Array.isArray(meta.keywords) ? meta.keywords : [],
|
|
566
|
+
robots: meta.robots || "index, follow",
|
|
567
|
+
canonical: meta.canonical || null,
|
|
568
|
+
og: {
|
|
569
|
+
type: "website",
|
|
570
|
+
locale: "en_US",
|
|
571
|
+
...meta.openGraph || {}
|
|
572
|
+
},
|
|
573
|
+
twitter: {
|
|
574
|
+
card: "summary_large_image",
|
|
575
|
+
...meta.twitter || {}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
scripts.push(`<script>_oF([1,"${metadataChunk.type}",${JSON.stringify(metadataChunk.data)}])</script>`);
|
|
580
|
+
const pageName = data.route === "/" ? "HomePage" : data.route.slice(1).split("/").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("") + "Page";
|
|
581
|
+
const treeChunk = {
|
|
582
|
+
type: "T",
|
|
583
|
+
data: {
|
|
584
|
+
layout: "RootLayout",
|
|
585
|
+
page: pageName,
|
|
586
|
+
template: null,
|
|
587
|
+
loading: null,
|
|
588
|
+
error: null,
|
|
589
|
+
notFound: null
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
scripts.push(`<script>_oF([2,"${treeChunk.type}",${JSON.stringify(treeChunk.data)}])</script>`);
|
|
593
|
+
const structuredData = {
|
|
594
|
+
type: "D",
|
|
595
|
+
data: [
|
|
596
|
+
{
|
|
597
|
+
"@context": "https://schema.org",
|
|
598
|
+
"@type": "WebPage",
|
|
599
|
+
name: meta.title || "Olova App",
|
|
600
|
+
description: meta.description || "",
|
|
601
|
+
url: data.route
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
"@context": "https://schema.org",
|
|
605
|
+
"@type": "WebApplication",
|
|
606
|
+
name: "Olova",
|
|
607
|
+
applicationCategory: "WebApplication",
|
|
608
|
+
operatingSystem: "Any"
|
|
609
|
+
}
|
|
610
|
+
]
|
|
611
|
+
};
|
|
612
|
+
scripts.push(`<script>_oF([3,"${structuredData.type}",${JSON.stringify(structuredData.data)}])</script>`);
|
|
613
|
+
const assetsChunk = {
|
|
614
|
+
type: "A",
|
|
615
|
+
data: {
|
|
616
|
+
chunks: data.chunks || [],
|
|
617
|
+
styles: [],
|
|
618
|
+
// Prefetch hints for next likely navigations
|
|
619
|
+
prefetch: (data.chunks || []).slice(0, 5)
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
scripts.push(`<script>_oF([4,"${assetsChunk.type}",${JSON.stringify(assetsChunk.data)}])</script>`);
|
|
623
|
+
const hintsChunk = {
|
|
624
|
+
type: "H",
|
|
625
|
+
data: {
|
|
626
|
+
dnsPrefetch: ["fonts.googleapis.com", "fonts.gstatic.com"],
|
|
627
|
+
preconnect: ["https://fonts.googleapis.com", "https://fonts.gstatic.com"],
|
|
628
|
+
modulePreload: (data.chunks || []).slice(0, 3)
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
scripts.push(`<script>_oF([5,"${hintsChunk.type}",${JSON.stringify(hintsChunk.data)}])</script>`);
|
|
632
|
+
const stateChunk = {
|
|
633
|
+
type: "S",
|
|
634
|
+
data: {
|
|
635
|
+
hydrated: false,
|
|
636
|
+
streaming: false,
|
|
637
|
+
ready: true,
|
|
638
|
+
timestamp: Date.now(),
|
|
639
|
+
version: "1.0.0",
|
|
640
|
+
buildId
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
scripts.push(`<script>_oF([6,"${stateChunk.type}",${JSON.stringify(stateChunk.data)}])</script>`);
|
|
644
|
+
scripts.push(`<script>_oF([7,"E",null])</script>`);
|
|
645
|
+
const globalPayload = {
|
|
646
|
+
$route: routeChunk.data,
|
|
647
|
+
$meta: metadataChunk.data,
|
|
648
|
+
$tree: treeChunk.data,
|
|
649
|
+
$schema: structuredData.data,
|
|
650
|
+
$assets: assetsChunk.data,
|
|
651
|
+
$hints: hintsChunk.data,
|
|
652
|
+
$state: stateChunk.data,
|
|
653
|
+
$build: {
|
|
654
|
+
id: buildId,
|
|
655
|
+
version: "1.0.0",
|
|
656
|
+
timestamp: Date.now(),
|
|
657
|
+
env: "production"
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
scripts.push(`<script>$OLOVA=${JSON.stringify(globalPayload)}</script>`);
|
|
661
|
+
return scripts.join("");
|
|
662
|
+
}
|
|
663
|
+
function generateJsonLd(data) {
|
|
664
|
+
const meta = data.metadata || {};
|
|
665
|
+
const jsonLd = {
|
|
666
|
+
"@context": "https://schema.org",
|
|
667
|
+
"@graph": [
|
|
668
|
+
{
|
|
669
|
+
"@type": "WebPage",
|
|
670
|
+
"@id": data.route,
|
|
671
|
+
name: meta.title || "Olova App",
|
|
672
|
+
description: meta.description || "",
|
|
673
|
+
url: data.route,
|
|
674
|
+
isPartOf: {
|
|
675
|
+
"@type": "WebSite",
|
|
676
|
+
name: "Olova App"
|
|
677
|
+
}
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
"@type": "BreadcrumbList",
|
|
681
|
+
itemListElement: data.route.split("/").filter(Boolean).map((segment, index, arr) => ({
|
|
682
|
+
"@type": "ListItem",
|
|
683
|
+
position: index + 1,
|
|
684
|
+
name: segment.charAt(0).toUpperCase() + segment.slice(1),
|
|
685
|
+
item: "/" + arr.slice(0, index + 1).join("/")
|
|
686
|
+
}))
|
|
687
|
+
}
|
|
688
|
+
]
|
|
689
|
+
};
|
|
690
|
+
return `<script type="application/ld+json">${JSON.stringify(jsonLd)}</script>`;
|
|
691
|
+
}
|
|
692
|
+
function parseFlightData() {
|
|
693
|
+
if (typeof globalThis === "undefined" || typeof globalThis.document === "undefined") {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
const flightArray = globalThis.__olova_f;
|
|
697
|
+
if (!flightArray) return null;
|
|
698
|
+
const result = {};
|
|
699
|
+
const typeMap = {
|
|
700
|
+
"M": "$meta",
|
|
701
|
+
"T": "$tree",
|
|
702
|
+
"R": "$route",
|
|
703
|
+
"P": "$params",
|
|
704
|
+
"A": "$assets",
|
|
705
|
+
"S": "$state",
|
|
706
|
+
"D": "$schema",
|
|
707
|
+
"H": "$hints",
|
|
708
|
+
"E": "$end"
|
|
709
|
+
};
|
|
710
|
+
for (const chunk of flightArray) {
|
|
711
|
+
if (Array.isArray(chunk) && chunk.length >= 3) {
|
|
712
|
+
const [_index, type, data] = chunk;
|
|
713
|
+
const key = typeMap[type];
|
|
714
|
+
if (key && key !== "$end") {
|
|
715
|
+
result[key] = data;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return result;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// router/virtual-html.ts
|
|
723
|
+
function virtualHtmlPlugin() {
|
|
724
|
+
const htmlTemplate = `<!DOCTYPE html>
|
|
725
|
+
<html lang="en">
|
|
726
|
+
<head>
|
|
727
|
+
<meta charset="UTF-8" />
|
|
728
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
729
|
+
<title>My Framework</title>
|
|
730
|
+
</head>
|
|
731
|
+
<body>
|
|
732
|
+
<div id="root"></div>
|
|
733
|
+
<script type="module">
|
|
734
|
+
import '/olova/client';
|
|
735
|
+
</script>
|
|
736
|
+
</body>
|
|
737
|
+
</html>`;
|
|
738
|
+
return {
|
|
739
|
+
name: "olova-virtual-html",
|
|
740
|
+
enforce: "pre",
|
|
741
|
+
// Handle virtual index.html resolution for build
|
|
742
|
+
resolveId(id) {
|
|
743
|
+
if (id === "index.html" || id === "virtual:index.html" || id === "olova.html") {
|
|
744
|
+
return "olova.html";
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
load(id) {
|
|
748
|
+
if (id === "olova.html" || id === "virtual:index.html") {
|
|
749
|
+
return htmlTemplate;
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
// Serve HTML for all routes in dev mode with SSR (like Next.js)
|
|
753
|
+
configureServer(server) {
|
|
754
|
+
const isStaticRoute = (routePath) => {
|
|
755
|
+
const srcDir = import_path3.default.resolve("src");
|
|
756
|
+
let filePath = routePath === "/" ? "index" : routePath.slice(1);
|
|
757
|
+
const staticPaths = [
|
|
758
|
+
import_path3.default.join(srcDir, filePath, "index.html"),
|
|
759
|
+
import_path3.default.join(srcDir, filePath, "index.md"),
|
|
760
|
+
import_path3.default.join(srcDir, filePath + ".html"),
|
|
761
|
+
import_path3.default.join(srcDir, filePath + ".md")
|
|
762
|
+
];
|
|
763
|
+
for (const p of staticPaths) {
|
|
764
|
+
if (import_fs3.default.existsSync(p)) {
|
|
765
|
+
return true;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const possiblePaths = [
|
|
769
|
+
import_path3.default.join(srcDir, filePath, "index.tsx"),
|
|
770
|
+
import_path3.default.join(srcDir, filePath, "index.jsx"),
|
|
771
|
+
import_path3.default.join(srcDir, filePath + ".tsx"),
|
|
772
|
+
import_path3.default.join(srcDir, filePath + ".jsx"),
|
|
773
|
+
import_path3.default.join(srcDir, "App.tsx"),
|
|
774
|
+
import_path3.default.join(srcDir, "App.jsx")
|
|
775
|
+
];
|
|
776
|
+
for (const p of possiblePaths) {
|
|
777
|
+
if (import_fs3.default.existsSync(p)) {
|
|
778
|
+
const content = import_fs3.default.readFileSync(p, "utf-8");
|
|
779
|
+
const firstLine = content.trim().split("\n")[0].trim();
|
|
780
|
+
return firstLine === '"static"' || firstLine === "'static'";
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return false;
|
|
784
|
+
};
|
|
785
|
+
return () => {
|
|
786
|
+
server.middlewares.use(async (req, res, next) => {
|
|
787
|
+
const url = req.url || "/";
|
|
788
|
+
const accept = req.headers.accept || "";
|
|
789
|
+
if (!accept.includes("text/html")) {
|
|
790
|
+
return next();
|
|
791
|
+
}
|
|
792
|
+
if (url.startsWith("/@") || url.startsWith("/__") || url.startsWith("/node_modules")) {
|
|
793
|
+
return next();
|
|
794
|
+
}
|
|
795
|
+
if (/\.\w+$/.test(url) && !url.endsWith(".html")) {
|
|
796
|
+
return next();
|
|
797
|
+
}
|
|
798
|
+
const routePath = url.split("?")[0];
|
|
799
|
+
const shouldSSR = isStaticRoute(routePath);
|
|
800
|
+
try {
|
|
801
|
+
if (shouldSSR) {
|
|
802
|
+
const { render } = await server.ssrLoadModule("olova/server");
|
|
803
|
+
const { html: ssrHtml, hydrationData } = await render(routePath);
|
|
804
|
+
let fullHtml = ssrHtml;
|
|
805
|
+
const hydrationScript = `<script>window.__OLOVA_DATA__ = ${JSON.stringify(hydrationData)};</script>`;
|
|
806
|
+
fullHtml = fullHtml.replace("</body>", `${hydrationScript}
|
|
807
|
+
</body>`);
|
|
808
|
+
const clientScript = `<script type="module" src="/olova/client"></script>`;
|
|
809
|
+
fullHtml = fullHtml.replace("</body>", `${clientScript}
|
|
810
|
+
</body>`);
|
|
811
|
+
const devBuildId = "dev-" + Date.now().toString(36);
|
|
812
|
+
const flightScripts = generateOlovaHydration({
|
|
813
|
+
route: routePath,
|
|
814
|
+
metadata: hydrationData.metadata,
|
|
815
|
+
params: hydrationData.params,
|
|
816
|
+
chunks: []
|
|
817
|
+
}, devBuildId);
|
|
818
|
+
fullHtml = fullHtml.replace("</body>", `${flightScripts}</body>`);
|
|
819
|
+
const jsonLdScript = generateJsonLd({ route: routePath, metadata: hydrationData.metadata });
|
|
820
|
+
fullHtml = fullHtml.replace("</head>", `${jsonLdScript}</head>`);
|
|
821
|
+
const html = await server.transformIndexHtml(url, fullHtml);
|
|
822
|
+
res.setHeader("Content-Type", "text/html");
|
|
823
|
+
res.statusCode = 200;
|
|
824
|
+
res.end(html);
|
|
825
|
+
} else {
|
|
826
|
+
const { renderShellWithMetadata } = await server.ssrLoadModule("olova/server");
|
|
827
|
+
const { loadRoute: clientLoadRoute } = await server.ssrLoadModule("/route.tsx");
|
|
828
|
+
let pageMetadata = {};
|
|
829
|
+
try {
|
|
830
|
+
const result = await clientLoadRoute(routePath);
|
|
831
|
+
if (result && result.metadata) {
|
|
832
|
+
pageMetadata = result.metadata;
|
|
833
|
+
}
|
|
834
|
+
} catch (e) {
|
|
835
|
+
}
|
|
836
|
+
let fullHtml = renderShellWithMetadata(pageMetadata);
|
|
837
|
+
const clientScript = `<script type="module" src="/olova/client"></script>`;
|
|
838
|
+
fullHtml = fullHtml.replace("</body>", `${clientScript}
|
|
839
|
+
</body>`);
|
|
840
|
+
const devBuildId = "dev-" + Date.now().toString(36);
|
|
841
|
+
const flightScripts = generateOlovaHydration({
|
|
842
|
+
route: routePath,
|
|
843
|
+
metadata: pageMetadata,
|
|
844
|
+
chunks: []
|
|
845
|
+
}, devBuildId);
|
|
846
|
+
fullHtml = fullHtml.replace("</body>", `${flightScripts}</body>`);
|
|
847
|
+
const jsonLdScript = generateJsonLd({ route: routePath, metadata: pageMetadata });
|
|
848
|
+
fullHtml = fullHtml.replace("</head>", `${jsonLdScript}</head>`);
|
|
849
|
+
const html = await server.transformIndexHtml(url, fullHtml);
|
|
850
|
+
res.setHeader("Content-Type", "text/html");
|
|
851
|
+
res.statusCode = 200;
|
|
852
|
+
res.end(html);
|
|
853
|
+
}
|
|
854
|
+
} catch (e) {
|
|
855
|
+
console.error("[Olova] Error:", e);
|
|
856
|
+
try {
|
|
857
|
+
const html = await server.transformIndexHtml(url, htmlTemplate);
|
|
858
|
+
res.setHeader("Content-Type", "text/html");
|
|
859
|
+
res.statusCode = 200;
|
|
860
|
+
res.end(html);
|
|
861
|
+
} catch (e2) {
|
|
862
|
+
next(e2);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// router/ssg.ts
|
|
872
|
+
var import_vite2 = require("vite");
|
|
873
|
+
var import_fs4 = __toESM(require("fs"));
|
|
874
|
+
var import_path4 = __toESM(require("path"));
|
|
875
|
+
var import_url = require("url");
|
|
876
|
+
|
|
877
|
+
// router/utils.ts
|
|
878
|
+
function minifyHtml(html) {
|
|
879
|
+
return html.replace(/<!--(?!\[if).*?-->/gs, "").replace(/\s+/g, " ").replace(/> </g, "><").replace(/\s*(<[^>]+>)\s*/g, "$1").trim();
|
|
880
|
+
}
|
|
881
|
+
function generateBuildId() {
|
|
882
|
+
return Math.random().toString(36).substring(2, 15) + Date.now().toString(36);
|
|
883
|
+
}
|
|
884
|
+
function generateResourceHints() {
|
|
885
|
+
return `
|
|
886
|
+
<link rel="dns-prefetch" href="//fonts.googleapis.com">
|
|
887
|
+
<link rel="dns-prefetch" href="//fonts.gstatic.com">
|
|
888
|
+
<link rel="dns-prefetch" href="//cdn.jsdelivr.net">
|
|
889
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
|
|
890
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
891
|
+
`.trim().replace(/\n\s+/g, "");
|
|
892
|
+
}
|
|
893
|
+
function generateCriticalCSS() {
|
|
894
|
+
return `<style id="olova-critical">
|
|
895
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
896
|
+
html{-webkit-text-size-adjust:100%;line-height:1.5}
|
|
897
|
+
body{font-family:system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}
|
|
898
|
+
img,video{max-width:100%;height:auto}
|
|
899
|
+
[hidden]{display:none!important}
|
|
900
|
+
</style>`.replace(/\n\s*/g, "");
|
|
901
|
+
}
|
|
902
|
+
function generateServiceWorkerScript() {
|
|
903
|
+
return `<script>if('serviceWorker'in navigator){window.addEventListener('load',()=>{navigator.serviceWorker.register('/sw.js').catch(()=>{})})}</script>`;
|
|
904
|
+
}
|
|
905
|
+
function generateServiceWorkerContent(buildId, assets) {
|
|
906
|
+
return `// Olova Service Worker v${buildId}
|
|
907
|
+
const CACHE_NAME = 'olova-cache-${buildId}';
|
|
908
|
+
const ASSETS = ${JSON.stringify(assets)};
|
|
909
|
+
|
|
910
|
+
self.addEventListener('install', (e) => {
|
|
911
|
+
e.waitUntil(caches.open(CACHE_NAME).then(cache => cache.addAll(ASSETS)));
|
|
912
|
+
self.skipWaiting();
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
self.addEventListener('activate', (e) => {
|
|
916
|
+
e.waitUntil(caches.keys().then(keys => Promise.all(
|
|
917
|
+
keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k))
|
|
918
|
+
)));
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
self.addEventListener('fetch', (e) => {
|
|
922
|
+
e.respondWith(caches.match(e.request).then(r => r || fetch(e.request)));
|
|
923
|
+
});
|
|
924
|
+
`;
|
|
925
|
+
}
|
|
926
|
+
function generatePerformanceMeta() {
|
|
927
|
+
return `<meta http-equiv="x-dns-prefetch-control" content="on"><meta name="color-scheme" content="light dark">`;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// router/ssg.ts
|
|
931
|
+
var colors2 = {
|
|
932
|
+
reset: "\x1B[0m",
|
|
933
|
+
bold: "\x1B[1m",
|
|
934
|
+
dim: "\x1B[2m",
|
|
935
|
+
green: "\x1B[32m",
|
|
936
|
+
cyan: "\x1B[36m",
|
|
937
|
+
yellow: "\x1B[33m",
|
|
938
|
+
magenta: "\x1B[35m",
|
|
939
|
+
blue: "\x1B[34m",
|
|
940
|
+
red: "\x1B[31m",
|
|
941
|
+
white: "\x1B[37m",
|
|
942
|
+
gray: "\x1B[90m",
|
|
943
|
+
bgGreen: "\x1B[42m",
|
|
944
|
+
bgCyan: "\x1B[46m",
|
|
945
|
+
bgMagenta: "\x1B[45m"
|
|
946
|
+
};
|
|
947
|
+
var symbols = {
|
|
948
|
+
success: "\u2713",
|
|
949
|
+
arrow: "\u2192",
|
|
950
|
+
bullet: "\u25CB",
|
|
951
|
+
filled: "\u25CF",
|
|
952
|
+
lambda: "\u03BB",
|
|
953
|
+
static: "\u25CB",
|
|
954
|
+
ssr: "\u25CF",
|
|
955
|
+
info: "\u2139"
|
|
956
|
+
};
|
|
957
|
+
function formatTime(ms) {
|
|
958
|
+
if (ms < 1) return "<1ms";
|
|
959
|
+
if (ms < 10) return `${ms.toFixed(1)}ms`;
|
|
960
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
961
|
+
if (ms < 6e4) return `${(ms / 1e3).toFixed(2)}s`;
|
|
962
|
+
const mins = Math.floor(ms / 6e4);
|
|
963
|
+
const secs = Math.round(ms % 6e4 / 1e3);
|
|
964
|
+
return `${mins}m ${secs}s`;
|
|
965
|
+
}
|
|
966
|
+
function formatBytes(bytes) {
|
|
967
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
968
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB`;
|
|
969
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
970
|
+
}
|
|
971
|
+
var logger = {
|
|
972
|
+
header: (text) => {
|
|
973
|
+
console.log(`
|
|
974
|
+
${colors2.bold}${colors2.white}${text}${colors2.reset}`);
|
|
975
|
+
},
|
|
976
|
+
info: (text) => {
|
|
977
|
+
console.log(`${colors2.cyan}${symbols.info}${colors2.reset} ${text}`);
|
|
978
|
+
},
|
|
979
|
+
success: (text) => {
|
|
980
|
+
console.log(`${colors2.green}${symbols.success}${colors2.reset} ${text}`);
|
|
981
|
+
},
|
|
982
|
+
route: (route, type, size, time) => {
|
|
983
|
+
const symbol = type === "static" ? symbols.static : symbols.ssr;
|
|
984
|
+
const color = type === "static" ? colors2.white : colors2.magenta;
|
|
985
|
+
const sizeStr = size ? ` ${colors2.dim}${formatBytes(size)}${colors2.reset}` : "";
|
|
986
|
+
const timeStr = time ? ` ${colors2.gray}(${formatTime(time)})${colors2.reset}` : "";
|
|
987
|
+
console.log(`${color}${symbol}${colors2.reset} ${route}${sizeStr}${timeStr}`);
|
|
988
|
+
},
|
|
989
|
+
step: (step, time) => {
|
|
990
|
+
const timeStr = time ? ` ${colors2.gray}${formatTime(time)}${colors2.reset}` : "";
|
|
991
|
+
console.log(` ${colors2.dim}${symbols.arrow}${colors2.reset} ${step}${timeStr}`);
|
|
992
|
+
},
|
|
993
|
+
buildComplete: (totalRoutes, staticRoutes, totalTime) => {
|
|
994
|
+
console.log("");
|
|
995
|
+
console.log(`${colors2.bold}${colors2.green}${symbols.success} Build completed!${colors2.reset}`);
|
|
996
|
+
console.log("");
|
|
997
|
+
console.log(` ${colors2.dim}Routes:${colors2.reset} ${totalRoutes} total`);
|
|
998
|
+
console.log(` ${colors2.dim}Static:${colors2.reset} ${staticRoutes} prerendered`);
|
|
999
|
+
console.log(` ${colors2.dim}Time:${colors2.reset} ${formatTime(totalTime)}`);
|
|
1000
|
+
console.log("");
|
|
1001
|
+
},
|
|
1002
|
+
legend: () => {
|
|
1003
|
+
console.log(`${colors2.dim}${symbols.static} Static${colors2.reset} ${colors2.dim}${symbols.ssr} SSR${colors2.reset} ${colors2.dim}\u03BB ISR${colors2.reset}`);
|
|
1004
|
+
},
|
|
1005
|
+
banner: (buildId) => {
|
|
1006
|
+
console.log("");
|
|
1007
|
+
console.log(`${colors2.bold}${colors2.cyan} \u25B2 Olova${colors2.reset} ${colors2.dim}Static Export${colors2.reset}`);
|
|
1008
|
+
console.log(`${colors2.dim} Build ID: ${buildId}${colors2.reset}`);
|
|
1009
|
+
console.log("");
|
|
1010
|
+
},
|
|
1011
|
+
error: (text) => {
|
|
1012
|
+
console.log(`${colors2.red}\u2717${colors2.reset} ${colors2.red}${text}${colors2.reset}`);
|
|
1013
|
+
},
|
|
1014
|
+
warn: (text) => {
|
|
1015
|
+
console.log(`${colors2.yellow}\u26A0${colors2.reset} ${colors2.yellow}${text}${colors2.reset}`);
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
function ssgPlugin() {
|
|
1019
|
+
return {
|
|
1020
|
+
name: "olova-ssg",
|
|
1021
|
+
apply: "build",
|
|
1022
|
+
config() {
|
|
1023
|
+
if (process.env.IS_SSG_BUILD) {
|
|
1024
|
+
return {};
|
|
1025
|
+
}
|
|
1026
|
+
return {
|
|
1027
|
+
build: {
|
|
1028
|
+
manifest: true,
|
|
1029
|
+
rollupOptions: {
|
|
1030
|
+
input: "index.html"
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
},
|
|
1035
|
+
async closeBundle() {
|
|
1036
|
+
if (process.env.IS_SSG_BUILD) return;
|
|
1037
|
+
process.env.IS_SSG_BUILD = "true";
|
|
1038
|
+
const totalStartTime = performance.now();
|
|
1039
|
+
const buildId = generateBuildId();
|
|
1040
|
+
logger.banner(buildId);
|
|
1041
|
+
logger.header("Generating static pages...");
|
|
1042
|
+
const serverBuildStart = performance.now();
|
|
1043
|
+
try {
|
|
1044
|
+
await (0, import_vite2.build)({
|
|
1045
|
+
configFile: "./vite.config.ts",
|
|
1046
|
+
build: {
|
|
1047
|
+
ssr: true,
|
|
1048
|
+
outDir: ".olova/server",
|
|
1049
|
+
rollupOptions: {
|
|
1050
|
+
input: "olova/server"
|
|
1051
|
+
},
|
|
1052
|
+
emptyOutDir: true,
|
|
1053
|
+
reportCompressedSize: false
|
|
1054
|
+
},
|
|
1055
|
+
logLevel: "silent"
|
|
1056
|
+
});
|
|
1057
|
+
logger.step("Server bundle compiled", performance.now() - serverBuildStart);
|
|
1058
|
+
} catch (e) {
|
|
1059
|
+
logger.error("SSG Build Failed");
|
|
1060
|
+
console.error(e);
|
|
1061
|
+
process.env.IS_SSG_BUILD = "";
|
|
1062
|
+
return;
|
|
1063
|
+
} finally {
|
|
1064
|
+
process.env.IS_SSG_BUILD = "";
|
|
1065
|
+
}
|
|
1066
|
+
const scanStart = performance.now();
|
|
1067
|
+
const allRoutes = [];
|
|
1068
|
+
const scan = (dir, base = "") => {
|
|
1069
|
+
if (!import_fs4.default.existsSync(dir)) return;
|
|
1070
|
+
const entries = import_fs4.default.readdirSync(dir, { withFileTypes: true });
|
|
1071
|
+
for (const entry of entries) {
|
|
1072
|
+
const fullPath = import_path4.default.join(dir, entry.name);
|
|
1073
|
+
if (entry.isDirectory()) {
|
|
1074
|
+
const isRouteGroup = /^\(.+\)$/.test(entry.name);
|
|
1075
|
+
if (isRouteGroup) {
|
|
1076
|
+
scan(fullPath, base);
|
|
1077
|
+
} else {
|
|
1078
|
+
scan(fullPath, `${base}/${entry.name}`);
|
|
1079
|
+
}
|
|
1080
|
+
} else {
|
|
1081
|
+
const ext = import_path4.default.extname(entry.name);
|
|
1082
|
+
const supportedExts = [".tsx", ".jsx", ".html", ".md"];
|
|
1083
|
+
if (supportedExts.includes(ext)) {
|
|
1084
|
+
const nameNoExt = entry.name.replace(/\.(tsx|jsx|html|md)$/, "");
|
|
1085
|
+
let route = base;
|
|
1086
|
+
if (nameNoExt !== "index" && nameNoExt !== "App") {
|
|
1087
|
+
route = `${base}/${nameNoExt}`;
|
|
1088
|
+
}
|
|
1089
|
+
if (route === "") route = "/";
|
|
1090
|
+
if (route.includes("$")) continue;
|
|
1091
|
+
if (ext === ".html" || ext === ".md") {
|
|
1092
|
+
allRoutes.push({ route, isStatic: true, filePath: fullPath });
|
|
1093
|
+
} else {
|
|
1094
|
+
const fileContent = import_fs4.default.readFileSync(fullPath, "utf-8");
|
|
1095
|
+
const firstLine = fileContent.trim().split("\n")[0].trim();
|
|
1096
|
+
const isStatic = firstLine === '"static"' || firstLine === "'static'";
|
|
1097
|
+
allRoutes.push({ route, isStatic, filePath: fullPath });
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
scan(import_path4.default.resolve("src"));
|
|
1104
|
+
logger.step(`Found ${allRoutes.length} routes`, performance.now() - scanStart);
|
|
1105
|
+
const serverDir = import_path4.default.resolve(".olova/server");
|
|
1106
|
+
let serverFile = import_fs4.default.readdirSync(serverDir).find(
|
|
1107
|
+
(f) => (f === "server.js" || f.includes("server-entry")) && f.endsWith(".js")
|
|
1108
|
+
);
|
|
1109
|
+
if (!serverFile) {
|
|
1110
|
+
logger.error("Could not find server build artifact");
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
const serverEntryPath = import_path4.default.join(serverDir, serverFile);
|
|
1114
|
+
const { render, renderShell, renderShellWithMetadata, loadRoute } = await import((0, import_url.pathToFileURL)(serverEntryPath).href);
|
|
1115
|
+
const assetsStart = performance.now();
|
|
1116
|
+
const distDir = import_path4.default.resolve(".olova/dist");
|
|
1117
|
+
const clientHtmlPath = import_path4.default.join(distDir, "olova.html");
|
|
1118
|
+
let shellHtml = renderShell();
|
|
1119
|
+
if (!shellHtml.startsWith("<!DOCTYPE html>")) {
|
|
1120
|
+
shellHtml = `<!DOCTYPE html>
|
|
1121
|
+
${shellHtml}`;
|
|
1122
|
+
}
|
|
1123
|
+
let template = "";
|
|
1124
|
+
if (import_fs4.default.existsSync(clientHtmlPath)) {
|
|
1125
|
+
template = import_fs4.default.readFileSync(clientHtmlPath, "utf-8");
|
|
1126
|
+
}
|
|
1127
|
+
const scripts = template.match(/<script[\s\S]*?>[\s\S]*?<\/script>/gi) || [];
|
|
1128
|
+
const links = template.match(/<link[\s\S]*?>/gi) || [];
|
|
1129
|
+
const chunksDir = import_path4.default.join(distDir, "pro_olova_static", "chunks");
|
|
1130
|
+
let preloadLinks = [];
|
|
1131
|
+
if (import_fs4.default.existsSync(chunksDir)) {
|
|
1132
|
+
const chunks = import_fs4.default.readdirSync(chunksDir).filter((f) => f.endsWith(".js"));
|
|
1133
|
+
preloadLinks = chunks.map(
|
|
1134
|
+
(chunk) => `<link rel="modulepreload" crossorigin href="/pro_olova_static/chunks/${chunk}" />`
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
const mainScriptMatch = template.match(/src="(\/pro_olova_static\/olova-[^"]+\.js)"/);
|
|
1138
|
+
if (mainScriptMatch) {
|
|
1139
|
+
preloadLinks.unshift(`<link rel="modulepreload" crossorigin href="${mainScriptMatch[1]}" />`);
|
|
1140
|
+
}
|
|
1141
|
+
const chunkList = [];
|
|
1142
|
+
if (import_fs4.default.existsSync(chunksDir)) {
|
|
1143
|
+
const chunks = import_fs4.default.readdirSync(chunksDir).filter((f) => f.endsWith(".js"));
|
|
1144
|
+
chunks.forEach((chunk) => chunkList.push(`/pro_olova_static/chunks/${chunk}`));
|
|
1145
|
+
}
|
|
1146
|
+
const resourceHints = generateResourceHints();
|
|
1147
|
+
const criticalCSS = generateCriticalCSS();
|
|
1148
|
+
const performanceMeta = generatePerformanceMeta();
|
|
1149
|
+
const swScript = generateServiceWorkerScript();
|
|
1150
|
+
const performanceHead = [performanceMeta, resourceHints, criticalCSS].join("");
|
|
1151
|
+
const allAssetsForSW = [
|
|
1152
|
+
"/",
|
|
1153
|
+
"/index.html",
|
|
1154
|
+
...chunkList,
|
|
1155
|
+
...links.map((l) => l.match(/href="([^"]+)"/)?.[1]).filter(Boolean)
|
|
1156
|
+
];
|
|
1157
|
+
const swContent = generateServiceWorkerContent(buildId, allAssetsForSW);
|
|
1158
|
+
import_fs4.default.writeFileSync(import_path4.default.join(distDir, "sw.js"), swContent);
|
|
1159
|
+
const assets = [performanceHead, ...links, ...preloadLinks, ...scripts, swScript].join("\n");
|
|
1160
|
+
logger.step(`Assets optimized (${preloadLinks.length} preloads, SW ready)`, performance.now() - assetsStart);
|
|
1161
|
+
console.log("");
|
|
1162
|
+
logger.header("Route");
|
|
1163
|
+
logger.legend();
|
|
1164
|
+
console.log("");
|
|
1165
|
+
let staticCount = 0;
|
|
1166
|
+
const routeResults = [];
|
|
1167
|
+
for (const { route, isStatic } of allRoutes) {
|
|
1168
|
+
const routeStart = performance.now();
|
|
1169
|
+
let finalHtml = "";
|
|
1170
|
+
if (isStatic) {
|
|
1171
|
+
staticCount++;
|
|
1172
|
+
const { html, hydrationData } = await render(route);
|
|
1173
|
+
finalHtml = html;
|
|
1174
|
+
if (Object.keys(hydrationData.params || {}).length > 0) {
|
|
1175
|
+
const scriptTag = `<script>window.__OLOVA_DATA__ = ${JSON.stringify(hydrationData)};</script>`;
|
|
1176
|
+
if (finalHtml.includes("</body>")) {
|
|
1177
|
+
finalHtml = finalHtml.replace("</body>", `${scriptTag}
|
|
1178
|
+
</body>`);
|
|
1179
|
+
} else {
|
|
1180
|
+
finalHtml += `
|
|
1181
|
+
${scriptTag}`;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
const jsonLdScript = generateJsonLd({ route, metadata: hydrationData.metadata });
|
|
1185
|
+
if (finalHtml.includes("</head>")) {
|
|
1186
|
+
finalHtml = finalHtml.replace("</head>", `${jsonLdScript}${assets}
|
|
1187
|
+
</head>`);
|
|
1188
|
+
} else {
|
|
1189
|
+
finalHtml = finalHtml.replace("</body>", `${assets}
|
|
1190
|
+
</body>`);
|
|
1191
|
+
}
|
|
1192
|
+
if (!finalHtml.startsWith("<!DOCTYPE html>")) {
|
|
1193
|
+
finalHtml = `<!DOCTYPE html>
|
|
1194
|
+
${finalHtml}`;
|
|
1195
|
+
}
|
|
1196
|
+
const flightScripts = generateOlovaHydration({
|
|
1197
|
+
route,
|
|
1198
|
+
metadata: hydrationData.metadata,
|
|
1199
|
+
params: hydrationData.params,
|
|
1200
|
+
chunks: chunkList,
|
|
1201
|
+
isStatic: true
|
|
1202
|
+
}, buildId);
|
|
1203
|
+
if (finalHtml.includes("</body>")) {
|
|
1204
|
+
finalHtml = finalHtml.replace("</body>", `${flightScripts}</body>`);
|
|
1205
|
+
}
|
|
1206
|
+
finalHtml = minifyHtml(finalHtml);
|
|
1207
|
+
} else {
|
|
1208
|
+
let pageMetadata = {};
|
|
1209
|
+
try {
|
|
1210
|
+
const routeResult = await loadRoute(route);
|
|
1211
|
+
if (routeResult && routeResult.metadata) {
|
|
1212
|
+
pageMetadata = routeResult.metadata;
|
|
1213
|
+
}
|
|
1214
|
+
} catch (e) {
|
|
1215
|
+
}
|
|
1216
|
+
finalHtml = renderShellWithMetadata(pageMetadata);
|
|
1217
|
+
if (!finalHtml.startsWith("<!DOCTYPE html>")) {
|
|
1218
|
+
finalHtml = `<!DOCTYPE html>
|
|
1219
|
+
${finalHtml}`;
|
|
1220
|
+
}
|
|
1221
|
+
const jsonLdScript = generateJsonLd({ route, metadata: pageMetadata });
|
|
1222
|
+
if (finalHtml.includes("</head>")) {
|
|
1223
|
+
finalHtml = finalHtml.replace("</head>", `${jsonLdScript}${assets}
|
|
1224
|
+
</head>`);
|
|
1225
|
+
} else {
|
|
1226
|
+
finalHtml = finalHtml.replace("</body>", `${assets}
|
|
1227
|
+
</body>`);
|
|
1228
|
+
}
|
|
1229
|
+
const flightScripts = generateOlovaHydration({
|
|
1230
|
+
route,
|
|
1231
|
+
metadata: pageMetadata,
|
|
1232
|
+
chunks: chunkList,
|
|
1233
|
+
isStatic: false
|
|
1234
|
+
}, buildId);
|
|
1235
|
+
if (finalHtml.includes("</body>")) {
|
|
1236
|
+
finalHtml = finalHtml.replace("</body>", `${flightScripts}</body>`);
|
|
1237
|
+
}
|
|
1238
|
+
finalHtml = minifyHtml(finalHtml);
|
|
1239
|
+
}
|
|
1240
|
+
const outPath = import_path4.default.join(distDir, route === "/" ? "index.html" : `${route}/index.html`);
|
|
1241
|
+
import_fs4.default.mkdirSync(import_path4.default.dirname(outPath), { recursive: true });
|
|
1242
|
+
import_fs4.default.writeFileSync(outPath, finalHtml);
|
|
1243
|
+
const routeTime = performance.now() - routeStart;
|
|
1244
|
+
const routeSize = Buffer.byteLength(finalHtml, "utf8");
|
|
1245
|
+
routeResults.push({ route, type: isStatic ? "static" : "ssr", size: routeSize, time: routeTime });
|
|
1246
|
+
logger.route(route, isStatic ? "static" : "ssr", routeSize, routeTime);
|
|
1247
|
+
}
|
|
1248
|
+
const fallbackPath = import_path4.default.join(distDir, "404.html");
|
|
1249
|
+
if (import_fs4.default.existsSync(clientHtmlPath)) {
|
|
1250
|
+
import_fs4.default.copyFileSync(clientHtmlPath, fallbackPath);
|
|
1251
|
+
}
|
|
1252
|
+
const totalTime = performance.now() - totalStartTime;
|
|
1253
|
+
logger.buildComplete(allRoutes.length, staticCount, totalTime);
|
|
1254
|
+
console.log(`${colors2.dim} Output:${colors2.reset} .olova/dist`);
|
|
1255
|
+
console.log("");
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// router/clean-url.ts
|
|
1261
|
+
var import_fs5 = __toESM(require("fs"));
|
|
1262
|
+
var import_path5 = __toESM(require("path"));
|
|
1263
|
+
function cleanUrlPlugin() {
|
|
1264
|
+
return {
|
|
1265
|
+
name: "olova-clean-url",
|
|
1266
|
+
configurePreviewServer(server) {
|
|
1267
|
+
server.middlewares.use((req, res, next) => {
|
|
1268
|
+
const urlPath = (req.url || "/").split("?")[0];
|
|
1269
|
+
const distDir = import_path5.default.resolve(".olova/dist");
|
|
1270
|
+
if (urlPath === "/") {
|
|
1271
|
+
const indexPath2 = import_path5.default.join(distDir, "index.html");
|
|
1272
|
+
if (import_fs5.default.existsSync(indexPath2)) {
|
|
1273
|
+
const content = import_fs5.default.readFileSync(indexPath2, "utf-8");
|
|
1274
|
+
res.setHeader("Content-Type", "text/html");
|
|
1275
|
+
res.end(content);
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
if (urlPath.includes(".")) return next();
|
|
1280
|
+
const indexPath = import_path5.default.join(distDir, urlPath, "index.html");
|
|
1281
|
+
if (import_fs5.default.existsSync(indexPath)) {
|
|
1282
|
+
const content = import_fs5.default.readFileSync(indexPath, "utf-8");
|
|
1283
|
+
res.setHeader("Content-Type", "text/html");
|
|
1284
|
+
res.end(content);
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
next();
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// router/auto-generate.ts
|
|
1294
|
+
var import_fs6 = __toESM(require("fs"));
|
|
1295
|
+
var import_path6 = __toESM(require("path"));
|
|
1296
|
+
function toPascalCase(str) {
|
|
1297
|
+
const cleanStr = str.replace(/^\$/, "");
|
|
1298
|
+
return cleanStr.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
1299
|
+
}
|
|
1300
|
+
function generateBoilerplate(folderName, isDynamic) {
|
|
1301
|
+
const componentName = toPascalCase(folderName);
|
|
1302
|
+
if (isDynamic) {
|
|
1303
|
+
const paramName = folderName.replace(/^\$/, "");
|
|
1304
|
+
return `import { useParams } from '../../route';
|
|
1305
|
+
|
|
1306
|
+
export default function ${componentName}() {
|
|
1307
|
+
const { ${paramName} } = useParams();
|
|
1308
|
+
return (
|
|
1309
|
+
<div>
|
|
1310
|
+
<h1>${componentName} Page</h1>
|
|
1311
|
+
<p>{${paramName}}</p>
|
|
1312
|
+
</div>
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
`;
|
|
1316
|
+
}
|
|
1317
|
+
return `export default function ${componentName}() {
|
|
1318
|
+
return (
|
|
1319
|
+
<div>
|
|
1320
|
+
<h1>${componentName} Page</h1>
|
|
1321
|
+
<p>Welcome to ${componentName}</p>
|
|
1322
|
+
</div>
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
`;
|
|
1326
|
+
}
|
|
1327
|
+
function autoGeneratePlugin() {
|
|
1328
|
+
let srcDir;
|
|
1329
|
+
return {
|
|
1330
|
+
name: "olova-auto-generate",
|
|
1331
|
+
configResolved(config) {
|
|
1332
|
+
srcDir = import_path6.default.resolve(config.root, "src");
|
|
1333
|
+
},
|
|
1334
|
+
configureServer(server) {
|
|
1335
|
+
console.log(
|
|
1336
|
+
"\x1B[36m[olova] Auto-generate plugin active - watching for new route files\x1B[0m"
|
|
1337
|
+
);
|
|
1338
|
+
server.watcher.on("add", (filePath) => {
|
|
1339
|
+
const normalizedPath = import_path6.default.normalize(filePath);
|
|
1340
|
+
const normalizedSrcDir = import_path6.default.normalize(srcDir);
|
|
1341
|
+
const ext = import_path6.default.extname(normalizedPath);
|
|
1342
|
+
if (ext !== ".tsx" && ext !== ".jsx") {
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
if (!normalizedPath.startsWith(normalizedSrcDir)) {
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
const fileName = import_path6.default.basename(normalizedPath, ext);
|
|
1349
|
+
if (fileName !== "index") {
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
const folderPath = import_path6.default.dirname(normalizedPath);
|
|
1353
|
+
const folderName = import_path6.default.basename(folderPath);
|
|
1354
|
+
if (folderName === "src" || folderName.startsWith("(")) {
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
try {
|
|
1358
|
+
import_fs6.default.statSync(normalizedPath);
|
|
1359
|
+
const content = import_fs6.default.readFileSync(normalizedPath, "utf-8");
|
|
1360
|
+
if (content.trim().length > 10) {
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
} catch (err) {
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
const isDynamic = folderName.startsWith("$");
|
|
1367
|
+
const boilerplate = generateBoilerplate(folderName, isDynamic);
|
|
1368
|
+
try {
|
|
1369
|
+
import_fs6.default.writeFileSync(normalizedPath, boilerplate, "utf-8");
|
|
1370
|
+
console.log(
|
|
1371
|
+
`\x1B[32m\u2713 [olova] Auto-generated: ${folderName}/index${ext}\x1B[0m`
|
|
1372
|
+
);
|
|
1373
|
+
} catch (err) {
|
|
1374
|
+
console.error(
|
|
1375
|
+
`\x1B[31m\u2717 [olova] Failed to write boilerplate:\x1B[0m`,
|
|
1376
|
+
err
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// router/error-overlay.ts
|
|
1385
|
+
var import_path7 = __toESM(require("path"));
|
|
1386
|
+
function proactiveErrorPlugin() {
|
|
1387
|
+
return {
|
|
1388
|
+
name: "olova-proactive-error",
|
|
1389
|
+
enforce: "post",
|
|
1390
|
+
configureServer(devServer) {
|
|
1391
|
+
const srcDir = import_path7.default.resolve(process.cwd(), "src");
|
|
1392
|
+
const validateFile = async (filePath) => {
|
|
1393
|
+
if (!filePath.startsWith(srcDir)) return;
|
|
1394
|
+
if (!/\.(tsx?|jsx?)$/.test(filePath)) return;
|
|
1395
|
+
const relativePath = import_path7.default.relative(srcDir, filePath);
|
|
1396
|
+
console.log("\x1B[36m[olova]\x1B[0m Checking:", relativePath);
|
|
1397
|
+
try {
|
|
1398
|
+
const url = "/" + import_path7.default.relative(process.cwd(), filePath).replace(/\\/g, "/");
|
|
1399
|
+
await devServer.transformRequest(url);
|
|
1400
|
+
console.log("\x1B[32m[olova]\x1B[0m \u2713", relativePath, "OK");
|
|
1401
|
+
} catch (err) {
|
|
1402
|
+
console.log("\x1B[31m[olova]\x1B[0m \u2717", relativePath);
|
|
1403
|
+
devServer.ws.send({
|
|
1404
|
+
type: "error",
|
|
1405
|
+
err: {
|
|
1406
|
+
message: err.message || String(err),
|
|
1407
|
+
stack: err.stack || "",
|
|
1408
|
+
id: err.id || filePath,
|
|
1409
|
+
frame: err.frame || "",
|
|
1410
|
+
plugin: err.plugin || "olova",
|
|
1411
|
+
loc: err.loc || void 0
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
};
|
|
1416
|
+
devServer.watcher.on("change", async (filePath) => {
|
|
1417
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1418
|
+
await validateFile(filePath);
|
|
1419
|
+
});
|
|
1420
|
+
devServer.watcher.on("add", async (filePath) => {
|
|
1421
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1422
|
+
await validateFile(filePath);
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// router/terminal.ts
|
|
1429
|
+
var colors3 = {
|
|
1430
|
+
// Reset
|
|
1431
|
+
reset: "\x1B[0m",
|
|
1432
|
+
// Styles
|
|
1433
|
+
bold: "\x1B[1m",
|
|
1434
|
+
dim: "\x1B[2m",
|
|
1435
|
+
italic: "\x1B[3m",
|
|
1436
|
+
underline: "\x1B[4m",
|
|
1437
|
+
// Foreground colors
|
|
1438
|
+
black: "\x1B[30m",
|
|
1439
|
+
red: "\x1B[31m",
|
|
1440
|
+
green: "\x1B[32m",
|
|
1441
|
+
yellow: "\x1B[33m",
|
|
1442
|
+
blue: "\x1B[34m",
|
|
1443
|
+
magenta: "\x1B[35m",
|
|
1444
|
+
cyan: "\x1B[36m",
|
|
1445
|
+
white: "\x1B[37m",
|
|
1446
|
+
gray: "\x1B[90m",
|
|
1447
|
+
// Background colors
|
|
1448
|
+
bgBlack: "\x1B[40m",
|
|
1449
|
+
bgRed: "\x1B[41m",
|
|
1450
|
+
bgGreen: "\x1B[42m",
|
|
1451
|
+
bgYellow: "\x1B[43m",
|
|
1452
|
+
bgBlue: "\x1B[44m",
|
|
1453
|
+
bgMagenta: "\x1B[45m",
|
|
1454
|
+
bgCyan: "\x1B[46m",
|
|
1455
|
+
bgWhite: "\x1B[47m"
|
|
1456
|
+
};
|
|
1457
|
+
var symbols2 = {
|
|
1458
|
+
success: "\u2713",
|
|
1459
|
+
error: "\u2717",
|
|
1460
|
+
warning: "\u26A0",
|
|
1461
|
+
info: "\u2139",
|
|
1462
|
+
arrow: "\u2192",
|
|
1463
|
+
arrowRight: "\u279C",
|
|
1464
|
+
bullet: "\u25CB",
|
|
1465
|
+
filled: "\u25CF",
|
|
1466
|
+
lambda: "\u03BB",
|
|
1467
|
+
triangle: "\u25B2",
|
|
1468
|
+
square: "\u25A0",
|
|
1469
|
+
diamond: "\u25C6",
|
|
1470
|
+
star: "\u2605",
|
|
1471
|
+
check: "\u2714",
|
|
1472
|
+
cross: "\u2716",
|
|
1473
|
+
pointer: "\u276F"
|
|
1474
|
+
};
|
|
1475
|
+
function formatTime2(ms) {
|
|
1476
|
+
if (ms < 1) return "<1ms";
|
|
1477
|
+
if (ms < 10) return `${ms.toFixed(1)}ms`;
|
|
1478
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
1479
|
+
if (ms < 6e4) return `${(ms / 1e3).toFixed(2)}s`;
|
|
1480
|
+
const mins = Math.floor(ms / 6e4);
|
|
1481
|
+
const secs = Math.round(ms % 6e4 / 1e3);
|
|
1482
|
+
return `${mins}m ${secs}s`;
|
|
1483
|
+
}
|
|
1484
|
+
function formatBytes2(bytes) {
|
|
1485
|
+
if (bytes === 0) return "0 B";
|
|
1486
|
+
const k = 1024;
|
|
1487
|
+
const sizes = ["B", "kB", "MB", "GB"];
|
|
1488
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1489
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
|
|
1490
|
+
}
|
|
1491
|
+
var logger2 = {
|
|
1492
|
+
// Print banner
|
|
1493
|
+
banner: (name, version = "1.0.0") => {
|
|
1494
|
+
console.log("");
|
|
1495
|
+
console.log(` ${colors3.bold}${colors3.cyan}${symbols2.triangle} ${name}${colors3.reset} ${colors3.dim}v${version}${colors3.reset}`);
|
|
1496
|
+
console.log("");
|
|
1497
|
+
},
|
|
1498
|
+
// Print header
|
|
1499
|
+
header: (text) => {
|
|
1500
|
+
console.log(`
|
|
1501
|
+
${colors3.bold}${colors3.white}${text}${colors3.reset}`);
|
|
1502
|
+
},
|
|
1503
|
+
// Info message
|
|
1504
|
+
info: (text) => {
|
|
1505
|
+
console.log(`${colors3.cyan}${symbols2.info}${colors3.reset} ${text}`);
|
|
1506
|
+
},
|
|
1507
|
+
// Success message
|
|
1508
|
+
success: (text, time) => {
|
|
1509
|
+
const timeStr = time !== void 0 ? ` ${colors3.dim}${formatTime2(time)}${colors3.reset}` : "";
|
|
1510
|
+
console.log(`${colors3.green}${symbols2.success}${colors3.reset} ${text}${timeStr}`);
|
|
1511
|
+
},
|
|
1512
|
+
// Error message
|
|
1513
|
+
error: (text) => {
|
|
1514
|
+
console.log(`${colors3.red}${symbols2.error}${colors3.reset} ${colors3.red}${text}${colors3.reset}`);
|
|
1515
|
+
},
|
|
1516
|
+
// Warning message
|
|
1517
|
+
warn: (text) => {
|
|
1518
|
+
console.log(`${colors3.yellow}${symbols2.warning}${colors3.reset} ${colors3.yellow}${text}${colors3.reset}`);
|
|
1519
|
+
},
|
|
1520
|
+
// Step in a process
|
|
1521
|
+
step: (text, time) => {
|
|
1522
|
+
const timeStr = time !== void 0 ? ` ${colors3.gray}${formatTime2(time)}${colors3.reset}` : "";
|
|
1523
|
+
console.log(` ${colors3.dim}${symbols2.arrow}${colors3.reset} ${text}${timeStr}`);
|
|
1524
|
+
},
|
|
1525
|
+
// Route log (for SSG)
|
|
1526
|
+
route: (route, type, size, time) => {
|
|
1527
|
+
const symbol = type === "static" ? symbols2.bullet : type === "ssr" ? symbols2.filled : symbols2.lambda;
|
|
1528
|
+
const color = type === "static" ? colors3.white : type === "ssr" ? colors3.magenta : colors3.cyan;
|
|
1529
|
+
const sizeStr = size ? ` ${colors3.dim}${formatBytes2(size)}${colors3.reset}` : "";
|
|
1530
|
+
const timeStr = time ? ` ${colors3.gray}(${formatTime2(time)})${colors3.reset}` : "";
|
|
1531
|
+
console.log(`${color}${symbol}${colors3.reset} ${route}${sizeStr}${timeStr}`);
|
|
1532
|
+
},
|
|
1533
|
+
// Indent text
|
|
1534
|
+
indent: (text, level = 1) => {
|
|
1535
|
+
console.log(`${" ".repeat(level)}${text}`);
|
|
1536
|
+
},
|
|
1537
|
+
// Empty line
|
|
1538
|
+
newline: () => console.log(""),
|
|
1539
|
+
// Dim text
|
|
1540
|
+
dim: (text) => `${colors3.dim}${text}${colors3.reset}`,
|
|
1541
|
+
// Bold text
|
|
1542
|
+
bold: (text) => `${colors3.bold}${text}${colors3.reset}`,
|
|
1543
|
+
// Colored text
|
|
1544
|
+
cyan: (text) => `${colors3.cyan}${text}${colors3.reset}`,
|
|
1545
|
+
green: (text) => `${colors3.green}${text}${colors3.reset}`,
|
|
1546
|
+
yellow: (text) => `${colors3.yellow}${text}${colors3.reset}`,
|
|
1547
|
+
red: (text) => `${colors3.red}${text}${colors3.reset}`,
|
|
1548
|
+
magenta: (text) => `${colors3.magenta}${text}${colors3.reset}`
|
|
1549
|
+
};
|
|
1550
|
+
function createTimer() {
|
|
1551
|
+
const start = performance.now();
|
|
1552
|
+
return {
|
|
1553
|
+
elapsed: () => performance.now() - start,
|
|
1554
|
+
format: () => formatTime2(performance.now() - start)
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// router/index.ts
|
|
1559
|
+
function olovaPlugins() {
|
|
1560
|
+
return [
|
|
1561
|
+
configPlugin(),
|
|
1562
|
+
// Must be first - creates .olova folder and sets config
|
|
1563
|
+
routerPlugin(),
|
|
1564
|
+
frameworkPlugin(),
|
|
1565
|
+
virtualHtmlPlugin(),
|
|
1566
|
+
ssgPlugin(),
|
|
1567
|
+
cleanUrlPlugin(),
|
|
1568
|
+
autoGeneratePlugin(),
|
|
1569
|
+
// Auto-generates boilerplate for new route files
|
|
1570
|
+
proactiveErrorPlugin()
|
|
1571
|
+
// Checks all files on save for instant error detection
|
|
1572
|
+
];
|
|
1573
|
+
}
|
|
1574
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1575
|
+
0 && (module.exports = {
|
|
1576
|
+
autoGeneratePlugin,
|
|
1577
|
+
cleanUrlPlugin,
|
|
1578
|
+
colors,
|
|
1579
|
+
configPlugin,
|
|
1580
|
+
createTimer,
|
|
1581
|
+
formatBytes,
|
|
1582
|
+
formatTime,
|
|
1583
|
+
frameworkPlugin,
|
|
1584
|
+
generateBuildId,
|
|
1585
|
+
generateCriticalCSS,
|
|
1586
|
+
generateJsonLd,
|
|
1587
|
+
generateOlovaHydration,
|
|
1588
|
+
generatePerformanceMeta,
|
|
1589
|
+
generateResourceHints,
|
|
1590
|
+
generateServiceWorkerContent,
|
|
1591
|
+
generateServiceWorkerScript,
|
|
1592
|
+
logger,
|
|
1593
|
+
minifyHtml,
|
|
1594
|
+
olovaPlugins,
|
|
1595
|
+
parseFlightData,
|
|
1596
|
+
proactiveErrorPlugin,
|
|
1597
|
+
routerPlugin,
|
|
1598
|
+
ssgPlugin,
|
|
1599
|
+
symbols,
|
|
1600
|
+
virtualHtmlPlugin
|
|
1601
|
+
});
|