Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: css insert order #9278

Closed
wants to merge 27 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 65 additions & 9 deletions packages/vite/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,13 +319,56 @@ const supportsConstructedSheet = (() => {
return false
})()

const sheetsMap = new Map<
string,
HTMLStyleElement | CSSStyleSheet | undefined
>()
const sheetsMap = new Map<string, StyleNode | CSSStyleSheet | undefined>()

export function updateStyle(id: string, content: string): void {
interface StyleNode extends HTMLStyleElement {
depth: number
weight: number
}

let weight = 0
const entryPointWeightMap = new Set<string>()

export function dynamicImportModule(
moduleLoad: () => Promise<any>,
id: string
): Promise<any> {
if (!entryPointWeightMap.has(id)) {
sendMessage('entry-point-weight', {
id,
weight: ++weight
})
}
return moduleLoad()
}

const styleList: StyleNode[] = []
function insertNode(weight: number, depth: number, style: StyleNode) {
// for debugging
style.setAttribute('w', weight.toString())
style.setAttribute('d', depth.toString())
const nodeIdx = styleList.findIndex(
(node) =>
(node.weight === weight && node.depth > depth) || node.weight > weight
)
if (nodeIdx !== -1) {
const node = styleList[nodeIdx]
document.head.insertBefore(style, node)
styleList.splice(nodeIdx, 0, style)
} else {
document.head.appendChild(style)
styleList.push(style)
}
}

export function updateStyle(
id: string,
content: string,
weight: number,
depth: number
): void {
let style = sheetsMap.get(id)
// TODO inject the truth order(small weight in head) for stylesheet
if (supportsConstructedSheet && !content.includes('@import')) {
if (style && !(style instanceof CSSStyleSheet)) {
removeStyle(id)
Expand All @@ -349,10 +392,16 @@ export function updateStyle(id: string, content: string): void {
}

if (!style) {
style = document.createElement('style')
style = document.createElement('style') as StyleNode
style.setAttribute('type', 'text/css')
style.innerHTML = content
document.head.appendChild(style)
style.weight = weight
style.depth = depth
if (weight) {
insertNode(weight, depth, style)
} else {
document.head.appendChild(style)
}
} else {
style.innerHTML = content
}
Expand Down Expand Up @@ -560,14 +609,21 @@ export function createHotContext(ownerPath: string): ViteHotContext {
},

send(event, data) {
messageBuffer.push(JSON.stringify({ type: 'custom', event, data }))
sendMessageBuffer()
sendMessage(event, data)
}
}

return hot
}

function sendMessage<T extends string>(
event: T,
data?: InferCustomEventPayload<T>
): void {
messageBuffer.push(JSON.stringify({ type: 'custom', event, data }))
sendMessageBuffer()
}

/**
* urls here are dynamic import() urls that couldn't be statically analyzed
*/
Expand Down
12 changes: 11 additions & 1 deletion packages/vite/src/node/plugins/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
moduleGraph.updateModuleInfo(
thisModule,
depModules,
depModules,
null,
// The root CSS proxy module is self-accepting and should not
// have an explicit accept list
Expand Down Expand Up @@ -298,6 +299,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
// styles initialization in buildStart causes a styling loss in watch
const styles: Map<string, string> = new Map<string, string>()
let pureCssChunks: Set<string>
let serve: ViteDevServer

// when there are multiple rollup outputs and extracting CSS, only emit once,
// since output formats have no effect on the generated CSS.
Expand Down Expand Up @@ -336,6 +338,10 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
hasEmitted = false
},

configureServer(_server) {
serve = _server
},

async transform(css, id, options) {
if (
!isCSSRequest(id) ||
Expand Down Expand Up @@ -379,13 +385,17 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {

const cssContent = await getContentWithSourcemap(css)
const devBase = config.base
const moduleNode = serve.moduleGraph.getModuleById(id)!

return [
`import { updateStyle as __vite__updateStyle, removeStyle as __vite__removeStyle } from ${JSON.stringify(
path.posix.join(devBase, CLIENT_PUBLIC_PATH)
)}`,
`const __vite__id = ${JSON.stringify(id)}`,
`const __vite__css = ${JSON.stringify(cssContent)}`,
`__vite__updateStyle(__vite__id, __vite__css)`,
`const __vite__entry = ${moduleNode.weight}`,
`const __mod__depth = ${moduleNode.depth}`,
`__vite__updateStyle(__vite__id, __vite__css, __vite__entry, __mod__depth)`,
// css modules exports change on edit so it can't self accept
`${
modulesCode ||
Expand Down
27 changes: 24 additions & 3 deletions packages/vite/src/node/plugins/importAnalysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
let isSelfAccepting = false
let hasEnv = false
let needQueryInjectHelper = false
let needDynamicImportHelper = false
let s: MagicString | undefined
const str = () => s || (s = new MagicString(source))
const importedUrls = new Set<string>()
Expand Down Expand Up @@ -527,9 +528,22 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
rewriteDone = true
}
if (!rewriteDone) {
str().overwrite(start, end, isDynamicImport ? `'${url}'` : url, {
contentOnly: true
})
if (isDynamicImport) {
if (!ssr) {
needDynamicImportHelper = true
const id = (await server.moduleGraph.getModuleByUrl(url))?.id
str().overwrite(
expStart,
expEnd,
`__vite_dynamicImportModule(() => import('${url}'), '${id}')`,
{ contentOnly: true }
)
} else {
str().overwrite(start, end, `'${url}'`, { contentOnly: true })
}
} else {
str().overwrite(start, end, url, { contentOnly: true })
}
}
}

Expand Down Expand Up @@ -636,6 +650,12 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
)
}

if (needDynamicImportHelper) {
str().prepend(
`import { dynamicImportModule as __vite_dynamicImportModule } from "${clientPublicPath}";`
)
}

// normalize and rewrite accepted urls
const normalizedAcceptedUrls = new Set<string>()
for (const { url, start, end } of acceptedUrls) {
Expand Down Expand Up @@ -683,6 +703,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
const prunedImports = await moduleGraph.updateModuleInfo(
importerModule,
importedUrls,
new Set(Array.from(staticImportedUrls).map((item) => item.url)),
importedBindings,
normalizedAcceptedUrls,
isPartiallySelfAccepting ? acceptedExports : null,
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/plugins/importAnalysisBuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {
// dynamic import to constant json may get inlined.
if (chunk.type === 'chunk' && chunk.code.indexOf(preloadMarker) > -1) {
const code = chunk.code
let imports: ImportSpecifier[]
let imports: ImportSpecifier[] = []
try {
imports = parseImports(code)[0].filter((i) => i.d > -1)
} catch (e: any) {
Expand Down
7 changes: 7 additions & 0 deletions packages/vite/src/node/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,13 @@ export async function createServer(
container.resolveId(url, undefined, { ssr })
)

ws.on('entry-point-weight', ({ id, weight }) => {
const mod = moduleGraph.getModuleById(id)
if (mod) {
mod.weight = weight
}
})

const container = await createPluginContainer(config, moduleGraph, watcher)
const closeHttpServer = createServerCloseFn(httpServer)

Expand Down
60 changes: 60 additions & 0 deletions packages/vite/src/node/server/moduleGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,36 @@ export class ModuleNode {
ssrError: Error | null = null
lastHMRTimestamp = 0
lastInvalidationTimestamp = 0
/**
* entryPoint of the module
* - null - no init
* - dynamic - it's entry pointer (the ModuleNode which is imported by dynamic import)
* - main - it's import by main entry point
* - ModuleNode - it's imported by dynamic import and the entry point is this field
*/
entryPoint: ModuleNode | 'dynamic' | 'main' | null = null
/**
* first importer of the module
*/
firstImporter: ModuleNode | null = null
/**
* lower weight had higher priority
*/
weight: number = 0
/*
* from entry point to module depth
* - 0 - after bundle this module will in the entry point
* - 1 - after bundle this module will be an chunk and import by entry point
* - n - after bundle this module will be an chunk and import by chunk ... chunk
*/
get depth(): number {
if (this.firstImporter) {
// https://rollupjs.org/guide/en/#code-splitting
// Rollup will never duplicate code and instead create additional chunks to only ever load the bare minimum necessary.
return this.firstImporter.depth + +(this.importers.size > 1)
}
return 0
}

/**
* @param setIsSelfAccepting - set `false` to set `isSelfAccepting` later. e.g. #7870
Expand Down Expand Up @@ -135,6 +165,7 @@ export class ModuleGraph {
async updateModuleInfo(
mod: ModuleNode,
importedModules: Set<string | ModuleNode>,
staticImportUrls: Set<string | ModuleNode>,
importedBindings: Map<string, Set<string>> | null,
acceptedModules: Set<string | ModuleNode>,
acceptedExports: Set<string> | null,
Expand All @@ -153,6 +184,9 @@ export class ModuleGraph {
: imported
dep.importers.add(mod)
nextImports.add(dep)
if (!staticImportUrls.has(imported)) {
dep.entryPoint = 'dynamic'
}
}
// remove the importer from deps that were imported but no longer are.
prevImports.forEach((dep) => {
Expand All @@ -164,6 +198,32 @@ export class ModuleGraph {
}
}
})
// ensure the module entry point
for (const importerMod of mod.importers) {
if (importerMod.entryPoint === 'dynamic') {
if (
(mod.entryPoint && importerMod.weight < mod.weight) ||
!mod.entryPoint
) {
mod.entryPoint = importerMod
mod.weight = importerMod.weight
mod.firstImporter = importerMod
}
} else if (importerMod.entryPoint instanceof ModuleNode) {
if (
(mod.entryPoint && importerMod.entryPoint.weight < mod.weight) ||
!mod.entryPoint
) {
mod.entryPoint = importerMod.entryPoint
mod.weight = importerMod.entryPoint.weight
mod.firstImporter = importerMod
}
}
}
if (mod.entryPoint == null) {
mod.entryPoint = 'main'
mod.weight = 0
}
// update accepted hmr deps
const deps = (mod.acceptedHmrDeps = new Set())
for (const accepted of acceptedModules) {
Expand Down
36 changes: 35 additions & 1 deletion playground/css/__tests__/css.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
page,
removeFile,
serverLogs,
untilUpdated
untilUpdated,
withRetry
} from '~utils'

// note: tests should retrieve the element at the beginning of test and reuse it
Expand Down Expand Up @@ -449,3 +450,36 @@ test.runIf(isBuild)('warning can be suppressed by esbuild.logOverride', () => {
expect(log).not.toMatch('unsupported-css-property')
})
})

// NOTE: the match inline snapshot should generate by build mode
test('async css modules', async () => {
await withRetry(async () => {
expect(await getColor('.async-modules-green')).toMatchInlineSnapshot(
'"green"'
)
expect(await getColor('.async-modules-blue2')).toMatchInlineSnapshot(
'"red"'
)
expect(await getColor('.async-modules-red')).toMatchInlineSnapshot('"red"')
expect(await getColor('.async-modules-blue')).toMatchInlineSnapshot(
'"black"'
)
}, true)
})

test('async css modules with normal css', async () => {
await withRetry(async () => {
expect(await getColor('.async-modules-and-css-blue')).toMatchInlineSnapshot(
'"rgb(51, 51, 51)"'
)
expect(
await getColor('.async-modules-and-css-blue50')
).toMatchInlineSnapshot('"rgb(0, 136, 255)"')
expect(
await getColor('.async-modules-and-css-fuchsia')
).toMatchInlineSnapshot('"red"')
expect(
await getColor('.async-modules-and-css-purple')
).toMatchInlineSnapshot('"purple"')
}, true)
})
6 changes: 6 additions & 0 deletions playground/css/async-module/depth/d-base-hotpink-text.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { baseText } from './d-base'
import './d-hotpink.css'

export function baseHotPinkText(className, content) {
baseText(`d-hotpink ${className}`, content)
}
6 changes: 6 additions & 0 deletions playground/css/async-module/depth/d-base-red-text.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { baseText } from './d-base'
import './d-red.css'

export function baseRedText(className, content) {
baseText(`d-red ${className}`, content)
}
8 changes: 8 additions & 0 deletions playground/css/async-module/depth/d-base.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import './d-black.css'

export function baseText(className, content) {
const div = document.createElement('div')
div.className = `d-black ${className}`
document.body.appendChild(div)
div.textContent = `${content} ${getComputedStyle(div).color}`
}
3 changes: 3 additions & 0 deletions playground/css/async-module/depth/d-black.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.d-black {
color: #333333;
}
3 changes: 3 additions & 0 deletions playground/css/async-module/depth/d-black.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.d-black {
color: black;
}
Loading