From b58b493948ef7b6453ba8c4e94c4bd2fcaea8452 Mon Sep 17 00:00:00 2001 From: Drew Powers <1369770+drwpow@users.noreply.github.com> Date: Fri, 2 Apr 2021 12:50:30 -0600 Subject: [PATCH] Fix body from being scoped (#56) --- examples/snowpack/package-lock.json | 39 +++++++------------ .../optimize/postcss-scoped-styles/index.ts | 9 +++++ src/compiler/optimize/styles.ts | 30 +++++++++----- test/astro-scoped-styles.test.js | 1 + 4 files changed, 44 insertions(+), 35 deletions(-) diff --git a/examples/snowpack/package-lock.json b/examples/snowpack/package-lock.json index ddcb402e2..afa079c75 100644 --- a/examples/snowpack/package-lock.json +++ b/examples/snowpack/package-lock.json @@ -980,14 +980,8 @@ "@babel/traverse": "^7.13.0", "@snowpack/plugin-sass": "^1.4.0", "@snowpack/plugin-svelte": "^3.6.0", - "@snowpack/plugin-vue": "^2.3.0", - "@types/babel__generator": "^7.6.2", - "@types/babel__traverse": "^7.11.1", - "@types/estree": "0.0.46", - "@types/node": "^14.14.31", - "@types/react": "^17.0.3", - "@types/react-dom": "^17.0.2", - "@vue/server-renderer": "^3.0.7", + "@snowpack/plugin-vue": "^2.4.0", + "@vue/server-renderer": "^3.0.10", "acorn": "^7.4.0", "acorn-jsx": "^5.3.1", "astring": "^1.7.0", @@ -997,6 +991,8 @@ "domhandler": "^4.0.0", "es-module-lexer": "^0.4.1", "esbuild": "^0.10.1", + "estree-walker": "^3.0.0", + "fdir": "^5.0.0", "find-up": "^5.0.0", "github-slugger": "^1.3.0", "gray-matter": "^4.0.2", @@ -1012,9 +1008,9 @@ "react-dom": "^17.0.1", "rollup": "^2.43.1", "sass": "^1.32.8", - "snowpack": "^3.1.2", + "snowpack": "^3.2.2", "svelte": "^3.35.0", - "vue": "^3.0.7", + "vue": "^3.0.10", "yargs-parser": "^20.2.7" }, "dependencies": { @@ -1168,7 +1164,6 @@ "version": "7.13.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.0.tgz", "integrity": "sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA==", - "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.12.11", "lodash": "^4.17.19", @@ -1284,7 +1279,6 @@ "version": "7.6.2", "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.2.tgz", "integrity": "sha512-MdSJnBjl+bdwkLskZ3NGFp9YcXGx5ggLpQQPqtgakVhsWK0hTtNYhjpZLlWQTviGTvF8at+Bvli3jV7faPdgeQ==", - "dev": true, "requires": { "@babel/types": "^7.0.0" } @@ -1293,7 +1287,6 @@ "version": "7.11.1", "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.11.1.tgz", "integrity": "sha512-Vs0hm0vPahPMYi9tDjtP66llufgO3ST16WXaSTtDGEl9cewAl3AibmxWw6TINOqHPT9z0uABKAYjT9jNSg4npw==", - "dev": true, "requires": { "@babel/types": "^7.3.0" } @@ -1301,8 +1294,7 @@ "@types/estree": { "version": "0.0.46", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.46.tgz", - "integrity": "sha512-laIjwTQaD+5DukBZaygQ79K1Z0jb1bPEMRrkXSLjtCcZm+abyp5YbrqpSLzD42FwWW6gK/aS4NYpJ804nG2brg==", - "dev": true + "integrity": "sha512-laIjwTQaD+5DukBZaygQ79K1Z0jb1bPEMRrkXSLjtCcZm+abyp5YbrqpSLzD42FwWW6gK/aS4NYpJ804nG2brg==" }, "@types/github-slugger": { "version": "1.3.0", @@ -1328,8 +1320,7 @@ "@types/prop-types": { "version": "15.7.3", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", - "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", - "dev": true + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" }, "@types/pug": { "version": "2.0.4", @@ -1341,7 +1332,6 @@ "version": "17.0.3", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.3.tgz", "integrity": "sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg==", - "dev": true, "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -1351,8 +1341,7 @@ "csstype": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.7.tgz", - "integrity": "sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g==", - "dev": true + "integrity": "sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g==" } } }, @@ -1360,7 +1349,6 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.2.tgz", "integrity": "sha512-Icd9KEgdnFfJs39KyRyr0jQ7EKhq8U6CcHRMGAS45fp5qgUvxL3ujUCfWFttUK2UErqZNj97t9gsVPNAqcwoCg==", - "dev": true, "requires": { "@types/react": "*" } @@ -1377,8 +1365,7 @@ "@types/scheduler": { "version": "0.16.1", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.1.tgz", - "integrity": "sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==", - "dev": true + "integrity": "sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==" }, "@types/yargs-parser": { "version": "20.2.0", @@ -2600,7 +2587,8 @@ "estree-walker": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.0.tgz", - "integrity": "sha512-s6ceX0NFiU/vKPiKvFdR83U1Zffu7upwZsGwpoqfg5rbbq1l50WQ5hCeIvM6E6oD4shUHCYMsiFPns4Jk0YfMQ==" + "integrity": "sha512-s6ceX0NFiU/vKPiKvFdR83U1Zffu7upwZsGwpoqfg5rbbq1l50WQ5hCeIvM6E6oD4shUHCYMsiFPns4Jk0YfMQ==", + "dev": true }, "esutils": { "version": "2.0.3", @@ -4500,8 +4488,7 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" }, "to-readable-stream": { "version": "1.0.0", diff --git a/src/compiler/optimize/postcss-scoped-styles/index.ts b/src/compiler/optimize/postcss-scoped-styles/index.ts index 0d1253350..01c0acd94 100644 --- a/src/compiler/optimize/postcss-scoped-styles/index.ts +++ b/src/compiler/optimize/postcss-scoped-styles/index.ts @@ -12,6 +12,9 @@ interface Selector { const CSS_SEPARATORS = new Set([' ', ',', '+', '>', '~']); +/** HTML tags that should never get scoped classes */ +export const NEVER_SCOPED_TAGS = new Set(['base', 'body', 'font', 'frame', 'frameset', 'head', 'html', 'link', 'meta', 'noframes', 'noscript', 'script', 'style', 'title']); + /** * Scope Selectors * Given a selector string (`.btn>span,.nav>span`), add an additional CSS class to every selector (`.btn.myClass>span.myClass,.nav.myClass>span.myClass`) @@ -62,6 +65,12 @@ export function scopeSelectors(selector: string, className: string) { continue; } + // don‘t scope body, title, etc. + if (NEVER_SCOPED_TAGS.has(value)) { + ss = head + value + tail; + continue; + } + // scope everything else let newSelector = ss.substring(start, end); const pseudoIndex = newSelector.indexOf(':'); diff --git a/src/compiler/optimize/styles.ts b/src/compiler/optimize/styles.ts index 36e6f1d7d..72781fefe 100644 --- a/src/compiler/optimize/styles.ts +++ b/src/compiler/optimize/styles.ts @@ -7,7 +7,7 @@ import sass from 'sass'; import { RuntimeMode } from '../../@types/astro'; import { OptimizeOptions, Optimizer } from '../../@types/optimizer'; import type { TemplateNode } from '../../parser/interfaces'; -import astroScopedStyles from './postcss-scoped-styles/index.js'; +import astroScopedStyles, { NEVER_SCOPED_TAGS } from './postcss-scoped-styles/index.js'; type StyleType = 'css' | 'scss' | 'sass' | 'postcss'; @@ -26,9 +26,6 @@ const getStyleType: Map = new Map([ ['text/scss', 'scss'], ]); -/** HTML tags that should never get scoped classes */ -const NEVER_SCOPED_TAGS = new Set(['html', 'head', 'body', 'script', 'style', 'link', 'meta']); - /** Should be deterministic, given a unique filename */ function hashFromFilename(filename: string): string { const hash = crypto.createHash('sha256'); @@ -55,6 +52,15 @@ export interface TransformStyleOptions { mode: RuntimeMode; } +/** given a class="" string, does it contain a given class? */ +function hasClass(classList: string, className: string): boolean { + if (!className) return false; + for (const c of classList.split(' ')) { + if (className === c.trim()) return true; + } + return false; +} + /** Convert styles to scoped CSS */ async function transformStyle(code: string, { type, filename, scopedClass, mode }: TransformStyleOptions): Promise { let styleType: StyleType = 'css'; // important: assume CSS as default @@ -149,12 +155,18 @@ export default function optimizeStyles({ compileOptions, filename, fileID }: Opt const attr = node.attributes[classIndex]; for (let k = 0; k < attr.value.length; k++) { if (attr.value[k].type === 'Text') { - // string literal - attr.value[k].raw += ' ' + scopedClass; - attr.value[k].data += ' ' + scopedClass; + // don‘t add same scopedClass twice + if (!hasClass(attr.value[k].data, scopedClass)) { + // string literal + attr.value[k].raw += ' ' + scopedClass; + attr.value[k].data += ' ' + scopedClass; + } } else if (attr.value[k].type === 'MustacheTag' && attr.value[k]) { - // MustacheTag - attr.value[k].content = `(${attr.value[k].content}) + ' ${scopedClass}'`; + // don‘t add same scopedClass twice (this check is a little more basic, but should suffice) + if (!attr.value[k].content.includes(`' ${scopedClass}'`)) { + // MustacheTag + attr.value[k].content = `(${attr.value[k].content}) + ' ${scopedClass}'`; + } } } } diff --git a/test/astro-scoped-styles.test.js b/test/astro-scoped-styles.test.js index 18870e6c6..ac84c9ffa 100644 --- a/test/astro-scoped-styles.test.js +++ b/test/astro-scoped-styles.test.js @@ -18,6 +18,7 @@ const tests = { '.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 correctly', () => {