@withl5e/l5e 0.3.3-alpha.0 → 0.3.3-alpha.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.
@@ -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
+ }
@@ -11,6 +11,7 @@ import { bundleCss, bundleScripts, getBundledFile } from './bundler';
11
11
  import type { RenderResult, RequestInfo } from './entry-server';
12
12
  import { escapeProp } from './render';
13
13
  import { createHeadersFromExpressRequest, parseCookies } from './request';
14
+ import { resolveGlobalStyleHref } from './global-style';
14
15
 
15
16
  export interface ServerOptions {
16
17
  root?: string;
@@ -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
- extraHead += `<link rel="stylesheet" crossorigin href="/${cssFile}">`;
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
- extraHead += cssSrcList
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
- cssHtml = [...new Set(cssSrcList)]
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,7 +406,7 @@ async function createPageResponse({
394
406
  const html = rendered.rawHtml
395
407
  ? rendered.html || ''
396
408
  : templateWithLang
397
- .replace(`<!--app-head-->`, () => (rendered.head ?? '') + extraHead + cssHtml)
409
+ .replace(`<!--app-head-->`, () => (rendered.head ?? '') + extraHead)
398
410
  .replace(`<!--app-html-->`, () => rendered.html ?? '')
399
411
  .replace(`<!--app-scripts-->`, () => scriptsHtml);
400
412
 
@@ -747,6 +759,7 @@ export async function createServer(options: ServerOptions = {}): Promise<ServerC
747
759
  root,
748
760
  distClientDir,
749
761
  isProduction,
762
+ assetBase: isProduction ? base : vite!.config.base,
750
763
  });
751
764
  };
752
765
 
@@ -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})`);