From 7b99d47215af2f65a2c4bea9ca202c3987dc150b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 29 May 2017 13:45:18 -0400 Subject: [PATCH] add cascade option, to prevent components inheriting styles (#583) --- package.json | 2 +- src/generators/Generator.ts | 6 +- .../dom/visitors/Element/Element.ts | 2 +- src/generators/server-side-rendering/index.ts | 1 + .../server-side-rendering/visitors/Element.ts | 2 +- src/generators/shared/processCss.ts | 76 ++++++++++++++----- src/interfaces.ts | 1 + test/css/index.js | 14 +++- .../expected.css | 13 ++++ .../cascade-false-global-keyframes/input.html | 17 +++++ .../samples/cascade-false-global/_config.js | 3 + .../samples/cascade-false-global/expected.css | 12 +++ .../samples/cascade-false-global/input.html | 16 ++++ .../cascade-false-keyframes/expected.css | 13 ++++ .../cascade-false-keyframes/input.html | 17 +++++ test/css/samples/cascade-false/_config.js | 3 + test/css/samples/cascade-false/expected.css | 12 +++ test/css/samples/cascade-false/input.html | 16 ++++ 18 files changed, 203 insertions(+), 23 deletions(-) create mode 100644 test/css/samples/cascade-false-global-keyframes/expected.css create mode 100644 test/css/samples/cascade-false-global-keyframes/input.html create mode 100644 test/css/samples/cascade-false-global/_config.js create mode 100644 test/css/samples/cascade-false-global/expected.css create mode 100644 test/css/samples/cascade-false-global/input.html create mode 100644 test/css/samples/cascade-false-keyframes/expected.css create mode 100644 test/css/samples/cascade-false-keyframes/input.html create mode 100644 test/css/samples/cascade-false/_config.js create mode 100644 test/css/samples/cascade-false/expected.css create mode 100644 test/css/samples/cascade-false/input.html diff --git a/package.json b/package.json index 584bacd2dd6b..958621042a60 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "glob": "^7.1.1", "jsdom": "^9.9.1", "locate-character": "^2.0.0", - "magic-string": "^0.19.0", + "magic-string": "^0.21.1", "mocha": "^3.2.0", "node-resolve": "^1.3.3", "nyc": "^10.0.0", diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts index 9fe322e7f9e8..f09de865509f 100644 --- a/src/generators/Generator.ts +++ b/src/generators/Generator.ts @@ -29,8 +29,11 @@ export default class Generator { transitions: Set; importedComponents: Map; + code: MagicString; + bindingGroups: string[]; expectedProperties: Set; + cascade: boolean; css: string; cssId: string; usesRefs: boolean; @@ -59,7 +62,8 @@ export default class Generator { this.expectedProperties = new Set(); this.code = new MagicString( source ); - this.css = parsed.css ? processCss( parsed, this.code ) : null; + this.cascade = options.cascade !== false; // TODO remove this option in v2 + this.css = parsed.css ? processCss( parsed, this.code, this.cascade ) : null; this.cssId = parsed.css ? `svelte-${parsed.hash}` : ''; this.usesRefs = false; diff --git a/src/generators/dom/visitors/Element/Element.ts b/src/generators/dom/visitors/Element/Element.ts index 5e831becd7e2..8ee8b7712d3c 100644 --- a/src/generators/dom/visitors/Element/Element.ts +++ b/src/generators/dom/visitors/Element/Element.ts @@ -46,7 +46,7 @@ export default function visitElement ( generator: DomGenerator, block: Block, st block.mount( name, state.parentNode ); // add CSS encapsulation attribute - if ( generator.cssId && state.isTopLevel ) { + if ( generator.cssId && ( !generator.cascade || state.isTopLevel ) ) { block.builders.create.addLine( `${generator.helper( 'setAttribute' )}( ${name}, '${generator.cssId}', '' );` ); } diff --git a/src/generators/server-side-rendering/index.ts b/src/generators/server-side-rendering/index.ts index 5a95b4896587..90c4e6cdf34a 100644 --- a/src/generators/server-side-rendering/index.ts +++ b/src/generators/server-side-rendering/index.ts @@ -13,6 +13,7 @@ export class SsrGenerator extends Generator { super( parsed, source, name, options ); this.bindings = []; this.renderCode = ''; + this.elementDepth = 0; } append ( code: string ) { diff --git a/src/generators/server-side-rendering/visitors/Element.ts b/src/generators/server-side-rendering/visitors/Element.ts index 05267f6bd0d8..2c351ed680ec 100644 --- a/src/generators/server-side-rendering/visitors/Element.ts +++ b/src/generators/server-side-rendering/visitors/Element.ts @@ -50,7 +50,7 @@ export default function visitElement ( generator: SsrGenerator, block: Block, no } }); - if ( generator.cssId && !generator.elementDepth ) { + if ( generator.cssId && ( !generator.cascade || generator.elementDepth === 0 ) ) { openingTag += ` ${generator.cssId}`; } diff --git a/src/generators/shared/processCss.ts b/src/generators/shared/processCss.ts index b83630d756f9..4eff6322442e 100644 --- a/src/generators/shared/processCss.ts +++ b/src/generators/shared/processCss.ts @@ -1,8 +1,9 @@ +import MagicString from 'magic-string'; import { Parsed, Node } from '../../interfaces'; const commentsPattern = /\/\*[\s\S]*?\*\//g; -export default function processCss ( parsed: Parsed, code ) { +export default function processCss ( parsed: Parsed, code: MagicString, cascade: boolean ) { const css = parsed.css.content.styles; const offset = parsed.css.content.start; @@ -14,9 +15,13 @@ export default function processCss ( parsed: Parsed, code ) { if ( node.type === 'Atrule' && node.name.toLowerCase() === 'keyframes' ) { node.expression.children.forEach( ( expression: Node ) => { if ( expression.type === 'Identifier' ) { - const newName = `svelte-${parsed.hash}-${expression.name}`; - code.overwrite( expression.start, expression.end, newName ); - keyframes.set( expression.name, newName ); + if ( expression.name.startsWith( '-global-' ) ) { + code.remove( expression.start, expression.start + 8 ); + } else { + const newName = `svelte-${parsed.hash}-${expression.name}`; + code.overwrite( expression.start, expression.end, newName ); + keyframes.set( expression.name, newName ); + } } }); } else if ( node.children ) { @@ -30,26 +35,63 @@ export default function processCss ( parsed: Parsed, code ) { function transform ( rule: Node ) { rule.selector.children.forEach( ( selector: Node ) => { - const start = selector.start - offset; - const end = selector.end - offset; + if ( cascade ) { + // TODO disable cascading (without :global(...)) in v2 + const start = selector.start - offset; + const end = selector.end - offset; - const selectorString = css.slice( start, end ); + const selectorString = css.slice( start, end ); - const firstToken = selector.children[0]; + const firstToken = selector.children[0]; - let transformed; + let transformed; - if ( firstToken.type === 'TypeSelector' ) { - const insert = firstToken.end - offset; - const head = css.slice( start, insert ); - const tail = css.slice( insert, end ); + if ( firstToken.type === 'TypeSelector' ) { + const insert = firstToken.end - offset; + const head = css.slice( start, insert ); + const tail = css.slice( insert, end ); - transformed = `${head}${attr}${tail}, ${attr} ${selectorString}`; - } else { - transformed = `${attr}${selectorString}, ${attr} ${selectorString}`; + transformed = `${head}${attr}${tail}, ${attr} ${selectorString}`; + } else { + transformed = `${attr}${selectorString}, ${attr} ${selectorString}`; + } + + code.overwrite( selector.start, selector.end, transformed ); } - code.overwrite( start + offset, end + offset, transformed ); + else { + let shouldTransform = true; + let c = selector.start; + + selector.children.forEach( ( child: Node ) => { + if ( child.type === 'WhiteSpace' || child.type === 'Combinator' ) { + code.appendLeft( c, attr ); + shouldTransform = true; + return; + } + + if ( !shouldTransform ) return; + + if ( child.type === 'PseudoClassSelector' ) { + // `:global(xyz)` > xyz + if ( child.name === 'global' ) { + const first = child.children[0]; + const last = child.children[child.children.length - 1]; + code.remove( child.start, first.start ).remove( last.end, child.end ); + } else { + code.prependRight( c, attr ); + } + + shouldTransform = false; + } + + c = child.end; + }); + + if ( shouldTransform ) { + code.appendLeft( c, attr ); + } + } }); rule.block.children.forEach( ( block: Node ) => { diff --git a/src/interfaces.ts b/src/interfaces.ts index 2a2d88e9e2f2..0caa4d6ed13a 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -40,6 +40,7 @@ export interface CompileOptions { dev?: boolean; shared?: boolean | string; + cascade?: boolean; onerror?: (error: Error) => void onwarn?: (warning: Warning) => void diff --git a/test/css/index.js b/test/css/index.js index 2311a806b39d..e333a9967f49 100644 --- a/test/css/index.js +++ b/test/css/index.js @@ -1,6 +1,15 @@ import assert from 'assert'; import * as fs from 'fs'; -import { svelte, exists } from '../helpers.js'; +import { svelte } from '../helpers.js'; + +function tryRequire ( file ) { + try { + return require( file ).default; + } catch ( err ) { + if ( err.code !== 'MODULE_NOT_FOUND' ) throw err; + return null; + } +} describe( 'css', () => { fs.readdirSync( 'test/css/samples' ).forEach( dir => { @@ -14,9 +23,10 @@ describe( 'css', () => { } ( solo ? it.only : it )( dir, () => { + const config = tryRequire( `./samples/${dir}/_config.js` ) || {}; const input = fs.readFileSync( `test/css/samples/${dir}/input.html`, 'utf-8' ).replace( /\s+$/, '' ); - const actual = svelte.compile( input ).css; + const actual = svelte.compile( input, config ).css; fs.writeFileSync( `test/css/samples/${dir}/_actual.css`, actual ); const expected = fs.readFileSync( `test/css/samples/${dir}/expected.css`, 'utf-8' ); diff --git a/test/css/samples/cascade-false-global-keyframes/expected.css b/test/css/samples/cascade-false-global-keyframes/expected.css new file mode 100644 index 000000000000..b13c5736bec5 --- /dev/null +++ b/test/css/samples/cascade-false-global-keyframes/expected.css @@ -0,0 +1,13 @@ + + @keyframes why { + 0% { color: red; } + 100% { color: blue; } + } + + [svelte-2486527405].animated, [svelte-2486527405] .animated { + animation: why 2s; + } + + [svelte-2486527405].also-animated, [svelte-2486527405] .also-animated { + animation: not-defined-here 2s; + } diff --git a/test/css/samples/cascade-false-global-keyframes/input.html b/test/css/samples/cascade-false-global-keyframes/input.html new file mode 100644 index 000000000000..3f2d568807f7 --- /dev/null +++ b/test/css/samples/cascade-false-global-keyframes/input.html @@ -0,0 +1,17 @@ +
animated
+
also animated
+ + \ No newline at end of file diff --git a/test/css/samples/cascade-false-global/_config.js b/test/css/samples/cascade-false-global/_config.js new file mode 100644 index 000000000000..b37866f9b61a --- /dev/null +++ b/test/css/samples/cascade-false-global/_config.js @@ -0,0 +1,3 @@ +export default { + cascade: false +}; \ No newline at end of file diff --git a/test/css/samples/cascade-false-global/expected.css b/test/css/samples/cascade-false-global/expected.css new file mode 100644 index 000000000000..96071a3d6ffc --- /dev/null +++ b/test/css/samples/cascade-false-global/expected.css @@ -0,0 +1,12 @@ + + div { + color: red; + } + + div.foo { + color: blue; + } + + .foo { + font-weight: bold; + } diff --git a/test/css/samples/cascade-false-global/input.html b/test/css/samples/cascade-false-global/input.html new file mode 100644 index 000000000000..947edb6ab57d --- /dev/null +++ b/test/css/samples/cascade-false-global/input.html @@ -0,0 +1,16 @@ +
red
+
bold/blue
+ + \ No newline at end of file diff --git a/test/css/samples/cascade-false-keyframes/expected.css b/test/css/samples/cascade-false-keyframes/expected.css new file mode 100644 index 000000000000..cf1d1714172f --- /dev/null +++ b/test/css/samples/cascade-false-keyframes/expected.css @@ -0,0 +1,13 @@ + + @keyframes svelte-776829126-why { + 0% { color: red; } + 100% { color: blue; } + } + + [svelte-776829126].animated, [svelte-776829126] .animated { + animation: svelte-776829126-why 2s; + } + + [svelte-776829126].also-animated, [svelte-776829126] .also-animated { + animation: not-defined-here 2s; + } diff --git a/test/css/samples/cascade-false-keyframes/input.html b/test/css/samples/cascade-false-keyframes/input.html new file mode 100644 index 000000000000..ba220a5e22ce --- /dev/null +++ b/test/css/samples/cascade-false-keyframes/input.html @@ -0,0 +1,17 @@ +
animated
+
also animated
+ + \ No newline at end of file diff --git a/test/css/samples/cascade-false/_config.js b/test/css/samples/cascade-false/_config.js new file mode 100644 index 000000000000..b37866f9b61a --- /dev/null +++ b/test/css/samples/cascade-false/_config.js @@ -0,0 +1,3 @@ +export default { + cascade: false +}; \ No newline at end of file diff --git a/test/css/samples/cascade-false/expected.css b/test/css/samples/cascade-false/expected.css new file mode 100644 index 000000000000..0a9cb4502e53 --- /dev/null +++ b/test/css/samples/cascade-false/expected.css @@ -0,0 +1,12 @@ + + div[svelte-4161687011] { + color: red; + } + + div.foo[svelte-4161687011] { + color: blue; + } + + .foo[svelte-4161687011] { + font-weight: bold; + } diff --git a/test/css/samples/cascade-false/input.html b/test/css/samples/cascade-false/input.html new file mode 100644 index 000000000000..f56b586ef8b2 --- /dev/null +++ b/test/css/samples/cascade-false/input.html @@ -0,0 +1,16 @@ +
red
+
bold/blue
+ + \ No newline at end of file