diff options
author | 2021-07-23 19:51:27 +0200 | |
---|---|---|
committer | 2021-07-23 13:51:27 -0400 | |
commit | 294a656ed92707939dbd3b771c31a5d6a8a05024 (patch) | |
tree | 8ec79481b8813cbc7061170eb3f895e3c66b2e07 | |
parent | 041788878d6091a9b5a064068cbacdb4761700ca (diff) | |
download | astro-294a656ed92707939dbd3b771c31a5d6a8a05024.tar.gz astro-294a656ed92707939dbd3b771c31a5d6a8a05024.tar.zst astro-294a656ed92707939dbd3b771c31a5d6a8a05024.zip |
Introduce `<style global>` (#824)
* Adding support for multiple <style> blocks
* Adding support for `<style global>`
* scoping @keyframes should also be skipped for <style global>
* Adding test coverage for muliple style blocks, global blocks, and scoped keyframes
* docs: Updating docs for `<style global>` support
* Adding yarn changeset
* Punctuation fix in styling docs
* docs: Clarifying example use cases given in the docs
Co-authored-by: Tony Sullivan <tony.f.sullivan@gmail.com>
-rw-r--r-- | .changeset/empty-trainers-chew.md | 8 | ||||
-rw-r--r-- | docs/src/pages/core-concepts/astro-components.md | 2 | ||||
-rw-r--r-- | docs/src/pages/guides/styling.md | 28 | ||||
-rw-r--r-- | packages/astro-parser/src/interfaces.ts | 2 | ||||
-rw-r--r-- | packages/astro-parser/src/parse/index.ts | 14 | ||||
-rw-r--r-- | packages/astro/src/compiler/codegen/index.ts | 2 | ||||
-rw-r--r-- | packages/astro/src/compiler/transform/index.ts | 2 | ||||
-rw-r--r-- | packages/astro/src/compiler/transform/styles.ts | 29 | ||||
-rw-r--r-- | packages/astro/test/astro-css-bundling.test.js | 14 | ||||
-rw-r--r-- | packages/astro/test/fixtures/astro-css-bundling/src/components/Nav.astro | 30 |
10 files changed, 103 insertions, 28 deletions
diff --git a/.changeset/empty-trainers-chew.md b/.changeset/empty-trainers-chew.md new file mode 100644 index 000000000..8e35c3aea --- /dev/null +++ b/.changeset/empty-trainers-chew.md @@ -0,0 +1,8 @@ +--- +'astro': patch +'@astrojs/parser': patch +--- + +Adds support for global style blocks via `<style global>` + +Be careful with this escape hatch! This is best reserved for uses like importing styling libraries like Tailwind, or changing global CSS variables. diff --git a/docs/src/pages/core-concepts/astro-components.md b/docs/src/pages/core-concepts/astro-components.md index 5c20169da..a2a476a37 100644 --- a/docs/src/pages/core-concepts/astro-components.md +++ b/docs/src/pages/core-concepts/astro-components.md @@ -63,6 +63,8 @@ For best results, you should only have one `<style>` tag per-Astro component. Th </html> ``` +Using `<style global>` will skip automatic scoping for every CSS rule in the `<style>` block. This escape hatch should be avoided if possible but can be useful if, for example, you need to modify styling for HTML elements added by an external library. + Sass (an alternative to CSS) is also available via `<style lang="scss">`. 📚 Read our full guide on [Component Styling](/guides/styling) to learn more. diff --git a/docs/src/pages/guides/styling.md b/docs/src/pages/guides/styling.md index d6a3691a2..43dc4f206 100644 --- a/docs/src/pages/guides/styling.md +++ b/docs/src/pages/guides/styling.md @@ -30,6 +30,32 @@ To create global styles, add a `:global()` wrapper around a selector (the same a <h1>I have both scoped and global styles</h1> ``` +To include every selector in a `<style>` as global styles, use `<style global>`. It's best to avoid using this escape hatch if possible, but it can be useful if you find yourself repeating `:global()` multiple times in the same `<style>`. + +```html +<!-- src/components/MyComponent.astro --> +<style> + /* Scoped class selector within the component */ + .scoped { + font-weight: bold; + } + /* Scoped element selector within the component */ + h1 { + color: red; + } +</style> + +<style global> + /* Global style */ + h1 { + font-size: 32px; + } +</style> + +<div class="scoped">I’m a scoped style and only apply to this component</div> +<h1>I have both scoped and global styles</h1> +``` + 📚 Read our full guide on [Astro component syntax](/core-concepts/astro-components#css-styles) to learn more about using the `<style>` tag. ## Cross-Browser Compatibility @@ -198,7 +224,7 @@ _Note: all the examples here use `lang="scss"` which is a great convenience for That `.btn` class is scoped within that component, and won’t leak out. It means that you can **focus on styling and not naming.** Local-first approach fits in very well with Astro’s ESM-powered design, favoring encapsulation and reusability over global scope. While this is a simple example, it should be noted that **this scales incredibly well.** And if you need to share common values between components, [Sass’ module system][sass-use] also gets our recommendation for being easy to use, and a great fit with component-first design. -By contrast, Astro does allow global styles via the `:global()` escape hatch, however, this should be avoided if possible. To illustrate this: say you used your button in a `<Nav />` component, and you wanted to style it differently there. You might be tempted to have something like: +By contrast, Astro does allow global styles via the `:global()` and `<style global>` escape hatches. However, this should be avoided if possible. To illustrate this: say you used your button in a `<Nav />` component, and you wanted to style it differently there. You might be tempted to have something like: ```jsx --- diff --git a/packages/astro-parser/src/interfaces.ts b/packages/astro-parser/src/interfaces.ts index 1e996027c..335643aa5 100644 --- a/packages/astro-parser/src/interfaces.ts +++ b/packages/astro-parser/src/interfaces.ts @@ -103,7 +103,7 @@ export interface Style extends BaseNode { export interface Ast { html: TemplateNode; - css: Style; + css: Style[]; module: Script; // instance: Script; meta: { diff --git a/packages/astro-parser/src/parse/index.ts b/packages/astro-parser/src/parse/index.ts index ef33bfaca..776d46e2b 100644 --- a/packages/astro-parser/src/parse/index.ts +++ b/packages/astro-parser/src/parse/index.ts @@ -226,18 +226,6 @@ export class Parser { export default function parse(template: string, options: ParserOptions = {}): Ast { const parser = new Parser(template, options); - // TODO we may want to allow multiple <style> tags — - // one scoped, one global. for now, only allow one - if (parser.css.length > 1) { - parser.error( - { - code: 'duplicate-style', - message: 'You can only have one <style> tag per Astro file', - }, - parser.css[1].start - ); - } - // const instance_scripts = parser.js.filter((script) => script.context === 'default'); // const module_scripts = parser.js.filter((script) => script.context === 'module'); const astro_scripts = parser.js.filter((script) => script.context === 'setup'); @@ -264,7 +252,7 @@ export default function parse(template: string, options: ParserOptions = {}): As return { html: parser.html, - css: parser.css[0], + css: parser.css, // instance: instance_scripts[0], module: astro_scripts[0], meta: { diff --git a/packages/astro/src/compiler/codegen/index.ts b/packages/astro/src/compiler/codegen/index.ts index f2a32e30f..85e6349db 100644 --- a/packages/astro/src/compiler/codegen/index.ts +++ b/packages/astro/src/compiler/codegen/index.ts @@ -880,7 +880,7 @@ export async function codegen(ast: Ast, { compileOptions, filename, fileID }: Co const { script, createCollection } = compileModule(ast, ast.module, state, compileOptions); - compileCss(ast.css, state); + (ast.css || []).map(css => compileCss(css, state)); const html = await compileHtml(ast.html, state, compileOptions); diff --git a/packages/astro/src/compiler/transform/index.ts b/packages/astro/src/compiler/transform/index.ts index c9943cc5f..3e0a971cd 100644 --- a/packages/astro/src/compiler/transform/index.ts +++ b/packages/astro/src/compiler/transform/index.ts @@ -91,7 +91,7 @@ export async function transform(ast: Ast, opts: TransformOptions) { collectVisitors(optimizer, htmlVisitors, cssVisitors, finalizers); } - walkAstWithVisitors(ast.css, cssVisitors); + (ast.css || []).map(css => walkAstWithVisitors(css, cssVisitors)); walkAstWithVisitors(ast.html, htmlVisitors); // Run all of the finalizer functions in parallel because why not. diff --git a/packages/astro/src/compiler/transform/styles.ts b/packages/astro/src/compiler/transform/styles.ts index ed163132c..715dfb942 100644 --- a/packages/astro/src/compiler/transform/styles.ts +++ b/packages/astro/src/compiler/transform/styles.ts @@ -66,6 +66,7 @@ export interface TransformStyleOptions { filename: string; scopedClass: string; tailwindConfig?: string; + global?: boolean; } /** given a class="" string, does it contain a given class? */ @@ -78,7 +79,7 @@ function hasClass(classList: string, className: string): boolean { } /** Convert styles to scoped CSS */ -async function transformStyle(code: string, { logging, type, filename, scopedClass, tailwindConfig }: TransformStyleOptions): Promise<StyleTransformResult> { +async function transformStyle(code: string, { logging, type, filename, scopedClass, tailwindConfig, global }: TransformStyleOptions): Promise<StyleTransformResult> { let styleType: StyleType = 'css'; // important: assume CSS as default if (type) { styleType = getStyleType.get(type) || styleType; @@ -131,17 +132,19 @@ async function transformStyle(code: string, { logging, type, filename, scopedCla } } - // 2b. Astro scoped styles (always on) - postcssPlugins.push(astroScopedStyles({ className: scopedClass })); + if (!global) { + // 2b. Astro scoped styles (skip for global style blocks) + postcssPlugins.push(astroScopedStyles({ className: scopedClass })); - // 2c. Scoped @keyframes - postcssPlugins.push( - postcssKeyframes({ - generateScopedName(keyframesName) { - return `${keyframesName}-${scopedClass}`; - }, - }) - ); + // 2c. Scoped @keyframes + postcssPlugins.push( + postcssKeyframes({ + generateScopedName(keyframesName) { + return `${keyframesName}-${scopedClass}`; + }, + }) + ); + } // 2d. Autoprefixer (always on) postcssPlugins.push(autoprefixer()); @@ -215,6 +218,7 @@ export default function transformStyles({ compileOptions, filename, fileID }: Tr const code = Array.isArray(node.children) ? node.children.map(({ data }: any) => data).join('\n') : ''; if (!code) return; const langAttr = (node.attributes || []).find(({ name }: any) => name === 'lang'); + const globalAttr = (node.attributes || []).find(({ name }: any) => name === 'global'); styleNodes.push(node); styleTransformPromises.push( transformStyle(code, { @@ -223,6 +227,7 @@ export default function transformStyles({ compileOptions, filename, fileID }: Tr filename, scopedClass, tailwindConfig: compileOptions.astroConfig.devOptions.tailwindConfig, + global: globalAttr && globalAttr.value, }) ); return; @@ -246,6 +251,7 @@ export default function transformStyles({ compileOptions, filename, fileID }: Tr if (!node.content || !node.content.styles) return; const code = node.content.styles; const langAttr = (node.attributes || []).find(({ name }: any) => name === 'lang'); + const globalAttr = (node.attributes || []).find(({ name }: any) => name === 'global'); styleNodes.push(node); styleTransformPromises.push( transformStyle(code, { @@ -253,6 +259,7 @@ export default function transformStyles({ compileOptions, filename, fileID }: Tr type: (langAttr && langAttr.value[0] && langAttr.value[0].data) || undefined, filename, scopedClass, + global: globalAttr && globalAttr.value, }) ); }, diff --git a/packages/astro/test/astro-css-bundling.test.js b/packages/astro/test/astro-css-bundling.test.js index 8b5ab993d..d674c6657 100644 --- a/packages/astro/test/astro-css-bundling.test.js +++ b/packages/astro/test/astro-css-bundling.test.js @@ -52,6 +52,20 @@ CSSBundling('Bundles CSS', async (context) => { const typographyIndex = bundledContents.indexOf('body{'); const colorsIndex = bundledContents.indexOf(':root{'); assert.ok(typographyIndex < colorsIndex); + + // test 5: assert multiple style blocks were bundled (Nav.astro includes 2 scoped style blocks) + const scopedNavStyles = [...bundledContents.matchAll('.nav.astro-')]; + assert.is(scopedNavStyles.length, 2); + + // test 6: assert <style global> was not scoped (in Nav.astro) + const globalStyles = [...bundledContents.matchAll('html{')]; + assert.is(globalStyles.length, 1); + + // test 7: assert keyframes are only scoped for non-global styles (from Nav.astro) + const scopedKeyframes = [...bundledContents.matchAll('nav-scoped-fade-astro')]; + const globalKeyframes = [...bundledContents.matchAll('nav-global-fade{')]; + assert.ok(scopedKeyframes.length > 0); + assert.ok(globalKeyframes.length > 0); }); CSSBundling.run(); diff --git a/packages/astro/test/fixtures/astro-css-bundling/src/components/Nav.astro b/packages/astro/test/fixtures/astro-css-bundling/src/components/Nav.astro index deafe668c..e54a600f4 100644 --- a/packages/astro/test/fixtures/astro-css-bundling/src/components/Nav.astro +++ b/packages/astro/test/fixtures/astro-css-bundling/src/components/Nav.astro @@ -2,6 +2,36 @@ .nav { display: block; } + +@keyframes nav-scoped-fade { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} +</style> + +<style> +.nav { + padding: 1em; +} +</style> + +<style global> +html { + --primary: aquamarine; +} + +@keyframes nav-global-fade { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} </style> <nav class=".nav"> |