Include styles imported by hoisted scripts (#4457)

* Include styles imported by hoisted scripts

* Add changeset

* remove unused imports
This commit is contained in:
Matthew Phillips 2022-08-25 11:14:22 -04:00 committed by GitHub
parent 467108730e
commit 9490f0e223
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 87 additions and 27 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Include styles imported by hoisted scripts

View file

@ -1,4 +1,5 @@
import type { GetModuleInfo, ModuleInfo } from 'rollup';
import { resolvedPagesVirtualModuleId } from '../app/index.js';
// This walks up the dependency graph and yields out each ModuleInfo object.
@ -22,14 +23,20 @@ export function* walkParentInfos(
}
}
// Returns true if a module is a top-level page. We determine this based on whether
// it is imported by the top-level virtual module.
export function moduleIsTopLevelPage(info: ModuleInfo): boolean {
return info.importers[0] === resolvedPagesVirtualModuleId;
}
// This function walks the dependency graph, going up until it finds a page component.
// This could be a .astro page or a .md page.
export function* getTopLevelPages(
id: string,
ctx: { getModuleInfo: GetModuleInfo }
ctx: { getModuleInfo: GetModuleInfo },
): Generator<[ModuleInfo, number], void, unknown> {
for (const res of walkParentInfos(id, ctx)) {
if (res[0]?.importers[0] === resolvedPagesVirtualModuleId) {
if (moduleIsTopLevelPage(res[0])) {
yield res;
}
}

View file

@ -1,4 +1,4 @@
import type { OutputChunk, RenderedChunk } from 'rollup';
import type { OutputChunk, RenderedChunk, ModuleInfo, GetModuleInfo } from 'rollup';
import type { PageBuildData, ViteID } from './types';
import { prependForwardSlash } from '../path.js';
@ -62,13 +62,6 @@ export function createBuildInternals(): BuildInternals {
// Pure CSS chunks are chunks that only contain CSS.
// This is all of them, and chunkToReferenceIdMap maps them to a hash id used to find the final file.
const pureCSSChunks = new Set<RenderedChunk>();
const chunkToReferenceIdMap = new Map<string, string>();
// This is a mapping of pathname to the string source of all collected
// inline <style> for a page.
const astroStyleMap = new Map<string, string>();
// This is a virtual JS module that imports all dependent styles for a page.
const astroPageStyleMap = new Map<string, string>();
// These are for tracking hoisted script bundling
const hoistedScriptIdToHoistedMap = new Map<string, Set<string>>();
@ -204,3 +197,22 @@ export function sortedCSS(pageData: PageBuildData) {
})
.map(([id]) => id);
}
export function isHoistedScript(internals: BuildInternals, id: string): boolean {
return internals.hoistedScriptIdToPagesMap.has(id);
}
export function* getPageDatasByHoistedScriptId(
internals: BuildInternals,
id: string
): Generator<PageBuildData, void, unknown> {
const set = internals.hoistedScriptIdToPagesMap.get(id);
if(set) {
for(const pageId of set) {
const pageData = getPageDataByComponent(internals, pageId.slice(1));
if(pageData) {
yield pageData;
}
}
}
}

View file

@ -9,8 +9,8 @@ import npath from 'path';
import { Plugin as VitePlugin, ResolvedConfig } from 'vite';
import { isCSSRequest } from '../render/util.js';
import { relativeToSrcDir } from '../util.js';
import { getTopLevelPages, walkParentInfos } from './graph.js';
import { eachPageData, getPageDataByViteID, getPageDatasByClientOnlyID } from './internal.js';
import { getTopLevelPages, walkParentInfos, moduleIsTopLevelPage } from './graph.js';
import { eachPageData, getPageDataByViteID, getPageDatasByClientOnlyID, getPageDatasByHoistedScriptId, isHoistedScript } from './internal.js';
interface PluginOptions {
internals: BuildInternals;
@ -104,6 +104,22 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[]
importedCss: Set<string>;
};
const appendCSSToPage = (pageData: PageBuildData, meta: ViteMetadata, depth: number) => {
for (const importedCssImport of meta.importedCss) {
// CSS is prioritized based on depth. Shared CSS has a higher depth due to being imported by multiple pages.
// Depth info is used when sorting the links on the page.
if (pageData?.css.has(importedCssImport)) {
// eslint-disable-next-line
const cssInfo = pageData?.css.get(importedCssImport)!;
if (depth < cssInfo.depth) {
cssInfo.depth = depth;
}
} else {
pageData?.css.set(importedCssImport, { depth });
}
}
}
for (const [_, chunk] of Object.entries(bundle)) {
if (chunk.type === 'chunk') {
const c = chunk;
@ -127,21 +143,16 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[]
// For this CSS chunk, walk parents until you find a page. Add the CSS to that page.
for (const [id] of Object.entries(c.modules)) {
for (const [pageInfo, depth] of getTopLevelPages(id, this)) {
const pageViteID = pageInfo.id;
const pageData = getPageDataByViteID(internals, pageViteID);
for (const importedCssImport of meta.importedCss) {
// CSS is prioritized based on depth. Shared CSS has a higher depth due to being imported by multiple pages.
// Depth info is used when sorting the links on the page.
if (pageData?.css.has(importedCssImport)) {
// eslint-disable-next-line
const cssInfo = pageData?.css.get(importedCssImport)!;
if (depth < cssInfo.depth) {
cssInfo.depth = depth;
}
} else {
pageData?.css.set(importedCssImport, { depth });
for (const [pageInfo, depth] of walkParentInfos(id, this)) {
if(moduleIsTopLevelPage(pageInfo)) {
const pageViteID = pageInfo.id;
const pageData = getPageDataByViteID(internals, pageViteID);
if(pageData) {
appendCSSToPage(pageData, meta, depth);
}
} else if(options.target === 'client' && isHoistedScript(internals, pageInfo.id)) {
for(const pageData of getPageDatasByHoistedScriptId(internals, pageInfo.id)) {
appendCSSToPage(pageData, meta, -1);
}
}
}

View file

@ -114,6 +114,13 @@ describe('Scripts (hoisted and not)', () => {
expect($('script[type="module"]').length).to.be.greaterThan(0);
});
it('Styles imported by hoisted scripts are included on the page', async () => {
let html = await fixture.readFile('/with-styles/index.html');
let $ = cheerio.load(html);
expect($('link[rel=stylesheet]')).to.have.a.lengthOf(1);
});
});
describe('Dev', () => {

View file

@ -0,0 +1,15 @@
<html>
<head>
<title>Testing</title>
</head>
<body>
<h1>Testing</h1>
<script>
import "../styles/one.css";
console.log("This component imports styles.");
</script>
</body>
</html>

View file

@ -0,0 +1,3 @@
body {
background: rebeccapurple;
}