Skip to content
This repository was archived by the owner on Jul 6, 2025. It is now read-only.

Commit b14a203

Browse files
committed
Fix stylesheet mismatch pages in SSR (#230)
1 parent dd9ea5e commit b14a203

File tree

4 files changed

+78
-24
lines changed

4 files changed

+78
-24
lines changed

framework/core/style.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import util from '../../shared/util.ts'
22

33
export const clientStyles = new Map<string, string>()
4-
export const serverStyles = new Map<string, string>()
54

65
export function removeCSS(url: string, recoverable?: boolean) {
76
const { document } = window as any
@@ -32,9 +31,7 @@ export function recoverCSS(url: string) {
3231
}
3332

3433
export function applyCSS(url: string, css?: string) {
35-
if (util.inDeno) {
36-
serverStyles.set(url, css || '')
37-
} else {
34+
if (!util.inDeno) {
3835
const { document } = window as any
3936
const ssr = Array.from<any>(document.head.children).find((el: any) => {
4037
return el.getAttribute('data-module-id') === url && el.hasAttribute('ssr')

server/app.ts

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,11 @@ import { Renderer } from './ssr.ts'
5151
/** A module includes the compilation details. */
5252
export type Module = {
5353
url: string
54-
jsFile: string
54+
deps: DependencyDescriptor[]
55+
isStyle: boolean
5556
sourceHash: string
5657
hash: string
57-
deps: DependencyDescriptor[]
58+
jsFile: string
5859
}
5960

6061
/** The dependency descriptor. */
@@ -449,6 +450,19 @@ export class Application implements ServerApplication {
449450
return null
450451
}
451452

453+
lookupStyleModules(...urls: string[]): Module[] {
454+
const mods: Module[] = []
455+
urls.forEach(url => {
456+
this.lookupDeps(url, ({ url }) => {
457+
const mod = this.#modules.get(url)
458+
if (mod && mod.isStyle) {
459+
mods.push({ ...mod, deps: [...mod.deps] })
460+
}
461+
})
462+
})
463+
return mods
464+
}
465+
452466
getPageRoute(location: { pathname: string, search?: string }): [RouterURL, RouteModule[]] {
453467
return this.#pageRouting.createRouter(location)
454468
}
@@ -567,6 +581,11 @@ export class Application implements ServerApplication {
567581
}
568582
}
569583

584+
const mod = this.#modules.get(url)
585+
if (mod && mod.isStyle) {
586+
return true
587+
}
588+
570589
return this.loaders.some(p => (
571590
p.test.test(url) &&
572591
(p.acceptHMR || p.allowPage)
@@ -917,12 +936,18 @@ export class Application implements ServerApplication {
917936
url: string,
918937
sourceContent: Uint8Array,
919938
contentType: string | null
920-
): Promise<{ code: string, type: SourceType, map: string | null } | null> {
939+
): Promise<{
940+
code: string
941+
type: SourceType
942+
isStyle: boolean
943+
map?: string
944+
} | null> {
921945
const encoder = new TextEncoder()
922946
const decoder = new TextDecoder()
923947

924948
let sourceType: SourceType | null = null
925949
let sourceMap: Uint8Array | null = null
950+
let isStyle = false
926951

927952
if (contentType !== null) {
928953
switch (contentType.split(';')[0].trim()) {
@@ -1000,6 +1025,7 @@ export class Application implements ServerApplication {
10001025
const { code, map } = await this.#cssProcesser.transform(url, (new TextDecoder).decode(sourceContent))
10011026
sourceContent = encoder.encode(code)
10021027
sourceType = SourceType.JS
1028+
isStyle = true
10031029
if (map) {
10041030
sourceMap = encoder.encode(map)
10051031
}
@@ -1008,7 +1034,8 @@ export class Application implements ServerApplication {
10081034
return {
10091035
code: decoder.decode(sourceContent),
10101036
type: sourceType,
1011-
map: sourceMap ? decoder.decode(sourceMap) : null
1037+
isStyle,
1038+
map: sourceMap ? decoder.decode(sourceMap) : undefined
10121039
}
10131040
}
10141041

@@ -1024,12 +1051,12 @@ export class Application implements ServerApplication {
10241051
once?: boolean,
10251052
} = {}
10261053
): Promise<Module> {
1054+
const { sourceCode, forceCompile, once } = options
10271055
const isRemote = util.isLikelyHttpURL(url)
10281056
const localUrl = toLocalUrl(url)
10291057
const saveDir = join(this.buildDir, dirname(localUrl))
10301058
const name = trimModuleExt(basename(localUrl))
10311059
const metaFile = join(saveDir, `${name}.meta.json`)
1032-
const { sourceCode, forceCompile, once } = options
10331060

10341061
let mod: Module
10351062
if (this.#modules.has(url)) {
@@ -1041,6 +1068,7 @@ export class Application implements ServerApplication {
10411068
mod = {
10421069
url,
10431070
deps: [],
1071+
isStyle: false,
10441072
sourceHash: '',
10451073
hash: '',
10461074
jsFile: join(saveDir, `${name}.js`),
@@ -1050,10 +1078,11 @@ export class Application implements ServerApplication {
10501078
}
10511079
if (existsFileSync(metaFile)) {
10521080
try {
1053-
const { url: __url, sourceHash, deps } = JSON.parse(await Deno.readTextFile(metaFile))
1054-
if (__url === url && util.isNEString(sourceHash) && util.isArray(deps)) {
1081+
const { url: _url, deps, isStyle, sourceHash } = JSON.parse(await Deno.readTextFile(metaFile))
1082+
if (_url === url && util.isNEString(sourceHash) && util.isArray(deps)) {
10551083
mod.sourceHash = sourceHash
10561084
mod.deps = deps
1085+
mod.isStyle = !!isStyle
10571086
} else {
10581087
log.warn(`removing invalid metadata '${name}.meta.json'`)
10591088
Deno.remove(metaFile)
@@ -1145,6 +1174,7 @@ export class Application implements ServerApplication {
11451174
}
11461175
}
11471176

1177+
mod.isStyle = source.isStyle
11481178
mod.deps = deps.map(({ specifier, isDynamic }) => {
11491179
const dep: DependencyDescriptor = { url: specifier, hash: '' }
11501180
if (isDynamic) {
@@ -1188,8 +1218,9 @@ export class Application implements ServerApplication {
11881218
await Promise.all([
11891219
ensureTextFile(metaFile, JSON.stringify({
11901220
url,
1191-
sourceHash: mod.sourceHash,
11921221
deps: mod.deps,
1222+
sourceHash: mod.sourceHash,
1223+
isStyle: mod.isStyle ? true : undefined
11931224
}, undefined, 2)),
11941225
ensureTextFile(mod.jsFile, jsContent + (jsSourceMap ? `//# sourceMappingURL=${basename(mod.jsFile)}.map` : '')),
11951226
jsSourceMap ? ensureTextFile(mod.jsFile + '.map', jsSourceMap) : Promise.resolve(),

server/css.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const productionOnlyPostcssPlugins = ['autoprefixer']
66

77
export class CSSProcessor {
88
#isProd: boolean
9-
#options: CSSOptions
9+
#options: Required<CSSOptions>
1010
#postcss: any
1111
#cleanCSS: any
1212
#modulesJSON: Record<string, Record<string, string>>
@@ -24,21 +24,24 @@ export class CSSProcessor {
2424

2525
config(isProd: boolean, options: CSSOptions) {
2626
this.#isProd = isProd
27+
if (util.isPlainObject(options.postcss) && Array.isArray(options.postcss.plugins)) {
28+
this.#options.postcss.plugins = options.postcss.plugins
29+
}
2730
if (util.isPlainObject(options.modules)) {
28-
options.postcss.plugins = options.postcss.plugins.filter(p => {
31+
const plugins = this.#options.postcss.plugins.filter(p => {
2932
if (p === 'postcss-modules' || (Array.isArray(p) && p[0] === 'postcss-modules')) {
3033
return false
3134
}
3235
return true
3336
})
34-
options.postcss.plugins.push(['postcss-modules', {
37+
plugins.push(['postcss-modules', {
3538
...options.modules,
3639
getJSON: (url: string, json: Record<string, string>) => {
3740
this.#modulesJSON = { [url]: json }
3841
},
3942
}])
43+
this.#options.postcss.plugins = plugins
4044
}
41-
this.#options = options
4245
}
4346

4447
private getModulesJSON(url: string) {

server/ssr.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ export type FrameworkRenderer = {
2828
render(
2929
url: RouterURL,
3030
AppComponent: any,
31-
nestedPageComponents: { url: string, Component?: any }[]
31+
nestedPageComponents: { url: string, Component?: any }[],
32+
styles: Record<string, string>
3233
): Promise<FrameworkRenderResult>
3334
}
3435

@@ -95,26 +96,31 @@ export class Renderer {
9596
async renderPage(url: RouterURL, nestedModules: RouteModule[]): Promise<[string, Record<string, SSRData> | null]> {
9697
const start = performance.now()
9798
const isDev = this.#app.isDev
99+
const state = { entryFile: '' }
98100
const appModule = this.#app.findModuleByName('app')
99101
const { default: App } = appModule ? await import(`file://${appModule.jsFile}#${appModule.hash.slice(0, 6)}`) : {} as any
100-
101-
let entryFile = ''
102102
const nestedPageComponents = await Promise.all(nestedModules
103103
.filter(({ url }) => this.#app.getModule(url) !== null)
104104
.map(async ({ url }) => {
105105
const { jsFile, hash } = this.#app.getModule(url)!
106106
const { default: Component } = await import(`file://${jsFile}#${hash.slice(0, 6)}`)
107-
entryFile = dirname(url) + '/' + basename(jsFile)
107+
state.entryFile = dirname(url) + '/' + basename(jsFile)
108108
return {
109109
url,
110110
Component
111111
}
112112
})
113113
)
114+
const styles = await this.lookupStyleModules(...[
115+
appModule ? appModule.url : [],
116+
nestedModules.map(({ url }) => url)
117+
].flat())
118+
114119
const { head, body, data, scripts } = await this.#renderer.render(
115120
url,
116121
App,
117-
nestedPageComponents
122+
nestedPageComponents,
123+
styles
118124
)
119125

120126
if (isDev) {
@@ -131,7 +137,7 @@ export class Renderer {
131137
type: 'application/json',
132138
innerText: JSON.stringify(data, undefined, isDev ? 2 : 0),
133139
} : '',
134-
...this.#app.getSSRHTMLScripts(entryFile),
140+
...this.#app.getSSRHTMLScripts(state.entryFile),
135141
...scripts.map((script: Record<string, any>) => {
136142
if (script.innerText && !isDev) {
137143
return { ...script, innerText: script.innerText }
@@ -152,10 +158,15 @@ export class Renderer {
152158
const e404Module = this.#app.findModuleByName('404')
153159
const { default: App } = appModule ? await import(`file://${appModule.jsFile}#${appModule.hash.slice(0, 6)}`) : {} as any
154160
const { default: E404 } = e404Module ? await import(`file://${e404Module.jsFile}#${e404Module.hash.slice(0, 6)}`) : {} as any
161+
const styles = await this.lookupStyleModules(...[
162+
appModule ? appModule.url : [],
163+
e404Module ? e404Module.url : []
164+
].flat())
155165
const { head, body, data, scripts } = await this.#renderer.render(
156166
url,
157167
App,
158-
e404Module ? [{ url: e404Module.url, Component: E404 }] : []
168+
e404Module ? [{ url: e404Module.url, Component: E404 }] : [],
169+
styles
159170
)
160171
return createHtml({
161172
lang: url.locale,
@@ -186,14 +197,16 @@ export class Renderer {
186197

187198
if (loadingModule) {
188199
const { default: Loading } = await import(`file://${loadingModule.jsFile}#${loadingModule.hash.slice(0, 6)}`)
200+
const styles = await this.lookupStyleModules(loadingModule.url)
189201
const {
190202
head,
191203
body,
192204
scripts
193205
} = await this.#renderer.render(
194206
createBlankRouterURL(baseUrl, defaultLocale),
195207
undefined,
196-
[{ url: loadingModule.url, Component: Loading }]
208+
[{ url: loadingModule.url, Component: Loading }],
209+
styles
197210
)
198211
return createHtml({
199212
lang: defaultLocale,
@@ -220,6 +233,16 @@ export class Renderer {
220233
minify: !this.#app.isDev
221234
})
222235
}
236+
237+
private async lookupStyleModules(...urls: string[]): Promise<Record<string, string>> {
238+
return (await Promise.all(this.#app.lookupStyleModules(...urls).map(async ({ jsFile, hash }) => {
239+
const { default: { __url$: url, __css$: css } } = await import(`file://${jsFile}#${hash.slice(0, 6)}`)
240+
return { url, css }
241+
}))).reduce((styles, mod) => {
242+
styles[mod.url] = mod.css
243+
return styles
244+
}, {} as Record<string, string>)
245+
}
223246
}
224247

225248
/** create html content by given arguments */

0 commit comments

Comments
 (0)