diff options
author | 2023-08-08 12:59:56 +0100 | |
---|---|---|
committer | 2023-08-08 12:59:56 +0100 | |
commit | 7bd1b86f85c06fdde0a1ed9146d01bac69990671 (patch) | |
tree | c684a68e359d643ac174f9f7ef5af802de0709de | |
parent | ba73dea0262e89c9145aeea13252ea2da3f5ecd1 (diff) | |
download | astro-7bd1b86f85c06fdde0a1ed9146d01bac69990671.tar.gz astro-7bd1b86f85c06fdde0a1ed9146d01bac69990671.tar.zst astro-7bd1b86f85c06fdde0a1ed9146d01bac69990671.zip |
feat: new attribute scope style strategy (#7893)
-rw-r--r-- | .changeset/neat-suns-search.md | 17 | ||||
-rw-r--r-- | packages/astro/package.json | 2 | ||||
-rw-r--r-- | packages/astro/src/@types/astro.ts | 13 | ||||
-rw-r--r-- | packages/astro/src/core/config/schema.ts | 4 | ||||
-rw-r--r-- | packages/astro/test/0-css.test.js | 42 | ||||
-rw-r--r-- | packages/astro/test/astro-partial-html.test.js | 2 | ||||
-rw-r--r-- | packages/astro/test/config-vite-css-target.test.js | 2 | ||||
-rw-r--r-- | packages/astro/test/scoped-style-strategy.test.js | 34 | ||||
-rw-r--r-- | pnpm-lock.yaml | 14 |
9 files changed, 101 insertions, 29 deletions
diff --git a/.changeset/neat-suns-search.md b/.changeset/neat-suns-search.md new file mode 100644 index 000000000..da743c9c7 --- /dev/null +++ b/.changeset/neat-suns-search.md @@ -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', +}); +``` diff --git a/packages/astro/package.json b/packages/astro/package.json index 0e1f5700d..8e505e8b0 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -116,7 +116,7 @@ "test:e2e:match": "playwright test -g" }, "dependencies": { - "@astrojs/compiler": "^1.8.0", + "@astrojs/compiler": "^1.8.1", "@astrojs/internal-helpers": "workspace:*", "@astrojs/markdown-remark": "workspace:*", "@astrojs/telemetry": "workspace:*", diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 4c51a7eaf..2cba086f5 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -20,9 +20,10 @@ import type { AstroConfigSchema } from '../core/config'; import type { AstroTimer } from '../core/config/timer'; import type { AstroCookies } from '../core/cookies'; 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 { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../core/constants.js'; + export type { MarkdownHeading, MarkdownMetadata, @@ -609,19 +610,21 @@ export interface AstroUserConfig { /** * @docs * @name scopedStyleStrategy - * @type {('where' | 'class')} + * @type {('where' | 'class' | 'attribute')} * @default `'where'` * @version 2.4 * @description * * 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. + * - `'where'` - Use `:where` selectors, causing no 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 `'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 diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index 282f7844e..417f918bb 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -87,9 +87,9 @@ export const AstroConfigSchema = z.object({ .optional() .default('static'), scopedStyleStrategy: z - .union([z.literal('where'), z.literal('class')]) + .union([z.literal('where'), z.literal('class'), z.literal('attribute')]) .optional() - .default('where'), + .default('attribute'), adapter: z.object({ name: z.string(), hooks: z.object({}).passthrough().default({}) }).optional(), integrations: z.preprocess( // preprocess diff --git a/packages/astro/test/0-css.test.js b/packages/astro/test/0-css.test.js index 76bfba296..9a05074fb 100644 --- a/packages/astro/test/0-css.test.js +++ b/packages/astro/test/0-css.test.js @@ -39,15 +39,27 @@ describe('CSS', function () { it('HTML and CSS scoped correctly', async () => { const el1 = $('#dynamic-class'); const el2 = $('#dynamic-vis'); - const classes = $('#class').attr('class').split(' '); - const scopedClass = classes.find((name) => /^astro-[A-Za-z0-9-]+/.test(name)); + const classes = $('#class'); + 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 - expect(el1.attr('class')).to.equal(`blue ${scopedClass}`); - expect(el2.attr('class')).to.equal(`visible ${scopedClass}`); + expect(el1.attr('class')).to.equal(`blue`); + expect(el2.attr('class')).to.equal(`visible`); // 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); }); @@ -60,8 +72,12 @@ describe('CSS', function () { expect($('#no-scope').attr('class')).to.equal(undefined); }); - it('Child inheritance', async () => { - expect($('#passed-in').attr('class')).to.match(/outer astro-[A-Z0-9]+ astro-[A-Z0-9]+/); + it('Child inheritance', (done) => { + 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 () => { @@ -70,11 +86,11 @@ describe('CSS', function () { }); 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 () => { - 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 () => { const allInjectedStyles = $('style').text(); - expect(allInjectedStyles).to.contain('.linked-css:where(.astro-'); - expect(allInjectedStyles).to.contain('.linked-sass:where(.astro-'); - expect(allInjectedStyles).to.contain('.linked-scss:where(.astro-'); - expect(allInjectedStyles).to.contain('.wrapper:where(.astro-'); + expect(allInjectedStyles).to.contain('.linked-css[data-astro-cid-'); + expect(allInjectedStyles).to.contain('.linked-sass[data-astro-cid-'); + expect(allInjectedStyles).to.contain('.linked-scss[data-astro-cid-'); + expect(allInjectedStyles).to.contain('.wrapper[data-astro-cid-'); }); it('resolves Styles from React', async () => { diff --git a/packages/astro/test/astro-partial-html.test.js b/packages/astro/test/astro-partial-html.test.js index 6073f1bd1..162c6985d 100644 --- a/packages/astro/test/astro-partial-html.test.js +++ b/packages/astro/test/astro-partial-html.test.js @@ -26,7 +26,7 @@ describe('Partial HTML', async () => { // test 2: correct CSS present 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 () => { diff --git a/packages/astro/test/config-vite-css-target.test.js b/packages/astro/test/config-vite-css-target.test.js index 1dc2cce32..cb9fa8de2 100644 --- a/packages/astro/test/config-vite-css-target.test.js +++ b/packages/astro/test/config-vite-css-target.test.js @@ -32,7 +32,7 @@ describe('CSS', function () { it('vite.build.cssTarget is respected', async () => { 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}') ); }); }); diff --git a/packages/astro/test/scoped-style-strategy.test.js b/packages/astro/test/scoped-style-strategy.test.js index 022ef3d6f..a59f227ad 100644 --- a/packages/astro/test/scoped-style-strategy.test.js +++ b/packages/astro/test/scoped-style-strategy.test.js @@ -3,7 +3,7 @@ import * as cheerio from 'cheerio'; import { loadFixture } from './test-utils.js'; describe('scopedStyleStrategy', () => { - describe('default', () => { + describe('scopedStyleStrategy: "where"', () => { /** @type {import('./test-utils').Fixture} */ let fixture; let stylesheet; @@ -11,6 +11,7 @@ describe('scopedStyleStrategy', () => { before(async () => { fixture = await loadFixture({ root: './fixtures/scoped-style-strategy/', + scopedStyleStrategy: 'where', }); await fixture.build(); @@ -57,4 +58,35 @@ describe('scopedStyleStrategy', () => { 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-'); + }); + }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 667ec98bf..fca6dde17 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -480,8 +480,8 @@ importers: packages/astro: dependencies: '@astrojs/compiler': - specifier: ^1.8.0 - version: 1.8.0 + specifier: ^1.8.1 + version: 1.8.1 '@astrojs/internal-helpers': specifier: workspace:* version: link:../internal-helpers @@ -5391,6 +5391,10 @@ packages: /@astrojs/compiler@1.8.0: 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: resolution: {integrity: sha512-YXLk1CUDdC9P5bjFZcGjz+cE/ZDceXObDTXn/GCID4r8LjThuexxi+dlJqukmUpkSItzQqgzfWnrPLxSFPejdA==} @@ -5400,7 +5404,7 @@ packages: resolution: {integrity: sha512-oEw7AwJmzjgy6HC9f5IdrphZ1GVgfV/+7xQuyf52cpTiRWd/tJISK3MsKP0cDkVlfodmNABNFnAaAWuLZEiiiA==} hasBin: true dependencies: - '@astrojs/compiler': 1.8.0 + '@astrojs/compiler': 1.8.1 '@jridgewell/trace-mapping': 0.3.18 '@vscode/emmet-helper': 2.8.8 events: 3.3.0 @@ -15600,7 +15604,7 @@ packages: resolution: {integrity: sha512-dPzop0gKZyVGpTDQmfy+e7FKXC9JT3mlpfYA2diOVz+Ui+QR1U4G/s+OesKl2Hib2JJOtAYJs/l+ovgT0ljlFA==} engines: {node: ^14.15.0 || >=16.0.0, pnpm: '>=7.14.0'} dependencies: - '@astrojs/compiler': 1.8.0 + '@astrojs/compiler': 1.8.1 prettier: 2.8.8 sass-formatter: 0.7.6 dev: true @@ -15609,7 +15613,7 @@ packages: resolution: {integrity: sha512-lJ/mG/Lz/ccSwNtwqpFS126mtMVzFVyYv0ddTF9wqwrEG4seECjKDAyw/oGv915rAcJi8jr89990nqfpmG+qdg==} engines: {node: ^14.15.0 || >=16.0.0, pnpm: '>=7.14.0'} dependencies: - '@astrojs/compiler': 1.8.0 + '@astrojs/compiler': 1.8.1 prettier: 2.8.8 sass-formatter: 0.7.6 synckit: 0.8.5 |