@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,475 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout Nesting Implementation
|
|
3
|
+
*
|
|
4
|
+
* Provides Next.js-style layout nesting:
|
|
5
|
+
* - _layout.tsx convention for nested layouts
|
|
6
|
+
* - Layout inheritance down directory tree
|
|
7
|
+
* - Layout state preservation on navigation
|
|
8
|
+
* - Per-segment layouts
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createLogger, type Logger } from "../logger/index.js";
|
|
12
|
+
import type {
|
|
13
|
+
LayoutDefinition,
|
|
14
|
+
LayoutNode,
|
|
15
|
+
LayoutTree,
|
|
16
|
+
LayoutProps,
|
|
17
|
+
LayoutRenderer,
|
|
18
|
+
LayoutMiddleware,
|
|
19
|
+
LayoutConfig,
|
|
20
|
+
PartialLayoutConfig,
|
|
21
|
+
LayoutRenderResult,
|
|
22
|
+
LayoutSegment,
|
|
23
|
+
} from "./types.js";
|
|
24
|
+
import type { SSRContext, SSRElement, RenderResult } from "./types.js";
|
|
25
|
+
|
|
26
|
+
// ============= Constants =============
|
|
27
|
+
|
|
28
|
+
const LAYOUT_FILE = "_layout";
|
|
29
|
+
const SUPPORTED_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
|
|
30
|
+
|
|
31
|
+
// ============= Layout Manager Class =============
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Layout Manager handles nested layout resolution and rendering
|
|
35
|
+
*
|
|
36
|
+
* Features:
|
|
37
|
+
* - _layout.tsx convention
|
|
38
|
+
* - Layout inheritance down directory tree
|
|
39
|
+
* - Layout state preservation
|
|
40
|
+
* - Per-segment layouts
|
|
41
|
+
*/
|
|
42
|
+
export class LayoutManager {
|
|
43
|
+
private config: LayoutConfig;
|
|
44
|
+
private logger: Logger;
|
|
45
|
+
private layouts: Map<string, LayoutDefinition> = new Map();
|
|
46
|
+
private layoutTree: LayoutTree | null = null;
|
|
47
|
+
|
|
48
|
+
constructor(config: PartialLayoutConfig = {}) {
|
|
49
|
+
this.config = this.normalizeConfig(config);
|
|
50
|
+
this.logger = createLogger({
|
|
51
|
+
level: "debug",
|
|
52
|
+
pretty: true,
|
|
53
|
+
context: { component: "LayoutManager" },
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Normalize partial config to full config with defaults
|
|
59
|
+
*/
|
|
60
|
+
private normalizeConfig(config: PartialLayoutConfig): LayoutConfig {
|
|
61
|
+
return {
|
|
62
|
+
pagesDir: config.pagesDir ?? "pages",
|
|
63
|
+
rootDir: config.rootDir ?? process.cwd(),
|
|
64
|
+
extensions: config.extensions ?? SUPPORTED_EXTENSIONS,
|
|
65
|
+
preserveState: config.preserveState ?? true,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Initialize the layout manager by scanning for layout files
|
|
71
|
+
*/
|
|
72
|
+
async init(): Promise<void> {
|
|
73
|
+
this.logger.info(`Initializing layout manager from: ${this.config.pagesDir}`);
|
|
74
|
+
await this.scanLayoutFiles();
|
|
75
|
+
this.buildLayoutTree();
|
|
76
|
+
this.logger.info(`Loaded ${this.layouts.size} layouts`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Scan for layout files in the pages directory
|
|
81
|
+
*/
|
|
82
|
+
private async scanLayoutFiles(): Promise<void> {
|
|
83
|
+
const pagesPath = this.config.pagesDir;
|
|
84
|
+
const glob = new Bun.Glob(`**/${LAYOUT_FILE}{${this.config.extensions.join(",")}}`);
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
for await (const file of glob.scan(pagesPath)) {
|
|
88
|
+
await this.processLayoutFile(file, pagesPath);
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
this.logger.error(`Failed to scan layouts: ${pagesPath}`, error);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Process a single layout file
|
|
97
|
+
*/
|
|
98
|
+
private async processLayoutFile(filePath: string, basePath: string): Promise<void> {
|
|
99
|
+
const fullPath = `${basePath}/${filePath}`;
|
|
100
|
+
const segment = this.getLayoutSegment(filePath);
|
|
101
|
+
|
|
102
|
+
const layout: LayoutDefinition = {
|
|
103
|
+
id: this.generateLayoutId(filePath),
|
|
104
|
+
filePath: fullPath,
|
|
105
|
+
segment,
|
|
106
|
+
depth: this.calculateDepth(segment),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
this.layouts.set(segment, layout);
|
|
110
|
+
this.logger.debug(`Processed layout: ${segment}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get layout segment from file path
|
|
115
|
+
*/
|
|
116
|
+
private getLayoutSegment(filePath: string): string {
|
|
117
|
+
// Remove _layout.tsx from path
|
|
118
|
+
const segment = filePath.replace(new RegExp(`/${LAYOUT_FILE}\\.(tsx?|jsx?)$`), "");
|
|
119
|
+
return segment === "" ? "/" : `/${segment}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Calculate depth of layout segment
|
|
124
|
+
*/
|
|
125
|
+
private calculateDepth(segment: string): number {
|
|
126
|
+
if (segment === "/") return 0;
|
|
127
|
+
return segment.split("/").length - 1;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Generate unique layout ID
|
|
132
|
+
*/
|
|
133
|
+
private generateLayoutId(filePath: string): string {
|
|
134
|
+
return filePath.replace(/[\/\\.]/g, "-").replace(/^-/, "");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Build the layout tree from collected layouts
|
|
139
|
+
*/
|
|
140
|
+
private buildLayoutTree(): void {
|
|
141
|
+
// Find root layout
|
|
142
|
+
const rootLayout = this.layouts.get("/");
|
|
143
|
+
if (!rootLayout) {
|
|
144
|
+
this.logger.warn("No root layout found");
|
|
145
|
+
this.layoutTree = null;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Build tree recursively
|
|
150
|
+
this.layoutTree = this.buildTreeNode(rootLayout, null);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Build a layout tree node
|
|
155
|
+
*/
|
|
156
|
+
private buildTreeNode(layout: LayoutDefinition, parent: LayoutNode | null): LayoutNode {
|
|
157
|
+
const node: LayoutNode = {
|
|
158
|
+
layout,
|
|
159
|
+
parent,
|
|
160
|
+
children: [],
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Find child layouts
|
|
164
|
+
for (const [segment, childLayout] of this.layouts) {
|
|
165
|
+
if (segment === layout.segment) continue;
|
|
166
|
+
|
|
167
|
+
// Check if this is a direct child
|
|
168
|
+
if (this.isDirectChild(layout.segment, segment)) {
|
|
169
|
+
node.children.push(this.buildTreeNode(childLayout, node));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Sort children by depth
|
|
174
|
+
node.children.sort((a, b) => a.layout.depth - b.layout.depth);
|
|
175
|
+
|
|
176
|
+
return node;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Check if a segment is a direct child of another
|
|
181
|
+
*/
|
|
182
|
+
private isDirectChild(parentSegment: string, childSegment: string): boolean {
|
|
183
|
+
if (parentSegment === "/") {
|
|
184
|
+
// Root layout: direct children have no other parent
|
|
185
|
+
const parts = childSegment.split("/").filter(Boolean);
|
|
186
|
+
return parts.length === 1;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check if child starts with parent and has exactly one more segment
|
|
190
|
+
if (!childSegment.startsWith(parentSegment + "/")) return false;
|
|
191
|
+
|
|
192
|
+
const remaining = childSegment.slice(parentSegment.length + 1);
|
|
193
|
+
return !remaining.includes("/");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get layout chain for a route
|
|
198
|
+
*/
|
|
199
|
+
getLayoutChain(routePath: string): LayoutDefinition[] {
|
|
200
|
+
const chain: LayoutDefinition[] = [];
|
|
201
|
+
|
|
202
|
+
// Start from root and work down
|
|
203
|
+
const segments = this.getRouteSegments(routePath);
|
|
204
|
+
|
|
205
|
+
for (let i = 0; i <= segments.length; i++) {
|
|
206
|
+
const segment = i === 0 ? "/" : "/" + segments.slice(0, i).join("/");
|
|
207
|
+
const layout = this.layouts.get(segment);
|
|
208
|
+
|
|
209
|
+
if (layout) {
|
|
210
|
+
chain.push(layout);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return chain;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get route segments from path
|
|
219
|
+
*/
|
|
220
|
+
private getRouteSegments(path: string): string[] {
|
|
221
|
+
return path.split("/").filter(Boolean);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Load layout module
|
|
226
|
+
*/
|
|
227
|
+
private async loadLayoutModule(filePath: string): Promise<LayoutRenderer | null> {
|
|
228
|
+
try {
|
|
229
|
+
const module = await import(filePath);
|
|
230
|
+
return module.default || module;
|
|
231
|
+
} catch (error) {
|
|
232
|
+
this.logger.error(`Failed to load layout module: ${filePath}`, error);
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Render layouts with nested content
|
|
239
|
+
*/
|
|
240
|
+
async renderLayouts(
|
|
241
|
+
routePath: string,
|
|
242
|
+
content: string,
|
|
243
|
+
context: SSRContext
|
|
244
|
+
): Promise<LayoutRenderResult> {
|
|
245
|
+
const chain = this.getLayoutChain(routePath);
|
|
246
|
+
|
|
247
|
+
if (chain.length === 0) {
|
|
248
|
+
return {
|
|
249
|
+
html: content,
|
|
250
|
+
head: [],
|
|
251
|
+
body: [],
|
|
252
|
+
layouts: [],
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let html = content;
|
|
257
|
+
const head: SSRElement[] = [];
|
|
258
|
+
const body: SSRElement[] = [];
|
|
259
|
+
const renderedLayouts: string[] = [];
|
|
260
|
+
|
|
261
|
+
// Render layouts from innermost to outermost
|
|
262
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
263
|
+
const layout = chain[i];
|
|
264
|
+
const renderer = await this.loadLayoutModule(layout.filePath);
|
|
265
|
+
|
|
266
|
+
if (renderer) {
|
|
267
|
+
const props: LayoutProps = {
|
|
268
|
+
children: html,
|
|
269
|
+
params: context.params,
|
|
270
|
+
query: context.query,
|
|
271
|
+
pathname: context.pathname,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// Render layout
|
|
275
|
+
const result = await renderer(props, context);
|
|
276
|
+
|
|
277
|
+
if (typeof result === "string") {
|
|
278
|
+
html = result;
|
|
279
|
+
} else {
|
|
280
|
+
html = result.html;
|
|
281
|
+
if (result.head) head.push(...result.head);
|
|
282
|
+
if (result.body) body.push(...result.body);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
renderedLayouts.push(layout.segment);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
html,
|
|
291
|
+
head,
|
|
292
|
+
body,
|
|
293
|
+
layouts: renderedLayouts,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get layout for a segment
|
|
299
|
+
*/
|
|
300
|
+
getLayout(segment: string): LayoutDefinition | undefined {
|
|
301
|
+
return this.layouts.get(segment);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Get all layouts
|
|
306
|
+
*/
|
|
307
|
+
getAllLayouts(): LayoutDefinition[] {
|
|
308
|
+
return Array.from(this.layouts.values());
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Get layout tree
|
|
313
|
+
*/
|
|
314
|
+
getLayoutTree(): LayoutNode | null {
|
|
315
|
+
return this.layoutTree;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Check if a route has a layout
|
|
320
|
+
*/
|
|
321
|
+
hasLayout(routePath: string): boolean {
|
|
322
|
+
const segments = this.getRouteSegments(routePath);
|
|
323
|
+
|
|
324
|
+
for (let i = 0; i <= segments.length; i++) {
|
|
325
|
+
const segment = i === 0 ? "/" : "/" + segments.slice(0, i).join("/");
|
|
326
|
+
if (this.layouts.has(segment)) {
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Get layout depth for a route
|
|
336
|
+
*/
|
|
337
|
+
getLayoutDepth(routePath: string): number {
|
|
338
|
+
return this.getLayoutChain(routePath).length;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Reload layouts (for hot reload)
|
|
343
|
+
*/
|
|
344
|
+
async reload(): Promise<void> {
|
|
345
|
+
this.logger.info("Reloading layouts...");
|
|
346
|
+
this.layouts.clear();
|
|
347
|
+
this.layoutTree = null;
|
|
348
|
+
await this.init();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Get configuration
|
|
353
|
+
*/
|
|
354
|
+
getConfig(): LayoutConfig {
|
|
355
|
+
return { ...this.config };
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ============= Factory Function =============
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Create a layout manager
|
|
363
|
+
*/
|
|
364
|
+
export function createLayoutManager(config: PartialLayoutConfig = {}): LayoutManager {
|
|
365
|
+
return new LayoutManager(config);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ============= Utility Functions =============
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Check if a file is a layout file
|
|
372
|
+
*/
|
|
373
|
+
export function isLayoutFile(filename: string): boolean {
|
|
374
|
+
const baseName = filename.replace(/\.(tsx?|jsx?)$/, "");
|
|
375
|
+
return baseName === LAYOUT_FILE;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Get layout segment from file path
|
|
380
|
+
*/
|
|
381
|
+
export function getLayoutSegmentFromPath(filePath: string): string {
|
|
382
|
+
const parts = filePath.split("/");
|
|
383
|
+
const dirParts = parts.slice(0, -1); // Remove filename
|
|
384
|
+
|
|
385
|
+
if (dirParts.length === 0) {
|
|
386
|
+
return "/";
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return "/" + dirParts.join("/");
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Build layout props for a page
|
|
394
|
+
*/
|
|
395
|
+
export function buildLayoutProps(
|
|
396
|
+
children: string,
|
|
397
|
+
context: SSRContext
|
|
398
|
+
): LayoutProps {
|
|
399
|
+
return {
|
|
400
|
+
children,
|
|
401
|
+
params: context.params,
|
|
402
|
+
query: context.query,
|
|
403
|
+
pathname: context.pathname,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Create a layout segment
|
|
409
|
+
*/
|
|
410
|
+
export function createLayoutSegment(
|
|
411
|
+
path: string,
|
|
412
|
+
params: Record<string, string> = {}
|
|
413
|
+
): LayoutSegment {
|
|
414
|
+
return {
|
|
415
|
+
path,
|
|
416
|
+
params,
|
|
417
|
+
component: null,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Merge layout head elements
|
|
423
|
+
*/
|
|
424
|
+
export function mergeLayoutHead(
|
|
425
|
+
...heads: SSRElement[][]
|
|
426
|
+
): SSRElement[] {
|
|
427
|
+
const merged: SSRElement[] = [];
|
|
428
|
+
const seen = new Set<string>();
|
|
429
|
+
|
|
430
|
+
for (const head of heads) {
|
|
431
|
+
for (const element of head) {
|
|
432
|
+
const key = getHeadElementKey(element);
|
|
433
|
+
if (!seen.has(key)) {
|
|
434
|
+
seen.add(key);
|
|
435
|
+
merged.push(element);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return merged;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Get unique key for head element
|
|
445
|
+
*/
|
|
446
|
+
function getHeadElementKey(element: SSRElement): string {
|
|
447
|
+
switch (element.tag) {
|
|
448
|
+
case "title":
|
|
449
|
+
return "title";
|
|
450
|
+
case "meta":
|
|
451
|
+
return `meta-${element.attrs.name || element.attrs.property || ""}`;
|
|
452
|
+
case "link":
|
|
453
|
+
return `link-${element.attrs.rel}-${element.attrs.href}`;
|
|
454
|
+
case "script":
|
|
455
|
+
return `script-${element.attrs.src || ""}`;
|
|
456
|
+
case "style":
|
|
457
|
+
return `style-${element.attrs.id || ""}`;
|
|
458
|
+
default:
|
|
459
|
+
return `${element.tag}-${JSON.stringify(element.attrs)}`;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Render layout tree to string (for debugging)
|
|
465
|
+
*/
|
|
466
|
+
export function layoutTreeToString(node: LayoutNode, indent = 0): string {
|
|
467
|
+
const prefix = " ".repeat(indent);
|
|
468
|
+
let result = `${prefix}${node.layout.segment}\n`;
|
|
469
|
+
|
|
470
|
+
for (const child of node.children) {
|
|
471
|
+
result += layoutTreeToString(child, indent + 1);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return result;
|
|
475
|
+
}
|