@buenojs/bueno 0.8.0
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/.env.example +109 -0
- package/.github/workflows/ci.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/architecture.md +652 -0
- package/bun.lock +70 -0
- package/dist/cli/index.js +3233 -0
- package/dist/index.js +9014 -0
- package/package.json +77 -0
- package/src/cache/index.ts +795 -0
- package/src/cli/ARCHITECTURE.md +837 -0
- package/src/cli/bin.ts +10 -0
- package/src/cli/commands/build.ts +425 -0
- package/src/cli/commands/dev.ts +248 -0
- package/src/cli/commands/generate.ts +541 -0
- package/src/cli/commands/help.ts +55 -0
- package/src/cli/commands/index.ts +112 -0
- package/src/cli/commands/migration.ts +355 -0
- package/src/cli/commands/new.ts +804 -0
- package/src/cli/commands/start.ts +208 -0
- package/src/cli/core/args.ts +283 -0
- package/src/cli/core/console.ts +349 -0
- package/src/cli/core/index.ts +60 -0
- package/src/cli/core/prompt.ts +424 -0
- package/src/cli/core/spinner.ts +265 -0
- package/src/cli/index.ts +135 -0
- package/src/cli/templates/deploy.ts +295 -0
- package/src/cli/templates/docker.ts +307 -0
- package/src/cli/templates/index.ts +24 -0
- package/src/cli/utils/fs.ts +428 -0
- package/src/cli/utils/index.ts +8 -0
- package/src/cli/utils/strings.ts +197 -0
- package/src/config/env.ts +408 -0
- package/src/config/index.ts +506 -0
- package/src/config/loader.ts +329 -0
- package/src/config/merge.ts +285 -0
- package/src/config/types.ts +320 -0
- package/src/config/validation.ts +441 -0
- package/src/container/forward-ref.ts +143 -0
- package/src/container/index.ts +386 -0
- package/src/context/index.ts +360 -0
- package/src/database/index.ts +1142 -0
- package/src/database/migrations/index.ts +371 -0
- package/src/database/schema/index.ts +619 -0
- package/src/frontend/api-routes.ts +640 -0
- package/src/frontend/bundler.ts +643 -0
- package/src/frontend/console-client.ts +419 -0
- package/src/frontend/console-stream.ts +587 -0
- package/src/frontend/dev-server.ts +846 -0
- package/src/frontend/file-router.ts +611 -0
- package/src/frontend/frameworks/index.ts +106 -0
- package/src/frontend/frameworks/react.ts +85 -0
- package/src/frontend/frameworks/solid.ts +104 -0
- package/src/frontend/frameworks/svelte.ts +110 -0
- package/src/frontend/frameworks/vue.ts +92 -0
- package/src/frontend/hmr-client.ts +663 -0
- package/src/frontend/hmr.ts +728 -0
- package/src/frontend/index.ts +342 -0
- package/src/frontend/islands.ts +552 -0
- package/src/frontend/isr.ts +555 -0
- package/src/frontend/layout.ts +475 -0
- package/src/frontend/ssr/react.ts +446 -0
- package/src/frontend/ssr/solid.ts +523 -0
- package/src/frontend/ssr/svelte.ts +546 -0
- package/src/frontend/ssr/vue.ts +504 -0
- package/src/frontend/ssr.ts +699 -0
- package/src/frontend/types.ts +2274 -0
- package/src/health/index.ts +604 -0
- package/src/index.ts +410 -0
- package/src/lock/index.ts +587 -0
- package/src/logger/index.ts +444 -0
- package/src/logger/transports/index.ts +969 -0
- package/src/metrics/index.ts +494 -0
- package/src/middleware/built-in.ts +360 -0
- package/src/middleware/index.ts +94 -0
- package/src/modules/filters.ts +458 -0
- package/src/modules/guards.ts +405 -0
- package/src/modules/index.ts +1256 -0
- package/src/modules/interceptors.ts +574 -0
- package/src/modules/lazy.ts +418 -0
- package/src/modules/lifecycle.ts +478 -0
- package/src/modules/metadata.ts +90 -0
- package/src/modules/pipes.ts +626 -0
- package/src/router/index.ts +339 -0
- package/src/router/linear.ts +371 -0
- package/src/router/regex.ts +292 -0
- package/src/router/tree.ts +562 -0
- package/src/rpc/index.ts +1263 -0
- package/src/security/index.ts +436 -0
- package/src/ssg/index.ts +631 -0
- package/src/storage/index.ts +456 -0
- package/src/telemetry/index.ts +1097 -0
- package/src/testing/index.ts +1586 -0
- package/src/types/index.ts +236 -0
- package/src/types/optional-deps.d.ts +219 -0
- package/src/validation/index.ts +276 -0
- package/src/websocket/index.ts +1004 -0
- package/tests/integration/cli.test.ts +1016 -0
- package/tests/integration/fullstack.test.ts +234 -0
- package/tests/unit/cache.test.ts +174 -0
- package/tests/unit/cli-commands.test.ts +892 -0
- package/tests/unit/cli.test.ts +1258 -0
- package/tests/unit/container.test.ts +279 -0
- package/tests/unit/context.test.ts +221 -0
- package/tests/unit/database.test.ts +183 -0
- package/tests/unit/linear-router.test.ts +280 -0
- package/tests/unit/lock.test.ts +336 -0
- package/tests/unit/middleware.test.ts +184 -0
- package/tests/unit/modules.test.ts +142 -0
- package/tests/unit/pubsub.test.ts +257 -0
- package/tests/unit/regex-router.test.ts +265 -0
- package/tests/unit/router.test.ts +373 -0
- package/tests/unit/rpc.test.ts +1248 -0
- package/tests/unit/security.test.ts +174 -0
- package/tests/unit/telemetry.test.ts +371 -0
- package/tests/unit/test-cache.test.ts +110 -0
- package/tests/unit/test-database.test.ts +282 -0
- package/tests/unit/tree-router.test.ts +325 -0
- package/tests/unit/validation.test.ts +794 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vue SSR Renderer
|
|
3
|
+
*
|
|
4
|
+
* Provides server-side rendering for Vue components using:
|
|
5
|
+
* - renderToString / renderToStream from vue/server-renderer
|
|
6
|
+
* - Vue Router integration
|
|
7
|
+
* - Vue Meta for head management
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
SSRContext,
|
|
12
|
+
SSRElement,
|
|
13
|
+
SSRPage,
|
|
14
|
+
FrameworkSSRRenderer,
|
|
15
|
+
} from "../types.js";
|
|
16
|
+
|
|
17
|
+
// Vue types (dynamically imported)
|
|
18
|
+
interface VueApp {
|
|
19
|
+
component(name: string, component: unknown): VueApp;
|
|
20
|
+
use(plugin: unknown, ...options: unknown[]): VueApp;
|
|
21
|
+
provide(key: string | symbol, value: unknown): VueApp;
|
|
22
|
+
config: {
|
|
23
|
+
globalProperties: Record<string, unknown>;
|
|
24
|
+
errorHandler: ((err: unknown, instance: unknown, info: string) => void) | null;
|
|
25
|
+
};
|
|
26
|
+
mount(selector: string): unknown;
|
|
27
|
+
unmount(): void;
|
|
28
|
+
ssrContext: SSRContext | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface VueRouter {
|
|
32
|
+
push(location: string): Promise<unknown>;
|
|
33
|
+
replace(location: string): Promise<unknown>;
|
|
34
|
+
currentRoute: {
|
|
35
|
+
value: {
|
|
36
|
+
path: string;
|
|
37
|
+
params: Record<string, string>;
|
|
38
|
+
query: Record<string, string>;
|
|
39
|
+
meta: Record<string, unknown>;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
isReady(): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface CreateSSRAppOptions {
|
|
46
|
+
/**
|
|
47
|
+
* Component to render
|
|
48
|
+
*/
|
|
49
|
+
component: unknown;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Props to pass to component
|
|
53
|
+
*/
|
|
54
|
+
props?: Record<string, unknown>;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Initial state for hydration
|
|
58
|
+
*/
|
|
59
|
+
state?: Record<string, unknown>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Head element storage
|
|
63
|
+
let headElements: SSRElement[] = [];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Reset head elements for a new render
|
|
67
|
+
*/
|
|
68
|
+
export function resetHead(): void {
|
|
69
|
+
headElements = [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get collected head elements
|
|
74
|
+
*/
|
|
75
|
+
export function getHeadElements(): SSRElement[] {
|
|
76
|
+
return [...headElements];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Add a head element
|
|
81
|
+
*/
|
|
82
|
+
export function addHeadElement(element: SSRElement): void {
|
|
83
|
+
headElements.push(element);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Vue SSR Renderer implementation
|
|
88
|
+
*/
|
|
89
|
+
export class VueSSRRenderer implements FrameworkSSRRenderer {
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
91
|
+
private vue: any = null;
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
93
|
+
private vueServerRenderer: any = null;
|
|
94
|
+
private initialized = false;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Initialize Vue modules
|
|
98
|
+
*/
|
|
99
|
+
async init(): Promise<void> {
|
|
100
|
+
if (this.initialized) return;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
this.vue = await import("vue");
|
|
104
|
+
this.vueServerRenderer = await import("vue/server-renderer");
|
|
105
|
+
this.initialized = true;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
"Vue is not installed. Install it with: bun add vue"
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Create a Vue SSR app instance
|
|
115
|
+
*/
|
|
116
|
+
async createSSRApp(options: CreateSSRAppOptions): Promise<VueApp> {
|
|
117
|
+
await this.init();
|
|
118
|
+
|
|
119
|
+
if (!this.vue) {
|
|
120
|
+
throw new Error("Vue not initialized");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const { createSSRApp } = this.vue;
|
|
124
|
+
const app = createSSRApp(options.component, options.props || {});
|
|
125
|
+
|
|
126
|
+
// Provide initial state for hydration
|
|
127
|
+
if (options.state) {
|
|
128
|
+
app.provide("__INITIAL_STATE__", options.state);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return app as VueApp;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Render a component to HTML string
|
|
136
|
+
*/
|
|
137
|
+
async renderToString(component: unknown, context: SSRContext): Promise<string> {
|
|
138
|
+
await this.init();
|
|
139
|
+
|
|
140
|
+
if (!this.vueServerRenderer) {
|
|
141
|
+
throw new Error("Vue Server Renderer not initialized");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
resetHead();
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
// If component is already a Vue app, render it directly
|
|
148
|
+
if (this.isVueApp(component)) {
|
|
149
|
+
component.ssrContext = context;
|
|
150
|
+
const html = await this.vueServerRenderer.renderToString(component);
|
|
151
|
+
return html;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Otherwise create an app and render
|
|
155
|
+
const app = await this.createSSRApp({
|
|
156
|
+
component,
|
|
157
|
+
props: { context },
|
|
158
|
+
});
|
|
159
|
+
app.ssrContext = context;
|
|
160
|
+
|
|
161
|
+
const html = await this.vueServerRenderer.renderToString(app);
|
|
162
|
+
return html;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
165
|
+
throw new Error(`Vue renderToString failed: ${errorMessage}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Render a component to a stream
|
|
171
|
+
*/
|
|
172
|
+
renderToStream(component: unknown, context: SSRContext): ReadableStream<Uint8Array> {
|
|
173
|
+
const encoder = new TextEncoder();
|
|
174
|
+
|
|
175
|
+
return new ReadableStream<Uint8Array>({
|
|
176
|
+
start: async (controller) => {
|
|
177
|
+
try {
|
|
178
|
+
await this.init();
|
|
179
|
+
|
|
180
|
+
if (!this.vueServerRenderer) {
|
|
181
|
+
controller.error(new Error("Vue Server Renderer not initialized"));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
resetHead();
|
|
186
|
+
|
|
187
|
+
// Set SSR context
|
|
188
|
+
if (this.isVueApp(component)) {
|
|
189
|
+
component.ssrContext = context;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Use renderToNodeStream equivalent via Web Streams
|
|
193
|
+
const { renderToWebStream } = this.vueServerRenderer;
|
|
194
|
+
|
|
195
|
+
if (renderToWebStream) {
|
|
196
|
+
const stream = renderToWebStream(component);
|
|
197
|
+
|
|
198
|
+
const reader = stream.getReader();
|
|
199
|
+
|
|
200
|
+
while (true) {
|
|
201
|
+
const { done, value } = await reader.read();
|
|
202
|
+
if (done) {
|
|
203
|
+
controller.close();
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
controller.enqueue(value);
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
// Fallback to renderToString
|
|
210
|
+
const html = await this.vueServerRenderer.renderToString(component);
|
|
211
|
+
controller.enqueue(encoder.encode(html));
|
|
212
|
+
controller.close();
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
216
|
+
controller.error(new Error(`Vue renderToStream failed: ${errorMessage}`));
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get head elements from component
|
|
224
|
+
*/
|
|
225
|
+
getHeadElements(context: SSRContext): SSRElement[] {
|
|
226
|
+
return getHeadElements();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Create the framework-specific component
|
|
231
|
+
*/
|
|
232
|
+
createComponent(page: SSRPage, context: SSRContext): unknown {
|
|
233
|
+
return {
|
|
234
|
+
page,
|
|
235
|
+
context,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Check if object is a Vue app instance
|
|
241
|
+
*/
|
|
242
|
+
private isVueApp(obj: unknown): obj is VueApp {
|
|
243
|
+
return (
|
|
244
|
+
typeof obj === "object" &&
|
|
245
|
+
obj !== null &&
|
|
246
|
+
"component" in obj &&
|
|
247
|
+
"use" in obj &&
|
|
248
|
+
"provide" in obj &&
|
|
249
|
+
"config" in obj
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Create Vue Router for SSR
|
|
255
|
+
*/
|
|
256
|
+
async createRouter(routes: unknown[]): Promise<{
|
|
257
|
+
router: VueRouter;
|
|
258
|
+
app: VueApp;
|
|
259
|
+
}> {
|
|
260
|
+
await this.init();
|
|
261
|
+
|
|
262
|
+
if (!this.vue) {
|
|
263
|
+
throw new Error("Vue not initialized");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const vueRouter = await import("vue-router");
|
|
268
|
+
const { createMemoryHistory, createRouter } = vueRouter;
|
|
269
|
+
|
|
270
|
+
const router = createRouter({
|
|
271
|
+
history: createMemoryHistory(),
|
|
272
|
+
routes: routes as [],
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const app = await this.createSSRApp({
|
|
276
|
+
component: {},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
app.use(router);
|
|
280
|
+
|
|
281
|
+
return { router: router as unknown as VueRouter, app };
|
|
282
|
+
} catch (error) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
"Vue Router is not installed. Install it with: bun add vue-router"
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Render with Vue Router
|
|
291
|
+
*/
|
|
292
|
+
async renderWithRouter(
|
|
293
|
+
app: VueApp,
|
|
294
|
+
router: VueRouter,
|
|
295
|
+
url: string,
|
|
296
|
+
context: SSRContext
|
|
297
|
+
): Promise<string> {
|
|
298
|
+
await this.init();
|
|
299
|
+
|
|
300
|
+
if (!this.vueServerRenderer) {
|
|
301
|
+
throw new Error("Vue Server Renderer not initialized");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
resetHead();
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
// Push to router and wait for ready
|
|
308
|
+
await router.push(url);
|
|
309
|
+
await router.isReady();
|
|
310
|
+
|
|
311
|
+
// Set SSR context
|
|
312
|
+
app.ssrContext = context;
|
|
313
|
+
|
|
314
|
+
// Render
|
|
315
|
+
const html = await this.vueServerRenderer.renderToString(app);
|
|
316
|
+
return html;
|
|
317
|
+
} catch (error) {
|
|
318
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
319
|
+
throw new Error(`Vue renderWithRouter failed: ${errorMessage}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Create a Vue SSR renderer
|
|
326
|
+
*/
|
|
327
|
+
export function createVueSSRRenderer(): VueSSRRenderer {
|
|
328
|
+
return new VueSSRRenderer();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Vue Meta-like head management
|
|
333
|
+
*/
|
|
334
|
+
export class VueMeta {
|
|
335
|
+
private static instance: VueMeta;
|
|
336
|
+
private title = "";
|
|
337
|
+
private metaTags: SSRElement[] = [];
|
|
338
|
+
private linkTags: SSRElement[] = [];
|
|
339
|
+
private scriptTags: SSRElement[] = [];
|
|
340
|
+
private styleTags: SSRElement[] = [];
|
|
341
|
+
|
|
342
|
+
static getInstance(): VueMeta {
|
|
343
|
+
if (!VueMeta.instance) {
|
|
344
|
+
VueMeta.instance = new VueMeta();
|
|
345
|
+
}
|
|
346
|
+
return VueMeta.instance;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
setTitle(title: string): this {
|
|
350
|
+
this.title = title;
|
|
351
|
+
addHeadElement({ tag: "title", attrs: {}, children: [{ tag: "#text", attrs: {}, innerHTML: title }] });
|
|
352
|
+
return this;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
addMeta(attrs: Record<string, string>): this {
|
|
356
|
+
this.metaTags.push({ tag: "meta", attrs });
|
|
357
|
+
addHeadElement({ tag: "meta", attrs });
|
|
358
|
+
return this;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
addLink(attrs: Record<string, string>): this {
|
|
362
|
+
this.linkTags.push({ tag: "link", attrs });
|
|
363
|
+
addHeadElement({ tag: "link", attrs });
|
|
364
|
+
return this;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
addScript(attrs: Record<string, string>, innerHTML?: string): this {
|
|
368
|
+
this.scriptTags.push({ tag: "script", attrs, innerHTML });
|
|
369
|
+
addHeadElement({ tag: "script", attrs, innerHTML });
|
|
370
|
+
return this;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
addStyle(innerHTML: string, attrs?: Record<string, string>): this {
|
|
374
|
+
this.styleTags.push({ tag: "style", attrs: attrs || {}, innerHTML });
|
|
375
|
+
addHeadElement({ tag: "style", attrs: attrs || {}, innerHTML });
|
|
376
|
+
return this;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
reset(): void {
|
|
380
|
+
this.title = "";
|
|
381
|
+
this.metaTags = [];
|
|
382
|
+
this.linkTags = [];
|
|
383
|
+
this.scriptTags = [];
|
|
384
|
+
this.styleTags = [];
|
|
385
|
+
resetHead();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
getTitle(): string {
|
|
389
|
+
return this.title;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
getMetaTags(): SSRElement[] {
|
|
393
|
+
return [...this.metaTags];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
getLinkTags(): SSRElement[] {
|
|
397
|
+
return [...this.linkTags];
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
getScriptTags(): SSRElement[] {
|
|
401
|
+
return [...this.scriptTags];
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
getStyleTags(): SSRElement[] {
|
|
405
|
+
return [...this.styleTags];
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Get Vue Meta instance
|
|
411
|
+
*/
|
|
412
|
+
export function vueMeta(): VueMeta {
|
|
413
|
+
return VueMeta.getInstance();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Convert SSRElement to HTML string
|
|
418
|
+
*/
|
|
419
|
+
export function ssrElementToString(element: SSRElement): string {
|
|
420
|
+
if (element.tag === "#text") {
|
|
421
|
+
return escapeHtml(element.innerHTML || "");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const attrs = Object.entries(element.attrs)
|
|
425
|
+
.map(([key, value]) => `${key}="${escapeHtml(value)}"`)
|
|
426
|
+
.join(" ");
|
|
427
|
+
|
|
428
|
+
const openTag = attrs ? `<${element.tag} ${attrs}>` : `<${element.tag}>`;
|
|
429
|
+
|
|
430
|
+
if (element.innerHTML) {
|
|
431
|
+
return `${openTag}${element.innerHTML}</${element.tag}>`;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (element.children && element.children.length > 0) {
|
|
435
|
+
const children = element.children.map(ssrElementToString).join("");
|
|
436
|
+
return `${openTag}${children}</${element.tag}>`;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Self-closing tags
|
|
440
|
+
const voidElements = ["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"];
|
|
441
|
+
if (voidElements.includes(element.tag)) {
|
|
442
|
+
return attrs ? `<${element.tag} ${attrs}>` : `<${element.tag}>`;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return `${openTag}</${element.tag}>`;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Escape HTML special characters
|
|
450
|
+
*/
|
|
451
|
+
function escapeHtml(str: string): string {
|
|
452
|
+
return str
|
|
453
|
+
.replace(/&/g, "\x26amp;")
|
|
454
|
+
.replace(/</g, "\x26lt;")
|
|
455
|
+
.replace(/>/g, "\x26gt;")
|
|
456
|
+
.replace(/"/g, "\x26quot;")
|
|
457
|
+
.replace(/'/g, "'");
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Vue useHead composable-like function
|
|
462
|
+
*/
|
|
463
|
+
export function useHead(head: {
|
|
464
|
+
title?: string;
|
|
465
|
+
meta?: Record<string, string>[];
|
|
466
|
+
link?: Record<string, string>[];
|
|
467
|
+
script?: Record<string, string>[];
|
|
468
|
+
style?: { innerHTML: string; attrs?: Record<string, string> }[];
|
|
469
|
+
}): void {
|
|
470
|
+
const meta = vueMeta();
|
|
471
|
+
|
|
472
|
+
if (head.title) {
|
|
473
|
+
meta.setTitle(head.title);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
head.meta?.forEach((attrs) => meta.addMeta(attrs));
|
|
477
|
+
head.link?.forEach((attrs) => meta.addLink(attrs));
|
|
478
|
+
head.script?.forEach((attrs) => meta.addScript(attrs));
|
|
479
|
+
head.style?.forEach(({ innerHTML, attrs }) => meta.addStyle(innerHTML, attrs));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Create Vue SSR context with initial state
|
|
484
|
+
*/
|
|
485
|
+
export function createVueSSRContext(
|
|
486
|
+
request: Request,
|
|
487
|
+
initialState: Record<string, unknown> = {}
|
|
488
|
+
): SSRContext {
|
|
489
|
+
const url = new URL(request.url);
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
url: request.url,
|
|
493
|
+
request,
|
|
494
|
+
headers: new Headers(),
|
|
495
|
+
status: 200,
|
|
496
|
+
head: [],
|
|
497
|
+
body: [],
|
|
498
|
+
data: initialState,
|
|
499
|
+
modules: new Set(),
|
|
500
|
+
pathname: url.pathname,
|
|
501
|
+
query: url.searchParams,
|
|
502
|
+
params: {},
|
|
503
|
+
};
|
|
504
|
+
}
|