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";