summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/weak-grapes-join.md5
-rw-r--r--packages/astro/src/core/build/plugins/plugin-css.ts53
-rw-r--r--packages/astro/src/core/build/static-build.ts13
-rw-r--r--packages/astro/src/vite-plugin-astro/index.ts22
-rw-r--r--packages/astro/src/vite-plugin-astro/types.ts21
-rw-r--r--packages/astro/test/0-css.test.js21
-rw-r--r--packages/astro/test/astro-css-bundling.test.js8
-rw-r--r--packages/astro/test/config-vite.test.js2
-rw-r--r--packages/astro/test/css-inline-stylesheets.test.js4
-rw-r--r--packages/astro/test/css-order-import.test.js4
-rw-r--r--packages/astro/test/css-order.test.js2
-rw-r--r--packages/astro/test/postcss.test.js14
-rw-r--r--packages/astro/test/remote-css.test.js12
-rw-r--r--packages/integrations/tailwind/test/fixtures/basic/astro.config.js5
14 files changed, 120 insertions, 66 deletions
diff --git a/.changeset/weak-grapes-join.md b/.changeset/weak-grapes-join.md
new file mode 100644
index 000000000..561d79716
--- /dev/null
+++ b/.changeset/weak-grapes-join.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+It fixes an issue that caused some regressions in how styles are bundled.
diff --git a/packages/astro/src/core/build/plugins/plugin-css.ts b/packages/astro/src/core/build/plugins/plugin-css.ts
index d985a6a53..c39d4da6f 100644
--- a/packages/astro/src/core/build/plugins/plugin-css.ts
+++ b/packages/astro/src/core/build/plugins/plugin-css.ts
@@ -1,11 +1,12 @@
import type { GetModuleInfo } from 'rollup';
-import type { BuildOptions, ResolvedConfig, Plugin as VitePlugin } from 'vite';
+import type { BuildOptions, ResolvedConfig, Rollup, Plugin as VitePlugin } from 'vite';
import { isBuildableCSSRequest } from '../../../vite-plugin-astro-server/util.js';
import type { BuildInternals } from '../internal.js';
import type { AstroBuildPlugin, BuildTarget } from '../plugin.js';
import type { PageBuildData, StaticBuildOptions, StylesheetAsset } from '../types.js';
import { hasAssetPropagationFlag } from '../../../content/index.js';
+import type { AstroPluginCssMetadata } from '../../../vite-plugin-astro/index.js';
import * as assetName from '../css-asset-name.js';
import {
getParentExtendedModuleInfos,
@@ -155,6 +156,32 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] {
},
};
+ /**
+ * This plugin is a port of https://github.com/vitejs/vite/pull/16058. It enables removing unused
+ * scoped CSS from the bundle if the scoped target (e.g. Astro files) were not bundled.
+ * Once/If that PR is merged, we can refactor this away, renaming `meta.astroCss` to `meta.vite`.
+ */
+ const cssScopeToPlugin: VitePlugin = {
+ name: 'astro:rollup-plugin-css-scope-to',
+ renderChunk(_, chunk, __, meta) {
+ for (const id in chunk.modules) {
+ // If this CSS is scoped to its importers exports, check if those importers exports
+ // are rendered in the chunks. If they are not, we can skip bundling this CSS.
+ const modMeta = this.getModuleInfo(id)?.meta as AstroPluginCssMetadata | undefined;
+ const cssScopeTo = modMeta?.astroCss?.cssScopeTo;
+ if (cssScopeTo && !isCssScopeToRendered(cssScopeTo, Object.values(meta.chunks))) {
+ // If this CSS is not used, delete it from the chunk modules so that Vite is unable
+ // to trace that it's used
+ delete chunk.modules[id];
+ const moduleIdsIndex = chunk.moduleIds.indexOf(id);
+ if (moduleIdsIndex > -1) {
+ chunk.moduleIds.splice(moduleIdsIndex, 1);
+ }
+ }
+ }
+ },
+ };
+
const singleCssPlugin: VitePlugin = {
name: 'astro:rollup-plugin-single-css',
enforce: 'post',
@@ -246,7 +273,7 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] {
},
};
- return [cssBuildPlugin, singleCssPlugin, inlineStylesheetsPlugin];
+ return [cssBuildPlugin, cssScopeToPlugin, singleCssPlugin, inlineStylesheetsPlugin];
}
/***** UTILITY FUNCTIONS *****/
@@ -294,3 +321,25 @@ function appendCSSToPage(
}
}
}
+
+/**
+ * `cssScopeTo` is a map of `importer`s to its `export`s. This function iterate each `cssScopeTo` entries
+ * and check if the `importer` and its `export`s exists in the final chunks. If at least one matches,
+ * `cssScopeTo` is considered "rendered" by Rollup and we return true.
+ */
+function isCssScopeToRendered(
+ cssScopeTo: Record<string, string[]>,
+ chunks: Rollup.RenderedChunk[],
+) {
+ for (const moduleId in cssScopeTo) {
+ const exports = cssScopeTo[moduleId];
+ // Find the chunk that renders this `moduleId` and get the rendered module
+ const renderedModule = chunks.find((c) => c.moduleIds.includes(moduleId))?.modules[moduleId];
+ // Return true if `renderedModule` exists and one of its exports is rendered
+ if (renderedModule?.renderedExports.some((e) => exports.includes(e))) {
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts
index eeecacf33..9e903696c 100644
--- a/packages/astro/src/core/build/static-build.ts
+++ b/packages/astro/src/core/build/static-build.ts
@@ -188,18 +188,7 @@ async function ssrBuild(
const encoded = encodeName(name);
return [prefix, encoded, suffix].join('');
},
- assetFileNames(chunkInfo) {
- const { names } = chunkInfo;
- const name = names[0] ?? '';
-
- // Sometimes chunks have the `@_@astro` suffix due to SSR logic. Remove it!
- // TODO: refactor our build logic to avoid this
- if (name.includes(ASTRO_PAGE_EXTENSION_POST_PATTERN)) {
- const [sanitizedName] = name.split(ASTRO_PAGE_EXTENSION_POST_PATTERN);
- return `${settings.config.build.assets}/${sanitizedName}.[hash][extname]`;
- }
- return `${settings.config.build.assets}/[name].[hash][extname]`;
- },
+ assetFileNames: `${settings.config.build.assets}/[name].[hash][extname]`,
...viteConfig.build?.rollupOptions?.output,
entryFileNames(chunkInfo) {
if (chunkInfo.facadeModuleId?.startsWith(ASTRO_PAGE_RESOLVED_MODULE_ID)) {
diff --git a/packages/astro/src/vite-plugin-astro/index.ts b/packages/astro/src/vite-plugin-astro/index.ts
index c86286e2d..21d9dcfb1 100644
--- a/packages/astro/src/vite-plugin-astro/index.ts
+++ b/packages/astro/src/vite-plugin-astro/index.ts
@@ -2,7 +2,11 @@ import type { SourceDescription } from 'rollup';
import type * as vite from 'vite';
import type { Logger } from '../core/logger/core.js';
import type { AstroSettings } from '../types/astro.js';
-import type { PluginMetadata as AstroPluginMetadata, CompileMetadata } from './types.js';
+import type {
+ PluginCssMetadata as AstroPluginCssMetadata,
+ PluginMetadata as AstroPluginMetadata,
+ CompileMetadata,
+} from './types.js';
import { defaultClientConditions, defaultServerConditions, normalizePath } from 'vite';
import type { AstroConfig } from '../types/public/config.js';
@@ -12,7 +16,7 @@ import { handleHotUpdate } from './hmr.js';
import { parseAstroRequest } from './query.js';
import { loadId } from './utils.js';
export { getAstroMetadata } from './metadata.js';
-export type { AstroPluginMetadata };
+export type { AstroPluginMetadata, AstroPluginCssMetadata };
interface AstroPluginOptions {
settings: AstroSettings;
@@ -134,9 +138,17 @@ export default function astro({ settings, logger }: AstroPluginOptions): vite.Pl
return {
code: result.code,
- // `vite.cssScopeTo` is a Vite feature that allows this CSS to be treeshaken
- // if the Astro component's default export is not used
- meta: result.isGlobal ? undefined : { vite: { cssScopeTo: [filename, 'default'] } },
+ // This metadata is used by `cssScopeToPlugin` to remove this module from the bundle
+ // if the `filename` default export (the Astro component) is unused.
+ meta: result.isGlobal
+ ? undefined
+ : ({
+ astroCss: {
+ cssScopeTo: {
+ [filename]: ['default'],
+ },
+ },
+ } satisfies AstroPluginCssMetadata),
};
}
case 'script': {
diff --git a/packages/astro/src/vite-plugin-astro/types.ts b/packages/astro/src/vite-plugin-astro/types.ts
index 61cd6db44..d85fd6483 100644
--- a/packages/astro/src/vite-plugin-astro/types.ts
+++ b/packages/astro/src/vite-plugin-astro/types.ts
@@ -18,6 +18,27 @@ export interface PluginMetadata {
};
}
+export interface PluginCssMetadata {
+ astroCss: {
+ /**
+ * For Astro CSS virtual modules, it can scope to the main Astro module's default export
+ * so that if those exports are treeshaken away, the CSS module will also be treeshaken.
+ *
+ * Example config if the CSS id is `/src/Foo.astro?astro&type=style&lang.css`:
+ * ```js
+ * cssScopeTo: {
+ * '/src/Foo.astro': ['default']
+ * }
+ * ```
+ *
+ * The above is the only config we use today, but we're exposing as a `Record` to follow the
+ * upstream Vite implementation: https://github.com/vitejs/vite/pull/16058. When/If that lands,
+ * we can also remove our custom implementation.
+ */
+ cssScopeTo: Record<string, string[]>;
+ };
+}
+
export interface CompileMetadata {
/** Used for HMR to compare code changes */
originalCode: string;
diff --git a/packages/astro/test/0-css.test.js b/packages/astro/test/0-css.test.js
index e200c7d56..65010f580 100644
--- a/packages/astro/test/0-css.test.js
+++ b/packages/astro/test/0-css.test.js
@@ -9,19 +9,10 @@ import { after, before, describe, it } from 'node:test';
import * as cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
-async function getCssContent($, fixture) {
- const contents = await Promise.all(
- $('link[rel=stylesheet][href^=/_astro/]').map((_, el) =>
- fixture.readFile(el.attribs.href.replace(/^\/?/, '/')),
- ),
- );
- return contents.join('').replace(/\s/g, '').replace('/n', '');
-}
+/** @type {import('./test-utils').Fixture} */
+let fixture;
describe('CSS', function () {
- /** @type {import('./test-utils').Fixture} */
- let fixture;
-
before(async () => {
fixture = await loadFixture({ root: './fixtures/0-css/' });
});
@@ -39,7 +30,10 @@ describe('CSS', function () {
// get bundled CSS (will be hashed, hence DOM query)
html = await fixture.readFile('/index.html');
$ = cheerio.load(html);
- bundledCSS = await getCssContent($, fixture);
+ const bundledCSSHREF = $('link[rel=stylesheet][href^=/_astro/]').attr('href');
+ bundledCSS = (await fixture.readFile(bundledCSSHREF.replace(/^\/?/, '/')))
+ .replace(/\s/g, '')
+ .replace('/n', '');
},
{
timeout: 45000,
@@ -105,7 +99,8 @@ describe('CSS', function () {
it('Styles through barrel files should only include used Astro scoped styles', async () => {
const barrelHtml = await fixture.readFile('/barrel-styles/index.html');
const barrel$ = cheerio.load(barrelHtml);
- const style = await getCssContent(barrel$, fixture);
+ const barrelBundledCssHref = barrel$('link[rel=stylesheet][href^=/_astro/]').attr('href');
+ const style = await fixture.readFile(barrelBundledCssHref.replace(/^\/?/, '/'));
assert.match(style, /\.comp-a\[data-astro-cid/);
assert.match(style, /\.comp-c\{/);
assert.doesNotMatch(style, /\.comp-b/);
diff --git a/packages/astro/test/astro-css-bundling.test.js b/packages/astro/test/astro-css-bundling.test.js
index 8319563ff..92aa6db80 100644
--- a/packages/astro/test/astro-css-bundling.test.js
+++ b/packages/astro/test/astro-css-bundling.test.js
@@ -63,9 +63,9 @@ describe('CSS Bundling', function () {
}
});
- it('there are 5 css files', async () => {
+ it('there are 4 css files', async () => {
const dir = await fixture.readdir('/_astro');
- assert.equal(dir.length, 5);
+ assert.equal(dir.length, 4);
});
it('CSS includes hashes', async () => {
@@ -96,9 +96,9 @@ describe('CSS Bundling', function () {
await fixture.build({ mode: 'production' });
});
- it('there are 5 css files', async () => {
+ it('there are 4 css files', async () => {
const dir = await fixture.readdir('/assets');
- assert.equal(dir.length, 5);
+ assert.equal(dir.length, 4);
});
it('CSS does not include hashes', async () => {
diff --git a/packages/astro/test/config-vite.test.js b/packages/astro/test/config-vite.test.js
index 8d9bb74f5..90d3487f9 100644
--- a/packages/astro/test/config-vite.test.js
+++ b/packages/astro/test/config-vite.test.js
@@ -20,7 +20,7 @@ describe('Vite Config', async () => {
it('Allows overriding bundle naming options in the build', async () => {
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
- assert.match($('link').attr('href'), /\/assets\/testing-.+\.css/);
+ assert.match($('link').attr('href'), /\/assets\/testing-[a-z\d]+\.css/);
});
});
diff --git a/packages/astro/test/css-inline-stylesheets.test.js b/packages/astro/test/css-inline-stylesheets.test.js
index 53771c99a..7bae334e1 100644
--- a/packages/astro/test/css-inline-stylesheets.test.js
+++ b/packages/astro/test/css-inline-stylesheets.test.js
@@ -113,7 +113,7 @@ describe('Setting inlineStylesheets to auto in static output', () => {
// the count of style/link tags depends on our css chunking logic
// this test should be updated if it changes
- assert.equal($('style').length, 2);
+ assert.equal($('style').length, 3);
assert.equal($('link[rel=stylesheet]').length, 1);
});
@@ -162,7 +162,7 @@ describe('Setting inlineStylesheets to auto in server output', () => {
// the count of style/link tags depends on our css chunking logic
// this test should be updated if it changes
- assert.equal($('style').length, 2);
+ assert.equal($('style').length, 3);
assert.equal($('link[rel=stylesheet]').length, 1);
});
diff --git a/packages/astro/test/css-order-import.test.js b/packages/astro/test/css-order-import.test.js
index 9b67f1580..f8a9cf5b2 100644
--- a/packages/astro/test/css-order-import.test.js
+++ b/packages/astro/test/css-order-import.test.js
@@ -147,9 +147,9 @@ describe('CSS ordering - import order', () => {
const content = await Promise.all(
getLinks(html).map((href) => getLinkContent(href, fixture)),
);
- let [link1, , link3] = content;
+ let [link1, link2] = content;
assert.ok(link1.css.includes('f0f8ff')); // aliceblue minified
- assert.ok(link3.css.includes('ff0')); // yellow minified
+ assert.ok(link2.css.includes('ff0')); // yellow minified
});
});
});
diff --git a/packages/astro/test/css-order.test.js b/packages/astro/test/css-order.test.js
index 552dae3e1..2cc937b6c 100644
--- a/packages/astro/test/css-order.test.js
+++ b/packages/astro/test/css-order.test.js
@@ -90,7 +90,7 @@ describe('CSS production ordering', () => {
);
assert.ok(content.length, 3, 'there are 3 stylesheets');
- const [, pageStyles, sharedStyles] = content;
+ const [, sharedStyles, pageStyles] = content;
assert.ok(/red/.exec(sharedStyles.css));
assert.ok(/#00f/.exec(pageStyles.css));
diff --git a/packages/astro/test/postcss.test.js b/packages/astro/test/postcss.test.js
index 35cb42c38..145db478f 100644
--- a/packages/astro/test/postcss.test.js
+++ b/packages/astro/test/postcss.test.js
@@ -4,15 +4,6 @@ import * as cheerio from 'cheerio';
import eol from 'eol';
import { loadFixture } from './test-utils.js';
-async function getCssContent($, fixture) {
- const contents = await Promise.all(
- $('link[rel=stylesheet][href^=/_astro/]').map((_, el) =>
- fixture.readFile(el.attribs.href.replace(/^\/?/, '/')),
- ),
- );
- return contents.join('').replace(/\s/g, '').replace('/n', '');
-}
-
describe('PostCSS', () => {
let fixture;
let bundledCSS;
@@ -28,7 +19,10 @@ describe('PostCSS', () => {
// get bundled CSS (will be hashed, hence DOM query)
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
- bundledCSS = await getCssContent($, fixture);
+ const bundledCSSHREF = $('link[rel=stylesheet][href^=/_astro/]').attr('href');
+ bundledCSS = (await fixture.readFile(bundledCSSHREF.replace(/^\/?/, '/')))
+ .replace(/\s/g, '')
+ .replace('/n', '');
},
{ timeout: 45000 },
);
diff --git a/packages/astro/test/remote-css.test.js b/packages/astro/test/remote-css.test.js
index af8706888..b6b0b03a1 100644
--- a/packages/astro/test/remote-css.test.js
+++ b/packages/astro/test/remote-css.test.js
@@ -3,15 +3,6 @@ import { before, describe, it } from 'node:test';
import * as cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
-async function getCssContent($, fixture) {
- const contents = await Promise.all(
- $('link[rel=stylesheet][href^=/_astro/]').map((_, el) =>
- fixture.readFile(el.attribs.href.replace(/^\/?/, '/')),
- ),
- );
- return contents.join('').replace(/\s/g, '').replace('/n', '');
-}
-
describe('Remote CSS', () => {
let fixture;
@@ -28,7 +19,8 @@ describe('Remote CSS', () => {
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
- const css = await getCssContent($, fixture);
+ const relPath = $('link').attr('href');
+ const css = await fixture.readFile(relPath);
assert.match(css, /https:\/\/unpkg.com\/open-props/);
assert.match(css, /body/);
diff --git a/packages/integrations/tailwind/test/fixtures/basic/astro.config.js b/packages/integrations/tailwind/test/fixtures/basic/astro.config.js
index 98f6925ae..502f5ca17 100644
--- a/packages/integrations/tailwind/test/fixtures/basic/astro.config.js
+++ b/packages/integrations/tailwind/test/fixtures/basic/astro.config.js
@@ -8,8 +8,5 @@ export default defineConfig({
configFile: fileURLToPath(new URL('./tailwind.config.js', import.meta.url)),
nesting: true
}),
- ],
- build: {
- inlineStylesheets: 'never'
- }
+ ]
});