@withl5e/l5e 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +24 -0
- package/dist/action.js +10 -0
- package/dist/action.js.map +1 -0
- package/dist/client-D67hK4Yy.js +9 -0
- package/dist/client-D67hK4Yy.js.map +1 -0
- package/dist/entry-server-Ckh6zfgm.js +258 -0
- package/dist/entry-server-Ckh6zfgm.js.map +1 -0
- package/dist/entry-server.js +12 -0
- package/dist/entry-server.js.map +1 -0
- package/dist/generateMetadata-C5QsMS-H.js +144 -0
- package/dist/generateMetadata-C5QsMS-H.js.map +1 -0
- package/dist/index-BIt7MJT9.js +163 -0
- package/dist/index-BIt7MJT9.js.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/island/client.js +5 -0
- package/dist/island/client.js.map +1 -0
- package/dist/island/runtime.js +98 -0
- package/dist/island/runtime.js.map +1 -0
- package/dist/island.js +39 -0
- package/dist/island.js.map +1 -0
- package/dist/jsx-runtime-C2Vw67N2.js +256 -0
- package/dist/jsx-runtime-C2Vw67N2.js.map +1 -0
- package/dist/jsx-runtime.js +26 -0
- package/dist/jsx-runtime.js.map +1 -0
- package/dist/middleware.js +9 -0
- package/dist/middleware.js.map +1 -0
- package/dist/seo.js +7 -0
- package/dist/seo.js.map +1 -0
- package/dist/server.js +489 -0
- package/dist/server.js.map +1 -0
- package/dist/swap/server.js +15 -0
- package/dist/swap/server.js.map +1 -0
- package/dist/swap.js +121 -0
- package/dist/swap.js.map +1 -0
- package/dist/tooltip.js +129 -0
- package/dist/tooltip.js.map +1 -0
- package/dist/vite-plugin.js +381 -0
- package/dist/vite-plugin.js.map +1 -0
- package/index.ts +1 -0
- package/package.json +129 -0
- package/src/action/define-action.ts +8 -0
- package/src/action/index.ts +2 -0
- package/src/action/types.ts +21 -0
- package/src/core/bundler.ts +275 -0
- package/src/core/const.ts +2 -0
- package/src/core/entry-server.d.ts +1 -0
- package/src/core/entry-server.ts +381 -0
- package/src/core/exceptions.ts +80 -0
- package/src/core/head-priority.ts +15 -0
- package/src/core/index.ts +40 -0
- package/src/core/jsx-runtime.ts +325 -0
- package/src/core/jsx-types.d.ts +548 -0
- package/src/core/render.ts +181 -0
- package/src/core/request.ts +31 -0
- package/src/core/server.ts +740 -0
- package/src/core/vite-plugin.ts +779 -0
- package/src/island/ClientIsland.ts +71 -0
- package/src/island/client.ts +3 -0
- package/src/island/index.ts +3 -0
- package/src/island/runtime.ts +149 -0
- package/src/island/strategy-registry.ts +10 -0
- package/src/island/types.ts +28 -0
- package/src/middleware/defineMiddleware.ts +5 -0
- package/src/middleware/index.ts +133 -0
- package/src/middleware/sequence.ts +105 -0
- package/src/middleware/types.ts +28 -0
- package/src/seo/generateMetadata.tsx +559 -0
- package/src/seo/index.ts +10 -0
- package/src/seo/mergeMetadata.ts +200 -0
- package/src/seo/types.ts +316 -0
- package/src/swap/SwapResponse.tsx +16 -0
- package/src/swap/create-swap.ts +121 -0
- package/src/swap/index.ts +8 -0
- package/src/swap/parse.ts +12 -0
- package/src/swap/server.ts +1 -0
- package/src/swap/swap.ts +57 -0
- package/src/swap/types.ts +47 -0
- package/src/swap/utils.ts +7 -0
- package/src/tooltip/index.ts +2 -0
- package/src/tooltip/tooltip-loader.ts +108 -0
- package/src/tooltip/tooltip-runtime.ts +173 -0
- package/types.d.ts +14 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base HTTP Exception class
|
|
3
|
+
*/
|
|
4
|
+
export class HttpException extends Error {
|
|
5
|
+
public readonly statusCode: number;
|
|
6
|
+
public readonly data?: Record<string, any>;
|
|
7
|
+
|
|
8
|
+
constructor(statusCode: number, message: string, data?: Record<string, any>) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'HttpException';
|
|
11
|
+
this.statusCode = statusCode;
|
|
12
|
+
this.data = data;
|
|
13
|
+
|
|
14
|
+
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
|
15
|
+
if (Error.captureStackTrace) {
|
|
16
|
+
Error.captureStackTrace(this, this.constructor);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 400 Bad Request Exception
|
|
23
|
+
*/
|
|
24
|
+
export class BadRequestException extends HttpException {
|
|
25
|
+
constructor(message: string = 'Bad Request', data?: Record<string, any>) {
|
|
26
|
+
super(400, message, data);
|
|
27
|
+
this.name = 'BadRequestException';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 404 Not Found Exception
|
|
33
|
+
*/
|
|
34
|
+
export class NotFoundException extends HttpException {
|
|
35
|
+
constructor(message: string = 'Not Found', data?: Record<string, any>) {
|
|
36
|
+
super(404, message, data);
|
|
37
|
+
this.name = 'NotFoundException';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 500 Internal Server Error Exception
|
|
43
|
+
*/
|
|
44
|
+
export class InternalServerErrorException extends HttpException {
|
|
45
|
+
constructor(message: string = 'Internal Server Error', data?: Record<string, any>) {
|
|
46
|
+
super(500, message, data);
|
|
47
|
+
this.name = 'InternalServerErrorException';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 503 Service Unavailable Exception
|
|
53
|
+
*/
|
|
54
|
+
export class ServiceUnavailableException extends HttpException {
|
|
55
|
+
constructor(message: string = 'Service Unavailable', data?: Record<string, any>) {
|
|
56
|
+
super(503, message, data);
|
|
57
|
+
this.name = 'ServiceUnavailableException';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Redirect Exception - for HTTP redirects (301/302)
|
|
63
|
+
*/
|
|
64
|
+
export class RedirectException extends Error {
|
|
65
|
+
public readonly url: string;
|
|
66
|
+
public readonly statusCode: number;
|
|
67
|
+
public readonly data?: Record<string, any>;
|
|
68
|
+
|
|
69
|
+
constructor(url: string, statusCode: number = 302, data?: Record<string, any>) {
|
|
70
|
+
super(`Redirect to ${url}`);
|
|
71
|
+
this.name = 'RedirectException';
|
|
72
|
+
this.url = url;
|
|
73
|
+
this.statusCode = statusCode;
|
|
74
|
+
this.data = data;
|
|
75
|
+
|
|
76
|
+
if (Error.captureStackTrace) {
|
|
77
|
+
Error.captureStackTrace(this, this.constructor);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Priority constants for Head component rendering order
|
|
3
|
+
* Lower numbers render first (higher priority)
|
|
4
|
+
*/
|
|
5
|
+
export const HEAD_PRIORITY = {
|
|
6
|
+
CRITICAL: 0, // charset, viewport - must be first
|
|
7
|
+
HIGH: 10, // title, description, canonical
|
|
8
|
+
MEDIUM: 50, // meta tags, robots
|
|
9
|
+
SEO: 80, // OpenGraph, Twitter
|
|
10
|
+
LOW: 100, // Custom head elements
|
|
11
|
+
SCRIPTS: 200, // Scripts
|
|
12
|
+
STYLES: 300, // Styles
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export type HeadPriority = (typeof HEAD_PRIORITY)[keyof typeof HEAD_PRIORITY] | number;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export {
|
|
2
|
+
render,
|
|
3
|
+
type GenerateMetadataFunction,
|
|
4
|
+
type GenerateSchemaFunction,
|
|
5
|
+
type LoaderFunction,
|
|
6
|
+
type LoaderResult,
|
|
7
|
+
type RenderResult,
|
|
8
|
+
type RequestInfo,
|
|
9
|
+
type SchemaMarkup,
|
|
10
|
+
} from './entry-server';
|
|
11
|
+
export {
|
|
12
|
+
BadRequestException,
|
|
13
|
+
HttpException,
|
|
14
|
+
InternalServerErrorException,
|
|
15
|
+
NotFoundException,
|
|
16
|
+
RedirectException,
|
|
17
|
+
ServiceUnavailableException,
|
|
18
|
+
} from './exceptions';
|
|
19
|
+
export { HEAD_PRIORITY, type HeadPriority } from './head-priority';
|
|
20
|
+
export * from './jsx-runtime';
|
|
21
|
+
export { escapeHTML, escapeProp, renderJsxToHtmlString, renderToRenderedNode } from './render';
|
|
22
|
+
export {
|
|
23
|
+
createServer,
|
|
24
|
+
startServer,
|
|
25
|
+
type ServerOptions,
|
|
26
|
+
type ServerContext,
|
|
27
|
+
} from './server';
|
|
28
|
+
export { coreVite } from './vite-plugin';
|
|
29
|
+
export {
|
|
30
|
+
createContext,
|
|
31
|
+
defineMiddleware,
|
|
32
|
+
isLocalsSerializable,
|
|
33
|
+
sequence,
|
|
34
|
+
trySerializeLocals,
|
|
35
|
+
type CreateContext,
|
|
36
|
+
type MiddlewareContext,
|
|
37
|
+
type MiddlewareHandler,
|
|
38
|
+
type MiddlewareNext,
|
|
39
|
+
type RewritePayload,
|
|
40
|
+
} from '../middleware';
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { mergeMetadata } from '../seo/mergeMetadata';
|
|
2
|
+
import type { Metadata } from '../seo/types';
|
|
3
|
+
import { RAW_HTML_MARKER } from './const';
|
|
4
|
+
import { RequestInfo } from './entry-server';
|
|
5
|
+
import { HEAD_PRIORITY, type HeadPriority } from './head-priority';
|
|
6
|
+
|
|
7
|
+
export type JSXChild =
|
|
8
|
+
| string
|
|
9
|
+
| number
|
|
10
|
+
| boolean
|
|
11
|
+
| null
|
|
12
|
+
| undefined
|
|
13
|
+
| JSXNode
|
|
14
|
+
| JSXChild[]
|
|
15
|
+
| RawHtmlObject
|
|
16
|
+
| HtmlContentObject;
|
|
17
|
+
|
|
18
|
+
export type RawHtmlObject = {
|
|
19
|
+
[RAW_HTML_MARKER]: true;
|
|
20
|
+
content: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type HtmlContentObject = {
|
|
24
|
+
htmlContent: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type RenderedNode = {
|
|
28
|
+
string: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type JSXNode = {
|
|
32
|
+
type: string | ((props: any) => JSXChild);
|
|
33
|
+
props: Record<string, any>;
|
|
34
|
+
children: JSXChild[];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function jsxFactory(type: any, props: any, ...children: any): JSXNode {
|
|
38
|
+
return { type, props: props || {}, children: children.flat() };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function Fragment({
|
|
42
|
+
children,
|
|
43
|
+
...props
|
|
44
|
+
}: { children?: JSXChild; setHtml?: unknown } & Record<string, any>): JSXChild {
|
|
45
|
+
// Hỗ trợ setHtml cho Fragment
|
|
46
|
+
if (props.setHtml !== undefined) {
|
|
47
|
+
// Trả về object đặc biệt để không bị escape
|
|
48
|
+
return { [RAW_HTML_MARKER]: true, content: props.setHtml?.toString() || '' };
|
|
49
|
+
}
|
|
50
|
+
return children;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// AsyncLocalStorage for render context (request-level)
|
|
54
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
55
|
+
|
|
56
|
+
interface HeadEntry {
|
|
57
|
+
content: JSXChild;
|
|
58
|
+
priority: number; // Số càng nhỏ, render càng sớm
|
|
59
|
+
source?: string; // Để debug (ví dụ: 'layout', 'page', 'seo')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface IslandEntry {
|
|
63
|
+
key: string; // "Counter_a3f2" — registry key
|
|
64
|
+
src: string; // "src/views/.../Counter.tsx" — manifest-compatible path
|
|
65
|
+
name: string; // "Counter" — export name
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface RenderContext {
|
|
69
|
+
clientJsRegistry: Array<{ path: string; from: string }>;
|
|
70
|
+
cssRegistry: Array<{ path: string; from: string }>;
|
|
71
|
+
islandRegistry: IslandEntry[];
|
|
72
|
+
cacheTags: Set<string>;
|
|
73
|
+
headRegistry: HeadEntry[]; // Thay vì JSXChild[]
|
|
74
|
+
metadataStack: Metadata[]; // Stack để track metadata hierarchy
|
|
75
|
+
schemaRegistry: Array<Record<string, any>>; // Schema.org structured data từ loaders
|
|
76
|
+
request: RequestInfo;
|
|
77
|
+
viewName?: string; // View name from route handler
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const renderStore = new AsyncLocalStorage<RenderContext>();
|
|
81
|
+
|
|
82
|
+
// Create context for each request
|
|
83
|
+
function createRequestContext(requestInfo: RequestInfo): RenderContext {
|
|
84
|
+
return {
|
|
85
|
+
clientJsRegistry: [],
|
|
86
|
+
cssRegistry: [],
|
|
87
|
+
islandRegistry: [],
|
|
88
|
+
cacheTags: new Set(),
|
|
89
|
+
headRegistry: [],
|
|
90
|
+
metadataStack: [],
|
|
91
|
+
schemaRegistry: [],
|
|
92
|
+
request: requestInfo,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function useClientJs(path: string): string {
|
|
97
|
+
if (typeof path === 'string' && path.length > 0) {
|
|
98
|
+
const renderContext = renderStore.getStore();
|
|
99
|
+
if (renderContext) {
|
|
100
|
+
renderContext.clientJsRegistry.push({ path, from: 'Unknown' });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return '';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function registerIsland(key: string, src: string, name: string): void {
|
|
107
|
+
const renderContext = renderStore.getStore();
|
|
108
|
+
if (renderContext) {
|
|
109
|
+
// Dedupe by key
|
|
110
|
+
if (!renderContext.islandRegistry.some((e) => e.key === key)) {
|
|
111
|
+
renderContext.islandRegistry.push({ key, src, name });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getIslandEntries(): IslandEntry[] {
|
|
117
|
+
const context = renderStore.getStore();
|
|
118
|
+
if (!context) return [];
|
|
119
|
+
return context.islandRegistry.slice();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function useCss(path: string): string {
|
|
123
|
+
if (typeof path === 'string' && path.length > 0) {
|
|
124
|
+
const renderContext = renderStore.getStore();
|
|
125
|
+
if (renderContext) {
|
|
126
|
+
renderContext.cssRegistry.push({ path, from: 'Unknown' });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return '';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Wrapper to run render in async context
|
|
133
|
+
export function runInRenderContext<T>(
|
|
134
|
+
renderFn: () => T | Promise<T>,
|
|
135
|
+
requestInfo: RequestInfo,
|
|
136
|
+
viewName?: string,
|
|
137
|
+
): Promise<T> {
|
|
138
|
+
const context = createRequestContext(requestInfo);
|
|
139
|
+
if (viewName) {
|
|
140
|
+
context.viewName = viewName;
|
|
141
|
+
}
|
|
142
|
+
return renderStore.run(context, () => Promise.resolve(renderFn()));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Set view name in current render context
|
|
146
|
+
export function setViewName(viewName: string): void {
|
|
147
|
+
const context = renderStore.getStore();
|
|
148
|
+
if (context) {
|
|
149
|
+
context.viewName = viewName;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Get entries from current context
|
|
154
|
+
export function getClientJsEntries(): Array<{ path: string; from: string }> {
|
|
155
|
+
const context = renderStore.getStore();
|
|
156
|
+
if (!context) return [];
|
|
157
|
+
return context.clientJsRegistry.slice();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Get cache tags from current context
|
|
161
|
+
export function getCacheTags(): string[] {
|
|
162
|
+
const context = renderStore.getStore();
|
|
163
|
+
if (!context) return [];
|
|
164
|
+
return Array.from(context.cacheTags);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Add cache tags to current context
|
|
168
|
+
export function addCacheTag(tag: string | string[] | Record<string, boolean>): void {
|
|
169
|
+
const context = renderStore.getStore();
|
|
170
|
+
if (!context) return;
|
|
171
|
+
|
|
172
|
+
if (Array.isArray(tag)) {
|
|
173
|
+
tag.forEach((t) => {
|
|
174
|
+
if (typeof t === 'string' && t.trim()) {
|
|
175
|
+
context.cacheTags.add(t.trim());
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
} else if (typeof tag === 'string' && tag.trim()) {
|
|
179
|
+
context.cacheTags.add(tag.trim());
|
|
180
|
+
} else if (typeof tag === 'object' && tag !== null) {
|
|
181
|
+
Object.entries(tag).forEach(([key, value]) => {
|
|
182
|
+
if (value && typeof key === 'string' && key.trim()) {
|
|
183
|
+
context.cacheTags.add(key.trim());
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Get CSS entries from current context
|
|
190
|
+
export function getCssEntries(): Array<{ path: string; from: string }> {
|
|
191
|
+
const context = renderStore.getStore();
|
|
192
|
+
if (!context) return [];
|
|
193
|
+
return context.cssRegistry.slice();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Head component to collect head elements with priority support
|
|
197
|
+
export function Head({
|
|
198
|
+
children,
|
|
199
|
+
priority = HEAD_PRIORITY.LOW, // Default priority
|
|
200
|
+
}: {
|
|
201
|
+
children?: JSXChild;
|
|
202
|
+
priority?: HeadPriority;
|
|
203
|
+
}): null {
|
|
204
|
+
const renderContext = renderStore.getStore();
|
|
205
|
+
if (renderContext && children) {
|
|
206
|
+
renderContext.headRegistry.push({
|
|
207
|
+
content: children,
|
|
208
|
+
priority: typeof priority === 'number' ? priority : HEAD_PRIORITY.LOW,
|
|
209
|
+
source: 'manual',
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Sort theo priority sau mỗi lần push để đảm bảo thứ tự đúng
|
|
213
|
+
renderContext.headRegistry.sort((a, b) => a.priority - b.priority);
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Get head content from current context (already sorted by priority)
|
|
219
|
+
export function getHeadContent(): JSXChild[] {
|
|
220
|
+
const context = renderStore.getStore();
|
|
221
|
+
if (!context) return [];
|
|
222
|
+
// Đã được sort trong Head component, chỉ cần map để lấy content
|
|
223
|
+
return context.headRegistry.map((entry) => entry.content);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Push metadata to stack (for hierarchical metadata support)
|
|
227
|
+
export function pushMetadata(metadata: Metadata): void {
|
|
228
|
+
const context = renderStore.getStore();
|
|
229
|
+
if (context && metadata) {
|
|
230
|
+
context.metadataStack.push(metadata);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Resolve and merge all metadata from stack (root → leaf)
|
|
235
|
+
export function resolveMetadata(): Metadata | null {
|
|
236
|
+
const context = renderStore.getStore();
|
|
237
|
+
if (!context || context.metadataStack.length === 0) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Merge from root → leaf (reduce left to right)
|
|
242
|
+
return context.metadataStack.reduce(
|
|
243
|
+
(acc, current) => mergeMetadata(acc, current),
|
|
244
|
+
null as Metadata | null,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Push schema to registry (for schema markup from loaders)
|
|
249
|
+
// Accepts schema-dts types (WithContext<T> or array of schemas)
|
|
250
|
+
export function pushSchema(schema: any | Array<any>): void {
|
|
251
|
+
const context = renderStore.getStore();
|
|
252
|
+
if (!context) return;
|
|
253
|
+
|
|
254
|
+
if (Array.isArray(schema)) {
|
|
255
|
+
context.schemaRegistry.push(...schema);
|
|
256
|
+
} else {
|
|
257
|
+
context.schemaRegistry.push(schema);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Get all schemas from registry
|
|
262
|
+
export function getSchemas(): Array<Record<string, any>> {
|
|
263
|
+
const context = renderStore.getStore();
|
|
264
|
+
if (!context) return [];
|
|
265
|
+
return context.schemaRegistry.slice();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Hook to get render request context
|
|
269
|
+
export function useRequest() {
|
|
270
|
+
const context = renderStore.getStore();
|
|
271
|
+
|
|
272
|
+
if (!context) {
|
|
273
|
+
throw new Error('useRequest called outside of render context');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
request: context.request,
|
|
278
|
+
view: context.viewName,
|
|
279
|
+
locals: (context.request.locals ?? {}) as Record<string, unknown>,
|
|
280
|
+
|
|
281
|
+
// Add cache tags
|
|
282
|
+
addCacheTag: (tag: string | string[] | Record<string, boolean>) => {
|
|
283
|
+
addCacheTag(tag);
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
// Get all cache tags
|
|
287
|
+
getCacheTags: () => {
|
|
288
|
+
return Array.from(context.cacheTags);
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Checks if a value is a valid JSX element (JSXNode)
|
|
295
|
+
* Similar to React.isValidElement
|
|
296
|
+
*/
|
|
297
|
+
export function isValidElement(value: any): value is JSXNode {
|
|
298
|
+
return (
|
|
299
|
+
value !== null &&
|
|
300
|
+
typeof value === 'object' &&
|
|
301
|
+
'type' in value &&
|
|
302
|
+
'props' in value &&
|
|
303
|
+
'children' in value &&
|
|
304
|
+
(typeof value.type === 'string' || typeof value.type === 'function')
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Clones a JSX element with new props and/or children
|
|
310
|
+
* Similar to React.cloneElement
|
|
311
|
+
*/
|
|
312
|
+
export function cloneElement(
|
|
313
|
+
element: JSXNode,
|
|
314
|
+
props?: Record<string, any>,
|
|
315
|
+
...children: JSXChild[]
|
|
316
|
+
): JSXNode {
|
|
317
|
+
const newProps = { ...element.props, ...props };
|
|
318
|
+
const newChildren = children.length > 0 ? children.flat() : element.children;
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
type: element.type,
|
|
322
|
+
props: newProps,
|
|
323
|
+
children: newChildren,
|
|
324
|
+
};
|
|
325
|
+
}
|