@withl5e/l5e 0.2.7 → 0.3.1
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/dist/defineMiddleware-VdHQuiUc.js +7 -0
- package/dist/defineMiddleware-VdHQuiUc.js.map +1 -0
- package/dist/entry-server.js +1 -1
- package/dist/{generateMetadata-CIFVlViV.js → generateMetadata-Cn2uJjg_.js} +6 -6
- package/dist/generateMetadata-Cn2uJjg_.js.map +1 -0
- package/dist/i18n.js +12 -0
- package/dist/i18n.js.map +1 -0
- package/dist/{index-BlfNBupp.js → index-CybB8Tdk.js} +30 -33
- package/dist/index-CybB8Tdk.js.map +1 -0
- package/dist/index.js +24 -23
- package/dist/index.js.map +1 -1
- package/dist/middleware.js +5 -4
- package/dist/middleware.js.map +1 -1
- package/dist/seo.js +1 -1
- package/dist/server.js +374 -380
- package/dist/server.js.map +1 -1
- package/dist/tooltip.js +92 -74
- package/dist/tooltip.js.map +1 -1
- package/package.json +5 -1
- package/src/core/bundler.ts +201 -205
- package/src/core/jsx-types.d.ts +1 -0
- package/src/core/server.ts +52 -29
- package/src/i18n/index.ts +2 -0
- package/src/i18n/middleware.ts +50 -0
- package/src/i18n/types.ts +13 -0
- package/src/seo/generateMetadata.tsx +7 -2
- package/src/seo/types.ts +16 -0
- package/src/tooltip/index.ts +2 -1
- package/src/tooltip/tooltip-runtime.ts +71 -4
- package/dist/generateMetadata-CIFVlViV.js.map +0 -1
- package/dist/index-BlfNBupp.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@withl5e/l5e",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "HTML-first SSR MPA framework with loaders, middleware, islands, actions, swap, SEO and cache controls.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -82,6 +82,10 @@
|
|
|
82
82
|
"./router": {
|
|
83
83
|
"types": "./src/router/index.ts",
|
|
84
84
|
"default": "./dist/router.js"
|
|
85
|
+
},
|
|
86
|
+
"./i18n": {
|
|
87
|
+
"types": "./src/i18n/index.ts",
|
|
88
|
+
"default": "./dist/i18n.js"
|
|
85
89
|
}
|
|
86
90
|
},
|
|
87
91
|
"dependencies": {
|
package/src/core/bundler.ts
CHANGED
|
@@ -4,7 +4,7 @@ import fs from 'node:fs/promises';
|
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { pathToFileURL } from 'node:url';
|
|
7
|
-
import type { OutputOptions, RollupOptions } from 'rollup';
|
|
7
|
+
import type { OutputOptions, Plugin, RollupOptions } from 'rollup';
|
|
8
8
|
|
|
9
9
|
let rollupModulePromise: Promise<typeof import('rollup')> | null = null;
|
|
10
10
|
|
|
@@ -34,12 +34,40 @@ interface BundledFile {
|
|
|
34
34
|
mimeType: string;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
interface BundleResult {
|
|
38
|
+
hash: string;
|
|
39
|
+
filename: string;
|
|
40
|
+
content: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const EMPTY_RESULT: BundleResult = { hash: '', filename: '', content: '' };
|
|
44
|
+
|
|
37
45
|
// Memory map để lưu bundled files
|
|
38
46
|
const bundledFilesMap = new Map<string, BundledFile>();
|
|
39
47
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Single-flight map: cacheKey → promise của lần bundle đang chạy (hoặc đã xong).
|
|
50
|
+
* Vì promise được giữ lại sau khi resolve, map này vừa là in-flight dedup vừa là
|
|
51
|
+
* result cache. Bundle lỗi bị xoá khỏi map để request sau được thử lại.
|
|
52
|
+
*/
|
|
53
|
+
const bundlePromises = new Map<string, Promise<BundleResult>>();
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Chạy `work` đúng một lần cho mỗi cacheKey, kể cả khi nhiều request đến đồng thời.
|
|
57
|
+
*/
|
|
58
|
+
function dedupe(cacheKey: string, work: () => Promise<BundleResult>): Promise<BundleResult> {
|
|
59
|
+
const pending = bundlePromises.get(cacheKey);
|
|
60
|
+
if (pending) {
|
|
61
|
+
return pending;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const promise = work().catch((error) => {
|
|
65
|
+
bundlePromises.delete(cacheKey);
|
|
66
|
+
throw error;
|
|
67
|
+
});
|
|
68
|
+
bundlePromises.set(cacheKey, promise);
|
|
69
|
+
return promise;
|
|
70
|
+
}
|
|
43
71
|
|
|
44
72
|
/**
|
|
45
73
|
* Generate hash từ content
|
|
@@ -48,167 +76,184 @@ function generateHash(content: string): string {
|
|
|
48
76
|
return createHash('sha256').update(content).digest('hex').substring(0, 16);
|
|
49
77
|
}
|
|
50
78
|
|
|
79
|
+
// Rollup coi id bắt đầu bằng \0 là virtual — nó sẽ không cố đọc từ đĩa.
|
|
80
|
+
const VIRTUAL_ENTRY_ID = '\0l5e:bundle-entry';
|
|
81
|
+
|
|
51
82
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
83
|
+
* Entry của mỗi lần bundle chỉ là một danh sách import. Giữ nó trong memory thay
|
|
84
|
+
* vì ghi ra đĩa: hai request đồng thời cùng một tập script sinh ra cùng nội dung
|
|
85
|
+
* entry, nên file tạm dùng chung path sẽ bị request này xoá trong lúc rollup của
|
|
86
|
+
* request kia còn đang đọc.
|
|
54
87
|
*/
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
88
|
+
function virtualEntryPlugin(entryContent: string): Plugin {
|
|
89
|
+
return {
|
|
90
|
+
name: 'l5e-virtual-entry',
|
|
91
|
+
resolveId(source) {
|
|
92
|
+
return source === VIRTUAL_ENTRY_ID ? VIRTUAL_ENTRY_ID : null;
|
|
93
|
+
},
|
|
94
|
+
load(id) {
|
|
95
|
+
return id === VIRTUAL_ENTRY_ID ? entryContent : null;
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Rewrite vendor/chunk/global imports thành web path và để chúng external.
|
|
102
|
+
* Global files (*.global.*) đã được client.global.ts load — bundle lại sẽ tạo
|
|
103
|
+
* module instance trùng (vd nanostores).
|
|
104
|
+
*/
|
|
105
|
+
function vendorPathRewriterPlugin(distClientDir: string): Plugin {
|
|
106
|
+
const toWebPath = (absolutePath: string) =>
|
|
107
|
+
'/' + path.relative(distClientDir, absolutePath).replace(/\\/g, '/');
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
name: 'vendor-path-rewriter',
|
|
111
|
+
resolveId(source, importer) {
|
|
112
|
+
if (
|
|
113
|
+
!source.includes('vendor-') &&
|
|
114
|
+
!source.includes('chunk-') &&
|
|
115
|
+
!source.includes('.global')
|
|
116
|
+
) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (path.isAbsolute(source)) {
|
|
121
|
+
// e.g. C:\...\dist\client\assets\vendor-react-XXX.js -> /assets/vendor-react-XXX.js
|
|
122
|
+
return { id: toWebPath(source), external: true };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (importer && source.startsWith('.')) {
|
|
126
|
+
// Relative path như ./auth.global-BOVr81Z5.js — resolve từ importer
|
|
127
|
+
return { id: toWebPath(path.resolve(path.dirname(importer), source)), external: true };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return null;
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function runScriptBundle(
|
|
136
|
+
uniquePaths: string[],
|
|
58
137
|
distClientDir: string,
|
|
59
|
-
): Promise<
|
|
60
|
-
|
|
61
|
-
|
|
138
|
+
): Promise<BundleResult> {
|
|
139
|
+
const entryContent = uniquePaths
|
|
140
|
+
.map((p) => {
|
|
141
|
+
const filePath = p.startsWith('/')
|
|
142
|
+
? path.join(distClientDir, p.substring(1))
|
|
143
|
+
: path.join(distClientDir, p);
|
|
144
|
+
return `import ${JSON.stringify(filePath)};`;
|
|
145
|
+
})
|
|
146
|
+
.join('\n');
|
|
147
|
+
|
|
148
|
+
const rollupOptions: RollupOptions = {
|
|
149
|
+
input: VIRTUAL_ENTRY_ID,
|
|
150
|
+
plugins: [virtualEntryPlugin(entryContent), vendorPathRewriterPlugin(distClientDir)],
|
|
151
|
+
external: (id) => {
|
|
152
|
+
// External node_modules
|
|
153
|
+
if (!id.startsWith('.') && !path.isAbsolute(id) && id !== VIRTUAL_ENTRY_ID) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Vendor/chunk/global do plugin resolveId lo phần rewrite path
|
|
158
|
+
return false;
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const outputOptions: OutputOptions = {
|
|
163
|
+
format: 'es',
|
|
164
|
+
inlineDynamicImports: false,
|
|
165
|
+
entryFileNames: 'bundle-[hash].js',
|
|
166
|
+
chunkFileNames: 'bundle-[hash].js',
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const { rollup } = await loadRollup();
|
|
170
|
+
const bundle = await rollup(rollupOptions);
|
|
171
|
+
let output;
|
|
172
|
+
try {
|
|
173
|
+
({ output } = await bundle.generate(outputOptions));
|
|
174
|
+
} finally {
|
|
175
|
+
await bundle.close();
|
|
62
176
|
}
|
|
63
177
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// Tạo cache key từ sorted unique paths
|
|
68
|
-
const cacheKey = `scripts:${uniquePaths.sort().join(',')}`;
|
|
69
|
-
|
|
70
|
-
// Kiểm tra cache - return entry chunk info if already bundled
|
|
71
|
-
const cachedEntryFileName = bundleCache.get(cacheKey);
|
|
72
|
-
if (cachedEntryFileName) {
|
|
73
|
-
const entryFile = bundledFilesMap.get(cachedEntryFileName);
|
|
74
|
-
if (entryFile) {
|
|
75
|
-
return {
|
|
76
|
-
hash: entryFile.hash,
|
|
77
|
-
filename: entryFile.filename,
|
|
78
|
-
content: entryFile.content,
|
|
79
|
-
};
|
|
178
|
+
for (const chunk of output) {
|
|
179
|
+
if (chunk.type !== 'chunk') {
|
|
180
|
+
continue;
|
|
80
181
|
}
|
|
182
|
+
bundledFilesMap.set(chunk.fileName, {
|
|
183
|
+
content: chunk.code || '',
|
|
184
|
+
hash: generateHash(chunk.code || ''),
|
|
185
|
+
filename: chunk.fileName,
|
|
186
|
+
mimeType: 'application/javascript',
|
|
187
|
+
});
|
|
81
188
|
}
|
|
82
189
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// Sử dụng rollup để bundle nếu cần (resolve imports, etc)
|
|
88
|
-
// Tạo temp entry file
|
|
89
|
-
const hash = generateHash(uniquePaths.join('\n'));
|
|
90
|
-
const tempDir = path.join(rootDir, '.temp-bundle');
|
|
91
|
-
await fs.mkdir(tempDir, { recursive: true }).catch(() => {});
|
|
92
|
-
|
|
93
|
-
entryFile = path.join(tempDir, `entry-${hash}.js`);
|
|
94
|
-
// Tạo entry file import tất cả scripts
|
|
95
|
-
const entryContent = uniquePaths
|
|
96
|
-
.map((p, i) => {
|
|
97
|
-
const filePath = p.startsWith('/')
|
|
98
|
-
? path.join(distClientDir, p.substring(1))
|
|
99
|
-
: path.join(distClientDir, p);
|
|
100
|
-
return `import ${JSON.stringify(filePath)};`;
|
|
101
|
-
})
|
|
102
|
-
.join('\n');
|
|
103
|
-
|
|
104
|
-
await fs.writeFile(entryFile, entryContent, 'utf-8');
|
|
105
|
-
console.log(`[bundler] Wrote entry file to ${entryFile}`);
|
|
106
|
-
console.log(`[bundler] Entry content: ${entryContent}`);
|
|
107
|
-
// Rollup config để bundle
|
|
108
|
-
const rollupOptions: RollupOptions = {
|
|
109
|
-
input: entryFile,
|
|
110
|
-
plugins: [
|
|
111
|
-
{
|
|
112
|
-
name: 'vendor-path-rewriter',
|
|
113
|
-
resolveId(source, importer, _options) {
|
|
114
|
-
// Handle vendor/chunk/global files: convert absolute paths to web paths
|
|
115
|
-
// Global files (*.global.*) are already loaded by client.global.ts —
|
|
116
|
-
// re-bundling them would create duplicate module instances (e.g. nanostores)
|
|
117
|
-
if (
|
|
118
|
-
source.includes('vendor-') ||
|
|
119
|
-
source.includes('chunk-') ||
|
|
120
|
-
source.includes('.global')
|
|
121
|
-
) {
|
|
122
|
-
console.log(`[bundler] Resolving source: ${source}`);
|
|
123
|
-
if (path.isAbsolute(source)) {
|
|
124
|
-
console.log(`[bundler] Resolving absolute path: ${source}`);
|
|
125
|
-
// e.g., C:\...\dist\client\assets\vendor-react-XXX.js -> /assets/vendor-react-XXX.js
|
|
126
|
-
const relativePath = path.relative(distClientDir, source);
|
|
127
|
-
const webPath = '/' + relativePath.replace(/\\/g, '/');
|
|
128
|
-
return { id: webPath, external: true };
|
|
129
|
-
} else if (importer && source.startsWith('.')) {
|
|
130
|
-
console.log(
|
|
131
|
-
`[bundler] Resolving relative path: ${source} from importer: ${importer}`,
|
|
132
|
-
);
|
|
133
|
-
// Relative path like ./auth.global-BOVr81Z5.js — resolve from importer
|
|
134
|
-
const resolved = path.resolve(path.dirname(importer), source);
|
|
135
|
-
const relativePath = path.relative(distClientDir, resolved);
|
|
136
|
-
const webPath = '/' + relativePath.replace(/\\/g, '/');
|
|
137
|
-
return { id: webPath, external: true };
|
|
138
|
-
} else {
|
|
139
|
-
console.log(`[bundler] Resolving source: ${source}`);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return null; // Let other plugins/external handle
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
],
|
|
146
|
-
external: (id) => {
|
|
147
|
-
// External node_modules
|
|
148
|
-
if (!id.startsWith('.') && !path.isAbsolute(id)) {
|
|
149
|
-
return true;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Let plugin handle vendor/chunk/global files (don't mark external here)
|
|
153
|
-
if (id.includes('vendor-') || id.includes('chunk-') || id.includes('.global')) {
|
|
154
|
-
return false; // Let plugin's resolveId handle path rewriting
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return false;
|
|
158
|
-
},
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
const outputOptions: OutputOptions = {
|
|
162
|
-
format: 'es',
|
|
163
|
-
inlineDynamicImports: false,
|
|
164
|
-
entryFileNames: 'bundle-[hash].js',
|
|
165
|
-
chunkFileNames: 'bundle-[hash].js',
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
const { rollup } = await loadRollup();
|
|
169
|
-
const bundle = await rollup(rollupOptions);
|
|
170
|
-
const { output } = await bundle.generate(outputOptions);
|
|
171
|
-
await bundle.close();
|
|
190
|
+
const entryChunk = output[0];
|
|
191
|
+
if (entryChunk?.type !== 'chunk') {
|
|
192
|
+
throw new Error('[bundler] rollup produced no entry chunk');
|
|
193
|
+
}
|
|
172
194
|
|
|
173
|
-
|
|
195
|
+
return {
|
|
196
|
+
hash: generateHash(entryChunk.code || ''),
|
|
197
|
+
filename: entryChunk.fileName,
|
|
198
|
+
content: entryChunk.code || '',
|
|
199
|
+
};
|
|
200
|
+
}
|
|
174
201
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
bundledFilesMap.set(o.fileName, bundledFile);
|
|
187
|
-
});
|
|
202
|
+
/**
|
|
203
|
+
* Bundle JavaScript files từ dist/client thành 1 file
|
|
204
|
+
* Trong production, các file đã được build sẵn trong dist/client
|
|
205
|
+
*/
|
|
206
|
+
export async function bundleScripts(
|
|
207
|
+
scriptPaths: string[],
|
|
208
|
+
distClientDir: string,
|
|
209
|
+
): Promise<BundleResult> {
|
|
210
|
+
if (scriptPaths.length === 0) {
|
|
211
|
+
return EMPTY_RESULT;
|
|
212
|
+
}
|
|
188
213
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (entryChunk?.type === 'chunk') {
|
|
192
|
-
bundleCache.set(cacheKey, entryChunk.fileName);
|
|
193
|
-
}
|
|
214
|
+
const uniquePaths = [...new Set(scriptPaths)].sort();
|
|
215
|
+
const cacheKey = `scripts:${uniquePaths.join(',')}`;
|
|
194
216
|
|
|
195
|
-
|
|
196
|
-
return
|
|
197
|
-
hash: generateHash(output[0]?.code || ''),
|
|
198
|
-
filename: output[0]?.fileName || '',
|
|
199
|
-
content: output[0]?.code || '',
|
|
200
|
-
};
|
|
217
|
+
try {
|
|
218
|
+
return await dedupe(cacheKey, () => runScriptBundle(uniquePaths, distClientDir));
|
|
201
219
|
} catch (error) {
|
|
202
220
|
console.error('[bundler] Error bundling scripts:', error);
|
|
203
|
-
return
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
221
|
+
return EMPTY_RESULT;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function runCssBundle(
|
|
226
|
+
uniquePaths: string[],
|
|
227
|
+
distClientDir: string,
|
|
228
|
+
): Promise<BundleResult> {
|
|
229
|
+
const cssContents: string[] = [];
|
|
230
|
+
|
|
231
|
+
for (const cssPath of uniquePaths) {
|
|
232
|
+
// cssPath có thể là "/assets/xxx.css" hoặc từ manifest
|
|
233
|
+
const filePath = cssPath.startsWith('/')
|
|
234
|
+
? path.join(distClientDir, cssPath.substring(1))
|
|
235
|
+
: path.join(distClientDir, cssPath);
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
239
|
+
cssContents.push(`/* ${cssPath} */\n${content}\n`);
|
|
240
|
+
} catch (err) {
|
|
241
|
+
console.warn(`[bundler] Failed to read CSS file: ${cssPath}`, err);
|
|
210
242
|
}
|
|
211
243
|
}
|
|
244
|
+
|
|
245
|
+
const bundledContent = cssContents.join('\n\n');
|
|
246
|
+
const hash = generateHash(bundledContent);
|
|
247
|
+
const filename = `bundle-${hash}.css`;
|
|
248
|
+
|
|
249
|
+
bundledFilesMap.set(filename, {
|
|
250
|
+
content: bundledContent,
|
|
251
|
+
hash,
|
|
252
|
+
filename,
|
|
253
|
+
mimeType: 'text/css',
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return { hash, filename, content: bundledContent };
|
|
212
257
|
}
|
|
213
258
|
|
|
214
259
|
/**
|
|
@@ -217,70 +262,20 @@ export async function bundleScripts(
|
|
|
217
262
|
*/
|
|
218
263
|
export async function bundleCss(
|
|
219
264
|
cssPaths: string[],
|
|
220
|
-
rootDir: string,
|
|
221
265
|
distClientDir: string,
|
|
222
|
-
): Promise<
|
|
266
|
+
): Promise<BundleResult> {
|
|
223
267
|
if (cssPaths.length === 0) {
|
|
224
|
-
return
|
|
268
|
+
return EMPTY_RESULT;
|
|
225
269
|
}
|
|
226
270
|
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
// Tạo cache key từ sorted unique paths
|
|
231
|
-
const cacheKey = `css:${uniquePaths.sort().join(',')}`;
|
|
232
|
-
|
|
233
|
-
// Kiểm tra cache - return cached file if already bundled
|
|
234
|
-
const cachedFileName = cssCache.get(cacheKey);
|
|
235
|
-
if (cachedFileName) {
|
|
236
|
-
const cachedFile = bundledFilesMap.get(cachedFileName);
|
|
237
|
-
if (cachedFile) {
|
|
238
|
-
return {
|
|
239
|
-
hash: cachedFile.hash,
|
|
240
|
-
filename: cachedFile.filename,
|
|
241
|
-
content: cachedFile.content,
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
}
|
|
271
|
+
const uniquePaths = [...new Set(cssPaths)].sort();
|
|
272
|
+
const cacheKey = `css:${uniquePaths.join(',')}`;
|
|
245
273
|
|
|
246
274
|
try {
|
|
247
|
-
|
|
248
|
-
const cssContents: string[] = [];
|
|
249
|
-
|
|
250
|
-
for (const cssPath of uniquePaths) {
|
|
251
|
-
// cssPath có thể là "/assets/xxx.css" hoặc từ manifest
|
|
252
|
-
const filePath = cssPath.startsWith('/')
|
|
253
|
-
? path.join(distClientDir, cssPath.substring(1))
|
|
254
|
-
: path.join(distClientDir, cssPath);
|
|
255
|
-
|
|
256
|
-
try {
|
|
257
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
258
|
-
cssContents.push(`/* ${cssPath} */\n${content}\n`);
|
|
259
|
-
} catch (err) {
|
|
260
|
-
console.warn(`[bundler] Failed to read CSS file: ${cssPath}`, err);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const bundledContent = cssContents.join('\n\n');
|
|
265
|
-
const hash = generateHash(bundledContent);
|
|
266
|
-
const filename = `bundle-${hash}.css`;
|
|
267
|
-
|
|
268
|
-
// Lưu vào map với key = filename
|
|
269
|
-
const bundledFile: BundledFile = {
|
|
270
|
-
content: bundledContent,
|
|
271
|
-
hash,
|
|
272
|
-
filename,
|
|
273
|
-
mimeType: 'text/css',
|
|
274
|
-
};
|
|
275
|
-
bundledFilesMap.set(filename, bundledFile);
|
|
276
|
-
|
|
277
|
-
// Cache filename for deduplication
|
|
278
|
-
cssCache.set(cacheKey, filename);
|
|
279
|
-
|
|
280
|
-
return { hash, filename, content: bundledContent };
|
|
275
|
+
return await dedupe(cacheKey, () => runCssBundle(uniquePaths, distClientDir));
|
|
281
276
|
} catch (error) {
|
|
282
277
|
console.error('[bundler] Error bundling CSS:', error);
|
|
283
|
-
return
|
|
278
|
+
return EMPTY_RESULT;
|
|
284
279
|
}
|
|
285
280
|
}
|
|
286
281
|
|
|
@@ -296,4 +291,5 @@ export function getBundledFile(filename: string): BundledFile | undefined {
|
|
|
296
291
|
*/
|
|
297
292
|
export function clearBundledFiles(): void {
|
|
298
293
|
bundledFilesMap.clear();
|
|
294
|
+
bundlePromises.clear();
|
|
299
295
|
}
|
package/src/core/jsx-types.d.ts
CHANGED
package/src/core/server.ts
CHANGED
|
@@ -285,12 +285,12 @@ async function createPageResponse({
|
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
if (mappedScripts.length > 0) {
|
|
288
|
-
const bundledScript = await bundleScripts(mappedScripts,
|
|
288
|
+
const bundledScript = await bundleScripts(mappedScripts, distClientDir);
|
|
289
289
|
scriptSrcList = bundledScript.filename ? [`/${bundledScript.filename}`] : mappedScripts;
|
|
290
290
|
}
|
|
291
291
|
|
|
292
292
|
if (mappedCssFiles.length > 0) {
|
|
293
|
-
const bundledCss = await bundleCss(mappedCssFiles,
|
|
293
|
+
const bundledCss = await bundleCss(mappedCssFiles, distClientDir);
|
|
294
294
|
if (bundledCss.filename) {
|
|
295
295
|
cssSrcList = [`/${bundledCss.filename}`];
|
|
296
296
|
}
|
|
@@ -574,24 +574,22 @@ export async function createServer(options: ServerOptions = {}): Promise<ServerC
|
|
|
574
574
|
return res.status(405).set('Allow', allowedMethod).send('Method Not Allowed');
|
|
575
575
|
}
|
|
576
576
|
|
|
577
|
-
//
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
body: req.body,
|
|
590
|
-
ip: requestIp.getClientIp(req),
|
|
591
|
-
};
|
|
592
|
-
|
|
593
|
-
// Import render utilities from entry-server (bundled in SSR build)
|
|
577
|
+
// Run the request through the middleware chain first — same as the HTML
|
|
578
|
+
// handler — so `locals` (locale, country, preview flags, ...) is
|
|
579
|
+
// populated for actions too. Previously this endpoint built its own
|
|
580
|
+
// bare `requestInfo` and never invoked middleware at all, so `locals`
|
|
581
|
+
// (and thus `getLocale()`) was always empty inside an action handler.
|
|
582
|
+
//
|
|
583
|
+
// Crucially, the action must run *inside* the `next` callback passed to
|
|
584
|
+
// the middleware — not after `await`ing the middleware call — because a
|
|
585
|
+
// middleware like `fromFetchMiddleware(paraglideMiddleware)` calls
|
|
586
|
+
// `next()` synchronously inside a library-owned `AsyncLocalStorage.run()`
|
|
587
|
+
// scope. Running the action afterward, once that scope has already
|
|
588
|
+
// closed, would silently lose the library's own ambient `getLocale()`.
|
|
594
589
|
const entryServerPath = path.join(root, './dist/server/entry-server.js');
|
|
590
|
+
const entryServerModule: EntryServerModule = isProduction
|
|
591
|
+
? await import(pathToFileURL(entryServerPath).href)
|
|
592
|
+
: await vite!.ssrLoadModule('@withl5e/l5e/entry-server');
|
|
595
593
|
const { runInRenderContext } = await (isProduction
|
|
596
594
|
? import(pathToFileURL(entryServerPath).href)
|
|
597
595
|
: vite!.ssrLoadModule('@withl5e/l5e/jsx-runtime'));
|
|
@@ -599,17 +597,42 @@ export async function createServer(options: ServerOptions = {}): Promise<ServerC
|
|
|
599
597
|
? import(pathToFileURL(entryServerPath).href)
|
|
600
598
|
: vite!.ssrLoadModule('@withl5e/l5e'));
|
|
601
599
|
|
|
602
|
-
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
);
|
|
600
|
+
const locals: Record<string, unknown> = {};
|
|
601
|
+
const initialRequest = createWebRequestFromExpress(req);
|
|
602
|
+
const middlewareContext = createContext({
|
|
603
|
+
request: initialRequest,
|
|
604
|
+
locals,
|
|
605
|
+
clientAddress: requestIp.getClientIp(req) ?? undefined,
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
const loadedMiddleware = await entryServerModule.loadMiddleware?.();
|
|
609
|
+
const middlewareHandler: MiddlewareHandler =
|
|
610
|
+
typeof loadedMiddleware === 'function' ? loadedMiddleware : (_ctx, dummyNext) => dummyNext();
|
|
611
|
+
|
|
612
|
+
// If middleware short-circuits (returns its own Response — a redirect, a
|
|
613
|
+
// 403, ...) instead of calling `next`, this callback never runs and that
|
|
614
|
+
// response wins, same as it would for a page request.
|
|
615
|
+
const response = await middlewareHandler(middlewareContext, async (payload) => {
|
|
616
|
+
const nextRequest = createRewriteRequest(payload, middlewareContext.request, middlewareContext.url);
|
|
617
|
+
const requestInfo = {
|
|
618
|
+
...createRequestInfo(req, nextRequest, base, locals),
|
|
619
|
+
path: req.originalUrl,
|
|
620
|
+
body: req.body,
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
const html = await runInRenderContext(
|
|
624
|
+
async () => {
|
|
625
|
+
const jsx = await action.handler(requestInfo);
|
|
626
|
+
return renderJsxToHtmlString(jsx);
|
|
627
|
+
},
|
|
628
|
+
requestInfo,
|
|
629
|
+
modulePath,
|
|
630
|
+
);
|
|
611
631
|
|
|
612
|
-
|
|
632
|
+
return new Response(html, { headers: { 'Content-Type': 'text/html' } });
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
await sendWebResponse(req, res, response);
|
|
613
636
|
} catch (e: any) {
|
|
614
637
|
vite?.ssrFixStacktrace?.(e);
|
|
615
638
|
console.error('[l5e] Action error:', e.stack || e);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { defineMiddleware } from '../middleware/defineMiddleware';
|
|
2
|
+
import type { MiddlewareHandler } from '../middleware/types';
|
|
3
|
+
import type { FetchLocaleMiddleware } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Adapt a fetch-based locale middleware — e.g. Paraglide's own
|
|
7
|
+
* `paraglideMiddleware` — into an l5e `MiddlewareHandler`.
|
|
8
|
+
*
|
|
9
|
+
* l5e doesn't ship its own locale-detection/URL-localization logic (strip
|
|
10
|
+
* prefix, cookie, redirect) or its own `getLocale()`: libraries like
|
|
11
|
+
* Paraglide already do both well, including an ambient `getLocale()` backed
|
|
12
|
+
* by their own `AsyncLocalStorage`. This adapter is only the bridge — it
|
|
13
|
+
* hands `context.request` to the library's middleware and, once resolved,
|
|
14
|
+
* (1) continues the chain by calling `next(request)` *synchronously inside*
|
|
15
|
+
* the library's own resolve callback, so the library's `AsyncLocalStorage`
|
|
16
|
+
* scope (already wrapping that callback) naturally extends over the rest of
|
|
17
|
+
* the request — loaders, route handler, every component — meaning the
|
|
18
|
+
* library's own `getLocale()` just works, no bridging needed; and (2) writes
|
|
19
|
+
* the resolved locale onto `context.locals.locale` too, since that's the
|
|
20
|
+
* idiomatic place other l5e code (global-loader's `lang`, cache tags, ...)
|
|
21
|
+
* already reads request-scoped values from — see the `locals` docs.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* // src/middleware.ts
|
|
26
|
+
* import { sequence } from '@withl5e/l5e/middleware';
|
|
27
|
+
* import { fromFetchMiddleware } from '@withl5e/l5e/i18n';
|
|
28
|
+
* import { paraglideMiddleware } from '~/paraglide/server.js';
|
|
29
|
+
*
|
|
30
|
+
* export const onRequest = sequence(fromFetchMiddleware(paraglideMiddleware));
|
|
31
|
+
*
|
|
32
|
+
* // Anywhere in the app — loaders, components, plain utils:
|
|
33
|
+
* import { getLocale } from '~/paraglide/runtime.js'; // Paraglide's own, ambient
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function fromFetchMiddleware<TLocale extends string = string, TOptions = unknown>(
|
|
37
|
+
middleware: FetchLocaleMiddleware<TLocale, TOptions>,
|
|
38
|
+
options?: TOptions,
|
|
39
|
+
): MiddlewareHandler {
|
|
40
|
+
return defineMiddleware((context, next) => {
|
|
41
|
+
return middleware(
|
|
42
|
+
context.request,
|
|
43
|
+
({ request, locale }) => {
|
|
44
|
+
context.locals.locale = locale;
|
|
45
|
+
return next(request);
|
|
46
|
+
},
|
|
47
|
+
options,
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape shared by fetch-based locale middleware — Paraglide's own
|
|
3
|
+
* `paraglideMiddleware(request, resolve, options?)` matches this exactly, so
|
|
4
|
+
* `fromFetchMiddleware(paraglideMiddleware)` works with zero glue code. Any
|
|
5
|
+
* other library (or hand-written function) exposing the same shape works too.
|
|
6
|
+
*/
|
|
7
|
+
export interface FetchLocaleMiddleware<TLocale extends string = string, TOptions = unknown> {
|
|
8
|
+
(
|
|
9
|
+
request: Request,
|
|
10
|
+
resolve: (args: { request: Request; locale: TLocale }) => Response | Promise<Response>,
|
|
11
|
+
options?: TOptions,
|
|
12
|
+
): Response | Promise<Response>;
|
|
13
|
+
}
|