feat: new attribute scope style strategy (#7893)
This commit is contained in:
parent
ba73dea026
commit
7bd1b86f85
9 changed files with 101 additions and 29 deletions
17
.changeset/neat-suns-search.md
Normal file
17
.changeset/neat-suns-search.md
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
'astro': major
|
||||||
|
---
|
||||||
|
|
||||||
|
Implements a new scope style strategy called `"attribute"`. When enabled, styles are applied using `data-*` attributes.
|
||||||
|
|
||||||
|
The **default** value of `scopedStyleStrategy` is `"attribute"`.
|
||||||
|
|
||||||
|
If you want to use the previous behaviour, you have to use the `"where"` option:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
+ scopedStyleStrategy: 'where',
|
||||||
|
});
|
||||||
|
```
|
|
@ -116,7 +116,7 @@
|
||||||
"test:e2e:match": "playwright test -g"
|
"test:e2e:match": "playwright test -g"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/compiler": "^1.8.0",
|
"@astrojs/compiler": "^1.8.1",
|
||||||
"@astrojs/internal-helpers": "workspace:*",
|
"@astrojs/internal-helpers": "workspace:*",
|
||||||
"@astrojs/markdown-remark": "workspace:*",
|
"@astrojs/markdown-remark": "workspace:*",
|
||||||
"@astrojs/telemetry": "workspace:*",
|
"@astrojs/telemetry": "workspace:*",
|
||||||
|
|
|
@ -20,9 +20,10 @@ import type { AstroConfigSchema } from '../core/config';
|
||||||
import type { AstroTimer } from '../core/config/timer';
|
import type { AstroTimer } from '../core/config/timer';
|
||||||
import type { AstroCookies } from '../core/cookies';
|
import type { AstroCookies } from '../core/cookies';
|
||||||
import type { LogOptions, LoggerLevel } from '../core/logger/core';
|
import type { LogOptions, LoggerLevel } from '../core/logger/core';
|
||||||
import { AstroIntegrationLogger } from '../core/logger/core';
|
import type { AstroIntegrationLogger } from '../core/logger/core';
|
||||||
import type { AstroComponentFactory, AstroComponentInstance } from '../runtime/server';
|
import type { AstroComponentFactory, AstroComponentInstance } from '../runtime/server';
|
||||||
import type { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../core/constants.js';
|
import type { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../core/constants.js';
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
MarkdownHeading,
|
MarkdownHeading,
|
||||||
MarkdownMetadata,
|
MarkdownMetadata,
|
||||||
|
@ -609,7 +610,7 @@ export interface AstroUserConfig {
|
||||||
/**
|
/**
|
||||||
* @docs
|
* @docs
|
||||||
* @name scopedStyleStrategy
|
* @name scopedStyleStrategy
|
||||||
* @type {('where' | 'class')}
|
* @type {('where' | 'class' | 'attribute')}
|
||||||
* @default `'where'`
|
* @default `'where'`
|
||||||
* @version 2.4
|
* @version 2.4
|
||||||
* @description
|
* @description
|
||||||
|
@ -617,11 +618,13 @@ export interface AstroUserConfig {
|
||||||
* Specify the strategy used for scoping styles within Astro components. Choose from:
|
* Specify the strategy used for scoping styles within Astro components. Choose from:
|
||||||
* - `'where'` - Use `:where` selectors, causing no specifity increase.
|
* - `'where'` - Use `:where` selectors, causing no specifity increase.
|
||||||
* - `'class'` - Use class-based selectors, causing a +1 specifity increase.
|
* - `'class'` - Use class-based selectors, causing a +1 specifity increase.
|
||||||
|
* - `'attribute'` - Use `data-` attributes, causing no 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 `'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.
|
* 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.
|
||||||
|
* Using `'attribute'` is useful in case there's manipulation of the class attributes, so the styling emitted by Astro doesn't go in conflict with the user's business logic.
|
||||||
*/
|
*/
|
||||||
scopedStyleStrategy?: 'where' | 'class';
|
scopedStyleStrategy?: 'where' | 'class' | 'attribute';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @docs
|
* @docs
|
||||||
|
|
|
@ -87,9 +87,9 @@ export const AstroConfigSchema = z.object({
|
||||||
.optional()
|
.optional()
|
||||||
.default('static'),
|
.default('static'),
|
||||||
scopedStyleStrategy: z
|
scopedStyleStrategy: z
|
||||||
.union([z.literal('where'), z.literal('class')])
|
.union([z.literal('where'), z.literal('class'), z.literal('attribute')])
|
||||||
.optional()
|
.optional()
|
||||||
.default('where'),
|
.default('attribute'),
|
||||||
adapter: z.object({ name: z.string(), hooks: z.object({}).passthrough().default({}) }).optional(),
|
adapter: z.object({ name: z.string(), hooks: z.object({}).passthrough().default({}) }).optional(),
|
||||||
integrations: z.preprocess(
|
integrations: z.preprocess(
|
||||||
// preprocess
|
// preprocess
|
||||||
|
|
|
@ -39,15 +39,27 @@ describe('CSS', function () {
|
||||||
it('HTML and CSS scoped correctly', async () => {
|
it('HTML and CSS scoped correctly', async () => {
|
||||||
const el1 = $('#dynamic-class');
|
const el1 = $('#dynamic-class');
|
||||||
const el2 = $('#dynamic-vis');
|
const el2 = $('#dynamic-vis');
|
||||||
const classes = $('#class').attr('class').split(' ');
|
const classes = $('#class');
|
||||||
const scopedClass = classes.find((name) => /^astro-[A-Za-z0-9-]+/.test(name));
|
let scopedAttribute;
|
||||||
|
for (const [key] of Object.entries(classes[0].attribs)) {
|
||||||
|
if (/^data-astro-cid-[A-Za-z0-9-]+/.test(key)) {
|
||||||
|
// Ema: this is ugly, but for reasons that I don't want to explore, cheerio
|
||||||
|
// lower case the hash of the attribute
|
||||||
|
scopedAttribute = key
|
||||||
|
.toUpperCase()
|
||||||
|
.replace('data-astro-cid-'.toUpperCase(), 'data-astro-cid-');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!scopedAttribute) {
|
||||||
|
throw new Error("Couldn't find scoped attribute");
|
||||||
|
}
|
||||||
|
|
||||||
// 1. check HTML
|
// 1. check HTML
|
||||||
expect(el1.attr('class')).to.equal(`blue ${scopedClass}`);
|
expect(el1.attr('class')).to.equal(`blue`);
|
||||||
expect(el2.attr('class')).to.equal(`visible ${scopedClass}`);
|
expect(el2.attr('class')).to.equal(`visible`);
|
||||||
|
|
||||||
// 2. check CSS
|
// 2. check CSS
|
||||||
const expected = `.blue:where(.${scopedClass}){color:#b0e0e6}.color\\:blue:where(.${scopedClass}){color:#b0e0e6}.visible:where(.${scopedClass}){display:block}`;
|
const expected = `.blue[${scopedAttribute}],.color\\:blue[${scopedAttribute}]{color:#b0e0e6}.visible[${scopedAttribute}]{display:block}`;
|
||||||
expect(bundledCSS).to.include(expected);
|
expect(bundledCSS).to.include(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -60,8 +72,12 @@ describe('CSS', function () {
|
||||||
expect($('#no-scope').attr('class')).to.equal(undefined);
|
expect($('#no-scope').attr('class')).to.equal(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Child inheritance', async () => {
|
it('Child inheritance', (done) => {
|
||||||
expect($('#passed-in').attr('class')).to.match(/outer astro-[A-Z0-9]+ astro-[A-Z0-9]+/);
|
for (const [key] of Object.entries($('#passed-in')[0].attribs)) {
|
||||||
|
if (/^data-astro-cid-[A-Za-z0-9-]+/.test(key)) {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Using hydrated components adds astro-island styles', async () => {
|
it('Using hydrated components adds astro-island styles', async () => {
|
||||||
|
@ -70,11 +86,11 @@ describe('CSS', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('<style lang="sass">', async () => {
|
it('<style lang="sass">', async () => {
|
||||||
expect(bundledCSS).to.match(new RegExp('h1\\:where\\(.astro-[^{]*{color:#90ee90}'));
|
expect(bundledCSS).to.match(new RegExp('h1\\[data-astro-cid-[^{]*{color:#90ee90}'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('<style lang="scss">', async () => {
|
it('<style lang="scss">', async () => {
|
||||||
expect(bundledCSS).to.match(new RegExp('h1\\:where\\(.astro-[^{]*{color:#ff69b4}'));
|
expect(bundledCSS).to.match(new RegExp('h1\\[data-astro-cid-[^{]*{color:#ff69b4}'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -331,10 +347,10 @@ describe('CSS', function () {
|
||||||
it('resolves Astro styles', async () => {
|
it('resolves Astro styles', async () => {
|
||||||
const allInjectedStyles = $('style').text();
|
const allInjectedStyles = $('style').text();
|
||||||
|
|
||||||
expect(allInjectedStyles).to.contain('.linked-css:where(.astro-');
|
expect(allInjectedStyles).to.contain('.linked-css[data-astro-cid-');
|
||||||
expect(allInjectedStyles).to.contain('.linked-sass:where(.astro-');
|
expect(allInjectedStyles).to.contain('.linked-sass[data-astro-cid-');
|
||||||
expect(allInjectedStyles).to.contain('.linked-scss:where(.astro-');
|
expect(allInjectedStyles).to.contain('.linked-scss[data-astro-cid-');
|
||||||
expect(allInjectedStyles).to.contain('.wrapper:where(.astro-');
|
expect(allInjectedStyles).to.contain('.wrapper[data-astro-cid-');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resolves Styles from React', async () => {
|
it('resolves Styles from React', async () => {
|
||||||
|
|
|
@ -26,7 +26,7 @@ describe('Partial HTML', async () => {
|
||||||
|
|
||||||
// test 2: correct CSS present
|
// test 2: correct CSS present
|
||||||
const allInjectedStyles = $('style').text();
|
const allInjectedStyles = $('style').text();
|
||||||
expect(allInjectedStyles).to.match(/\:where\(\.astro-[^{]+{color:red}/);
|
expect(allInjectedStyles).to.match(/\[data-astro-cid-[^{]+{color:red}/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('injects framework styles', async () => {
|
it('injects framework styles', async () => {
|
||||||
|
|
|
@ -32,7 +32,7 @@ describe('CSS', function () {
|
||||||
|
|
||||||
it('vite.build.cssTarget is respected', async () => {
|
it('vite.build.cssTarget is respected', async () => {
|
||||||
expect(bundledCSS).to.match(
|
expect(bundledCSS).to.match(
|
||||||
new RegExp('.class\\:where\\(.astro-[^{]*{top:0;right:0;bottom:0;left:0}')
|
new RegExp('.class\\[data-astro-[^{]*{top:0;right:0;bottom:0;left:0}')
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,7 +3,7 @@ import * as cheerio from 'cheerio';
|
||||||
import { loadFixture } from './test-utils.js';
|
import { loadFixture } from './test-utils.js';
|
||||||
|
|
||||||
describe('scopedStyleStrategy', () => {
|
describe('scopedStyleStrategy', () => {
|
||||||
describe('default', () => {
|
describe('scopedStyleStrategy: "where"', () => {
|
||||||
/** @type {import('./test-utils').Fixture} */
|
/** @type {import('./test-utils').Fixture} */
|
||||||
let fixture;
|
let fixture;
|
||||||
let stylesheet;
|
let stylesheet;
|
||||||
|
@ -11,6 +11,7 @@ describe('scopedStyleStrategy', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
fixture = await loadFixture({
|
fixture = await loadFixture({
|
||||||
root: './fixtures/scoped-style-strategy/',
|
root: './fixtures/scoped-style-strategy/',
|
||||||
|
scopedStyleStrategy: 'where',
|
||||||
});
|
});
|
||||||
await fixture.build();
|
await fixture.build();
|
||||||
|
|
||||||
|
@ -57,4 +58,35 @@ describe('scopedStyleStrategy', () => {
|
||||||
expect(stylesheet).to.match(/h1\.astro/);
|
expect(stylesheet).to.match(/h1\.astro/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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('does not include :where pseudo-selector', () => {
|
||||||
|
expect(stylesheet).to.not.match(/:where/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not include the class name directly in the selector', () => {
|
||||||
|
expect(stylesheet).to.not.match(/h1\.astro/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes the data attribute hash', () => {
|
||||||
|
expect(stylesheet).to.include('h1[data-astro-cid-');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -480,8 +480,8 @@ importers:
|
||||||
packages/astro:
|
packages/astro:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/compiler':
|
'@astrojs/compiler':
|
||||||
specifier: ^1.8.0
|
specifier: ^1.8.1
|
||||||
version: 1.8.0
|
version: 1.8.1
|
||||||
'@astrojs/internal-helpers':
|
'@astrojs/internal-helpers':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../internal-helpers
|
version: link:../internal-helpers
|
||||||
|
@ -5391,6 +5391,10 @@ packages:
|
||||||
|
|
||||||
/@astrojs/compiler@1.8.0:
|
/@astrojs/compiler@1.8.0:
|
||||||
resolution: {integrity: sha512-E0TI/uyO8n+IPSZ4Fvl9Lne8JKEasR6ZMGvE2G096oTWOXSsPAhRs2LomV3z+/VRepo2h+t/SdVo54wox4eJwA==}
|
resolution: {integrity: sha512-E0TI/uyO8n+IPSZ4Fvl9Lne8JKEasR6ZMGvE2G096oTWOXSsPAhRs2LomV3z+/VRepo2h+t/SdVo54wox4eJwA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@astrojs/compiler@1.8.1:
|
||||||
|
resolution: {integrity: sha512-C28qplQzgIJ+JU9S+1wNx+ue2KCBUp0TTAd10EWAEkk4RsL3Tzlw0BYvLDDb4KP9jS48lXmR4/1TtZ4aavYJ8Q==}
|
||||||
|
|
||||||
/@astrojs/internal-helpers@0.1.2:
|
/@astrojs/internal-helpers@0.1.2:
|
||||||
resolution: {integrity: sha512-YXLk1CUDdC9P5bjFZcGjz+cE/ZDceXObDTXn/GCID4r8LjThuexxi+dlJqukmUpkSItzQqgzfWnrPLxSFPejdA==}
|
resolution: {integrity: sha512-YXLk1CUDdC9P5bjFZcGjz+cE/ZDceXObDTXn/GCID4r8LjThuexxi+dlJqukmUpkSItzQqgzfWnrPLxSFPejdA==}
|
||||||
|
@ -5400,7 +5404,7 @@ packages:
|
||||||
resolution: {integrity: sha512-oEw7AwJmzjgy6HC9f5IdrphZ1GVgfV/+7xQuyf52cpTiRWd/tJISK3MsKP0cDkVlfodmNABNFnAaAWuLZEiiiA==}
|
resolution: {integrity: sha512-oEw7AwJmzjgy6HC9f5IdrphZ1GVgfV/+7xQuyf52cpTiRWd/tJISK3MsKP0cDkVlfodmNABNFnAaAWuLZEiiiA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/compiler': 1.8.0
|
'@astrojs/compiler': 1.8.1
|
||||||
'@jridgewell/trace-mapping': 0.3.18
|
'@jridgewell/trace-mapping': 0.3.18
|
||||||
'@vscode/emmet-helper': 2.8.8
|
'@vscode/emmet-helper': 2.8.8
|
||||||
events: 3.3.0
|
events: 3.3.0
|
||||||
|
@ -15600,7 +15604,7 @@ packages:
|
||||||
resolution: {integrity: sha512-dPzop0gKZyVGpTDQmfy+e7FKXC9JT3mlpfYA2diOVz+Ui+QR1U4G/s+OesKl2Hib2JJOtAYJs/l+ovgT0ljlFA==}
|
resolution: {integrity: sha512-dPzop0gKZyVGpTDQmfy+e7FKXC9JT3mlpfYA2diOVz+Ui+QR1U4G/s+OesKl2Hib2JJOtAYJs/l+ovgT0ljlFA==}
|
||||||
engines: {node: ^14.15.0 || >=16.0.0, pnpm: '>=7.14.0'}
|
engines: {node: ^14.15.0 || >=16.0.0, pnpm: '>=7.14.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/compiler': 1.8.0
|
'@astrojs/compiler': 1.8.1
|
||||||
prettier: 2.8.8
|
prettier: 2.8.8
|
||||||
sass-formatter: 0.7.6
|
sass-formatter: 0.7.6
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -15609,7 +15613,7 @@ packages:
|
||||||
resolution: {integrity: sha512-lJ/mG/Lz/ccSwNtwqpFS126mtMVzFVyYv0ddTF9wqwrEG4seECjKDAyw/oGv915rAcJi8jr89990nqfpmG+qdg==}
|
resolution: {integrity: sha512-lJ/mG/Lz/ccSwNtwqpFS126mtMVzFVyYv0ddTF9wqwrEG4seECjKDAyw/oGv915rAcJi8jr89990nqfpmG+qdg==}
|
||||||
engines: {node: ^14.15.0 || >=16.0.0, pnpm: '>=7.14.0'}
|
engines: {node: ^14.15.0 || >=16.0.0, pnpm: '>=7.14.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/compiler': 1.8.0
|
'@astrojs/compiler': 1.8.1
|
||||||
prettier: 2.8.8
|
prettier: 2.8.8
|
||||||
sass-formatter: 0.7.6
|
sass-formatter: 0.7.6
|
||||||
synckit: 0.8.5
|
synckit: 0.8.5
|
||||||
|
|
Loading…
Reference in a new issue