diff options
-rw-r--r-- | package-lock.json | 182 | ||||
-rw-r--r-- | package.json | 5 | ||||
-rw-r--r-- | src/@types/postcss-icss-keyframes.d.ts | 5 | ||||
-rw-r--r-- | src/compiler/optimize/postcss-scoped-styles/index.ts | 20 | ||||
-rw-r--r-- | src/compiler/optimize/styles.ts | 15 | ||||
-rw-r--r-- | test/astro-scoped-styles.test.js | 40 |
6 files changed, 159 insertions, 108 deletions
diff --git a/package-lock.json b/package-lock.json index 64e8f4b8c..8cf22a3fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -479,13 +479,13 @@ } }, "@vue/compiler-core": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.0.10.tgz", - "integrity": "sha512-rayD+aODgX9CWgWv0cAI+whPLyMmtkWfNGsZpdpsaIloh8mY2hX8+SvE1Nn3755YhGWJ/7oaDEcNpOctGwZbsA==", + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.0.11.tgz", + "integrity": "sha512-6sFj6TBac1y2cWCvYCA8YzHJEbsVkX7zdRs/3yK/n1ilvRqcn983XvpBbnN3v4mZ1UiQycTvOiajJmOgN9EVgw==", "requires": { "@babel/parser": "^7.12.0", "@babel/types": "^7.12.0", - "@vue/shared": "3.0.10", + "@vue/shared": "3.0.11", "estree-walker": "^2.0.1", "source-map": "^0.6.1" }, @@ -498,12 +498,12 @@ } }, "@vue/compiler-dom": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.0.10.tgz", - "integrity": "sha512-SzN1li9xAxtqkZimR1AFU2t1N0vzsAJxR/5764xoS0xedwhUU9s8s+Tks2FNMLsXiqdkP2Qd4zAM+9EwTbZmRw==", + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.0.11.tgz", + "integrity": "sha512-+3xB50uGeY5Fv9eMKVJs2WSRULfgwaTJsy23OIltKgMrynnIj8hTYY2UL97HCoz78aDw1VDXdrBQ4qepWjnQcw==", "requires": { - "@vue/compiler-core": "3.0.10", - "@vue/shared": "3.0.10" + "@vue/compiler-core": "3.0.11", + "@vue/shared": "3.0.11" } }, "@vue/compiler-sfc": { @@ -618,87 +618,45 @@ } }, "@vue/reactivity": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.0.10.tgz", - "integrity": "sha512-0GOSqlIv/a5wy4r6fAcdaglQ8v2sLYMRUpu49yK8Z2vHccK85Ym3R9C9K3vo6dfBRGbbCVvoKxYtQw49LvE8Ug==", + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.0.11.tgz", + "integrity": "sha512-SKM3YKxtXHBPMf7yufXeBhCZ4XZDKP9/iXeQSC8bBO3ivBuzAi4aZi0bNoeE2IF2iGfP/AHEt1OU4ARj4ao/Xw==", "requires": { - "@vue/shared": "3.0.10" + "@vue/shared": "3.0.11" } }, "@vue/runtime-core": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.0.10.tgz", - "integrity": "sha512-qKhCOwHGff5YEdyClO1gf9Q9xgaPPz/qJ2GyzNZkPb00WcXJ3l+yTgHZWaSywRLs9GD1y9Ff3C0MIowzj95NHA==", + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.0.11.tgz", + "integrity": "sha512-87XPNwHfz9JkmOlayBeCCfMh9PT2NBnv795DSbi//C/RaAnc/bGZgECjmkD7oXJ526BZbgk9QZBPdFT8KMxkAg==", "requires": { - "@vue/reactivity": "3.0.10", - "@vue/shared": "3.0.10" + "@vue/reactivity": "3.0.11", + "@vue/shared": "3.0.11" } }, "@vue/runtime-dom": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.0.10.tgz", - "integrity": "sha512-8yRAALc/884UlYWY7hJImecvow1Cngbl2B6n0ThYTms08FVQ3W9tdW0MEvR3JVit06JyQLS1Qvwdn1PwNPPDqg==", + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.0.11.tgz", + "integrity": "sha512-jm3FVQESY3y2hKZ2wlkcmFDDyqaPyU3p1IdAX92zTNeCH7I8zZ37PtlE1b9NlCtzV53WjB4TZAYh9yDCMIEumA==", "requires": { - "@vue/runtime-core": "3.0.10", - "@vue/shared": "3.0.10", + "@vue/runtime-core": "3.0.11", + "@vue/shared": "3.0.11", "csstype": "^2.6.8" } }, "@vue/server-renderer": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.0.10.tgz", - "integrity": "sha512-8WR/OdFdwLIhLmw82euU4NEQcucz1h3LWgKsfFfzmx0OLaQiaafXm2vNpYlN3lPpVi3s2GzDbMBmIrUNSliI7g==", + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.0.11.tgz", + "integrity": "sha512-NtXRxCq+jJWohce7s2kgUdO7gD6LRrWhvpGUMrpp65ODxuwolVHVyacyvAnU9bxTj11xw+ErC7Q2+su9mJusEg==", "requires": { - "@vue/compiler-ssr": "3.0.10", - "@vue/shared": "3.0.10" - }, - "dependencies": { - "@vue/compiler-core": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.0.10.tgz", - "integrity": "sha512-rayD+aODgX9CWgWv0cAI+whPLyMmtkWfNGsZpdpsaIloh8mY2hX8+SvE1Nn3755YhGWJ/7oaDEcNpOctGwZbsA==", - "requires": { - "@babel/parser": "^7.12.0", - "@babel/types": "^7.12.0", - "@vue/shared": "3.0.10", - "estree-walker": "^2.0.1", - "source-map": "^0.6.1" - } - }, - "@vue/compiler-dom": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.0.10.tgz", - "integrity": "sha512-SzN1li9xAxtqkZimR1AFU2t1N0vzsAJxR/5764xoS0xedwhUU9s8s+Tks2FNMLsXiqdkP2Qd4zAM+9EwTbZmRw==", - "requires": { - "@vue/compiler-core": "3.0.10", - "@vue/shared": "3.0.10" - } - }, - "@vue/compiler-ssr": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.0.10.tgz", - "integrity": "sha512-skrPSp9pjZG3unqHpUaEaRRpO1yYxbCXRfJ1kZW8PTGAg5g3Y/hrUet5+Q6zCIZwr5j1mSMBSLXMDCjFuyyZLg==", - "requires": { - "@vue/compiler-dom": "3.0.10", - "@vue/shared": "3.0.10" - } - }, - "@vue/shared": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.0.10.tgz", - "integrity": "sha512-p8GJ+bGpEGiEHICwcCH/EtJnkZQllrOfm1J2J+Ep0ydMte25bPnArgrY/h2Tn1LKqqR3LXyQlOSYY6gJgiW2LQ==" - }, - "estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" - } + "@vue/compiler-ssr": "3.0.11", + "@vue/shared": "3.0.11" } }, "@vue/shared": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.0.10.tgz", - "integrity": "sha512-p8GJ+bGpEGiEHICwcCH/EtJnkZQllrOfm1J2J+Ep0ydMte25bPnArgrY/h2Tn1LKqqR3LXyQlOSYY6gJgiW2LQ==" + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.0.11.tgz", + "integrity": "sha512-b+zB8A2so8eCE0JsxjL24J7vdGl8rzPQ09hZNhystm+KqSbKcAej1A+Hbva1rCMmTTqA+hFnUSDc5kouEo0JzA==" }, "abbrev": { "version": "1.1.1", @@ -2988,6 +2946,72 @@ "source-map": "^0.6.1" } }, + "postcss-icss-keyframes": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/postcss-icss-keyframes/-/postcss-icss-keyframes-0.2.1.tgz", + "integrity": "sha1-gMRFXgESsPL5w8Bax1FQYruf8pU=", + "requires": { + "icss-utils": "^3.0.1", + "postcss": "^6.0.2", + "postcss-value-parser": "^3.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "icss-utils": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-3.0.1.tgz", + "integrity": "sha1-7nDTroysOMa+XtkehRsn7tNDrQ8=", + "requires": { + "postcss": "^6.0.2" + } + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + } + } + }, "postcss-modules": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-4.0.0.tgz", @@ -3909,13 +3933,13 @@ } }, "vue": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.0.10.tgz", - "integrity": "sha512-6arZ722uqIArSNUU94aqx0Pq0IMHFqYZuJ+U+q9HGdZZu11VFpyFP/L/hakijGFKp56Jr0yxJdWbDiJGWPxwww==", + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.0.11.tgz", + "integrity": "sha512-3/eUi4InQz8MPzruHYSTQPxtM3LdZ1/S/BvaU021zBnZi0laRUyH6pfuE4wtUeLvI8wmUNwj5wrZFvbHUXL9dw==", "requires": { - "@vue/compiler-dom": "3.0.10", - "@vue/runtime-dom": "3.0.10", - "@vue/shared": "3.0.10" + "@vue/compiler-dom": "3.0.11", + "@vue/runtime-dom": "3.0.11", + "@vue/shared": "3.0.11" } }, "which": { diff --git a/package.json b/package.json index 6b5b0d4a6..cc5868522 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "@snowpack/plugin-sass": "^1.4.0", "@snowpack/plugin-svelte": "^3.6.0", "@snowpack/plugin-vue": "^2.4.0", - "@vue/server-renderer": "^3.0.10", + "@vue/server-renderer": "^3.0.11", "acorn": "^7.4.0", "acorn-jsx": "^5.3.1", "astring": "^1.7.0", @@ -54,6 +54,7 @@ "micromark-extension-mdx-jsx": "^0.3.3", "node-fetch": "^2.6.1", "postcss": "^8.2.8", + "postcss-icss-keyframes": "^0.2.1", "preact": "^10.5.13", "preact-render-to-string": "^5.1.18", "react": "^17.0.1", @@ -63,7 +64,7 @@ "sass": "^1.32.8", "snowpack": "^3.2.2", "svelte": "^3.35.0", - "vue": "^3.0.10", + "vue": "^3.0.11", "yargs-parser": "^20.2.7" }, "devDependencies": { diff --git a/src/@types/postcss-icss-keyframes.d.ts b/src/@types/postcss-icss-keyframes.d.ts new file mode 100644 index 000000000..14c330b6e --- /dev/null +++ b/src/@types/postcss-icss-keyframes.d.ts @@ -0,0 +1,5 @@ +declare module 'postcss-icss-keyframes' { + import type { Plugin } from 'postcss'; + + export default function (options: { generateScopedName(keyframesName: string, filepath: string, css: string): string }): Plugin; +} diff --git a/src/compiler/optimize/postcss-scoped-styles/index.ts b/src/compiler/optimize/postcss-scoped-styles/index.ts index 01c0acd94..23350869c 100644 --- a/src/compiler/optimize/postcss-scoped-styles/index.ts +++ b/src/compiler/optimize/postcss-scoped-styles/index.ts @@ -1,4 +1,4 @@ -import { Plugin } from 'postcss'; +import { Declaration, Plugin } from 'postcss'; interface AstroScopedOptions { className: string; @@ -11,17 +11,24 @@ interface Selector { } const CSS_SEPARATORS = new Set([' ', ',', '+', '>', '~']); +const KEYFRAME_PERCENT = /\d+\.?\d*%/; /** HTML tags that should never get scoped classes */ export const NEVER_SCOPED_TAGS = new Set<string>(['base', 'body', 'font', 'frame', 'frameset', 'head', 'html', 'link', 'meta', 'noframes', 'noscript', 'script', 'style', 'title']); /** - * Scope Selectors + * Scope Rules * Given a selector string (`.btn>span,.nav>span`), add an additional CSS class to every selector (`.btn.myClass>span.myClass,.nav.myClass>span.myClass`) * @param {string} selector The minified selector string to parse. Cannot contain arbitrary whitespace (other than child selector syntax). * @param {string} className The CSS class to apply. */ -export function scopeSelectors(selector: string, className: string) { +export function scopeRule(selector: string, className: string) { + // if this is a keyframe keyword, return original selector + if (selector === 'from' || selector === 'to' || KEYFRAME_PERCENT.test(selector)) { + return selector; + } + + // For everything else, parse & scope const c = className.replace(/^\.?/, '.'); // make sure class always has leading '.' const selectors: Selector[] = []; let ss = selector; // final output @@ -30,9 +37,12 @@ export function scopeSelectors(selector: string, className: string) { { let start = 0; let lastValue = ''; + let parensOpen = false; for (let n = 0; n < ss.length; n++) { const isEnd = n === selector.length - 1; - if (isEnd || CSS_SEPARATORS.has(selector[n])) { + if (selector[n] === '(') parensOpen = true; + if (selector[n] === ')') parensOpen = false; + if (isEnd || (parensOpen === false && CSS_SEPARATORS.has(selector[n]))) { lastValue = selector.substring(start, isEnd ? undefined : n); if (!lastValue) continue; selectors.push({ start, end: isEnd ? n + 1 : n, value: lastValue }); @@ -90,7 +100,7 @@ export default function astroScopedStyles(options: AstroScopedOptions): Plugin { return { postcssPlugin: '@astro/postcss-scoped-styles', Rule(rule) { - rule.selector = scopeSelectors(rule.selector, options.className); + rule.selector = scopeRule(rule.selector, options.className); }, }; } diff --git a/src/compiler/optimize/styles.ts b/src/compiler/optimize/styles.ts index 65b429fef..807d869c9 100644 --- a/src/compiler/optimize/styles.ts +++ b/src/compiler/optimize/styles.ts @@ -2,8 +2,8 @@ import crypto from 'crypto'; import fs from 'fs'; import path from 'path'; import autoprefixer from 'autoprefixer'; -import esbuild from 'esbuild'; import postcss, { Plugin } from 'postcss'; +import postcssKeyframes from 'postcss-icss-keyframes'; import findUp from 'find-up'; import sass from 'sass'; import type { RuntimeMode } from '../../@types/astro'; @@ -27,11 +27,9 @@ const getStyleType: Map<string, StyleType> = new Map([ ['.sass', 'sass'], ['.scss', 'scss'], ['css', 'css'], - ['postcss', 'postcss'], ['sass', 'sass'], ['scss', 'scss'], ['text/css', 'css'], - ['text/postcss', 'postcss'], ['text/sass', 'sass'], ['text/scss', 'scss'], ]); @@ -134,7 +132,16 @@ async function transformStyle(code: string, { type, filename, scopedClass, mode // 2b. Astro scoped styles (always on) postcssPlugins.push(astroScopedStyles({ className: scopedClass })); - // 2c. Autoprefixer (always on) + // 2c. Scoped @keyframes + postcssPlugins.push( + postcssKeyframes({ + generateScopedName(keyframesName) { + return `${keyframesName}-${scopedClass}`; + }, + }) + ); + + // 2d. Autoprefixer (always on) postcssPlugins.push(autoprefixer()); // 2e. Run PostCSS diff --git a/test/astro-scoped-styles.test.js b/test/astro-scoped-styles.test.js index ac84c9ffa..5c01a31fb 100644 --- a/test/astro-scoped-styles.test.js +++ b/test/astro-scoped-styles.test.js @@ -1,29 +1,33 @@ import { suite } from 'uvu'; import * as assert from 'uvu/assert'; -import { scopeSelectors } from '../lib/compiler/optimize/postcss-scoped-styles/index.js'; +import { scopeRule } from '../lib/compiler/optimize/postcss-scoped-styles/index.js'; const ScopedStyles = suite('Astro PostCSS Scoped Styles Plugin'); -const className = '.astro-abcd1234'; +const className = 'astro-abcd1234'; -// Note: assume all selectors have no unnecessary spaces (i.e. must be minified) -const tests = { - '.class': `.class${className}`, - h1: `h1${className}`, - '.nav h1': `.nav${className} h1${className}`, - '.class+.class': `.class${className}+.class${className}`, - '.class~:global(a)': `.class${className}~a`, - '.class *': `.class${className} ${className}`, - '.class>*': `.class${className}>${className}`, - '.class :global(*)': `.class${className} *`, - '.class :global(.nav:not(.is-active))': `.class${className} .nav:not(.is-active)`, // preserve nested parens - '.class:not(.is-active)': `.class${className}:not(.is-active)`, // Note: the :not() selector can NOT contain multiple classes, so this is correct; if this causes issues for some people then it‘s worth a discussion - 'body h1': `body h1${className}`, // body shouldn‘t be scoped; it‘s not a component -}; +ScopedStyles('Scopes rules correctly', () => { + // Note: assume all selectors have no unnecessary spaces (i.e. must be minified) + const tests = { + '.class': `.class.${className}`, + h1: `h1.${className}`, + '.nav h1': `.nav.${className} h1.${className}`, + '.class+.class': `.class.${className}+.class.${className}`, + '.class~:global(a)': `.class.${className}~a`, + '.class *': `.class.${className} .${className}`, + '.class>*': `.class.${className}>.${className}`, + '.class :global(*)': `.class.${className} *`, + '.class :global(.nav:not(.is-active))': `.class.${className} .nav:not(.is-active)`, // preserve nested parens + '.class :global(ul li)': `.class.${className} ul li`, // allow doubly-scoped selectors + '.class:not(.is-active)': `.class.${className}:not(.is-active)`, // Note: the :not() selector can NOT contain multiple classes, so this is correct; if this causes issues for some people then it‘s worth a discussion + 'body h1': `body h1.${className}`, // body shouldn‘t be scoped; it‘s not a component + from: 'from', // ignore keyframe keywords (below) + to: 'to', + '55%': '55%', + }; -ScopedStyles('Scopes correctly', () => { for (const [given, expected] of Object.entries(tests)) { - assert.equal(scopeSelectors(given, className), expected); + assert.equal(scopeRule(given, className), expected); } }); |