Skip to content

Commit 54f2b04

Browse files
bluwyhkbertoson
authored andcommitted
Replace internal cssScopeTo implementation to vite.cssScopeTo (withastro#13347)
* Replace internal cssScopeTo implementation to vite.cssScopeTo * Fix test * Fix more tests * Again * Again :( * Fix weird name * Add changeset
1 parent 8ed2ced commit 54f2b04

14 files changed

+71
-115
lines changed

.changeset/hungry-sheep-sneeze.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
Updates internal CSS chunking behavior for Astro components' scoped styles. This may result in slightly more CSS chunks created, but should allow the scoped styles to only be included on pages that use them.

packages/astro/src/core/build/plugins/plugin-css.ts

+2-51
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import type { GetModuleInfo } from 'rollup';
2-
import type { BuildOptions, ResolvedConfig, Rollup, Plugin as VitePlugin } from 'vite';
2+
import type { BuildOptions, ResolvedConfig, Plugin as VitePlugin } from 'vite';
33
import { isBuildableCSSRequest } from '../../../vite-plugin-astro-server/util.js';
44
import type { BuildInternals } from '../internal.js';
55
import type { AstroBuildPlugin, BuildTarget } from '../plugin.js';
66
import type { PageBuildData, StaticBuildOptions, StylesheetAsset } from '../types.js';
77

88
import { hasAssetPropagationFlag } from '../../../content/index.js';
9-
import type { AstroPluginCssMetadata } from '../../../vite-plugin-astro/index.js';
109
import * as assetName from '../css-asset-name.js';
1110
import {
1211
getParentExtendedModuleInfos,
@@ -156,32 +155,6 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] {
156155
},
157156
};
158157

159-
/**
160-
* This plugin is a port of https://github.com/vitejs/vite/pull/16058. It enables removing unused
161-
* scoped CSS from the bundle if the scoped target (e.g. Astro files) were not bundled.
162-
* Once/If that PR is merged, we can refactor this away, renaming `meta.astroCss` to `meta.vite`.
163-
*/
164-
const cssScopeToPlugin: VitePlugin = {
165-
name: 'astro:rollup-plugin-css-scope-to',
166-
renderChunk(_, chunk, __, meta) {
167-
for (const id in chunk.modules) {
168-
// If this CSS is scoped to its importers exports, check if those importers exports
169-
// are rendered in the chunks. If they are not, we can skip bundling this CSS.
170-
const modMeta = this.getModuleInfo(id)?.meta as AstroPluginCssMetadata | undefined;
171-
const cssScopeTo = modMeta?.astroCss?.cssScopeTo;
172-
if (cssScopeTo && !isCssScopeToRendered(cssScopeTo, Object.values(meta.chunks))) {
173-
// If this CSS is not used, delete it from the chunk modules so that Vite is unable
174-
// to trace that it's used
175-
delete chunk.modules[id];
176-
const moduleIdsIndex = chunk.moduleIds.indexOf(id);
177-
if (moduleIdsIndex > -1) {
178-
chunk.moduleIds.splice(moduleIdsIndex, 1);
179-
}
180-
}
181-
}
182-
},
183-
};
184-
185158
const singleCssPlugin: VitePlugin = {
186159
name: 'astro:rollup-plugin-single-css',
187160
enforce: 'post',
@@ -273,7 +246,7 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] {
273246
},
274247
};
275248

276-
return [cssBuildPlugin, cssScopeToPlugin, singleCssPlugin, inlineStylesheetsPlugin];
249+
return [cssBuildPlugin, singleCssPlugin, inlineStylesheetsPlugin];
277250
}
278251

279252
/***** UTILITY FUNCTIONS *****/
@@ -321,25 +294,3 @@ function appendCSSToPage(
321294
}
322295
}
323296
}
324-
325-
/**
326-
* `cssScopeTo` is a map of `importer`s to its `export`s. This function iterate each `cssScopeTo` entries
327-
* and check if the `importer` and its `export`s exists in the final chunks. If at least one matches,
328-
* `cssScopeTo` is considered "rendered" by Rollup and we return true.
329-
*/
330-
function isCssScopeToRendered(
331-
cssScopeTo: Record<string, string[]>,
332-
chunks: Rollup.RenderedChunk[],
333-
) {
334-
for (const moduleId in cssScopeTo) {
335-
const exports = cssScopeTo[moduleId];
336-
// Find the chunk that renders this `moduleId` and get the rendered module
337-
const renderedModule = chunks.find((c) => c.moduleIds.includes(moduleId))?.modules[moduleId];
338-
// Return true if `renderedModule` exists and one of its exports is rendered
339-
if (renderedModule?.renderedExports.some((e) => exports.includes(e))) {
340-
return true;
341-
}
342-
}
343-
344-
return false;
345-
}

