@@ -6,6 +6,38 @@ import { normalizePath } from 'vite';
6
6
import { basename , join } from 'node:path' ;
7
7
import { create_node_analyser } from '../static_analysis/index.js' ;
8
8
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
+
9
41
10
42
/**
11
43
* @param {string } out
@@ -29,31 +61,110 @@ export async function build_server_nodes(out, kit, manifest_data, server_manifes
29
61
const client = get_stylesheets ( client_chunks ) ;
30
62
const server = get_stylesheets ( Object . values ( server_bundle ) ) ;
31
63
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
33
71
for ( const [ id , client_stylesheet ] of client . stylesheets_used ) {
34
72
const server_stylesheet = server . stylesheets_used . get ( id ) ;
35
73
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
+ }
36
87
continue ;
37
88
}
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
+ }
41
136
}
42
137
43
- // filter out stylesheets that should not be inlined
138
+ // filter out stylesheets that should not be inlined based on size
44
139
for ( const [ fileName , content ] of client . stylesheet_content ) {
45
140
if ( content . length >= kit . inlineStyleThreshold ) {
46
- stylesheets_to_inline . delete ( fileName ) ;
141
+ client_to_server_files . delete ( fileName ) ;
47
142
}
48
143
}
49
144
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 ;
55
153
}
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 ) ;
57
168
}
58
169
}
59
170
@@ -123,7 +234,7 @@ export async function build_server_nodes(out, kit, manifest_data, server_manifes
123
234
// eagerly load client stylesheets and fonts imported by the SSR-ed page to avoid FOUC.
124
235
// However, if it is not used during SSR (not present in the server manifest),
125
236
// then it can be lazily loaded in the browser.
126
-
237
+
127
238
/** @type {import('types').AssetDependencies | undefined } */
128
239
let component ;
129
240
if ( node . component ) {
@@ -196,7 +307,7 @@ export async function build_server_nodes(out, kit, manifest_data, server_manifes
196
307
}
197
308
198
309
/**
199
- * @param {(import('vite').Rollup.OutputAsset | import('vite').Rollup.OutputChunk)[] } chunks
310
+ * @param {(import('vite').Rollup.OutputAsset | import('vite').Rollup.OutputChunk)[] } chunks
200
311
*/
201
312
function get_stylesheets ( chunks ) {
202
313
/**
0 commit comments