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