@@ -6,6 +6,38 @@ import { normalizePath } from 'vite';
66import { basename , join } from 'node:path' ;
77import { create_node_analyser } from '../static_analysis/index.js' ;
88
9+ /**
10+ * Calculate similarity between two CSS content strings
11+ * @param {string } content1
12+ * @param {string } content2
13+ * @returns {number } Similarity score between 0 and 1
14+ */
15+ function calculateCSSContentSimilarity ( content1 , content2 ) {
16+ if ( content1 === content2 ) return 1 ;
17+
18+ // Normalize CSS content for comparison
19+ const normalize = ( /** @type {string } */ css ) => css . replace ( / \s + / g, ' ' ) . replace ( / ; \s * } / g, '}' ) . trim ( ) ;
20+ const norm1 = normalize ( content1 ) ;
21+ const norm2 = normalize ( content2 ) ;
22+
23+ if ( norm1 === norm2 ) return 1 ;
24+
25+ // Simple length-based similarity
26+ const lengthDiff = Math . abs ( norm1 . length - norm2 . length ) ;
27+ const maxLength = Math . max ( norm1 . length , norm2 . length ) ;
28+ return maxLength > 0 ? 1 - ( lengthDiff / maxLength ) : 0 ;
29+ }
30+
31+ /**
32+ * Extract base name from CSS filename
33+ * @param {string } filename
34+ * @returns {string }
35+ */
36+ function extractCSSBaseName ( filename ) {
37+ const basename = filename . split ( '/' ) . pop ( ) || '' ;
38+ return basename . split ( '.' ) [ 0 ] || basename ;
39+ }
40+
941
1042/**
1143 * @param {string } out
@@ -29,31 +61,110 @@ export async function build_server_nodes(out, kit, manifest_data, server_manifes
2961 const client = get_stylesheets ( client_chunks ) ;
3062 const server = get_stylesheets ( Object . values ( server_bundle ) ) ;
3163
32- // map server stylesheet name to the client stylesheet name
64+
65+
66+ // Create a separate map for client-to-server file mapping
67+ /** @type {Map<string, string> } */
68+ const client_to_server_files = new Map ( ) ;
69+
70+ // Enhanced mapping strategy with multiple fallback mechanisms
3371 for ( const [ id , client_stylesheet ] of client . stylesheets_used ) {
3472 const server_stylesheet = server . stylesheets_used . get ( id ) ;
3573 if ( ! server_stylesheet ) {
74+ // Try to find CSS files with the same content in server build
75+ for ( const client_file of client_stylesheet ) {
76+ const client_content = client . stylesheet_content . get ( client_file ) ;
77+ if ( client_content ) {
78+ // Find server file with matching content
79+ for ( const [ server_file , server_content ] of server . stylesheet_content ) {
80+ if ( client_content === server_content ) {
81+ client_to_server_files . set ( client_file , server_file ) ;
82+ break ;
83+ }
84+ }
85+ }
86+ }
3687 continue ;
3788 }
38- client_stylesheet . forEach ( ( file , i ) => {
39- stylesheets_to_inline . set ( file , server_stylesheet [ i ] ) ;
40- } )
89+
90+ // Strategy 1: Direct index mapping (works when chunking is consistent)
91+ if ( client_stylesheet . length === server_stylesheet . length ) {
92+ client_stylesheet . forEach ( ( client_file , i ) => {
93+ if ( server_stylesheet [ i ] ) {
94+ client_to_server_files . set ( client_file , server_stylesheet [ i ] ) ;
95+ }
96+ } ) ;
97+ } else {
98+ // Strategy 2: Content-based matching (most reliable)
99+ for ( const client_file of client_stylesheet ) {
100+ const client_content = client . stylesheet_content . get ( client_file ) ;
101+ if ( ! client_content ) continue ;
102+
103+ let best_match = null ;
104+ let best_similarity = 0 ;
105+
106+ for ( const server_file of server_stylesheet ) {
107+ const server_content = server . stylesheet_content . get ( server_file ) ;
108+ if ( ! server_content ) continue ;
109+
110+ // Calculate content similarity
111+ const similarity = calculateCSSContentSimilarity ( client_content , server_content ) ;
112+ if ( similarity > best_similarity && similarity > 0.8 ) {
113+ best_similarity = similarity ;
114+ best_match = server_file ;
115+ }
116+ }
117+
118+ if ( best_match ) {
119+ client_to_server_files . set ( client_file , best_match ) ;
120+ } else {
121+ // Strategy 3: Filename-based fallback
122+ const client_base = extractCSSBaseName ( client_file ) ;
123+ const matching_server_file = server_stylesheet . find ( server_file => {
124+ const server_base = extractCSSBaseName ( server_file ) ;
125+ return client_base === server_base ;
126+ } ) ;
127+
128+ if ( matching_server_file ) {
129+ client_to_server_files . set ( client_file , matching_server_file ) ;
130+ } else {
131+ console . warn ( `[SvelteKit CSS] No matching server stylesheet found for client file: ${ client_file } (module: ${ id } )` ) ;
132+ }
133+ }
134+ }
135+ }
41136 }
42137
43- // filter out stylesheets that should not be inlined
138+ // filter out stylesheets that should not be inlined based on size
44139 for ( const [ fileName , content ] of client . stylesheet_content ) {
45140 if ( content . length >= kit . inlineStyleThreshold ) {
46- stylesheets_to_inline . delete ( fileName ) ;
141+ client_to_server_files . delete ( fileName ) ;
47142 }
48143 }
49144
50- // map server stylesheet source to the client stylesheet name
51- for ( const [ client_file , server_file ] of stylesheets_to_inline ) {
52- const source = server . stylesheet_content . get ( server_file ) ;
53- if ( ! source ) {
54- throw new Error ( `Server stylesheet source not found for client stylesheet ${ client_file } ` ) ;
145+ // map client stylesheet name to the server stylesheet source content
146+ for ( const [ client_file , server_file ] of client_to_server_files ) {
147+ const client_content = client . stylesheet_content . get ( client_file ) ;
148+ const server_content = server . stylesheet_content . get ( server_file ) ;
149+
150+ if ( ! server_content ) {
151+ console . warn ( `[SvelteKit CSS] Server stylesheet source not found for: ${ server_file } , skipping ${ client_file } ` ) ;
152+ continue ;
55153 }
56- stylesheets_to_inline . set ( client_file , source ) ;
154+
155+ // Verify content similarity to catch mapping errors
156+ if ( client_content && server_content ) {
157+ // Simple similarity check: compare normalized lengths
158+ const client_normalized = client_content . replace ( / \s + / g, '' ) . length ;
159+ const server_normalized = server_content . replace ( / \s + / g, '' ) . length ;
160+ const length_diff = Math . abs ( client_normalized - server_normalized ) / Math . max ( client_normalized , server_normalized ) ;
161+
162+ if ( length_diff > 0.5 ) {
163+ console . warn ( `[SvelteKit CSS] Content mismatch detected: ${ client_file } -> ${ server_file } (${ Math . round ( length_diff * 100 ) } % difference), using server content` ) ;
164+ }
165+ }
166+
167+ stylesheets_to_inline . set ( client_file , server_content ) ;
57168 }
58169 }
59170
@@ -123,7 +234,7 @@ export async function build_server_nodes(out, kit, manifest_data, server_manifes
123234 // eagerly load client stylesheets and fonts imported by the SSR-ed page to avoid FOUC.
124235 // However, if it is not used during SSR (not present in the server manifest),
125236 // then it can be lazily loaded in the browser.
126-
237+
127238 /** @type {import('types').AssetDependencies | undefined } */
128239 let component ;
129240 if ( node . component ) {
@@ -196,7 +307,7 @@ export async function build_server_nodes(out, kit, manifest_data, server_manifes
196307}
197308
198309/**
199- * @param {(import('vite').Rollup.OutputAsset | import('vite').Rollup.OutputChunk)[] } chunks
310+ * @param {(import('vite').Rollup.OutputAsset | import('vite').Rollup.OutputChunk)[] } chunks
200311 */
201312function get_stylesheets ( chunks ) {
202313 /**
0 commit comments