@withl5e/l5e 0.3.3-alpha.0 → 0.3.3-alpha.2
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/entry-server.js.map +1 -1
- package/dist/global-style-DFvaBcuR.js +27 -0
- package/dist/global-style-DFvaBcuR.js.map +1 -0
- package/dist/server.js +290 -276
- package/dist/server.js.map +1 -1
- package/dist/vite-plugin.js +124 -119
- package/dist/vite-plugin.js.map +1 -1
- package/package.json +1 -1
- package/src/core/entry-server.ts +16 -4
- package/src/core/global-style.ts +35 -0
- package/src/core/server.ts +46 -15
- package/src/core/vite-plugin.ts +13 -0
package/src/core/entry-server.ts
CHANGED
|
@@ -155,8 +155,20 @@ export type SchemaMarkup = any | Array<any>;
|
|
|
155
155
|
export interface LoaderResult {
|
|
156
156
|
props?: Record<string, any>;
|
|
157
157
|
lang?: string;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Browser, CDN max-age
|
|
161
|
+
*/
|
|
158
162
|
maxAge?: number;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* CDN max-age
|
|
166
|
+
*/
|
|
159
167
|
sMaxAge?: number;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* CDN stale-while-revalidate, browser must revalidate
|
|
171
|
+
*/
|
|
160
172
|
swr?: number;
|
|
161
173
|
cacheTags?: string[] | Record<string, boolean>;
|
|
162
174
|
rawResponse?: RawResponse;
|
|
@@ -479,10 +491,10 @@ export async function render(
|
|
|
479
491
|
const serviceError = isProduction
|
|
480
492
|
? new ServiceUnavailableException('Internal Server Error')
|
|
481
493
|
: new ServiceUnavailableException(err.message || 'Internal Server Error', {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
494
|
+
originalError: err.name,
|
|
495
|
+
stack: err.stack,
|
|
496
|
+
timestamp: new Date().toISOString(),
|
|
497
|
+
});
|
|
486
498
|
return await renderErrorView(serviceError);
|
|
487
499
|
}
|
|
488
500
|
}, requestInfo, undefined, { externalizeIslandProps });
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const GLOBAL_STYLE_SOURCE = '/src/global.css';
|
|
5
|
+
export const GLOBAL_STYLE_MANIFEST_KEY = 'src/global.css';
|
|
6
|
+
|
|
7
|
+
export function findGlobalStyleInput(root: string): string | null {
|
|
8
|
+
const absolutePath = join(root, GLOBAL_STYLE_MANIFEST_KEY);
|
|
9
|
+
return existsSync(absolutePath) ? absolutePath : null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function withAssetBase(base: string, assetPath: string): string {
|
|
13
|
+
const cleanPath = assetPath.replace(/^\/+/, '');
|
|
14
|
+
const normalizedBase = base.endsWith('/') ? base : `${base}/`;
|
|
15
|
+
return `${normalizedBase}${cleanPath}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resolveGlobalStyleHref({
|
|
19
|
+
root,
|
|
20
|
+
manifest,
|
|
21
|
+
isProduction,
|
|
22
|
+
base,
|
|
23
|
+
}: {
|
|
24
|
+
root: string;
|
|
25
|
+
manifest?: Record<string, any>;
|
|
26
|
+
isProduction: boolean;
|
|
27
|
+
base: string;
|
|
28
|
+
}): string | null {
|
|
29
|
+
if (!isProduction) {
|
|
30
|
+
return findGlobalStyleInput(root) ? withAssetBase(base, GLOBAL_STYLE_SOURCE) : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const file = manifest?.[GLOBAL_STYLE_MANIFEST_KEY]?.file;
|
|
34
|
+
return typeof file === 'string' ? withAssetBase(base, file) : null;
|
|
35
|
+
}
|
package/src/core/server.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { ViteDevServer } from 'vite';
|
|
|
9
9
|
import { createContext, type MiddlewareHandler, type RewritePayload } from '../middleware';
|
|
10
10
|
import { bundleCss, bundleScripts, getBundledFile } from './bundler';
|
|
11
11
|
import type { RenderResult, RequestInfo } from './entry-server';
|
|
12
|
+
import { resolveGlobalStyleHref } from './global-style';
|
|
12
13
|
import { escapeProp } from './render';
|
|
13
14
|
import { createHeadersFromExpressRequest, parseCookies } from './request';
|
|
14
15
|
|
|
@@ -213,6 +214,7 @@ async function createPageResponse({
|
|
|
213
214
|
root,
|
|
214
215
|
distClientDir,
|
|
215
216
|
isProduction,
|
|
217
|
+
assetBase,
|
|
216
218
|
}: {
|
|
217
219
|
rendered: RenderResult;
|
|
218
220
|
template: string;
|
|
@@ -220,6 +222,7 @@ async function createPageResponse({
|
|
|
220
222
|
root: string;
|
|
221
223
|
distClientDir: string;
|
|
222
224
|
isProduction: boolean;
|
|
225
|
+
assetBase: string;
|
|
223
226
|
}): Promise<globalThis.Response> {
|
|
224
227
|
const rawResponse = createRawResponse(rendered);
|
|
225
228
|
if (rawResponse) {
|
|
@@ -244,9 +247,23 @@ async function createPageResponse({
|
|
|
244
247
|
const swr: number | undefined = rendered.swr;
|
|
245
248
|
|
|
246
249
|
let extraHead = '';
|
|
250
|
+
const emittedStyles = new Set<string>();
|
|
251
|
+
const appendStylesheet = (href: string, crossorigin = false) => {
|
|
252
|
+
if (emittedStyles.has(href)) return;
|
|
253
|
+
emittedStyles.add(href);
|
|
254
|
+
extraHead += `<link rel="stylesheet"${crossorigin ? ' crossorigin' : ''} href="${escapeProp(href)}">`;
|
|
255
|
+
};
|
|
247
256
|
let globalScripts: string[] = [];
|
|
248
257
|
let islandRegistryScript = '';
|
|
249
258
|
|
|
259
|
+
const globalStyleHref = resolveGlobalStyleHref({
|
|
260
|
+
root,
|
|
261
|
+
manifest,
|
|
262
|
+
isProduction,
|
|
263
|
+
base: assetBase,
|
|
264
|
+
});
|
|
265
|
+
if (globalStyleHref) appendStylesheet(globalStyleHref, isProduction);
|
|
266
|
+
|
|
250
267
|
// Externalized island props → a single JSON script at the end of the document.
|
|
251
268
|
// serialize-javascript with isJSON escapes `<`, `>`, `&` (and U+2028/2029) so the
|
|
252
269
|
// props can't break out of the <script> block or inject markup (XSS). The runtime
|
|
@@ -324,7 +341,7 @@ async function createPageResponse({
|
|
|
324
341
|
if (globalEntry) {
|
|
325
342
|
if (globalEntry.css && globalEntry.css.length > 0) {
|
|
326
343
|
globalEntry.css.forEach((cssFile: string) => {
|
|
327
|
-
|
|
344
|
+
appendStylesheet(`/${cssFile}`, true);
|
|
328
345
|
});
|
|
329
346
|
}
|
|
330
347
|
if (globalEntry.file) {
|
|
@@ -346,18 +363,13 @@ async function createPageResponse({
|
|
|
346
363
|
}
|
|
347
364
|
|
|
348
365
|
if (cssSrcList.length > 0) {
|
|
349
|
-
|
|
350
|
-
.map((file) => `<link rel="stylesheet" crossorigin href="${file}">`)
|
|
351
|
-
.join('');
|
|
366
|
+
cssSrcList.forEach((file) => appendStylesheet(file, true));
|
|
352
367
|
}
|
|
353
368
|
}
|
|
354
369
|
|
|
355
|
-
let cssHtml = '';
|
|
356
370
|
if (!isProduction) {
|
|
357
371
|
// Registry đã dedupe, nhưng vẫn lọc lại ở đây để dev không bao giờ ra thẻ trùng
|
|
358
|
-
|
|
359
|
-
.map((src) => `<link rel="stylesheet" href="${src}">`)
|
|
360
|
-
.join('');
|
|
372
|
+
[...new Set(cssSrcList)].forEach((src) => appendStylesheet(src));
|
|
361
373
|
}
|
|
362
374
|
|
|
363
375
|
let allScripts = [...globalScripts, ...scriptSrcList];
|
|
@@ -394,21 +406,39 @@ async function createPageResponse({
|
|
|
394
406
|
const html = rendered.rawHtml
|
|
395
407
|
? rendered.html || ''
|
|
396
408
|
: templateWithLang
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
409
|
+
.replace(`<!--app-head-->`, () => (rendered.head ?? '') + extraHead)
|
|
410
|
+
.replace(`<!--app-html-->`, () => rendered.html ?? '')
|
|
411
|
+
.replace(`<!--app-scripts-->`, () => scriptsHtml);
|
|
400
412
|
|
|
401
413
|
const headers = new Headers({
|
|
402
414
|
'Content-Type': 'text/html',
|
|
403
415
|
});
|
|
404
416
|
|
|
405
|
-
const cacheControlParts: string[] = [
|
|
406
|
-
|
|
407
|
-
if (
|
|
408
|
-
|
|
417
|
+
const cacheControlParts: string[] = [];
|
|
418
|
+
const cdnCacheControlParts: string[] = [];
|
|
419
|
+
if (maxAge !== undefined) {
|
|
420
|
+
cacheControlParts.push(`max-age=${maxAge}`);
|
|
421
|
+
cdnCacheControlParts.push(`max-age=${maxAge}`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (sMaxAge !== undefined) {
|
|
425
|
+
cdnCacheControlParts.push(`s-maxage=${sMaxAge}`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ko cho phép browser swr
|
|
429
|
+
if (swr !== undefined) {
|
|
430
|
+
cdnCacheControlParts.push(`stale-while-revalidate=${swr}`);
|
|
431
|
+
}
|
|
409
432
|
|
|
410
433
|
if (cacheControlParts.length > 1 && isProduction) {
|
|
434
|
+
cacheControlParts.push('public');
|
|
435
|
+
cacheControlParts.push('must-revalidate');
|
|
436
|
+
|
|
437
|
+
cdnCacheControlParts.push('public');
|
|
438
|
+
|
|
439
|
+
|
|
411
440
|
headers.set('Cache-Control', cacheControlParts.join(', '));
|
|
441
|
+
headers.set('CDN-Cache-Control', cdnCacheControlParts.join(', '));
|
|
412
442
|
}
|
|
413
443
|
|
|
414
444
|
if (process.env.NODE_ENV === 'production') {
|
|
@@ -747,6 +777,7 @@ export async function createServer(options: ServerOptions = {}): Promise<ServerC
|
|
|
747
777
|
root,
|
|
748
778
|
distClientDir,
|
|
749
779
|
isProduction,
|
|
780
|
+
assetBase: isProduction ? base : vite!.config.base,
|
|
750
781
|
});
|
|
751
782
|
};
|
|
752
783
|
|
package/src/core/vite-plugin.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from 'fs';
|
|
11
11
|
import { dirname, join, relative, resolve } from 'path';
|
|
12
12
|
import type { Plugin, UserConfig } from 'vite';
|
|
13
|
+
import { findGlobalStyleInput } from './global-style';
|
|
13
14
|
|
|
14
15
|
const VIRTUAL_L5E_VIEWS = 'virtual:l5e-views';
|
|
15
16
|
const VIRTUAL_L5E_ROUTE = 'virtual:l5e-route';
|
|
@@ -246,6 +247,12 @@ function discoverRollupInput(rootDir: string): {
|
|
|
246
247
|
console.log('[l5e] Detected src/client.global.ts and added as entry');
|
|
247
248
|
}
|
|
248
249
|
|
|
250
|
+
const globalStyleInput = findGlobalStyleInput(rootDir);
|
|
251
|
+
if (globalStyleInput) {
|
|
252
|
+
input['global-style'] = globalStyleInput;
|
|
253
|
+
console.log('[l5e] Detected src/global.css and added as global stylesheet');
|
|
254
|
+
}
|
|
255
|
+
|
|
249
256
|
// Scan all .tsx and .ts files
|
|
250
257
|
const tsFiles = scanTsFiles(srcDir);
|
|
251
258
|
|
|
@@ -297,6 +304,12 @@ function discoverRollupInput(rootDir: string): {
|
|
|
297
304
|
|
|
298
305
|
const absolutePath = resolve(rootDir, relativePath);
|
|
299
306
|
|
|
307
|
+
// src/global.css is already a dedicated convention entry. Keeping one
|
|
308
|
+
// Rollup input avoids collisions if a shared layout also calls useCss().
|
|
309
|
+
if (globalStyleInput && resolve(absolutePath) === resolve(globalStyleInput)) {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
300
313
|
// Only add if file exists
|
|
301
314
|
if (!existsSync(absolutePath)) {
|
|
302
315
|
console.warn(`[l5e] CSS file not found: ${absolutePath} (from ${cssPath})`);
|