Implement scopedStyleStrategy (#6771)

* Implement scopedStyleStrategy

* Add changeset

* Update compiler

* Specify the eswalker version

* Update compiler

* Update .changeset/green-cups-hammer.md

Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>

* Update .changeset/green-cups-hammer.md

Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>

* Update packages/astro/src/@types/astro.ts

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* Update packages/astro/src/@types/astro.ts

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* Update .changeset/green-cups-hammer.md

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

---------

Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
Matthew Phillips 2023-05-03 11:16:03 -04:00 committed by GitHub
parent 49514e4ce4
commit 3326492b94
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 2200 additions and 2037 deletions

View file

@ -0,0 +1,21 @@
---
'astro': minor
---
Implements a new class-based scoping strategy
This implements the [Scoping RFC](https://github.com/withastro/roadmap/pull/543), providing a way to opt in to increased style specificity for Astro component styles.
This prevents bugs where global styles override Astro component styles due to CSS ordering and the use of element selectors.
To enable class-based scoping, you can set it in your config:
```js
import { defineConfig } from 'astro/config';
export default defineConfig({
scopedStyleStrategy: 'class'
});
```
Note that the 0-specificity `:where` pseudo-selector is still the default strategy. The intent is to change `'class'` to be the default in 3.0.

View file

@ -106,7 +106,7 @@
"test:e2e:match": "playwright test -g"
},
"dependencies": {
"@astrojs/compiler": "^1.3.2",
"@astrojs/compiler": "^1.4.0",
"@astrojs/language-server": "^1.0.0",
"@astrojs/markdown-remark": "^2.1.4",
"@astrojs/telemetry": "^2.1.1",
@ -119,7 +119,7 @@
"@babel/types": "^7.18.4",
"@types/babel__core": "^7.1.19",
"@types/yargs-parser": "^21.0.0",
"acorn": "^8.8.1",
"acorn": "^8.8.2",
"boxen": "^6.2.1",
"chokidar": "^3.5.3",
"ci-info": "^3.3.1",
@ -130,7 +130,7 @@
"devalue": "^4.2.0",
"diff": "^5.1.0",
"es-module-lexer": "^1.1.0",
"estree-walker": "^3.0.1",
"estree-walker": "3.0.0",
"execa": "^6.1.0",
"fast-glob": "^3.2.11",
"github-slugger": "^2.0.0",

View file

@ -494,6 +494,23 @@ export interface AstroUserConfig {
*/
trailingSlash?: 'always' | 'never' | 'ignore';
/**
* @docs
* @name scopedStyleStrategy
* @type {('where' | 'class')}
* @default `'where'`
* @description
* @version 2.3.5
*
* Specify the strategy used for scoping styles within Astro components. Choose from:
* - `'where'` - Use `:where` selectors, causing no specifity increase.
* - `'class'` - Use class-based selectors, causing a +1 specifity increase.
*
* Using `'class'` is helpful when you want to ensure that element selectors within an Astro component override global style defaults (e.g. from a global stylesheet).
* Using `'where'` gives you more control over specifity, but requires that you use higher-specifity selectors, layers, and other tools to control which selectors are applied.
*/
scopedStyleStrategy?: 'where' | 'class';
/**
* @docs
* @name adapter

View file

@ -42,6 +42,7 @@ export async function compile({
sourcemap: 'both',
internalURL: 'astro/server/index.js',
astroGlobalArgs: JSON.stringify(astroConfig.site),
scopedStyleStrategy: astroConfig.scopedStyleStrategy,
resultScopedSlot: true,
preprocessStyle: createStylePreprocessor({
filename,

View file

@ -71,6 +71,10 @@ export const AstroConfigSchema = z.object({
.union([z.literal('static'), z.literal('server')])
.optional()
.default('static'),
scopedStyleStrategy: z
.union([z.literal('where'), z.literal('class')])
.optional()
.default('where'),
adapter: z.object({ name: z.string(), hooks: z.object({}).passthrough().default({}) }).optional(),
integrations: z.preprocess(
// preprocess

View file

@ -0,0 +1,8 @@
{
"name": "@test/scoped-style-strategy",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}

View file

@ -0,0 +1,13 @@
<html>
<head>
<title>scopedStyleStrategy</title>
<style>
h1 {
color: green;
}
</style>
</head>
<body>
<h1>scopedStyleStrategy</h1>
</body>
</html>

View file

@ -0,0 +1,60 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
describe('scopedStyleStrategy', () => {
describe('default', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
let stylesheet;
before(async () => {
fixture = await loadFixture({
root: './fixtures/scoped-style-strategy/',
});
await fixture.build();
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
const $link = $('link[rel=stylesheet]');
const href = $link.attr('href');
stylesheet = await fixture.readFile(href);
});
it('includes :where pseudo-selector', () => {
expect(stylesheet).to.match(/:where/);
});
it('does not includes the class name directly in the selector', () => {
expect(stylesheet).to.not.match(/h1\.astro/);
});
});
describe('scopedStyleStrategy: "class"', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
let stylesheet;
before(async () => {
fixture = await loadFixture({
root: './fixtures/scoped-style-strategy/',
scopedStyleStrategy: 'class'
});
await fixture.build();
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
const $link = $('link[rel=stylesheet]');
const href = $link.attr('href');
stylesheet = await fixture.readFile(href);
});
it('does not include :where pseudo-selector', () => {
expect(stylesheet).to.not.match(/:where/);
});
it('includes the class name directly in the selector', () => {
expect(stylesheet).to.match(/h1\.astro/);
});
});
});

File diff suppressed because it is too large Load diff