diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index b2a62f0dd98eca..3ae8aa206ba06c 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -319,13 +319,56 @@ const supportsConstructedSheet = (() => { return false })() -const sheetsMap = new Map< - string, - HTMLStyleElement | CSSStyleSheet | undefined ->() +const sheetsMap = new Map() -export function updateStyle(id: string, content: string): void { +interface StyleNode extends HTMLStyleElement { + depth: number + weight: number +} + +let weight = 0 +const entryPointWeightMap = new Set() + +export function dynamicImportModule( + moduleLoad: () => Promise, + id: string +): Promise { + 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) @@ -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 } @@ -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( + event: T, + data?: InferCustomEventPayload +): void { + messageBuffer.push(JSON.stringify({ type: 'custom', event, data })) + sendMessageBuffer() +} + /** * urls here are dynamic import() urls that couldn't be statically analyzed */ diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index b4429961ef479a..f658febd8e7ff5 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -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 @@ -298,6 +299,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { // styles initialization in buildStart causes a styling loss in watch const styles: Map = new Map() let pureCssChunks: Set + 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. @@ -336,6 +338,10 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { hasEmitted = false }, + configureServer(_server) { + serve = _server + }, + async transform(css, id, options) { if ( !isCSSRequest(id) || @@ -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 || diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index f04cb8625f864b..50ef2b649a7b93 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -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() @@ -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 }) + } } } @@ -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() for (const { url, start, end } of acceptedUrls) { @@ -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, diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index f392cfda538584..dee652387bff3b 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -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) { diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index a8222be65ae7b2..57d3d511a995f2 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -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) diff --git a/packages/vite/src/node/server/moduleGraph.ts b/packages/vite/src/node/server/moduleGraph.ts index 4bbd79cd5cc2f6..7390a18215d02f 100644 --- a/packages/vite/src/node/server/moduleGraph.ts +++ b/packages/vite/src/node/server/moduleGraph.ts @@ -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 @@ -135,6 +165,7 @@ export class ModuleGraph { async updateModuleInfo( mod: ModuleNode, importedModules: Set, + staticImportUrls: Set, importedBindings: Map> | null, acceptedModules: Set, acceptedExports: Set | null, @@ -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) => { @@ -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) { diff --git a/playground/css/__tests__/css.spec.ts b/playground/css/__tests__/css.spec.ts index 31425a19fc2c98..df191be753566b 100644 --- a/playground/css/__tests__/css.spec.ts +++ b/playground/css/__tests__/css.spec.ts @@ -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 @@ -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) +}) diff --git a/playground/css/async-module/depth/d-base-hotpink-text.js b/playground/css/async-module/depth/d-base-hotpink-text.js new file mode 100644 index 00000000000000..3ffa151ac16b02 --- /dev/null +++ b/playground/css/async-module/depth/d-base-hotpink-text.js @@ -0,0 +1,6 @@ +import { baseText } from './d-base' +import './d-hotpink.css' + +export function baseHotPinkText(className, content) { + baseText(`d-hotpink ${className}`, content) +} diff --git a/playground/css/async-module/depth/d-base-red-text.js b/playground/css/async-module/depth/d-base-red-text.js new file mode 100644 index 00000000000000..cd7b6fdce1d71c --- /dev/null +++ b/playground/css/async-module/depth/d-base-red-text.js @@ -0,0 +1,6 @@ +import { baseText } from './d-base' +import './d-red.css' + +export function baseRedText(className, content) { + baseText(`d-red ${className}`, content) +} diff --git a/playground/css/async-module/depth/d-base.js b/playground/css/async-module/depth/d-base.js new file mode 100644 index 00000000000000..07122a4dbcae20 --- /dev/null +++ b/playground/css/async-module/depth/d-base.js @@ -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}` +} diff --git a/playground/css/async-module/depth/d-black.css b/playground/css/async-module/depth/d-black.css new file mode 100644 index 00000000000000..8fed1790526585 --- /dev/null +++ b/playground/css/async-module/depth/d-black.css @@ -0,0 +1,3 @@ +.d-black { + color: #333333; +} diff --git a/playground/css/async-module/depth/d-black.module.css b/playground/css/async-module/depth/d-black.module.css new file mode 100644 index 00000000000000..40ca48c41d02fb --- /dev/null +++ b/playground/css/async-module/depth/d-black.module.css @@ -0,0 +1,3 @@ +.d-black { + color: black; +} diff --git a/playground/css/async-module/depth/d-blue.js b/playground/css/async-module/depth/d-blue.js new file mode 100644 index 00000000000000..493253d464aa08 --- /dev/null +++ b/playground/css/async-module/depth/d-blue.js @@ -0,0 +1,7 @@ +import { baseHotPinkText } from './d-base-hotpink-text' +import styles from './d-blue.module.css' + +baseHotPinkText( + `${styles['d-blue']} async-modules-and-css-blue`, + '[depth] (blue)' +) diff --git a/playground/css/async-module/depth/d-blue.module.css b/playground/css/async-module/depth/d-blue.module.css new file mode 100644 index 00000000000000..89b3748f223fb1 --- /dev/null +++ b/playground/css/async-module/depth/d-blue.module.css @@ -0,0 +1,3 @@ +.d-blue { + color: blue; +} diff --git a/playground/css/async-module/depth/d-blue50.js b/playground/css/async-module/depth/d-blue50.js new file mode 100644 index 00000000000000..94c9a7f621464e --- /dev/null +++ b/playground/css/async-module/depth/d-blue50.js @@ -0,0 +1,7 @@ +import { baseHotPinkText } from './d-base-hotpink-text' +import styles from './d-blue50.module.css' + +baseHotPinkText( + `${styles['d-blue50']} async-modules-and-css-blue50`, + '[depth] (blue50)' +) diff --git a/playground/css/async-module/depth/d-blue50.module.css b/playground/css/async-module/depth/d-blue50.module.css new file mode 100644 index 00000000000000..9d14362cd20e8d --- /dev/null +++ b/playground/css/async-module/depth/d-blue50.module.css @@ -0,0 +1,3 @@ +.d-blue50 { + color: #0088ff; +} diff --git a/playground/css/async-module/depth/d-fuchsia.js b/playground/css/async-module/depth/d-fuchsia.js new file mode 100644 index 00000000000000..2f574d86d566be --- /dev/null +++ b/playground/css/async-module/depth/d-fuchsia.js @@ -0,0 +1,8 @@ +import './d-red.css' // confuse the compiler +import { baseRedText } from './d-base-red-text' +import styles from './d-fuchsia.module.css' + +baseRedText( + `${styles['d-fuchsia']} async-modules-and-css-fuchsia`, + '[depth] (fuchsia)' +) diff --git a/playground/css/async-module/depth/d-fuchsia.module.css b/playground/css/async-module/depth/d-fuchsia.module.css new file mode 100644 index 00000000000000..42d45c0869b3b3 --- /dev/null +++ b/playground/css/async-module/depth/d-fuchsia.module.css @@ -0,0 +1,3 @@ +.d-fuchsia { + color: #ff00ff; +} diff --git a/playground/css/async-module/depth/d-hotpink.css b/playground/css/async-module/depth/d-hotpink.css new file mode 100644 index 00000000000000..15eca0b715af47 --- /dev/null +++ b/playground/css/async-module/depth/d-hotpink.css @@ -0,0 +1,4 @@ +.d-hotpink { + font-size: 1rem; + color: hotpink; +} diff --git a/playground/css/async-module/depth/d-purple.js b/playground/css/async-module/depth/d-purple.js new file mode 100644 index 00000000000000..e27396b83d3f39 --- /dev/null +++ b/playground/css/async-module/depth/d-purple.js @@ -0,0 +1,7 @@ +import { baseRedText } from './d-base-red-text' +import styles from './d-purple.module.css' + +baseRedText( + `${styles['d-purple']} async-modules-and-css-purple`, + '[depth] (purple)' +) diff --git a/playground/css/async-module/depth/d-purple.module.css b/playground/css/async-module/depth/d-purple.module.css new file mode 100644 index 00000000000000..e7222fec1d5ff6 --- /dev/null +++ b/playground/css/async-module/depth/d-purple.module.css @@ -0,0 +1,3 @@ +.d-purple { + color: #800080; +} diff --git a/playground/css/async-module/depth/d-red.css b/playground/css/async-module/depth/d-red.css new file mode 100644 index 00000000000000..e4e09a0af8ccc4 --- /dev/null +++ b/playground/css/async-module/depth/d-red.css @@ -0,0 +1,3 @@ +.d-red { + color: #ff0000; +} diff --git a/playground/css/async-module/depth/index.js b/playground/css/async-module/depth/index.js new file mode 100644 index 00000000000000..7f9dce759aa127 --- /dev/null +++ b/playground/css/async-module/depth/index.js @@ -0,0 +1,4 @@ +import('./d-blue') +import('./d-blue50') +import('./d-fuchsia') +import('./d-purple') diff --git a/playground/css/async-module/weight/base.js b/playground/css/async-module/weight/base.js new file mode 100644 index 00000000000000..260138fbff0428 --- /dev/null +++ b/playground/css/async-module/weight/base.js @@ -0,0 +1,8 @@ +import styles from './black.module.css' + +export function baseAsync(className, color) { + const div = document.createElement('div') + div.className = `${styles.black} ${className} async-modules-${color}` + document.body.appendChild(div) + div.textContent = `[weight] (${color}) ${getComputedStyle(div).color}` +} diff --git a/playground/css/async-module/weight/base2.js b/playground/css/async-module/weight/base2.js new file mode 100644 index 00000000000000..c1804accdcee4b --- /dev/null +++ b/playground/css/async-module/weight/base2.js @@ -0,0 +1,8 @@ +import styles from './red.module.css' + +export function baseAsync(className, color) { + const div = document.createElement('div') + div.className = `${styles.red} ${className} async-modules-${color}` + document.body.appendChild(div) + div.textContent = `[weight2] (${color}) ${getComputedStyle(div).color}` +} diff --git a/playground/css/async-module/weight/black.module.css b/playground/css/async-module/weight/black.module.css new file mode 100644 index 00000000000000..2491c2d607632b --- /dev/null +++ b/playground/css/async-module/weight/black.module.css @@ -0,0 +1,3 @@ +.black { + color: black; +} diff --git a/playground/css/async-module/weight/blue.js b/playground/css/async-module/weight/blue.js new file mode 100644 index 00000000000000..30f8adc83e7189 --- /dev/null +++ b/playground/css/async-module/weight/blue.js @@ -0,0 +1,4 @@ +import styles from './blue.module.css' +import { baseAsync } from './base' + +baseAsync(styles.blue, 'blue') diff --git a/playground/css/async-module/weight/blue.module.css b/playground/css/async-module/weight/blue.module.css new file mode 100644 index 00000000000000..659edcf511a928 --- /dev/null +++ b/playground/css/async-module/weight/blue.module.css @@ -0,0 +1,3 @@ +.blue { + color: blue; +} diff --git a/playground/css/async-module/weight/blue50.js b/playground/css/async-module/weight/blue50.js new file mode 100644 index 00000000000000..82d6ebaf612954 --- /dev/null +++ b/playground/css/async-module/weight/blue50.js @@ -0,0 +1,4 @@ +import { baseAsync } from './base' +import styles from './blue50.module.css' + +baseAsync(styles.blue50, 'blue50') diff --git a/playground/css/async-module/weight/blue50.module.css b/playground/css/async-module/weight/blue50.module.css new file mode 100644 index 00000000000000..866466ffa8fcbe --- /dev/null +++ b/playground/css/async-module/weight/blue50.module.css @@ -0,0 +1,3 @@ +.blue50 { + color: #0088ff; +} diff --git a/playground/css/async-module/weight/green.js b/playground/css/async-module/weight/green.js new file mode 100644 index 00000000000000..412d8ff905cbab --- /dev/null +++ b/playground/css/async-module/weight/green.js @@ -0,0 +1,6 @@ +import { baseAsync } from './base2' +import green from './green.module.css' +import blue from './blue.module.css' + +baseAsync(green.green, 'green') +baseAsync(blue.blue, 'blue2') diff --git a/playground/css/async-module/weight/green.module.css b/playground/css/async-module/weight/green.module.css new file mode 100644 index 00000000000000..fa95e11ba9ef9a --- /dev/null +++ b/playground/css/async-module/weight/green.module.css @@ -0,0 +1,3 @@ +.green { + color: green; +} diff --git a/playground/css/async-module/weight/index.js b/playground/css/async-module/weight/index.js new file mode 100644 index 00000000000000..eaa5ea65a20a46 --- /dev/null +++ b/playground/css/async-module/weight/index.js @@ -0,0 +1,6 @@ +import('./blue') +import('./red') +import('./green') +setTimeout(() => { + import('./blue50') +}) diff --git a/playground/css/async-module/weight/red.js b/playground/css/async-module/weight/red.js new file mode 100644 index 00000000000000..2a98267dedfdf6 --- /dev/null +++ b/playground/css/async-module/weight/red.js @@ -0,0 +1,4 @@ +import { baseAsync } from './base' +import styles from './red.module.css' + +baseAsync(styles.red, 'red') diff --git a/playground/css/async-module/weight/red.module.css b/playground/css/async-module/weight/red.module.css new file mode 100644 index 00000000000000..c6dd2c88fabe35 --- /dev/null +++ b/playground/css/async-module/weight/red.module.css @@ -0,0 +1,3 @@ +.red { + color: red; +} diff --git a/playground/css/index.html b/playground/css/index.html index 39e4305ceda7b8..3b6d5b1cb67cd1 100644 --- a/playground/css/index.html +++ b/playground/css/index.html @@ -18,6 +18,11 @@

CSS


   

   

+  
+

import.meta.glob import css

+ dir-import + dir-import-2 +

PostCSS nesting plugin: this should be pink diff --git a/playground/css/main.js b/playground/css/main.js index 45e7730b868eb1..68aa889267f365 100644 --- a/playground/css/main.js +++ b/playground/css/main.js @@ -106,3 +106,6 @@ document .classList.add(aliasModule.aliasedModule) import './unsupported.css' + +import './async-module/weight' +import './async-module/depth'