From cccc63d31ae77ee9afdde6e70d4e1dfe01bfdd86 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Dartus Date: Tue, 12 Jun 2018 20:11:08 +0200 Subject: [PATCH] fix: Use token for the styling host element instead of tag name (#390) ## Details This PR changes the way the host element gets targetted in CSS. Instead of relying on the tag name, the compiler now reuses the token to style the host element. Fix #383 ## Does this PR introduce a breaking change? * [ ] Yes * [X] No --- ...e-with-no-options-and-default-namespace.js | 3 +- .../__tests__/fixtures/expected-dev-mode.js | 3 +- .../fixtures/expected-external-dependency.js | 3 +- .../expected-mapping-namespace-from-path.js | 3 +- .../fixtures/expected-node-env-dev.js | 3 +- .../fixtures/expected-node-env-prod.js | 3 +- .../fixtures/expected-relative-import.js | 3 +- .../fixtures/expected-sources-metadata.js | 3 +- .../expected-sources-namespaced-format.js | 3 +- .../fixtures/expected-sources-namespaced.js | 3 +- .../fixtures/expected-styled-prod.js | 2 +- .../src/__tests__/fixtures/expected-styled.js | 5 +- .../__tests__/module-resolver.spec.ts | 6 +- .../transformers/__tests__/transform.spec.ts | 3 +- .../lwc-compiler/src/transformers/style.ts | 1 - .../lwc-compiler/src/transformers/template.ts | 15 +- .../src/framework/__tests__/template.spec.ts | 63 +++++--- packages/lwc-engine/src/framework/api.ts | 8 +- packages/lwc-engine/src/framework/context.ts | 5 +- .../framework/modules/__tests__/token.spec.ts | 8 +- packages/lwc-engine/src/framework/template.ts | 25 +++- packages/postcss-plugin-lwc/README.md | 10 +- .../src/__tests__/main.spec.ts | 19 +-- .../src/__tests__/selector-transform.spec.ts | 40 ++---- .../src/__tests__/shared.ts | 2 - packages/postcss-plugin-lwc/src/config.ts | 7 +- .../src/selector-scoping/transform.ts | 136 +++++++----------- .../expected_default_config_simple_app.js | 3 +- 28 files changed, 185 insertions(+), 203 deletions(-) diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-compile-with-no-options-and-default-namespace.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-compile-with-no-options-and-default-namespace.js index 527031d01c..4ccd333ac1 100755 --- a/packages/lwc-compiler/src/__tests__/fixtures/expected-compile-with-no-options-and-default-namespace.js +++ b/packages/lwc-compiler/src/__tests__/fixtures/expected-compile-with-no-options-and-default-namespace.js @@ -13,7 +13,8 @@ function tmpl($api, $cmp, $slotset, $ctx) { } if (style) { - tmpl.token = 'x-default_default'; + tmpl.hostToken = 'x-default_default-host'; + tmpl.shadowToken = 'x-default_default'; const style$$1 = document.createElement('style'); style$$1.type = 'text/css'; diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-dev-mode.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-dev-mode.js index 9709d837ba..2c1443fe5f 100755 --- a/packages/lwc-compiler/src/__tests__/fixtures/expected-dev-mode.js +++ b/packages/lwc-compiler/src/__tests__/fixtures/expected-dev-mode.js @@ -13,7 +13,8 @@ function tmpl($api, $cmp, $slotset, $ctx) { } if (style) { - tmpl.token = 'x-class_and_template_class_and_template'; + tmpl.hostToken = 'x-class_and_template_class_and_template-host'; + tmpl.shadowToken = 'x-class_and_template_class_and_template'; const style$$1 = document.createElement('style'); style$$1.type = 'text/css'; diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-external-dependency.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-external-dependency.js index d9e3145421..3951d88c4f 100755 --- a/packages/lwc-compiler/src/__tests__/fixtures/expected-external-dependency.js +++ b/packages/lwc-compiler/src/__tests__/fixtures/expected-external-dependency.js @@ -14,7 +14,8 @@ function tmpl($api, $cmp, $slotset, $ctx) { } if (style) { - tmpl.token = 'x-external_external'; + tmpl.hostToken = 'x-external_external-host'; + tmpl.shadowToken = 'x-external_external'; const style$$1 = document.createElement('style'); style$$1.type = 'text/css'; diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-mapping-namespace-from-path.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-mapping-namespace-from-path.js index b6c8396480..654c990611 100755 --- a/packages/lwc-compiler/src/__tests__/fixtures/expected-mapping-namespace-from-path.js +++ b/packages/lwc-compiler/src/__tests__/fixtures/expected-mapping-namespace-from-path.js @@ -14,7 +14,8 @@ function tmpl($api, $cmp, $slotset, $ctx) { } if (style) { - tmpl.token = 'x-cmp1_cmp1'; + tmpl.hostToken = 'x-cmp1_cmp1-host'; + tmpl.shadowToken = 'x-cmp1_cmp1'; const style$$1 = document.createElement('style'); style$$1.type = 'text/css'; diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-node-env-dev.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-node-env-dev.js index a6a14564e5..4167f9d7f8 100755 --- a/packages/lwc-compiler/src/__tests__/fixtures/expected-node-env-dev.js +++ b/packages/lwc-compiler/src/__tests__/fixtures/expected-node-env-dev.js @@ -4,7 +4,8 @@ function tmpl($api, $cmp, $slotset, $ctx) { return []; } if (style) { - tmpl.token = 'x-node_env_node_env'; + tmpl.hostToken = 'x-node_env_node_env-host'; + tmpl.shadowToken = 'x-node_env_node_env'; const style$$1 = document.createElement('style'); style$$1.type = 'text/css'; style$$1.dataset.token = 'x-node_env_node_env'; diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-node-env-prod.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-node-env-prod.js index eb1eaca353..8701335943 100755 --- a/packages/lwc-compiler/src/__tests__/fixtures/expected-node-env-prod.js +++ b/packages/lwc-compiler/src/__tests__/fixtures/expected-node-env-prod.js @@ -4,7 +4,8 @@ function tmpl($api, $cmp, $slotset, $ctx) { return []; } if (style) { - tmpl.token = 'x-node_env_node_env'; + tmpl.hostToken = 'x-node_env_node_env-host'; + tmpl.shadowToken = 'x-node_env_node_env'; const style$$1 = document.createElement('style'); style$$1.type = 'text/css'; style$$1.dataset.token = 'x-node_env_node_env'; diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-relative-import.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-relative-import.js index 4638a60382..96fa62d083 100755 --- a/packages/lwc-compiler/src/__tests__/fixtures/expected-relative-import.js +++ b/packages/lwc-compiler/src/__tests__/fixtures/expected-relative-import.js @@ -13,7 +13,8 @@ function tmpl($api, $cmp, $slotset, $ctx) { } if (style) { - tmpl.token = 'myns-relative_import_relative_import'; + tmpl.hostToken = 'myns-relative_import_relative-host'; + tmpl.shadowToken = 'myns-relative_import_relative'; const style$$1 = document.createElement('style'); style$$1.type = 'text/css'; diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-sources-metadata.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-sources-metadata.js index 243ecd8624..4c78883c58 100755 --- a/packages/lwc-compiler/src/__tests__/fixtures/expected-sources-metadata.js +++ b/packages/lwc-compiler/src/__tests__/fixtures/expected-sources-metadata.js @@ -12,7 +12,8 @@ function tmpl($api, $cmp, $slotset, $ctx) { }, [])]; } if (style) { - tmpl.token = 'x-foo_foo'; + tmpl.hostToken = 'x-foo_foo-host'; + tmpl.shadowToken = 'x-foo_foo'; const style$$1 = document.createElement('style'); style$$1.type = 'text/css'; style$$1.dataset.token = 'x-foo_foo'; diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-sources-namespaced-format.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-sources-namespaced-format.js index 14f5311d9e..db152f8b5e 100755 --- a/packages/lwc-compiler/src/__tests__/fixtures/expected-sources-namespaced-format.js +++ b/packages/lwc-compiler/src/__tests__/fixtures/expected-sources-namespaced-format.js @@ -13,7 +13,8 @@ function tmpl($api, $cmp, $slotset, $ctx) { } if (style) { - tmpl.token = 'x-foo_foo'; + tmpl.hostToken = 'x-foo_foo-host'; + tmpl.shadowToken = 'x-foo_foo'; const style$$1 = document.createElement('style'); style$$1.type = 'text/css'; diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-sources-namespaced.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-sources-namespaced.js index 490d8261b8..aef995e428 100755 --- a/packages/lwc-compiler/src/__tests__/fixtures/expected-sources-namespaced.js +++ b/packages/lwc-compiler/src/__tests__/fixtures/expected-sources-namespaced.js @@ -13,7 +13,8 @@ function tmpl($api, $cmp, $slotset, $ctx) { } if (style) { - tmpl.token = 'x-foo_foo'; + tmpl.hostToken = 'x-foo_foo-host'; + tmpl.shadowToken = 'x-foo_foo'; const style$$1 = document.createElement('style'); style$$1.type = 'text/css'; diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-styled-prod.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-styled-prod.js index 2271a10416..99504439d8 100755 --- a/packages/lwc-compiler/src/__tests__/fixtures/expected-styled-prod.js +++ b/packages/lwc-compiler/src/__tests__/fixtures/expected-styled-prod.js @@ -1 +1 @@ -import _xFoo from"x-foo";import{Element}from"engine";function style(a,b){return`[is=${a}][${b}],${a}[${b}]{color:blue}div[${b}]{color:red}[is=x-foo][${b}],x-foo[${b}]{color:green}`}function tmpl(a){const{h:b,c:c}=a;return[b("div",{key:1},[]),c("x-foo",_xFoo,{key:2},[])]}if(style){tmpl.token="x-styled_styled";const a=document.createElement("style");a.type="text/css",a.dataset.token="x-styled_styled",a.textContent=style("x-styled","x-styled_styled"),document.head.appendChild(a)}class Styled extends Element{render(){return tmpl}}Styled.style=tmpl.style;export default Styled; +import _xFoo from"x-foo";import{Element}from"engine";function style(a,b){return`[${b}-host]{color:blue}div[${b}]{color:red}[is=x-foo][${b}],x-foo[${b}]{color:green}`}function tmpl(a){const{h:b,c:c}=a;return[b("div",{key:1},[]),c("x-foo",_xFoo,{key:2},[])]}if(style){tmpl.hostToken="x-styled_styled-host",tmpl.shadowToken="x-styled_styled";const a=document.createElement("style");a.type="text/css",a.dataset.token="x-styled_styled",a.textContent=style("x-styled","x-styled_styled"),document.head.appendChild(a)}class Styled extends Element{render(){return tmpl}}Styled.style=tmpl.style;export default Styled; diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-styled.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-styled.js index 83c3d1a364..abe4b9daf9 100755 --- a/packages/lwc-compiler/src/__tests__/fixtures/expected-styled.js +++ b/packages/lwc-compiler/src/__tests__/fixtures/expected-styled.js @@ -2,7 +2,7 @@ import _xFoo from 'x-foo'; import { Element } from 'engine'; function style(tagName, token) { - return `${tagName}[${token}],[is="${tagName}"][${token}] { + return `[${token}-host] { color: blue; } div[${token}] { @@ -28,7 +28,8 @@ function tmpl($api, $cmp, $slotset, $ctx) { } if (style) { - tmpl.token = 'x-styled_styled'; + tmpl.hostToken = 'x-styled_styled-host'; + tmpl.shadowToken = 'x-styled_styled'; const style$$1 = document.createElement('style'); style$$1.type = 'text/css'; style$$1.dataset.token = 'x-styled_styled'; diff --git a/packages/lwc-compiler/src/rollup-plugins/__tests__/module-resolver.spec.ts b/packages/lwc-compiler/src/rollup-plugins/__tests__/module-resolver.spec.ts index 5688104863..35c13102ad 100644 --- a/packages/lwc-compiler/src/rollup-plugins/__tests__/module-resolver.spec.ts +++ b/packages/lwc-compiler/src/rollup-plugins/__tests__/module-resolver.spec.ts @@ -45,7 +45,8 @@ describe("module resolver", () => { }, [api_text(\"Manually Imported Template\")])]; } if (style) { - tmpl.token = 'x-class_and_template_class_and_template'; + tmpl.hostToken = 'x-class_and_template_class_and_template-host'; + tmpl.shadowToken = 'x-class_and_template_class_and_template'; const style$$1 = document.createElement('style'); style$$1.type = 'text/css'; style$$1.dataset.token = 'x-class_and_template_class_and_template'; @@ -93,7 +94,8 @@ describe("module resolver", () => { }, [api_text(\"Another Template\")])]; } if (style) { - tmpl.token = 'x-class_and_template_anotherTemplate'; + tmpl.hostToken = 'x-class_and_template_anotherTemplate-host'; + tmpl.shadowToken = 'x-class_and_template_anotherTemplate'; const style$$1 = document.createElement('style'); style$$1.type = 'text/css'; style$$1.dataset.token = 'x-class_and_template_anotherTemplate'; diff --git a/packages/lwc-compiler/src/transformers/__tests__/transform.spec.ts b/packages/lwc-compiler/src/transformers/__tests__/transform.spec.ts index 8f09529126..ab157397e9 100755 --- a/packages/lwc-compiler/src/transformers/__tests__/transform.spec.ts +++ b/packages/lwc-compiler/src/transformers/__tests__/transform.spec.ts @@ -114,7 +114,8 @@ describe("transform", () => { }, [api_text(\"Hello\")])]; } if (stylesheet) { - tmpl.token = 'x-foo_foo'; + tmpl.hostToken = 'x-foo_foo-host'; + tmpl.shadowToken = 'x-foo_foo'; const style = document.createElement('style'); style.type = 'text/css'; style.dataset.token = 'x-foo_foo' diff --git a/packages/lwc-compiler/src/transformers/style.ts b/packages/lwc-compiler/src/transformers/style.ts index 33685a697b..b7026e2b49 100755 --- a/packages/lwc-compiler/src/transformers/style.ts +++ b/packages/lwc-compiler/src/transformers/style.ts @@ -46,7 +46,6 @@ export default function transformStyle( const plugins = [ postcssPluginRaptor({ token: TOKEN_PLACEHOLDER, - tagName: TAG_NAME_PLACEHOLDER }) ]; diff --git a/packages/lwc-compiler/src/transformers/template.ts b/packages/lwc-compiler/src/transformers/template.ts index 01b7db4df3..af54c14f55 100755 --- a/packages/lwc-compiler/src/transformers/template.ts +++ b/packages/lwc-compiler/src/transformers/template.ts @@ -23,7 +23,8 @@ function attachStyleToTemplate( // Use the component tagname and a unique style token to scope the compiled // styles to the component. const tagName = `${namespace}-${name}`; - const scopingToken = `${tagName}_${templateFilename}`; + const shadowToken = `${tagName}_${templateFilename}`; + const hostToken = `${shadowToken}-host`; return [ `import stylesheet from './${templateFilename}.css'`, @@ -35,16 +36,18 @@ function attachStyleToTemplate( // doesn't exists. `if (stylesheet) {`, - // The engine picks the style token from the template during rendering to - // add the token to all generated elements. - ` tmpl.token = '${scopingToken}';`, + // The engine picks the tokens from the template during rendering: + // * `hostToken`: is applied only to the host element + // * `shadowToken`: is applied to all the element generated by the template + ` tmpl.hostToken = '${hostToken}';`, + ` tmpl.shadowToken = '${shadowToken}';`, ``, // Inject the component style in a new style tag the document head. ` const style = document.createElement('style');`, ` style.type = 'text/css';`, - ` style.dataset.token = '${scopingToken}'`, - ` style.textContent = stylesheet('${tagName}', '${scopingToken}');`, + ` style.dataset.token = '${shadowToken}'`, + ` style.textContent = stylesheet('${tagName}', '${shadowToken}');`, ` document.head.appendChild(style);`, `}` ].join("\n"); diff --git a/packages/lwc-engine/src/framework/__tests__/template.spec.ts b/packages/lwc-engine/src/framework/__tests__/template.spec.ts index c5f6c2d95f..2793ea8bdd 100644 --- a/packages/lwc-engine/src/framework/__tests__/template.spec.ts +++ b/packages/lwc-engine/src/framework/__tests__/template.spec.ts @@ -4,7 +4,6 @@ import { Element } from "../html-element"; import { createElement } from '../main'; import { ViewModelReflection } from '../utils'; import { Template } from '../template'; -import { querySelector } from '../dom/element'; function createCustomComponent(html: Template, slotset?) { class MyComponent extends Element { @@ -295,9 +294,9 @@ describe('template', () => { }) describe('token', () => { - it('adds token to the host element if template has a token', () => { + it('adds the host token to the host element if template has a token', () => { const styledTmpl: Template = () => []; - styledTmpl.token = 'token'; + styledTmpl.hostToken = 'token-host'; class Component extends Element { render() { @@ -307,14 +306,39 @@ describe('template', () => { const cmp = createElement('x-cmp', { is: Component }); - expect(cmp.hasAttribute('token')).toBe(false); + expect(cmp.hasAttribute('token-host')).toBe(false); document.body.appendChild(cmp); - expect(cmp.hasAttribute('token')).toBe(true); + expect(cmp.hasAttribute('token-host')).toBe(true); }); - it('removes token from the host element when changing template', () => { + it('adds the token to all the rendered elements if the template has a token', () => { + const styledTmpl: Template = ($api) => [ + $api.h('div', { + key: 1, + }, [ + $api.h('div', { + key: 2, + }, []) + ]), + ]; + styledTmpl.shadowToken = 'token'; + + class Component extends Element { + render() { + return styledTmpl; + } + } + + const cmp = createElement('x-cmp', { is: Component }); + document.body.appendChild(cmp); + + const divs = cmp.shadowRoot.querySelectorAll('div[token]'); + expect(divs.length).toBe(2); + }); + + it('removes the host token from the host element when changing template', () => { const styledTmpl: Template = () => []; - styledTmpl.token = 'token'; + styledTmpl.hostToken = 'token-host'; const unstyledTmpl: Template = () => []; @@ -331,21 +355,21 @@ describe('template', () => { const cmp = createElement('x-cmp', { is: Component }); document.body.appendChild(cmp); - expect(cmp.hasAttribute('token')).toBe(true); + expect(cmp.hasAttribute('token-host')).toBe(true); cmp.tmpl = unstyledTmpl; return Promise.resolve().then(() => { - expect(cmp.hasAttribute('token')).toBe(false); + expect(cmp.hasAttribute('token-host')).toBe(false); }); }); - it('swaps the token when replacing the template with a different token', () => { + it('swaps the host token when replacing the template with a different token', () => { const styledTmplA: Template = () => []; - styledTmplA.token = 'tokenA'; + styledTmplA.hostToken = 'tokenA-host'; const styledTmplB: Template = () => []; - styledTmplB.token = 'tokenB'; + styledTmplB.hostToken = 'tokenB-host'; class Component extends Element { tmpl = styledTmplA; @@ -360,14 +384,14 @@ describe('template', () => { const cmp = createElement('x-cmp', { is: Component }); document.body.appendChild(cmp); - expect(cmp.hasAttribute('tokenA')).toBe(true); - expect(cmp.hasAttribute('tokenB')).toBe(false); + expect(cmp.hasAttribute('tokenA-host')).toBe(true); + expect(cmp.hasAttribute('tokenB-host')).toBe(false); cmp.tmpl = styledTmplB; return Promise.resolve().then(() => { - expect(cmp.hasAttribute('tokenA')).toBe(false); - expect(cmp.hasAttribute('tokenB')).toBe(true); + expect(cmp.hasAttribute('tokenA-host')).toBe(false); + expect(cmp.hasAttribute('tokenB-host')).toBe(true); }); }); }); @@ -461,7 +485,8 @@ describe('template', () => { const element = createElement('x-attr-cmp', { is: MyComponent }); document.body.appendChild(element); - expect(querySelector.call(element, 'div').getAttribute('title')).toBe('foo'); + const div = element.shadowRoot.querySelector('div'); + expect(div.getAttribute('title')).toBe('foo'); }); it('should remove attribute when value is null', () => { @@ -493,10 +518,10 @@ describe('template', () => { const element = createElement('x-attr-cmp', { is: MyComponent }); document.body.appendChild(element); - expect(querySelector.call(element, 'div').getAttribute('title')).toBe('initial'); + expect(element.shadowRoot.querySelector('div').getAttribute('title')).toBe('initial'); element.setInner(null); return Promise.resolve().then(() => { - expect(querySelector.call(element, 'div').hasAttribute('title')).toBe(false); + expect(element.shadowRoot.querySelector('div').hasAttribute('title')).toBe(false); }); }); }); diff --git a/packages/lwc-engine/src/framework/api.ts b/packages/lwc-engine/src/framework/api.ts index 4a9ad053fc..039b1f062c 100644 --- a/packages/lwc-engine/src/framework/api.ts +++ b/packages/lwc-engine/src/framework/api.ts @@ -149,12 +149,12 @@ function getCurrentFallback(): boolean { return isNull(vmBeingRendered) || vmBeingRendered.fallback; } -function getCurrentTplToken(): string | undefined { +function getCurrentShadowToken(): string | undefined { // For root elements and other special cases the vm is not set. if (isNull(vmBeingRendered)) { return; } - return vmBeingRendered.context.tplToken; + return vmBeingRendered.context.shadowToken; } function normalizeStyleString(value: any): string | undefined { @@ -190,7 +190,7 @@ export function h(sel: string, data: VNodeData, children: VNodes): VElement { const { classMap, className, style, styleMap, key } = data; data.class = classMap || getMapFromClassName(normalizeStyleString(className)); data.style = styleMap || normalizeStyleString(style); - data.token = getCurrentTplToken(); + data.token = getCurrentShadowToken(); data.uid = getCurrentOwnerId(); let text, elm; // tslint:disable-line const vnode: VElement = { @@ -261,7 +261,7 @@ export function c(sel: string, Ctor: ComponentConstructor, data: VNodeData, chil data = { hook, key, attrs, on, props, ctor: Ctor }; data.class = classMap || getMapFromClassName(normalizeStyleString(className)); data.style = styleMap || normalizeStyleString(style); - data.token = getCurrentTplToken(); + data.token = getCurrentShadowToken(); data.uid = getCurrentOwnerId(); data.fallback = getCurrentFallback(); data.mode = 'open'; // TODO: this should be defined in Ctor diff --git a/packages/lwc-engine/src/framework/context.ts b/packages/lwc-engine/src/framework/context.ts index 503afb1f08..42a9b90c4d 100644 --- a/packages/lwc-engine/src/framework/context.ts +++ b/packages/lwc-engine/src/framework/context.ts @@ -4,8 +4,9 @@ export const TopLevelContextSymbol = Symbol(); export interface Context { [TopLevelContextSymbol]?: boolean; - tplToken?: string; - tplCache?: Template | undefined; + hostToken?: string; + shadowToken?: string; + tplCache?: Template; [key: string]: any; } diff --git a/packages/lwc-engine/src/framework/modules/__tests__/token.spec.ts b/packages/lwc-engine/src/framework/modules/__tests__/token.spec.ts index 26e989547b..e02a35b7e1 100644 --- a/packages/lwc-engine/src/framework/modules/__tests__/token.spec.ts +++ b/packages/lwc-engine/src/framework/modules/__tests__/token.spec.ts @@ -8,7 +8,7 @@ describe('modules/token', () => { const tmpl = $api => [ $api.h('section', { key: 0 }, [ $api.t('test') ]), ]; - tmpl.token = 'test'; + tmpl.shadowToken = 'test'; class Component extends Element { render() { @@ -26,7 +26,7 @@ describe('modules/token', () => { const styledTmpl = $api => [ $api.h('section', { key: 0 }, [ $api.t('test') ]), ]; - styledTmpl.token = 'test'; + styledTmpl.shadowToken = 'test'; const unstyledTmpl = $api => [ $api.h('section', { key: 0 }, [ $api.t('test') ]), @@ -60,12 +60,12 @@ describe('modules/token', () => { const styledTmplA: Template = $api => [ $api.h('section', { key: 0 }, [ $api.t('test') ]), ]; - styledTmplA.token = 'testA'; + styledTmplA.shadowToken = 'testA'; const styledTmplB: Template = $api => [ $api.h('section', { key: 0 }, [ $api.t('test') ]), ]; - styledTmplB.token = 'testB'; + styledTmplB.shadowToken = 'testB'; class Component extends Element { tmpl = styledTmplA; diff --git a/packages/lwc-engine/src/framework/template.ts b/packages/lwc-engine/src/framework/template.ts index b77b65cef4..d2ce2b87e5 100644 --- a/packages/lwc-engine/src/framework/template.ts +++ b/packages/lwc-engine/src/framework/template.ts @@ -11,8 +11,18 @@ import { removeAttribute, setAttribute } from "./dom/element"; export interface Template { (api: RenderAPI, cmp: object, slotset: SlotSet, ctx: Context): undefined | VNodes; - style?: string; - token?: string; + + /** + * HTML attribute that need to be applied to the host element. + * This attribute is used for the `:host` pseudo class CSS selector. + */ + hostToken?: string; + + /** + * HTML attribute that need to the applied to all the element that the template produces. + * This attribute is used for style encapsulation when the engine runs in fallback mode. + */ + shadowToken?: string; } const EmptySlots: SlotSet = create(null); @@ -53,11 +63,14 @@ function validateTemplate(vm: VM, html: any) { validateFields(vm, html); } +/** + * Apply/Update the styling token applied to the host element. + */ function applyTokenToHost(vm: VM, html: Template): void { const { context } = vm; - const oldToken = context.tplToken; - const newToken = html.token; + const oldToken = context.hostToken; + const newToken = html.hostToken; if (oldToken !== newToken) { const host = vm.elm; @@ -92,8 +105,10 @@ export function evaluateTemplate(vm: VM, html: Template): Array { vm.cmpTemplate = html; + // Populate context with template information context.tplCache = create(null); - context.tplToken = html.token; + context.hostToken = html.hostToken; + context.shadowToken = html.shadowToken; if (process.env.NODE_ENV !== 'production') { validateTemplate(vm, html); diff --git a/packages/postcss-plugin-lwc/README.md b/packages/postcss-plugin-lwc/README.md index ce1b02018c..6dd699d077 100644 --- a/packages/postcss-plugin-lwc/README.md +++ b/packages/postcss-plugin-lwc/README.md @@ -32,13 +32,12 @@ span { postcss([ lwcPlugin({ - tagName: 'x-btn', token: 'x-btn_tmpl' }) ]).process(source).then(res => { console.log(res) /* - x-btn[x-btn_tmpl], [is="x-btn"][x-btn_tmpl] { + [x-btn_tmpl-host] { opacity: 0.4; } @@ -51,13 +50,6 @@ postcss([ ## Options -#### `tagName` - -Type: `string` -Required: `true` - -The tag name of the host element the styles are applied to. - #### `token` Type: `string` diff --git a/packages/postcss-plugin-lwc/src/__tests__/main.spec.ts b/packages/postcss-plugin-lwc/src/__tests__/main.spec.ts index a456ba637d..c820445c17 100644 --- a/packages/postcss-plugin-lwc/src/__tests__/main.spec.ts +++ b/packages/postcss-plugin-lwc/src/__tests__/main.spec.ts @@ -1,17 +1,11 @@ import * as postcss from 'postcss'; import { transformSelector } from '../index'; -import { process, DEFAULT_TAGNAME, DEFAULT_TOKEN } from './shared'; +import { process, DEFAULT_TOKEN } from './shared'; describe('default export (postcss plugin)', () => { - it('assert tagName option', () => { - expect(() => process('', {})).toThrow( - /tagName option must be a string but instead received undefined/, - ); - }); - it('assert token option', () => { - expect(() => process('', { tagName: DEFAULT_TAGNAME })).toThrow( + expect(() => process('', { })).toThrow( /token option must be a string but instead received undefined/, ); }); @@ -21,7 +15,7 @@ describe('default export (postcss plugin)', () => { ':host { display: block; } h1 { color: red; }', ); expect(css).toBe( - `x-foo[x-foo_tmpl],[is=\"x-foo\"][x-foo_tmpl] { display: block; } h1[x-foo_tmpl] { color: red; }`, + `[x-foo_tmpl-host] { display: block; } h1[x-foo_tmpl] { color: red; }`, ); }); }); @@ -29,10 +23,9 @@ describe('default export (postcss plugin)', () => { describe('transformSelector', () => { it('transforms string selector', () => { const res = transformSelector(':host', { - tagName: DEFAULT_TAGNAME, token: DEFAULT_TOKEN, }); - expect(res).toBe('x-foo[x-foo_tmpl],[is="x-foo"][x-foo_tmpl]'); + expect(res).toBe('[x-foo_tmpl-host]'); }); it('transforms postCSS node rules', () => { @@ -40,8 +33,8 @@ describe('transformSelector', () => { postcss.rule({ selector: ':host', }), - { tagName: DEFAULT_TAGNAME, token: DEFAULT_TOKEN }, + { token: DEFAULT_TOKEN }, ); - expect(res).toBe('x-foo[x-foo_tmpl],[is="x-foo"][x-foo_tmpl]'); + expect(res).toBe('[x-foo_tmpl-host]'); }); }); diff --git a/packages/postcss-plugin-lwc/src/__tests__/selector-transform.spec.ts b/packages/postcss-plugin-lwc/src/__tests__/selector-transform.spec.ts index 04a863a158..0d7d063b8f 100644 --- a/packages/postcss-plugin-lwc/src/__tests__/selector-transform.spec.ts +++ b/packages/postcss-plugin-lwc/src/__tests__/selector-transform.spec.ts @@ -109,10 +109,7 @@ describe('custom-element', () => { it('should handle custom elements in the :host-context selector', async () => { const { css } = await process(':host-context(x-bar) {}'); expect(css).toBe( - [ - `x-bar x-foo[x-foo_tmpl],x-bar [is="x-foo"][x-foo_tmpl],`, - `[is="x-bar"] x-foo[x-foo_tmpl],[is="x-bar"] [is="x-foo"][x-foo_tmpl] {}`, - ].join(''), + `x-bar [x-foo_tmpl-host],[is="x-bar"] [x-foo_tmpl-host] {}`, ); }); }); @@ -120,47 +117,36 @@ describe('custom-element', () => { describe(':host', () => { it('should handle no context', async () => { const { css } = await process(':host {}'); - expect(css).toBe(`x-foo[x-foo_tmpl],[is="x-foo"][x-foo_tmpl] {}`); + expect(css).toBe(`[x-foo_tmpl-host] {}`); }); it('should handle class', async () => { const { css } = await process(':host(.active) {}'); - expect(css).toBe( - `x-foo[x-foo_tmpl].active,[is="x-foo"][x-foo_tmpl].active {}`, - ); + expect(css).toBe(`[x-foo_tmpl-host].active {}`); }); it('should handle attribute', async () => { const { css } = await process(':host([draggable]) {}'); - expect(css).toBe( - `x-foo[x-foo_tmpl][draggable],[is="x-foo"][x-foo_tmpl][draggable] {}`, - ); + expect(css).toBe(`[x-foo_tmpl-host][draggable] {}`); }); it('should handle multiple selectors', async () => { const { css } = await process(':host(.a, .b) > p {}'); expect(css).toBe( - [ - `x-foo[x-foo_tmpl].a > p[x-foo_tmpl],[is="x-foo"][x-foo_tmpl].a > p[x-foo_tmpl],`, - `x-foo[x-foo_tmpl].b > p[x-foo_tmpl],[is="x-foo"][x-foo_tmpl].b > p[x-foo_tmpl] {}`, - ].join(''), + `[x-foo_tmpl-host].a > p[x-foo_tmpl],[x-foo_tmpl-host].b > p[x-foo_tmpl] {}`, ); }); it('should handle pseudo-element', async () => { const { css } = await process(':host(:hover) {}'); - expect(css).toBe( - `x-foo[x-foo_tmpl]:hover,[is="x-foo"][x-foo_tmpl]:hover {}`, - ); + expect(css).toBe(`[x-foo_tmpl-host]:hover {}`); }); }); describe(':host-context', () => { it('should handle selector', async () => { const { css } = await process(':host-context(.darktheme) {}'); - expect(css).toBe( - `.darktheme x-foo[x-foo_tmpl],.darktheme [is="x-foo"][x-foo_tmpl] {}`, - ); + expect(css).toBe(`.darktheme [x-foo_tmpl-host] {}`); }); it('should handle multiple selectors', async () => { @@ -168,17 +154,7 @@ describe(':host-context', () => { ':host-context(.darktheme, .nighttheme) {}', ); expect(css).toBe( - [ - `.darktheme x-foo[x-foo_tmpl],.darktheme [is="x-foo"][x-foo_tmpl],`, - `.nighttheme x-foo[x-foo_tmpl],.nighttheme [is="x-foo"][x-foo_tmpl] {}`, - ].join(''), - ); - }); - - it('should handle getting associated with host', async () => { - const { css } = await process(':host-context(.darktheme):host {}'); - expect(css).toBe( - `.darktheme x-foo[x-foo_tmpl],.darktheme [is="x-foo"][x-foo_tmpl] {}`, + `.darktheme [x-foo_tmpl-host],.nighttheme [x-foo_tmpl-host] {}`, ); }); }); diff --git a/packages/postcss-plugin-lwc/src/__tests__/shared.ts b/packages/postcss-plugin-lwc/src/__tests__/shared.ts index c1d5038581..f2dbb57379 100644 --- a/packages/postcss-plugin-lwc/src/__tests__/shared.ts +++ b/packages/postcss-plugin-lwc/src/__tests__/shared.ts @@ -5,7 +5,6 @@ import { PluginConfig } from '../config'; export const FILE_NAME = '/test.css'; -export const DEFAULT_TAGNAME = 'x-foo'; export const DEFAULT_TOKEN = 'x-foo_tmpl'; export const DEFAULT_CUSTOM_PROPERTIES_CONFIG = { allowDefinition: false, @@ -17,7 +16,6 @@ export const DEFAULT_CUSTOM_PROPERTIES_CONFIG = { export function process( source: string, options: PluginConfig = { - tagName: DEFAULT_TAGNAME, token: DEFAULT_TOKEN, customProperties: DEFAULT_CUSTOM_PROPERTIES_CONFIG, }, diff --git a/packages/postcss-plugin-lwc/src/config.ts b/packages/postcss-plugin-lwc/src/config.ts index 541e44a451..d96a334af9 100644 --- a/packages/postcss-plugin-lwc/src/config.ts +++ b/packages/postcss-plugin-lwc/src/config.ts @@ -1,7 +1,6 @@ export type VarTransformer = (name: string, fallback: string) => string; export interface PluginConfig { - tagName: string; token: string; customProperties?: { allowDefinition?: boolean; @@ -14,11 +13,7 @@ export function validateConfig(options: PluginConfig) { throw new TypeError('Expected options with tagName and token properties'); } - if (!options.tagName || typeof options.tagName !== 'string') { - throw new TypeError( - `tagName option must be a string but instead received ${typeof options.tagName}`, - ); - } else if (!options.token || typeof options.token !== 'string') { + if (!options.token || typeof options.token !== 'string') { throw new TypeError( `token option must be a string but instead received ${typeof options.token}`, ); diff --git a/packages/postcss-plugin-lwc/src/selector-scoping/transform.ts b/packages/postcss-plugin-lwc/src/selector-scoping/transform.ts index bddbf4bf62..87b74523ab 100644 --- a/packages/postcss-plugin-lwc/src/selector-scoping/transform.ts +++ b/packages/postcss-plugin-lwc/src/selector-scoping/transform.ts @@ -3,7 +3,6 @@ import * as parser from 'postcss-selector-parser'; import { attribute, - tag, combinator, isTag, isPseudoElement, @@ -28,35 +27,18 @@ import { } from './utils'; import { PluginConfig } from '../config'; -const HOST_SELECTOR_PLACEHOLDER = '$HOST$'; const CUSTOM_ELEMENT_SELECTOR_PREFIX = '$CUSTOM$'; -function hostPlaceholder() { - return tag({ value: HOST_SELECTOR_PLACEHOLDER }); -} - -function isHostPlaceholder(node: Node) { - return isTag(node) && node.value === HOST_SELECTOR_PLACEHOLDER; -} - /** Generate a scoping attribute based on the passed token */ -function scopeAttribute({ token }: PluginConfig) { - return attribute({ - attribute: token, - value: undefined, - raws: {}, - }); -} +function scopeAttribute({ token }: PluginConfig, { host } = { host: false }) { + let value = token; -/** Generate a host selector by tag name */ -function hostByTag({ tagName }: PluginConfig) { - return tag({ value: tagName }); -} + if (host) { + value += '-host'; + } -/** Generate a host selector via the "is" attribute: [is="x-foo"] */ -function hostByIsAttribute({ tagName }: PluginConfig) { return attribute({ - attribute: `is="${tagName}"`, + attribute: value, value: undefined, raws: {}, }); @@ -172,118 +154,106 @@ function scopeSelector(selector: Selector, config: PluginConfig) { /** * Mark the :host selector with a placeholder. If the selector has a list of * contextual selector it will generate a rule for each of them. - * :host -> $HOST$ - * :host(.foo, .bar) -> $HOST$.foo, $HOST$.bar + * :host -> [x-foo_tmpl-host] + * :host(.foo, .bar) -> [x-foo_tmpl-host].foo, [x-foo_tmpl-host].bar */ -function transformHost(selector: Selector) { +function transformHost(selector: Selector, config: PluginConfig) { + // Locate the first :host pseudo-class const hostNode = findNode(selector, isHostPseudoClass) as | Pseudo | undefined; if (hostNode) { - const placeholder = hostPlaceholder(); - hostNode.replaceWith(placeholder); + // Store the original location of the :host in the selector + const hostIndex = selector.index(hostNode); + + // Swap the :host pseudo-class with the host scoping token + const hostScopeAttr = scopeAttribute(config, { host: true }); + hostNode.replaceWith(hostScopeAttr); + // Generate a unique contextualized version of the selector for each selector pass as argument + // to the :host const contextualSelectors = hostNode.nodes.map( (contextSelectors: Selector) => { - const clone = selector.clone({}) as Selector; - const clonePlaceholder = findNode( - clone, - isHostPlaceholder, - )! as Tag; + const clonedSelector = selector.clone({}) as Selector; + const clonedHostNode = clonedSelector.at(hostIndex) as Tag; + // Add to the compound selector previously containing the :host pseudo class + // the contextual selectors. contextSelectors.each(node => { trimNodeWhitespaces(node); - clone.insertAfter(clonePlaceholder, node); + clonedSelector.insertAfter(clonedHostNode, node); }); - return clone; + return clonedSelector; }, ); + // Replace the current selector with the different variants replaceNodeWith(selector, ...contextualSelectors); } } /** * Mark transform :host-context by prepending the selector with the contextual selectors. - * :host-context(.dark) -> .dark x-foo, .dark [is="x-foo"] - * - * If the selector already contains :host, the selector should not be scoped twice. + * :host-context(.bar) -> .bar [x-foo_tmpl-host] + * :host-context(.bar, .baz) -> .bar [x-foo_tmpl-host], .baz [x-foo_tmpl-host] */ -function transformHostContext(selector: Selector) { +function transformHostContext(selector: Selector, config: PluginConfig) { + // Locate the first :host-context pseudo-selector const hostContextNode = findNode(selector, isHostContextPseudoClass) as | Pseudo | undefined; - const hostNode = findNode(selector, isHostPlaceholder); - if (hostContextNode) { - hostContextNode.remove(); + // Swap the :host-context pseudo-class with the host scoping token + const hostScopeAttr = scopeAttribute(config, { host: true }); + hostContextNode.replaceWith(hostScopeAttr); + // Generate a unique contextualized version of the selector for each selector pass as argument + // to the :host-context const contextualSelectors = hostContextNode.nodes.map( (contextSelectors: Selector) => { - const clone = selector.clone({}) as Selector; - - if (!hostNode) { - clone.insertBefore(clone.first, hostPlaceholder()); - } + const cloneSelector = selector.clone({}) as Selector; - clone.insertBefore(clone.first, combinator({ value: ' ' })); + // Prepend the cloned selector with the context selector + cloneSelector.insertBefore( + cloneSelector.first, + combinator({ value: ' ' }), + ); contextSelectors.each(node => { trimNodeWhitespaces(node); - clone.insertBefore(clone.first, node); + cloneSelector.insertBefore(cloneSelector.first, node); }); - return clone; + return cloneSelector; }, ); + // Replace the current selector with the different variants replaceNodeWith(selector, ...contextualSelectors); } } -/** - * Replace the $HOST$ selectors with the actual scoped selectors. - * $HOST$ -> x-foo[x-foo_tmpl], [is="x-foo"][x-foo_tmpl] - */ -function replaceHostPlaceholder(selector: Selector, config: PluginConfig) { - const hasHostPlaceholder = findNode(selector, isHostPlaceholder); - - if (hasHostPlaceholder) { - const hostActualSelectors = [ - hostByTag(config), - hostByIsAttribute(config), - ].map(actualSelector => { - const clone = selector.clone({}) as Selector; - - const placeholder = findNode(clone, isHostPlaceholder)!; - placeholder.replaceWith(actualSelector); - clone.insertAfter(actualSelector, scopeAttribute(config)); - - return clone; - }); - - replaceNodeWith(selector, ...hostActualSelectors); - } -} - /** Returns selector processor based on the passed config */ function selectorProcessor(config: PluginConfig) { return parser(root => { validateSelectors(root); - root.each((selector: Selector) => scopeSelector(selector, config)); + root.each((selector: Selector) => { + scopeSelector(selector, config); + }); - root.each(transformHost); - root.each(transformHostContext); + root.each((selector: Selector) => { + transformHost(selector, config); + }); - customElementSelector(root); + root.each((selector: Selector) => { + transformHostContext(selector, config); + }); - root.each((selector: Selector) => - replaceHostPlaceholder(selector, config), - ); + customElementSelector(root); }) as Processor; } diff --git a/packages/rollup-plugin-lwc-compiler/__tests__/fixtures/expected_default_config_simple_app.js b/packages/rollup-plugin-lwc-compiler/__tests__/fixtures/expected_default_config_simple_app.js index d524649ff5..106d05e2f1 100644 --- a/packages/rollup-plugin-lwc-compiler/__tests__/fixtures/expected_default_config_simple_app.js +++ b/packages/rollup-plugin-lwc-compiler/__tests__/fixtures/expected_default_config_simple_app.js @@ -20,7 +20,8 @@ } if (style$1) { - tmpl.token = "x-foo_foo"; + tmpl.hostToken = 'x-foo_foo-host'; + tmpl.shadowToken = 'x-foo_foo'; const style = document.createElement("style"); style.type = "text/css";