@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,275 @@
|
|
|
1
|
+
/// <reference path="./jsx-types.d.ts" />
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { rollup, type OutputOptions, type RollupOptions } from 'rollup';
|
|
6
|
+
|
|
7
|
+
interface BundledFile {
|
|
8
|
+
content: string;
|
|
9
|
+
hash: string;
|
|
10
|
+
filename: string;
|
|
11
|
+
mimeType: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Memory map để lưu bundled files
|
|
15
|
+
const bundledFilesMap = new Map<string, BundledFile>();
|
|
16
|
+
|
|
17
|
+
// Cache map để deduplicate bundling requests (cacheKey → entry chunk fileName)
|
|
18
|
+
const bundleCache = new Map<string, string>();
|
|
19
|
+
const cssCache = new Map<string, string>();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate hash từ content
|
|
23
|
+
*/
|
|
24
|
+
function generateHash(content: string): string {
|
|
25
|
+
return createHash('sha256').update(content).digest('hex').substring(0, 16);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Bundle JavaScript files từ dist/client thành 1 file
|
|
30
|
+
* Trong production, các file đã được build sẵn trong dist/client
|
|
31
|
+
*/
|
|
32
|
+
export async function bundleScripts(
|
|
33
|
+
scriptPaths: string[],
|
|
34
|
+
rootDir: string,
|
|
35
|
+
distClientDir: string,
|
|
36
|
+
): Promise<{ hash: string; filename: string; content: string }> {
|
|
37
|
+
if (scriptPaths.length === 0) {
|
|
38
|
+
return { hash: '', filename: '', content: '' };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Dedupe paths (remove duplicates)
|
|
42
|
+
const uniquePaths = [...new Set(scriptPaths)];
|
|
43
|
+
|
|
44
|
+
// Tạo cache key từ sorted unique paths
|
|
45
|
+
const cacheKey = `scripts:${uniquePaths.sort().join(',')}`;
|
|
46
|
+
|
|
47
|
+
// Kiểm tra cache - return entry chunk info if already bundled
|
|
48
|
+
const cachedEntryFileName = bundleCache.get(cacheKey);
|
|
49
|
+
if (cachedEntryFileName) {
|
|
50
|
+
const entryFile = bundledFilesMap.get(cachedEntryFileName);
|
|
51
|
+
if (entryFile) {
|
|
52
|
+
return {
|
|
53
|
+
hash: entryFile.hash,
|
|
54
|
+
filename: entryFile.filename,
|
|
55
|
+
content: entryFile.content,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Temp file path for cleanup
|
|
61
|
+
let entryFile: string | null = null;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
// Sử dụng rollup để bundle nếu cần (resolve imports, etc)
|
|
65
|
+
// Tạo temp entry file
|
|
66
|
+
const hash = generateHash(uniquePaths.join('\n'));
|
|
67
|
+
const tempDir = path.join(rootDir, '.temp-bundle');
|
|
68
|
+
await fs.mkdir(tempDir, { recursive: true }).catch(() => {});
|
|
69
|
+
|
|
70
|
+
entryFile = path.join(tempDir, `entry-${hash}.js`);
|
|
71
|
+
// Tạo entry file import tất cả scripts
|
|
72
|
+
const entryContent = uniquePaths
|
|
73
|
+
.map((p, i) => {
|
|
74
|
+
const filePath = p.startsWith('/')
|
|
75
|
+
? path.join(distClientDir, p.substring(1))
|
|
76
|
+
: path.join(distClientDir, p);
|
|
77
|
+
return `import ${JSON.stringify(filePath)};`;
|
|
78
|
+
})
|
|
79
|
+
.join('\n');
|
|
80
|
+
|
|
81
|
+
await fs.writeFile(entryFile, entryContent, 'utf-8');
|
|
82
|
+
console.log(`[bundler] Wrote entry file to ${entryFile}`);
|
|
83
|
+
console.log(`[bundler] Entry content: ${entryContent}`);
|
|
84
|
+
// Rollup config để bundle
|
|
85
|
+
const rollupOptions: RollupOptions = {
|
|
86
|
+
input: entryFile,
|
|
87
|
+
plugins: [
|
|
88
|
+
{
|
|
89
|
+
name: 'vendor-path-rewriter',
|
|
90
|
+
resolveId(source, importer, _options) {
|
|
91
|
+
// Handle vendor/chunk/global files: convert absolute paths to web paths
|
|
92
|
+
// Global files (*.global.*) are already loaded by client.global.ts —
|
|
93
|
+
// re-bundling them would create duplicate module instances (e.g. nanostores)
|
|
94
|
+
if (
|
|
95
|
+
source.includes('vendor-') ||
|
|
96
|
+
source.includes('chunk-') ||
|
|
97
|
+
source.includes('.global')
|
|
98
|
+
) {
|
|
99
|
+
console.log(`[bundler] Resolving source: ${source}`);
|
|
100
|
+
if (path.isAbsolute(source)) {
|
|
101
|
+
console.log(`[bundler] Resolving absolute path: ${source}`);
|
|
102
|
+
// e.g., C:\...\dist\client\assets\vendor-react-XXX.js -> /assets/vendor-react-XXX.js
|
|
103
|
+
const relativePath = path.relative(distClientDir, source);
|
|
104
|
+
const webPath = '/' + relativePath.replace(/\\/g, '/');
|
|
105
|
+
return { id: webPath, external: true };
|
|
106
|
+
} else if (importer && source.startsWith('.')) {
|
|
107
|
+
console.log(
|
|
108
|
+
`[bundler] Resolving relative path: ${source} from importer: ${importer}`,
|
|
109
|
+
);
|
|
110
|
+
// Relative path like ./auth.global-BOVr81Z5.js — resolve from importer
|
|
111
|
+
const resolved = path.resolve(path.dirname(importer), source);
|
|
112
|
+
const relativePath = path.relative(distClientDir, resolved);
|
|
113
|
+
const webPath = '/' + relativePath.replace(/\\/g, '/');
|
|
114
|
+
return { id: webPath, external: true };
|
|
115
|
+
} else {
|
|
116
|
+
console.log(`[bundler] Resolving source: ${source}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return null; // Let other plugins/external handle
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
external: (id) => {
|
|
124
|
+
// External node_modules
|
|
125
|
+
if (!id.startsWith('.') && !path.isAbsolute(id)) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Let plugin handle vendor/chunk/global files (don't mark external here)
|
|
130
|
+
if (id.includes('vendor-') || id.includes('chunk-') || id.includes('.global')) {
|
|
131
|
+
return false; // Let plugin's resolveId handle path rewriting
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return false;
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const outputOptions: OutputOptions = {
|
|
139
|
+
format: 'es',
|
|
140
|
+
inlineDynamicImports: false,
|
|
141
|
+
entryFileNames: 'bundle-[hash].js',
|
|
142
|
+
chunkFileNames: 'bundle-[hash].js',
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const bundle = await rollup(rollupOptions);
|
|
146
|
+
const { output } = await bundle.generate(outputOptions);
|
|
147
|
+
await bundle.close();
|
|
148
|
+
|
|
149
|
+
// Lấy bundled content từ rollup
|
|
150
|
+
|
|
151
|
+
output.forEach((o) => {
|
|
152
|
+
if (o.type !== 'chunk') {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// Lưu vào map với key = fileName
|
|
156
|
+
const bundledFile: BundledFile = {
|
|
157
|
+
content: o.code || '',
|
|
158
|
+
hash: generateHash(o.code || ''),
|
|
159
|
+
filename: o.fileName,
|
|
160
|
+
mimeType: 'application/javascript',
|
|
161
|
+
};
|
|
162
|
+
bundledFilesMap.set(o.fileName, bundledFile);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Cache entry chunk fileName for deduplication
|
|
166
|
+
const entryChunk = output[0];
|
|
167
|
+
if (entryChunk?.type === 'chunk') {
|
|
168
|
+
bundleCache.set(cacheKey, entryChunk.fileName);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Return entry chunk info
|
|
172
|
+
return {
|
|
173
|
+
hash: generateHash(output[0]?.code || ''),
|
|
174
|
+
filename: output[0]?.fileName || '',
|
|
175
|
+
content: output[0]?.code || '',
|
|
176
|
+
};
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.error('[bundler] Error bundling scripts:', error);
|
|
179
|
+
return { hash: '', filename: '', content: '' };
|
|
180
|
+
} finally {
|
|
181
|
+
// Cleanup temp entry file
|
|
182
|
+
if (entryFile) {
|
|
183
|
+
await fs.unlink(entryFile).catch(() => {
|
|
184
|
+
// Ignore cleanup errors
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Bundle CSS files từ dist/client thành 1 file
|
|
192
|
+
* Trong production, các file đã được build sẵn trong dist/client
|
|
193
|
+
*/
|
|
194
|
+
export async function bundleCss(
|
|
195
|
+
cssPaths: string[],
|
|
196
|
+
rootDir: string,
|
|
197
|
+
distClientDir: string,
|
|
198
|
+
): Promise<{ hash: string; filename: string; content: string }> {
|
|
199
|
+
if (cssPaths.length === 0) {
|
|
200
|
+
return { hash: '', filename: '', content: '' };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Dedupe paths (remove duplicates)
|
|
204
|
+
const uniquePaths = [...new Set(cssPaths)];
|
|
205
|
+
|
|
206
|
+
// Tạo cache key từ sorted unique paths
|
|
207
|
+
const cacheKey = `css:${uniquePaths.sort().join(',')}`;
|
|
208
|
+
|
|
209
|
+
// Kiểm tra cache - return cached file if already bundled
|
|
210
|
+
const cachedFileName = cssCache.get(cacheKey);
|
|
211
|
+
if (cachedFileName) {
|
|
212
|
+
const cachedFile = bundledFilesMap.get(cachedFileName);
|
|
213
|
+
if (cachedFile) {
|
|
214
|
+
return {
|
|
215
|
+
hash: cachedFile.hash,
|
|
216
|
+
filename: cachedFile.filename,
|
|
217
|
+
content: cachedFile.content,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
// Đọc và gộp tất cả CSS files từ dist/client
|
|
224
|
+
const cssContents: string[] = [];
|
|
225
|
+
|
|
226
|
+
for (const cssPath of uniquePaths) {
|
|
227
|
+
// cssPath có thể là "/assets/xxx.css" hoặc từ manifest
|
|
228
|
+
const filePath = cssPath.startsWith('/')
|
|
229
|
+
? path.join(distClientDir, cssPath.substring(1))
|
|
230
|
+
: path.join(distClientDir, cssPath);
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
234
|
+
cssContents.push(`/* ${cssPath} */\n${content}\n`);
|
|
235
|
+
} catch (err) {
|
|
236
|
+
console.warn(`[bundler] Failed to read CSS file: ${cssPath}`, err);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const bundledContent = cssContents.join('\n\n');
|
|
241
|
+
const hash = generateHash(bundledContent);
|
|
242
|
+
const filename = `bundle-${hash}.css`;
|
|
243
|
+
|
|
244
|
+
// Lưu vào map với key = filename
|
|
245
|
+
const bundledFile: BundledFile = {
|
|
246
|
+
content: bundledContent,
|
|
247
|
+
hash,
|
|
248
|
+
filename,
|
|
249
|
+
mimeType: 'text/css',
|
|
250
|
+
};
|
|
251
|
+
bundledFilesMap.set(filename, bundledFile);
|
|
252
|
+
|
|
253
|
+
// Cache filename for deduplication
|
|
254
|
+
cssCache.set(cacheKey, filename);
|
|
255
|
+
|
|
256
|
+
return { hash, filename, content: bundledContent };
|
|
257
|
+
} catch (error) {
|
|
258
|
+
console.error('[bundler] Error bundling CSS:', error);
|
|
259
|
+
return { hash: '', filename: '', content: '' };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Get bundled file từ map
|
|
265
|
+
*/
|
|
266
|
+
export function getBundledFile(filename: string): BundledFile | undefined {
|
|
267
|
+
return bundledFilesMap.get(filename);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Clear bundled files map (useful for testing)
|
|
272
|
+
*/
|
|
273
|
+
export function clearBundledFiles(): void {
|
|
274
|
+
bundledFilesMap.clear();
|
|
275
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './entry-server';
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/// <reference path="./jsx-types.d.ts" />
|
|
2
|
+
import { MetadataRenderer } from '../seo/generateMetadata';
|
|
3
|
+
import type { Metadata } from '../seo/types';
|
|
4
|
+
import {
|
|
5
|
+
HttpException,
|
|
6
|
+
InternalServerErrorException,
|
|
7
|
+
NotFoundException,
|
|
8
|
+
RedirectException,
|
|
9
|
+
ServiceUnavailableException,
|
|
10
|
+
} from './exceptions';
|
|
11
|
+
import { HEAD_PRIORITY } from './head-priority';
|
|
12
|
+
import {
|
|
13
|
+
addCacheTag,
|
|
14
|
+
getCacheTags,
|
|
15
|
+
getClientJsEntries,
|
|
16
|
+
getCssEntries,
|
|
17
|
+
getHeadContent,
|
|
18
|
+
getIslandEntries,
|
|
19
|
+
getSchemas,
|
|
20
|
+
jsxFactory as h,
|
|
21
|
+
Head,
|
|
22
|
+
pushMetadata,
|
|
23
|
+
pushSchema,
|
|
24
|
+
runInRenderContext,
|
|
25
|
+
setViewName,
|
|
26
|
+
} from './jsx-runtime';
|
|
27
|
+
import { renderJsxToHtmlString } from './render';
|
|
28
|
+
// @ts-ignore - Virtual modules provided by Vite plugin
|
|
29
|
+
import { viewComponents, viewLoaders } from 'virtual:l5e-views';
|
|
30
|
+
// @ts-ignore - Virtual modules provided by Vite plugin
|
|
31
|
+
import routeHandler from 'virtual:l5e-route';
|
|
32
|
+
// @ts-ignore - Virtual modules provided by Vite plugin
|
|
33
|
+
import { globalLoader } from 'virtual:l5e-global-loader';
|
|
34
|
+
// @ts-ignore - Virtual modules provided by Vite plugin
|
|
35
|
+
export { loadMiddleware } from 'virtual:l5e-middleware';
|
|
36
|
+
|
|
37
|
+
export interface RawResponse {
|
|
38
|
+
body: string | Buffer;
|
|
39
|
+
contentType: string;
|
|
40
|
+
statusCode?: number;
|
|
41
|
+
headers?: Record<string, string>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface RenderResult {
|
|
45
|
+
html?: string;
|
|
46
|
+
scripts?: string[];
|
|
47
|
+
styles?: string[];
|
|
48
|
+
islands?: Array<{ key: string; src: string; name: string }>;
|
|
49
|
+
head?: string;
|
|
50
|
+
lang?: string;
|
|
51
|
+
statusCode?: number;
|
|
52
|
+
maxAge?: number;
|
|
53
|
+
sMaxAge?: number;
|
|
54
|
+
swr?: number;
|
|
55
|
+
cacheTags?: string[];
|
|
56
|
+
redirect?: { url: string; statusCode: number };
|
|
57
|
+
rawResponse?: RawResponse;
|
|
58
|
+
rawHtml?: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface RequestInfo {
|
|
62
|
+
url?: URL;
|
|
63
|
+
path?: string;
|
|
64
|
+
pathname?: string;
|
|
65
|
+
method?: string;
|
|
66
|
+
headers?: Record<string, any>;
|
|
67
|
+
cookies?: Record<string, string>;
|
|
68
|
+
query?: Record<string, any>;
|
|
69
|
+
ip?: string;
|
|
70
|
+
locals?: Record<string, unknown>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// SchemaMarkup type - có thể là single schema hoặc array of schemas
|
|
74
|
+
// Sử dụng any để tương thích với schema-dts types từ frontend
|
|
75
|
+
export type SchemaMarkup = any | Array<any>;
|
|
76
|
+
|
|
77
|
+
export interface LoaderResult {
|
|
78
|
+
props?: Record<string, any>;
|
|
79
|
+
lang?: string;
|
|
80
|
+
maxAge?: number;
|
|
81
|
+
sMaxAge?: number;
|
|
82
|
+
swr?: number;
|
|
83
|
+
cacheTags?: string[] | Record<string, boolean>;
|
|
84
|
+
rawResponse?: RawResponse;
|
|
85
|
+
rawHtml?: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type LoaderFunction = (requestInfo: RequestInfo) => Promise<LoaderResult>;
|
|
89
|
+
|
|
90
|
+
export type GenerateMetadataFunction = (requestInfo: RequestInfo, props: any) => Metadata | null;
|
|
91
|
+
|
|
92
|
+
export type GenerateSchemaFunction = (requestInfo: RequestInfo, props: any) => SchemaMarkup | null;
|
|
93
|
+
|
|
94
|
+
export interface GlobalLoaderModule {
|
|
95
|
+
loader: LoaderFunction;
|
|
96
|
+
generateMetadata?: GenerateMetadataFunction;
|
|
97
|
+
generateSchema?: GenerateSchemaFunction;
|
|
98
|
+
shouldIgnore?: (viewName: string) => boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Helper function to render error view
|
|
103
|
+
*/
|
|
104
|
+
async function renderErrorView(err: HttpException, lang?: string): Promise<RenderResult> {
|
|
105
|
+
const errorViewName = `_error`;
|
|
106
|
+
const errorComponentPath = `/src/views/${errorViewName}/index.tsx`;
|
|
107
|
+
|
|
108
|
+
// Set error view name in context
|
|
109
|
+
setViewName(errorViewName);
|
|
110
|
+
|
|
111
|
+
// Try to load error view
|
|
112
|
+
const errorComponentModule = viewComponents[errorComponentPath]
|
|
113
|
+
? await viewComponents[errorComponentPath]()
|
|
114
|
+
: null;
|
|
115
|
+
|
|
116
|
+
if (errorComponentModule?.default) {
|
|
117
|
+
// Render error view with exception data
|
|
118
|
+
const errorProps = {
|
|
119
|
+
statusCode: err.statusCode,
|
|
120
|
+
message: err.message,
|
|
121
|
+
data: err.data,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const Component = errorComponentModule.default;
|
|
125
|
+
const htmlBody = renderJsxToHtmlString(h(Component, errorProps));
|
|
126
|
+
const clientEntries = getClientJsEntries();
|
|
127
|
+
const cssEntries = getCssEntries();
|
|
128
|
+
const headContent = getHeadContent();
|
|
129
|
+
|
|
130
|
+
const headHtml =
|
|
131
|
+
headContent.length > 0
|
|
132
|
+
? headContent.map((content) => renderJsxToHtmlString(content)).join('\n ')
|
|
133
|
+
: undefined;
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
html: htmlBody,
|
|
137
|
+
scripts: clientEntries.map((entry) => entry.path),
|
|
138
|
+
styles: cssEntries.map((entry) => entry.path),
|
|
139
|
+
head: headHtml,
|
|
140
|
+
lang,
|
|
141
|
+
statusCode: err.statusCode,
|
|
142
|
+
};
|
|
143
|
+
} else {
|
|
144
|
+
// No error view found, render default error message
|
|
145
|
+
const html = h('div', {}, `${err.statusCode} - ${err.message}`);
|
|
146
|
+
return {
|
|
147
|
+
html: renderJsxToHtmlString(html),
|
|
148
|
+
statusCode: err.statusCode,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function render(url: string, requestInfo: RequestInfo = {}): Promise<RenderResult> {
|
|
154
|
+
return runInRenderContext(async () => {
|
|
155
|
+
try {
|
|
156
|
+
// Step 1: Call route handler to get view name
|
|
157
|
+
const viewName = await routeHandler(requestInfo);
|
|
158
|
+
|
|
159
|
+
if (!viewName) {
|
|
160
|
+
// Throw NotFoundException to render error_404 view
|
|
161
|
+
throw new NotFoundException('Page not found', {
|
|
162
|
+
path: requestInfo.path,
|
|
163
|
+
pathname: requestInfo.pathname,
|
|
164
|
+
url: requestInfo.url?.href,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Set view name in render context
|
|
169
|
+
setViewName(viewName);
|
|
170
|
+
|
|
171
|
+
// Step 2: Load global loader (optional)
|
|
172
|
+
let globalProps: Record<string, any> = {};
|
|
173
|
+
let lang: string | undefined;
|
|
174
|
+
|
|
175
|
+
// Try to load global loader
|
|
176
|
+
const globalLoaderPathTs = '/src/global-loader.ts';
|
|
177
|
+
|
|
178
|
+
const globalLoaderModule: GlobalLoaderModule | null = globalLoader[globalLoaderPathTs]
|
|
179
|
+
? await globalLoader[globalLoaderPathTs]()
|
|
180
|
+
: null;
|
|
181
|
+
|
|
182
|
+
// Run global loader if exists and not ignored
|
|
183
|
+
if (globalLoaderModule?.loader) {
|
|
184
|
+
const shouldIgnore = globalLoaderModule.shouldIgnore?.(viewName) || false;
|
|
185
|
+
|
|
186
|
+
if (!shouldIgnore) {
|
|
187
|
+
const globalLoaderResult = await globalLoaderModule.loader(requestInfo);
|
|
188
|
+
|
|
189
|
+
globalProps = globalLoaderResult.props || {};
|
|
190
|
+
lang = globalLoaderResult.lang; // Extract lang from global loader
|
|
191
|
+
|
|
192
|
+
if (globalLoaderResult.cacheTags) {
|
|
193
|
+
addCacheTag(globalLoaderResult.cacheTags);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// generateMetadata và generateSchema sẽ được gọi sau khi có props
|
|
197
|
+
} else {
|
|
198
|
+
console.info(`Global loader ignored for view: ${viewName}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Step 3: Dynamic import view loader (optional)
|
|
203
|
+
let viewProps: Record<string, any> = {};
|
|
204
|
+
let maxAge: number | undefined;
|
|
205
|
+
let sMaxAge: number | undefined;
|
|
206
|
+
let swr: number | undefined;
|
|
207
|
+
let rawHtml: boolean = false;
|
|
208
|
+
const loaderPathTs = `/src/views/${viewName}/loader.ts`;
|
|
209
|
+
|
|
210
|
+
// Try TypeScript loader formats only
|
|
211
|
+
const loaderModule = viewLoaders[loaderPathTs] ? await viewLoaders[loaderPathTs]() : null;
|
|
212
|
+
|
|
213
|
+
if (loaderModule?.loader) {
|
|
214
|
+
const loaderResult = await loaderModule.loader(requestInfo);
|
|
215
|
+
|
|
216
|
+
// Check if loader returns raw response
|
|
217
|
+
if (loaderResult.rawResponse) {
|
|
218
|
+
return {
|
|
219
|
+
rawResponse: loaderResult.rawResponse,
|
|
220
|
+
statusCode: loaderResult.rawResponse.statusCode || 200,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
viewProps = loaderResult.props || {};
|
|
225
|
+
maxAge = loaderResult.maxAge;
|
|
226
|
+
sMaxAge = loaderResult.sMaxAge;
|
|
227
|
+
swr = loaderResult.swr;
|
|
228
|
+
rawHtml = loaderResult.rawHtml || false;
|
|
229
|
+
|
|
230
|
+
if (loaderResult.cacheTags) {
|
|
231
|
+
addCacheTag(loaderResult.cacheTags);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// View loader lang overrides global loader lang
|
|
235
|
+
if (loaderResult.lang) {
|
|
236
|
+
lang = loaderResult.lang;
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
console.info(`No loader for view: ${viewName}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Merge props: global props first, then view props (view can override)
|
|
243
|
+
const props = { ...globalProps, ...viewProps };
|
|
244
|
+
|
|
245
|
+
// Step 3.5: Generate metadata và schema từ generateMetadata và generateSchema functions
|
|
246
|
+
// Global generateMetadata (parent metadata)
|
|
247
|
+
if (globalLoaderModule?.generateMetadata) {
|
|
248
|
+
const globalMetadata = globalLoaderModule.generateMetadata(requestInfo, props);
|
|
249
|
+
if (globalMetadata) {
|
|
250
|
+
pushMetadata(globalMetadata);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// View generateMetadata (child metadata, sẽ merge với parent)
|
|
255
|
+
if (loaderModule?.generateMetadata) {
|
|
256
|
+
const viewMetadata = loaderModule.generateMetadata(requestInfo, props);
|
|
257
|
+
if (viewMetadata) {
|
|
258
|
+
pushMetadata(viewMetadata);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Global generateSchema (base schemas)
|
|
263
|
+
if (globalLoaderModule?.generateSchema) {
|
|
264
|
+
const globalSchema = globalLoaderModule.generateSchema(requestInfo, props);
|
|
265
|
+
if (globalSchema) {
|
|
266
|
+
pushSchema(globalSchema);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// View generateSchema (view-specific schemas)
|
|
271
|
+
if (loaderModule?.generateSchema) {
|
|
272
|
+
const viewSchema = loaderModule.generateSchema(requestInfo, props);
|
|
273
|
+
if (viewSchema) {
|
|
274
|
+
pushSchema(viewSchema);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Step 4: Dynamic import component (required)
|
|
279
|
+
const componentPathTsx = `/src/views/${viewName}/index.tsx`;
|
|
280
|
+
|
|
281
|
+
const componentModule = viewComponents[componentPathTsx]
|
|
282
|
+
? await viewComponents[componentPathTsx]()
|
|
283
|
+
: null;
|
|
284
|
+
|
|
285
|
+
const Component = componentModule?.default;
|
|
286
|
+
|
|
287
|
+
if (!Component) {
|
|
288
|
+
console.error(`View component not found: ${viewName}`);
|
|
289
|
+
|
|
290
|
+
// Check if in development or production mode
|
|
291
|
+
const isDevelopment = process.env.NODE_ENV !== 'production';
|
|
292
|
+
|
|
293
|
+
if (isDevelopment) {
|
|
294
|
+
// Development: provide detailed error information
|
|
295
|
+
throw new InternalServerErrorException(`View component not found: "${viewName}"`, {
|
|
296
|
+
viewName,
|
|
297
|
+
expectedPath: componentPathTsx,
|
|
298
|
+
availableViews: Object.keys(viewComponents),
|
|
299
|
+
hint: `Make sure the view component exists at ${componentPathTsx} and exports a default component`,
|
|
300
|
+
timestamp: new Date().toISOString(),
|
|
301
|
+
});
|
|
302
|
+
} else {
|
|
303
|
+
// Production: simple error message
|
|
304
|
+
throw new InternalServerErrorException('Internal Server Error');
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Auto-render MetadataRenderer trước khi render component
|
|
309
|
+
// MetadataRenderer sẽ push metadata vào headRegistry thông qua Head component
|
|
310
|
+
renderJsxToHtmlString(h(MetadataRenderer, {}));
|
|
311
|
+
|
|
312
|
+
// Auto-render schemas vào headRegistry
|
|
313
|
+
const schemas = getSchemas();
|
|
314
|
+
schemas.forEach((schema) => {
|
|
315
|
+
const schemaJson = JSON.stringify(schema);
|
|
316
|
+
// Push schema vào headRegistry thông qua Head component
|
|
317
|
+
// Head component chỉ push vào registry, không cần renderJsxToHtmlString
|
|
318
|
+
Head({
|
|
319
|
+
priority: HEAD_PRIORITY.SEO,
|
|
320
|
+
children: h('script', {
|
|
321
|
+
type: 'application/ld+json',
|
|
322
|
+
setHtml: schemaJson,
|
|
323
|
+
}),
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Render component (có thể có Head components khác)
|
|
328
|
+
const htmlBody = renderJsxToHtmlString(h(Component, props));
|
|
329
|
+
const clientEntries = getClientJsEntries();
|
|
330
|
+
const cssEntries = getCssEntries();
|
|
331
|
+
const islandEntries = getIslandEntries();
|
|
332
|
+
const cacheTags = getCacheTags();
|
|
333
|
+
const headContent = getHeadContent();
|
|
334
|
+
|
|
335
|
+
// Render head content to HTML string
|
|
336
|
+
const headHtml =
|
|
337
|
+
headContent.length > 0
|
|
338
|
+
? headContent.map((content) => renderJsxToHtmlString(content)).join('\n ')
|
|
339
|
+
: undefined;
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
html: htmlBody,
|
|
343
|
+
scripts: clientEntries.map((entry) => entry.path),
|
|
344
|
+
styles: cssEntries.map((entry) => entry.path),
|
|
345
|
+
islands: islandEntries.length > 0 ? islandEntries : undefined,
|
|
346
|
+
head: headHtml,
|
|
347
|
+
lang,
|
|
348
|
+
maxAge,
|
|
349
|
+
sMaxAge,
|
|
350
|
+
swr,
|
|
351
|
+
cacheTags,
|
|
352
|
+
rawHtml,
|
|
353
|
+
};
|
|
354
|
+
} catch (err: any) {
|
|
355
|
+
// Handle RedirectException
|
|
356
|
+
if (err instanceof RedirectException) {
|
|
357
|
+
return {
|
|
358
|
+
html: '',
|
|
359
|
+
redirect: {
|
|
360
|
+
url: err.url,
|
|
361
|
+
statusCode: err.statusCode,
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Handle HttpException
|
|
367
|
+
if (err instanceof HttpException) {
|
|
368
|
+
return await renderErrorView(err);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// For other errors, convert to ServiceUnavailableException
|
|
372
|
+
console.error(`Failed to render:`, err);
|
|
373
|
+
const serviceError = new ServiceUnavailableException(err.message || 'Internal Server Error', {
|
|
374
|
+
originalError: err.name,
|
|
375
|
+
stack: err.stack,
|
|
376
|
+
timestamp: new Date().toISOString(),
|
|
377
|
+
});
|
|
378
|
+
return await renderErrorView(serviceError);
|
|
379
|
+
}
|
|
380
|
+
}, requestInfo);
|
|
381
|
+
}
|