diff --git a/src/compiler/transformers/add-static-style.ts b/src/compiler/transformers/add-static-style.ts index b7a08229472d..d5253c3a1fee 100644 --- a/src/compiler/transformers/add-static-style.ts +++ b/src/compiler/transformers/add-static-style.ts @@ -4,7 +4,7 @@ import ts from 'typescript'; import type * as d from '../../declarations'; import { scopeCss } from '../../utils/shadow-css'; import { getScopeId } from '../style/scope-css'; -import { createStaticGetter } from './transform-utils'; +import { createStaticGetter, getExternalStyles } from './transform-utils'; /** * Adds static "style" getter within the class @@ -91,7 +91,7 @@ const getMultipleModeStyle = ( // import generated from @Component() styleUrls option // import myTagIosStyle from './import-path.css'; // static get style() { return { ios: myTagIosStyle }; } - const styleUrlIdentifier = createStyleIdentifierFromUrl(cmp, style); + const styleUrlIdentifier = createStyleIdentifier(cmp, style); const propUrlIdentifier = ts.factory.createPropertyAssignment(style.modeName, styleUrlIdentifier); styleModes.push(propUrlIdentifier); } @@ -107,18 +107,18 @@ const getSingleStyle = (cmp: d.ComponentCompilerMeta, style: d.StyleCompiler, co return createStyleLiteral(cmp, style, commentOriginalSelector); } - if (typeof style.styleIdentifier === 'string') { - // direct import already written in the source code - // import myTagStyle from './import-path.css'; - // static get style() { return myTagStyle; } - return ts.factory.createIdentifier(style.styleIdentifier); - } - if (Array.isArray(style.externalStyles) && style.externalStyles.length > 0) { // import generated from @Component() styleUrls option // import myTagStyle from './import-path.css'; // static get style() { return myTagStyle; } - return createStyleIdentifierFromUrl(cmp, style); + return createStyleIdentifier(cmp, style); + } + + if (typeof style.styleIdentifier === 'string') { + // direct import already written in the source code + // import myTagStyle0 from './import-path.css'; + // static get style() { return myTagStyle0; } + return ts.factory.createIdentifier(style.styleIdentifier); } return null; @@ -134,16 +134,75 @@ const createStyleLiteral = (cmp: d.ComponentCompilerMeta, style: d.StyleCompiler return ts.factory.createStringLiteral(style.styleStr); }; -const createStyleIdentifierFromUrl = (cmp: d.ComponentCompilerMeta, style: d.StyleCompiler) => { - style.styleIdentifier = dashToPascalCase(cmp.tagName); - style.styleIdentifier = style.styleIdentifier.charAt(0).toLowerCase() + style.styleIdentifier.substring(1); - +/** + * Helper method to create a style identifier for a component using {@link createStyleIdentifierFromUrl}. + * The method ensures that duplicate styles are removed and that the order of the styles is preserved. + * It also ensures that the style identifier is unique. + * + * @param cmp the metadata associated with the component being evaluated + * @param style style meta data + * @returns an assignment expression to be applied to the `style` property of a component class (e.g. `_myComponentCssStyle + _myComponentIosCssStyle` based on the example) + */ +export const createStyleIdentifier = (cmp: d.ComponentCompilerMeta, style: d.StyleCompiler) => { + const externalStyles = getExternalStyles(style); + /** + * Set a styleIdentifier which will be propagated to the component and + * later picked up by rollup when it injects the parsed CSS directly into + * the component, see `compilerCtx.worker.transformCssToEsm` in + * `src/compiler/bundle/ext-transforms-plugin.ts` + */ + style.styleIdentifier = dashToPascalCase(cmp.tagName.charAt(0).toLowerCase() + cmp.tagName.substring(1)); if (style.modeName !== DEFAULT_STYLE_MODE) { style.styleIdentifier += dashToPascalCase(style.modeName); } - style.styleIdentifier += 'Style'; - style.externalStyles = [style.externalStyles[0]]; + return createIdentifierFromStyleIdentifier(style.styleIdentifier, Object.keys(externalStyles)); +}; + +/** + * Creates an expression to be assigned to the `style` property of a component class. For example + * given the following component: + * + * ```ts + * @Component({ + * styleUrls: ['my-component.css', 'my-component.ios.css'] + * tag: 'cmp', + * }) + * export class MyComponent { + * // ... + * } + * ``` + * + * it would generate the following expression: + * + * ```ts + * import MyComponentStyle0 from './my-component.css'; + * import MyComponentStyle1 from './my-component.ios.css'; + * export class MyComponent { + * // ... + * } + * MyComponent.style = MyComponentStyle0 + MyComponentStyle1; + * ``` + * + * Note: style imports are made in [`createEsmStyleImport`](src/compiler/transformers/style-imports.ts). + * + * @param styleIdentifier identifier to be used for the style + * @param externalStyleIds numeric ids of the external styles + * @returns an assignment expression to be applied to the `style` property of a component class (e.g. `_myComponentCssStyle + _myComponentIosCssStyle` based on the example) + */ +const createIdentifierFromStyleIdentifier = ( + styleIdentifier: string, + externalStyleIds: string[], +): ts.Identifier | ts.BinaryExpression => { + const id = externalStyleIds[0]; + + if (externalStyleIds.length === 1) { + return ts.factory.createIdentifier(styleIdentifier + id); + } - return ts.factory.createIdentifier(style.styleIdentifier); + return ts.factory.createBinaryExpression( + createIdentifierFromStyleIdentifier(styleIdentifier, [id]), + ts.SyntaxKind.PlusToken, + createIdentifierFromStyleIdentifier(styleIdentifier, externalStyleIds.slice(1)), + ); }; diff --git a/src/compiler/transformers/component-native/native-static-style.ts b/src/compiler/transformers/component-native/native-static-style.ts index b6d787630f04..2071f73b6603 100644 --- a/src/compiler/transformers/component-native/native-static-style.ts +++ b/src/compiler/transformers/component-native/native-static-style.ts @@ -1,9 +1,10 @@ -import { dashToPascalCase, DEFAULT_STYLE_MODE } from '@utils'; +import { DEFAULT_STYLE_MODE } from '@utils'; import ts from 'typescript'; import type * as d from '../../../declarations'; import { scopeCss } from '../../../utils/shadow-css'; import { getScopeId } from '../../style/scope-css'; +import { createStyleIdentifier } from '../add-static-style'; import { createStaticGetter } from '../transform-utils'; export const addNativeStaticStyle = (classMembers: ts.ClassElement[], cmp: d.ComponentCompilerMeta) => { @@ -43,7 +44,7 @@ const addMultipleModeStyleGetter = ( // import generated from @Component() styleUrls option // import myTagIosStyle from './import-path.css'; // static get style() { return { "ios": myTagIosStyle }; } - const styleUrlIdentifier = createStyleIdentifierFromUrl(cmp, style); + const styleUrlIdentifier = createStyleIdentifier(cmp, style); const propUrlIdentifier = ts.factory.createPropertyAssignment(style.modeName, styleUrlIdentifier); styleModes.push(propUrlIdentifier); } @@ -74,7 +75,7 @@ const addSingleStyleGetter = ( // import generated from @Component() styleUrls option // import myTagStyle from './import-path.css'; // static get style() { return myTagStyle; } - const styleUrlIdentifier = createStyleIdentifierFromUrl(cmp, style); + const styleUrlIdentifier = createStyleIdentifier(cmp, style); classMembers.push(createStaticGetter('style', styleUrlIdentifier)); } }; @@ -88,17 +89,3 @@ const createStyleLiteral = (cmp: d.ComponentCompilerMeta, style: d.StyleCompiler return ts.factory.createStringLiteral(style.styleStr); }; - -const createStyleIdentifierFromUrl = (cmp: d.ComponentCompilerMeta, style: d.StyleCompiler) => { - style.styleIdentifier = dashToPascalCase(cmp.tagName); - style.styleIdentifier = style.styleIdentifier.charAt(0).toLowerCase() + style.styleIdentifier.substring(1); - - if (style.modeName !== DEFAULT_STYLE_MODE) { - style.styleIdentifier += dashToPascalCase(style.modeName); - } - - style.styleIdentifier += 'Style'; - style.externalStyles = [style.externalStyles[0]]; - - return ts.factory.createIdentifier(style.styleIdentifier); -}; diff --git a/src/compiler/transformers/style-imports.ts b/src/compiler/transformers/style-imports.ts index e5556444bdc7..28ae1533b319 100644 --- a/src/compiler/transformers/style-imports.ts +++ b/src/compiler/transformers/style-imports.ts @@ -2,8 +2,37 @@ import ts from 'typescript'; import type * as d from '../../declarations'; import { serializeImportPath } from './stencil-import-path'; -import { retrieveTsModifiers } from './transform-utils'; +import { getExternalStyles, retrieveTsModifiers } from './transform-utils'; +/** + * This function adds imports (either in ESM or CJS syntax) for styles that are + * imported from the component's styleUrls option. For example, if a component + * has the following: + * + * ```ts + * @Component({ + * styleUrls: ['my-component.css', 'my-component.ios.css'] + * }) + * export class MyComponent { + * // ... + * } + * ``` + * + * then this function will add the following import statement: + * + * ```ts + * import MyComponentStyle0 from './my-component.css'; + * import MyComponentStyle1 from './my-component.ios.css'; + * ``` + * + * Note that import identifier are used in [`addStaticStyleGetterWithinClass`](src/compiler/transformers/add-static-style.ts) + * to attach them to a components static style property. + * + * @param transformOpts transform options configured for the current output target transpilation + * @param tsSourceFile the TypeScript source file that is being updated + * @param moduleFile component file to update + * @returns an updated source file with the added import statements + */ export const updateStyleImports = ( transformOpts: d.TransformOptions, tsSourceFile: ts.SourceFile, @@ -17,6 +46,14 @@ export const updateStyleImports = ( return updateEsmStyleImports(transformOpts, tsSourceFile, moduleFile); }; +/** + * Iterate over all components defined in given module, collect import + * statements to be added and update source file with them. + * @param transformOpts transform options configured for the current output target transpilation + * @param tsSourceFile the TypeScript source file that is being updated + * @param moduleFile component file to update + * @returns update source file with added import statements + */ const updateEsmStyleImports = ( transformOpts: d.TransformOptions, tsSourceFile: ts.SourceFile, @@ -32,7 +69,7 @@ const updateEsmStyleImports = ( updateSourceFile = true; if (style.externalStyles.length > 0) { // add style imports built from @Component() styleUrl option - styleImports.push(createEsmStyleImport(transformOpts, tsSourceFile, cmp, style)); + styleImports.push(...createStyleImport(transformOpts, tsSourceFile, cmp, style)); } else { // update existing esm import of a style identifier statements = updateEsmStyleImportPath(transformOpts, tsSourceFile, statements, cmp, style); @@ -86,22 +123,105 @@ const updateEsmStyleImportPath = ( return statements; }; -const createEsmStyleImport = ( +/** + * Add import or require statement for each style + * e.g. `import CMP__import_path_css from './import-path.css';` + * + * @param transformOpts transform options configured for the current output target transpilation + * @param tsSourceFile the TypeScript source file that is being updated + * @param cmp component meta data + * @param style style meta data + * @param moduleType module type (either 'esm' or 'cjs') + * @returns an set or import or require statements to add to the source file + */ +const createStyleImport = ( transformOpts: d.TransformOptions, tsSourceFile: ts.SourceFile, cmp: d.ComponentCompilerMeta, style: d.StyleCompiler, + /** + * default to 'esm' if not provided, behavior defined in `updateStyleImports` + */ + moduleType: ModuleType = 'esm' as ModuleType, ) => { - const importName = ts.factory.createIdentifier(style.styleIdentifier); - const importPath = getStyleImportPath(transformOpts, tsSourceFile, cmp, style, style.externalStyles[0].absolutePath); - - return ts.factory.createImportDeclaration( - undefined, - ts.factory.createImportClause(false, importName, undefined), - ts.factory.createStringLiteral(importPath), - ); + type ImportDeclarationOrVariableStatementType = ModuleType extends 'esm' + ? ts.ImportDeclaration + : ts.VariableStatement; + const imports: ImportDeclarationOrVariableStatementType[] = []; + + for (const [i, externalStyle] of Object.entries(getExternalStyles(style))) { + /** + * Concat styleId and absolutePath to get a unique identifier for each style. + * + * For example: + * ```ts + * @Component({ + * styleUrls: { + * md: './foo/bar.css', + * ios: './bar/foo.css' + * }, + * tag: 'cmp-a' + * }) + * ``` + * + * it would create the following identifiers: + * ```ts + * import CmpAStyle0 from './foo/bar.css'; + * import CmpAStyle1 from './bar/foo.css'; + * ``` + * + * Attention: if you make changes to how this identifier is created you also need + * to update this in [`createStyleIdentifierFromUrl`](`src/compiler/transformers/add-static-style.ts`). + */ + const styleIdentifier = `${style.styleIdentifier}${i}`; + const importIdentifier = ts.factory.createIdentifier(styleIdentifier); + const importPath = getStyleImportPath(transformOpts, tsSourceFile, cmp, style, externalStyle); + + if (moduleType === 'esm') { + imports.push( + ts.factory.createImportDeclaration( + undefined, + ts.factory.createImportClause(false, importIdentifier, undefined), + ts.factory.createStringLiteral(importPath), + ) as ImportDeclarationOrVariableStatementType, + ); + } else if (moduleType === 'cjs') { + imports.push( + ts.factory.createVariableStatement( + undefined, + ts.factory.createVariableDeclarationList( + [ + ts.factory.createVariableDeclaration( + importIdentifier, + undefined, + undefined, + ts.factory.createCallExpression( + ts.factory.createIdentifier('require'), + [], + [ts.factory.createStringLiteral(importPath)], + ), + ), + ], + ts.NodeFlags.Const, + ), + ) as ImportDeclarationOrVariableStatementType, + ); + } else { + throw new Error(`Invalid module type: ${moduleType}`); + } + } + + return imports; }; +/** + * Iterate over all components defined in given module, collect require + * statements to be added and update source file with them. + * @param transformOpts transform options configured for the current output target transpilation + * @param tsSourceFile the TypeScript source file that is being updated + * @param moduleFile component file to update + * @returns update source file with added import statements + */ const updateCjsStyleRequires = ( transformOpts: d.TransformOptions, tsSourceFile: ts.SourceFile, @@ -113,7 +233,7 @@ const updateCjsStyleRequires = ( cmp.styles.forEach((style) => { if (typeof style.styleIdentifier === 'string' && style.externalStyles.length > 0) { // add style imports built from @Component() styleUrl option - styleRequires.push(createCjsStyleRequire(transformOpts, tsSourceFile, cmp, style)); + styleRequires.push(...createStyleImport(transformOpts, tsSourceFile, cmp, style)); } }); }); @@ -125,35 +245,6 @@ const updateCjsStyleRequires = ( return tsSourceFile; }; -const createCjsStyleRequire = ( - transformOpts: d.TransformOptions, - tsSourceFile: ts.SourceFile, - cmp: d.ComponentCompilerMeta, - style: d.StyleCompiler, -) => { - const importName = ts.factory.createIdentifier(style.styleIdentifier); - const importPath = getStyleImportPath(transformOpts, tsSourceFile, cmp, style, style.externalStyles[0].absolutePath); - - return ts.factory.createVariableStatement( - undefined, - ts.factory.createVariableDeclarationList( - [ - ts.factory.createVariableDeclaration( - importName, - undefined, - undefined, - ts.factory.createCallExpression( - ts.factory.createIdentifier('require'), - [], - [ts.factory.createStringLiteral(importPath)], - ), - ), - ], - ts.NodeFlags.Const, - ), - ); -}; - const getStyleImportPath = ( transformOpts: d.TransformOptions, tsSourceFile: ts.SourceFile, diff --git a/src/compiler/transformers/test/lazy-component.spec.ts b/src/compiler/transformers/test/lazy-component.spec.ts index fdce3d70f51a..de5c4b244106 100644 --- a/src/compiler/transformers/test/lazy-component.spec.ts +++ b/src/compiler/transformers/test/lazy-component.spec.ts @@ -108,4 +108,102 @@ describe('lazy-component', () => { }`, ); }); + + describe('styling', () => { + function verifyStylingUsingComponent(inputComponent: string, expectedOutput: string) { + return async () => { + const compilerCtx = mockCompilerCtx(); + const transformOpts: d.TransformOptions = { + coreImportPath: '@stencil/core', + componentExport: 'lazy', + componentMetadata: null, + currentDirectory: '/', + proxy: null, + style: 'static', + styleImportData: null, + }; + + const transformer = lazyComponentTransform(compilerCtx, transformOpts); + const t = transpileModule(inputComponent, null, compilerCtx, [], [transformer]); + expect(await formatCode(t.outputText)).toBe(await formatCode(expectedOutput)); + }; + } + + // eslint-disable-next-line jest/expect-expect + it( + 'using `styleUrl` parameter', + verifyStylingUsingComponent( + ` + @Component({ + tag: 'cmp-a', + styleUrl: 'cmp-a.css' + }) + export class CmpA {} + `, + ` + import { registerInstance as __stencil_registerInstance } from "@stencil/core"; + import CmpAStyle0 from './cmp-a.css'; + export const CmpA = class { + constructor (hostRef) { + __stencil_registerInstance(this, hostRef); + } + }; + CmpA.style = CmpAStyle0; + `, + ), + ); + + // eslint-disable-next-line jest/expect-expect + it( + 'using `styleUrls` parameter as object', + verifyStylingUsingComponent( + ` + @Component({ + tag: 'cmp-a', + styleUrls: { + foo: 'cmp-a.foo.css', + bar: 'cmp-a.bar.css', + } + }) + export class CmpA {} + `, + ` + import { registerInstance as __stencil_registerInstance } from "@stencil/core"; + import CmpABarStyle0 from './cmp-a.bar.css'; + import CmpAFooStyle0 from './cmp-a.foo.css'; + export const CmpA = class { + constructor (hostRef) { + __stencil_registerInstance(this, hostRef); + } + }; + CmpA.style = { bar: CmpABarStyle0, foo: CmpAFooStyle0 }; + `, + ), + ); + + // eslint-disable-next-line jest/expect-expect + it( + 'using `styleUrls` parameter as array', + verifyStylingUsingComponent( + ` + @Component({ + tag: 'cmp-a', + styleUrls: ['cmp-a.foo.css', 'cmp-a.bar.css', 'cmp-a.foo.css'], + }) + export class CmpA {} + `, + ` + import { registerInstance as __stencil_registerInstance } from "@stencil/core"; + import CmpAStyle0 from './cmp-a.bar.css'; + import CmpAStyle1 from './cmp-a.foo.css'; + export const CmpA = class { + constructor (hostRef) { + __stencil_registerInstance(this, hostRef); + } + }; + CmpA.style = CmpAStyle0 + CmpAStyle1; + `, + ), + ); + }); }); diff --git a/src/compiler/transformers/test/parse-styles.spec.ts b/src/compiler/transformers/test/parse-styles.spec.ts index 674dc3f8de49..c75d0f295aea 100644 --- a/src/compiler/transformers/test/parse-styles.spec.ts +++ b/src/compiler/transformers/test/parse-styles.spec.ts @@ -18,11 +18,12 @@ describe('parse styles', () => { const t = transpileModule(` @Component({ tag: 'cmp-a', - styleUrls: ['style.css'] + styleUrls: ['style.css', 'style2.css'] }) export class CmpA {} `); - expect(getStaticGetter(t.outputText, 'styleUrls')).toEqual({ $: ['style.css'] }); + + expect(getStaticGetter(t.outputText, 'styleUrls')).toEqual({ $: ['style.css', 'style2.css'] }); }); it('add static "styles"', () => { diff --git a/src/compiler/transformers/transform-utils.ts b/src/compiler/transformers/transform-utils.ts index 28ef392ff82b..8fc162ddcd5d 100644 --- a/src/compiler/transformers/transform-utils.ts +++ b/src/compiler/transformers/transform-utils.ts @@ -1097,3 +1097,35 @@ export const tsPropDeclNameAsString = (node: ts.PropertyDeclaration, typeChecker return memberName; }; + +/** + * Reverse order and reduce to remove duplicates. This will make sure that duplicate + * styles applied to the same component will be applied in the order they are + * defined in the component, e.g. + * ``` + * @Component({ + * styleUrls: ['cmp-a.css', 'cmp-b.css', 'cmp-a.css'] + * }) + * ``` + * will be applied in the order `cmp-b.css`, `cmp-a.css`. + * + * @param style style meta data + * @returns a list of external styles sorted in order + */ +export function getExternalStyles(style: d.StyleCompiler) { + return ( + style.externalStyles + .map((s) => s.absolutePath) + .reverse() + .reduce((extStyles, styleUrl) => { + if (!extStyles.includes(styleUrl)) { + extStyles.push(styleUrl); + } + return extStyles; + }, []) + /** + * Reverse back to the original order + */ + .reverse() + ); +} diff --git a/test/karma/test-app/components.d.ts b/test/karma/test-app/components.d.ts index 6061c71ae714..9da45007b6de 100644 --- a/test/karma/test-app/components.d.ts +++ b/test/karma/test-app/components.d.ts @@ -213,6 +213,8 @@ export namespace Components { } interface ListenWindow { } + interface MultipleStylesCmp { + } interface NoDelegatesFocus { } interface NodeResolution { @@ -970,6 +972,12 @@ declare global { prototype: HTMLListenWindowElement; new (): HTMLListenWindowElement; }; + interface HTMLMultipleStylesCmpElement extends Components.MultipleStylesCmp, HTMLStencilElement { + } + var HTMLMultipleStylesCmpElement: { + prototype: HTMLMultipleStylesCmpElement; + new (): HTMLMultipleStylesCmpElement; + }; interface HTMLNoDelegatesFocusElement extends Components.NoDelegatesFocus, HTMLStencilElement { } var HTMLNoDelegatesFocusElement: { @@ -1482,6 +1490,7 @@ declare global { "listen-jsx-root": HTMLListenJsxRootElement; "listen-reattach": HTMLListenReattachElement; "listen-window": HTMLListenWindowElement; + "multiple-styles-cmp": HTMLMultipleStylesCmpElement; "no-delegates-focus": HTMLNoDelegatesFocusElement; "node-resolution": HTMLNodeResolutionElement; "non-shadow-host": HTMLNonShadowHostElement; @@ -1766,6 +1775,8 @@ declare namespace LocalJSX { } interface ListenWindow { } + interface MultipleStylesCmp { + } interface NoDelegatesFocus { } interface NodeResolution { @@ -2018,6 +2029,7 @@ declare namespace LocalJSX { "listen-jsx-root": ListenJsxRoot; "listen-reattach": ListenReattach; "listen-window": ListenWindow; + "multiple-styles-cmp": MultipleStylesCmp; "no-delegates-focus": NoDelegatesFocus; "node-resolution": NodeResolution; "non-shadow-host": NonShadowHost; @@ -2175,6 +2187,7 @@ declare module "@stencil/core" { "listen-jsx-root": LocalJSX.ListenJsxRoot & JSXBase.HTMLAttributes; "listen-reattach": LocalJSX.ListenReattach & JSXBase.HTMLAttributes; "listen-window": LocalJSX.ListenWindow & JSXBase.HTMLAttributes; + "multiple-styles-cmp": LocalJSX.MultipleStylesCmp & JSXBase.HTMLAttributes; "no-delegates-focus": LocalJSX.NoDelegatesFocus & JSXBase.HTMLAttributes; "node-resolution": LocalJSX.NodeResolution & JSXBase.HTMLAttributes; "non-shadow-host": LocalJSX.NonShadowHost & JSXBase.HTMLAttributes; diff --git a/test/karma/test-app/style-plugin/bar.scss b/test/karma/test-app/style-plugin/bar.scss new file mode 100644 index 000000000000..e3a6e271445e --- /dev/null +++ b/test/karma/test-app/style-plugin/bar.scss @@ -0,0 +1,13 @@ +:host { + display: block; + font-style: italic; +} + +h1 { + color: blue; +} + +p { + // change + color: blue; +} diff --git a/test/karma/test-app/style-plugin/foo.scss b/test/karma/test-app/style-plugin/foo.scss new file mode 100644 index 000000000000..87c0b921db85 --- /dev/null +++ b/test/karma/test-app/style-plugin/foo.scss @@ -0,0 +1,11 @@ +:host { + display: block; +} + +h1 { + color: red; +} + +p { + color: red; +} diff --git a/test/karma/test-app/style-plugin/index.html b/test/karma/test-app/style-plugin/index.html index 3c16bee713f8..e37977b0642d 100644 --- a/test/karma/test-app/style-plugin/index.html +++ b/test/karma/test-app/style-plugin/index.html @@ -11,3 +11,5 @@

Hurray!

+ + diff --git a/test/karma/test-app/style-plugin/karma.spec.ts b/test/karma/test-app/style-plugin/karma.spec.ts index 94a0e4d7211c..f8fbe0497e3c 100644 --- a/test/karma/test-app/style-plugin/karma.spec.ts +++ b/test/karma/test-app/style-plugin/karma.spec.ts @@ -38,4 +38,18 @@ describe('style-plugin', function () { expect(window.getComputedStyle(cssImportee).color).toBe('rgb(0, 0, 255)'); expect(window.getComputedStyle(hr).height).toBe('0px'); }); + + it('multiple-styles-cmp', async () => { + const cssHost = app.querySelector('multiple-styles-cmp'); + const shadowRoot = cssHost.shadowRoot; + + const h1 = getComputedStyle(shadowRoot.querySelector('h1')); + const div = getComputedStyle(shadowRoot.querySelector('p')); + // color is red because foo.scss is mentioned last and overwrites bar.scss + expect(h1.color).toEqual('rgb(255, 0, 0)'); + expect(div.color).toEqual('rgb(255, 0, 0)'); + // ensure styles defined in bar.scss are applied too + expect(h1.fontStyle).toEqual('italic'); + expect(div.fontStyle).toEqual('italic'); + }); }); diff --git a/test/karma/test-app/style-plugin/multiple-styles.tsx b/test/karma/test-app/style-plugin/multiple-styles.tsx new file mode 100644 index 000000000000..e0fd702ceed3 --- /dev/null +++ b/test/karma/test-app/style-plugin/multiple-styles.tsx @@ -0,0 +1,21 @@ +import { Component, h } from '@stencil/core'; + +@Component({ + tag: 'multiple-styles-cmp', + /** + * styles are intentionally duplicated to ensure that `foo.scss` can overwrite + * `bar.scss` since it is set last in the `styleUrls` array. + */ + styleUrls: ['foo.scss', 'bar.scss', 'foo.scss'], + shadow: true, +}) +export class SassCmp { + render() { + return ( +
+

Hello World

+

What's your name?

+
+ ); + } +}