packages/astro/src/core/build/static-build.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,18 @@ async function ssrBuild(
188188
const encoded = encodeName(name);
189189
return [prefix, encoded, suffix].join('');
190190
},
191-
assetFileNames: `${settings.config.build.assets}/[name].[hash][extname]`,
191+
assetFileNames(chunkInfo) {
192+
const { names } = chunkInfo;
193+
const name = names[0] ?? '';
194+
195+
// Sometimes chunks have the `@_@astro` suffix due to SSR logic. Remove it!
196+
// TODO: refactor our build logic to avoid this
197+
if (name.includes(ASTRO_PAGE_EXTENSION_POST_PATTERN)) {
198+
const [sanitizedName] = name.split(ASTRO_PAGE_EXTENSION_POST_PATTERN);
199+
return `${settings.config.build.assets}/${sanitizedName}.[hash][extname]`;
200+
}
201+
return `${settings.config.build.assets}/[name].[hash][extname]`;
202+
},
192203
...viteConfig.build?.rollupOptions?.output,
193204
entryFileNames(chunkInfo) {
194205
if (chunkInfo.facadeModuleId?.startsWith(ASTRO_PAGE_RESOLVED_MODULE_ID)) {

packages/astro/src/vite-plugin-astro/index.ts

+5-17
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,7 @@ import type { SourceDescription } from 'rollup';
22
import type * as vite from 'vite';
33
import type { Logger } from '../core/logger/core.js';
44
import type { AstroSettings } from '../types/astro.js';
5-
import type {
6-
PluginCssMetadata as AstroPluginCssMetadata,
7-
PluginMetadata as AstroPluginMetadata,
8-
CompileMetadata,
9-
} from './types.js';
5+
import type { PluginMetadata as AstroPluginMetadata, CompileMetadata } from './types.js';
106

117
import { defaultClientConditions, defaultServerConditions, normalizePath } from 'vite';
128
import type { AstroConfig } from '../types/public/config.js';
@@ -16,7 +12,7 @@ import { handleHotUpdate } from './hmr.js';
1612
import { parseAstroRequest } from './query.js';
1713
import { loadId } from './utils.js';
1814
export { getAstroMetadata } from './metadata.js';
19-
export type { AstroPluginMetadata, AstroPluginCssMetadata };
15+
export type { AstroPluginMetadata };
2016

2117
interface AstroPluginOptions {
2218
settings: AstroSettings;
@@ -138,17 +134,9 @@ export default function astro({ settings, logger }: AstroPluginOptions): vite.Pl
138134

139135
return {
140136
code: result.code,
141-
// This metadata is used by `cssScopeToPlugin` to remove this module from the bundle
142-
// if the `filename` default export (the Astro component) is unused.
143-
meta: result.isGlobal
144-
? undefined
145-
: ({
146-
astroCss: {
147-
cssScopeTo: {
148-
[filename]: ['default'],
149-
},
150-
},
151-
} satisfies AstroPluginCssMetadata),
137+
// `vite.cssScopeTo` is a Vite feature that allows this CSS to be treeshaken
138+
// if the Astro component's default export is not used
139+
meta: result.isGlobal ? undefined : { vite: { cssScopeTo: [filename, 'default'] } },
152140
};
153141
}
154142
case 'script': {

packages/astro/src/vite-plugin-astro/types.ts

-21
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,6 @@ export interface PluginMetadata {
1818
};
1919
}
2020

21-
export interface PluginCssMetadata {
22-
astroCss: {
23-
/**
24-
* For Astro CSS virtual modules, it can scope to the main Astro module's default export
25-
* so that if those exports are treeshaken away, the CSS module will also be treeshaken.
26-
*
27-
* Example config if the CSS id is `/src/Foo.astro?astro&type=style&lang.css`:
28-
* ```js
29-
* cssScopeTo: {
30-
* '/src/Foo.astro': ['default']
31-
* }
32-
* ```
33-
*
34-
* The above is the only config we use today, but we're exposing as a `Record` to follow the
35-
* upstream Vite implementation: https://github.com/vitejs/vite/pull/16058. When/If that lands,
36-
* we can also remove our custom implementation.
37-
*/
38-
cssScopeTo: Record<string, string[]>;
39-
};
40-
}
41-
4221
export interface CompileMetadata {
4322
/** Used for HMR to compare code changes */
4423
originalCode: string;

packages/astro/test/0-css.test.js

+13-8
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,19 @@ import { after, before, describe, it } from 'node:test';
99
import * as cheerio from 'cheerio';
1010
import { loadFixture } from './test-utils.js';
1111

12-
/** @type {import('./test-utils').Fixture} */
13-
let fixture;
12+
async function getCssContent($, fixture) {
13+
const contents = await Promise.all(
14+
$('link[rel=stylesheet][href^=/_astro/]').map((_, el) =>
15+
fixture.readFile(el.attribs.href.replace(/^\/?/, '/')),
16+
),
17+
);
18+
return contents.join('').replace(/\s/g, '').replace('/n', '');
19+
}
1420

1521
describe('CSS', function () {
22+
/** @type {import('./test-utils').Fixture} */
23+
let fixture;
24+
1625
before(async () => {
1726
fixture = await loadFixture({ root: './fixtures/0-css/' });
1827
});
@@ -30,10 +39,7 @@ describe('CSS', function () {
3039
// get bundled CSS (will be hashed, hence DOM query)
3140
html = await fixture.readFile('/index.html');
3241
$ = cheerio.load(html);
33-
const bundledCSSHREF = $('link[rel=stylesheet][href^=/_astro/]').attr('href');
34-
bundledCSS = (await fixture.readFile(bundledCSSHREF.replace(/^\/?/, '/')))
35-
.replace(/\s/g, '')
36-
.replace('/n', '');
42+
bundledCSS = await getCssContent($, fixture);
3743
},
3844
{
3945
timeout: 45000,
@@ -99,8 +105,7 @@ describe('CSS', function () {
99105
it('Styles through barrel files should only include used Astro scoped styles', async () => {
100106
const barrelHtml = await fixture.readFile('/barrel-styles/index.html');
101107
const barrel$ = cheerio.load(barrelHtml);
102-
const barrelBundledCssHref = barrel$('link[rel=stylesheet][href^=/_astro/]').attr('href');
103-
const style = await fixture.readFile(barrelBundledCssHref.replace(/^\/?/, '/'));
108+
const style = await getCssContent(barrel$, fixture);
104109
assert.match(style, /\.comp-a\[data-astro-cid/);
105110
assert.match(style, /\.comp-c\{/);
106111
assert.doesNotMatch(style, /\.comp-b/);

packages/astro/test/astro-css-bundling.test.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,9 @@ describe('CSS Bundling', function () {
6363
}
6464
});
6565

66-
it('there are 4 css files', async () => {
66+
it('there are 5 css files', async () => {
6767
const dir = await fixture.readdir('/_astro');
68-
assert.equal(dir.length, 4);
68+
assert.equal(dir.length, 5);
6969
});
7070

7171
it('CSS includes hashes', async () => {
@@ -96,9 +96,9 @@ describe('CSS Bundling', function () {
9696
await fixture.build({ mode: 'production' });
9797
});
9898

99-
it('there are 4 css files', async () => {
99+
it('there are 5 css files', async () => {
100100
const dir = await fixture.readdir('/assets');
101-
assert.equal(dir.length, 4);
101+
assert.equal(dir.length, 5);
102102
});
103103

104104
it('CSS does not include hashes', async () => {

packages/astro/test/config-vite.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe('Vite Config', async () => {
2020
it('Allows overriding bundle naming options in the build', async () => {
2121
const html = await fixture.readFile('/index.html');
2222
const $ = cheerio.load(html);
23-
assert.match($('link').attr('href'), /\/assets\/testing-[a-z\d]+\.css/);
23+
assert.match($('link').attr('href'), /\/assets\/testing-.+\.css/);
2424
});
2525
});
2626

packages/astro/test/css-inline-stylesheets.test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ describe('Setting inlineStylesheets to auto in static output', () => {
113113

114114
// the count of style/link tags depends on our css chunking logic
115115
// this test should be updated if it changes
116-
assert.equal($('style').length, 3);
116+
assert.equal($('style').length, 2);
117117
assert.equal($('link[rel=stylesheet]').length, 1);
118118
});
119119

@@ -162,7 +162,7 @@ describe('Setting inlineStylesheets to auto in server output', () => {
162162

163163
// the count of style/link tags depends on our css chunking logic
164164
// this test should be updated if it changes
165-
assert.equal($('style').length, 3);
165+
assert.equal($('style').length, 2);
166166
assert.equal($('link[rel=stylesheet]').length, 1);
167167
});
168168

packages/astro/test/css-order-import.test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,9 @@ describe('CSS ordering - import order', () => {
147147
const content = await Promise.all(
148148
getLinks(html).map((href) => getLinkContent(href, fixture)),
149149
);
150-
let [link1, link2] = content;
150+
let [link1, , link3] = content;
151151
assert.ok(link1.css.includes('f0f8ff')); // aliceblue minified
152-
assert.ok(link2.css.includes('ff0')); // yellow minified
152+
assert.ok(link3.css.includes('ff0')); // yellow minified
153153
});
154154
});
155155
});

packages/astro/test/css-order.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ describe('CSS production ordering', () => {
9090
);
9191

9292
assert.ok(content.length, 3, 'there are 3 stylesheets');
93-
const [, sharedStyles, pageStyles] = content;
93+
const [, pageStyles, sharedStyles] = content;
9494

9595
assert.ok(/red/.exec(sharedStyles.css));
9696
assert.ok(/#00f/.exec(pageStyles.css));

packages/astro/test/postcss.test.js

+10-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ import * as cheerio from 'cheerio';
44
import eol from 'eol';
55
import { loadFixture } from './test-utils.js';
66

7+
async function getCssContent($, fixture) {
8+
const contents = await Promise.all(
9+
$('link[rel=stylesheet][href^=/_astro/]').map((_, el) =>
10+
fixture.readFile(el.attribs.href.replace(/^\/?/, '/')),
11+
),
12+
);
13+
return contents.join('').replace(/\s/g, '').replace('/n', '');
14+
}
15+
716
describe('PostCSS', () => {
817
let fixture;
918
let bundledCSS;
@@ -19,10 +28,7 @@ describe('PostCSS', () => {
1928
// get bundled CSS (will be hashed, hence DOM query)
2029
const html = await fixture.readFile('/index.html');
2130
const $ = cheerio.load(html);
22-
const bundledCSSHREF = $('link[rel=stylesheet][href^=/_astro/]').attr('href');
23-
bundledCSS = (await fixture.readFile(bundledCSSHREF.replace(/^\/?/, '/')))
24-
.replace(/\s/g, '')
25-
.replace('/n', '');
31+
bundledCSS = await getCssContent($, fixture);
2632
},
2733
{ timeout: 45000 },
2834
);

packages/astro/test/remote-css.test.js

+10-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@ import { before, describe, it } from 'node:test';
33
import * as cheerio from 'cheerio';
44
import { loadFixture } from './test-utils.js';
55

6+
async function getCssContent($, fixture) {
7+
const contents = await Promise.all(
8+
$('link[rel=stylesheet][href^=/_astro/]').map((_, el) =>
9+
fixture.readFile(el.attribs.href.replace(/^\/?/, '/')),
10+
),
11+
);
12+
return contents.join('').replace(/\s/g, '').replace('/n', '');
13+
}
14+
615
describe('Remote CSS', () => {
716
let fixture;
817

@@ -19,8 +28,7 @@ describe('Remote CSS', () => {
1928
const html = await fixture.readFile('/index.html');
2029
const $ = cheerio.load(html);
2130

22-
const relPath = $('link').attr('href');
23-
const css = await fixture.readFile(relPath);
31+
const css = await getCssContent($, fixture);
2432

2533
assert.match(css, /https:\/\/unpkg.com\/open-props/);
2634
assert.match(css, /body/);

packages/integrations/tailwind/test/fixtures/basic/astro.config.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,8 @@ export default defineConfig({
88
configFile: fileURLToPath(new URL('./tailwind.config.js', import.meta.url)),
99
nesting: true
1010
}),
11-
]
11+
],
12+
build: {
13+
inlineStylesheets: 'never'
14+
}
1215
});

0 commit comments

Comments
 (0)