From d5b39de42a9c7ec8b9f931d3427455850f82b6bd Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Mon, 4 Dec 2023 10:12:08 -0800 Subject: [PATCH] fix(compiler): reapply changes to style import transformer (#5125) This reverts commit cf4a70150c91e9dcc6690396a7e1b3a0a1e92215. fixes: #5016 STENCIL-999 improve tests more e2e tests --- src/compiler/transformers/add-static-style.ts | 63 ++++-- .../component-native/native-static-style.ts | 23 +- .../transformers/stencil-import-path.ts | 9 +- src/compiler/transformers/style-imports.ts | 204 +++++++++++++----- .../transformers/test/lazy-component.spec.ts | 133 ++++++++++++ .../transformers/test/parse-styles.spec.ts | 5 +- .../transformers/test/transform-utils.spec.ts | 18 ++ src/compiler/transformers/transform-utils.ts | 28 +++ test/karma/test-app/components.d.ts | 13 ++ test/karma/test-app/style-plugin/bar.scss | 3 + test/karma/test-app/style-plugin/foo.scss | 3 + test/karma/test-app/style-plugin/index.html | 2 + .../karma/test-app/style-plugin/karma.spec.ts | 6 + .../test-app/style-plugin/multiple-styles.tsx | 17 ++ 14 files changed, 442 insertions(+), 85 deletions(-) create mode 100644 test/karma/test-app/style-plugin/bar.scss create mode 100644 test/karma/test-app/style-plugin/foo.scss create mode 100644 test/karma/test-app/style-plugin/multiple-styles.tsx diff --git a/src/compiler/transformers/add-static-style.ts b/src/compiler/transformers/add-static-style.ts index b7a08229472d..40a7d3350d9b 100644 --- a/src/compiler/transformers/add-static-style.ts +++ b/src/compiler/transformers/add-static-style.ts @@ -1,10 +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 { createStaticGetter } from './transform-utils'; +import { createStaticGetter, getIdentifierFromResourceUrl } from './transform-utils'; /** * Adds static "style" getter within the class @@ -91,7 +91,8 @@ 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 externalStyles = Array.from(new Set(style.externalStyles.map((s) => s.absolutePath))); + const styleUrlIdentifier = createStyleIdentifierFromUrl(style.styleId, externalStyles); const propUrlIdentifier = ts.factory.createPropertyAssignment(style.modeName, styleUrlIdentifier); styleModes.push(propUrlIdentifier); } @@ -118,7 +119,8 @@ const getSingleStyle = (cmp: d.ComponentCompilerMeta, style: d.StyleCompiler, co // import generated from @Component() styleUrls option // import myTagStyle from './import-path.css'; // static get style() { return myTagStyle; } - return createStyleIdentifierFromUrl(cmp, style); + const externalStyles = Array.from(new Set(style.externalStyles.map((s) => s.absolutePath))); + return createStyleIdentifierFromUrl(style.styleId, externalStyles); } return null; @@ -134,16 +136,49 @@ 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); +/** + * 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 CMP_my_component_css from './my-component.css'; + * import CMP_my_component_ios_css from './my-component.ios.css'; + * export class MyComponent { + * // ... + * } + * MyComponent.style = CMP_my_component_css + CMP_my_component_ios_css; + * ``` + * + * Note: style imports are made in [`createEsmStyleImport`](src/compiler/transformers/style-imports.ts). + * + * @param styleId a unique identifier for the component style + * @param externalStyles a list of external styles to be applied the component + * @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 createStyleIdentifierFromUrl = ( + styleId: string, + externalStyles: string[], +): ts.Identifier | ts.BinaryExpression => { + if (externalStyles.length === 1) { + return ts.factory.createIdentifier(getIdentifierFromResourceUrl(styleId + externalStyles[0])); } - style.styleIdentifier += 'Style'; - style.externalStyles = [style.externalStyles[0]]; - - return ts.factory.createIdentifier(style.styleIdentifier); + const firstExternalStyle = externalStyles[0]; + return ts.factory.createBinaryExpression( + createStyleIdentifierFromUrl(styleId, [firstExternalStyle]), + ts.SyntaxKind.PlusToken, + createStyleIdentifierFromUrl(styleId, externalStyles.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..53366d462d96 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 { createStyleIdentifierFromUrl } from '../add-static-style'; import { createStaticGetter } from '../transform-utils'; export const addNativeStaticStyle = (classMembers: ts.ClassElement[], cmp: d.ComponentCompilerMeta) => { @@ -43,7 +44,8 @@ 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 externalStyles = Array.from(new Set(style.externalStyles.map((s) => s.absolutePath))); + const styleUrlIdentifier = createStyleIdentifierFromUrl(style.styleId, externalStyles); const propUrlIdentifier = ts.factory.createPropertyAssignment(style.modeName, styleUrlIdentifier); styleModes.push(propUrlIdentifier); } @@ -74,7 +76,8 @@ 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 externalStyles = Array.from(new Set(style.externalStyles.map((s) => s.absolutePath))); + const styleUrlIdentifier = createStyleIdentifierFromUrl(style.styleId, externalStyles); classMembers.push(createStaticGetter('style', styleUrlIdentifier)); } }; @@ -88,17 +91,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/stencil-import-path.ts b/src/compiler/transformers/stencil-import-path.ts index 65c28e416293..b02166ecae9a 100644 --- a/src/compiler/transformers/stencil-import-path.ts +++ b/src/compiler/transformers/stencil-import-path.ts @@ -15,9 +15,14 @@ import type { ImportData, ParsedImport, SerializeImportData } from '../../declar * @param data import data to be serialized * @param styleImportData an argument which controls whether the import data * will be added to the path (formatted as query params) + * @param moduleSystem the module system we compile to * @returns a formatted string */ -export const serializeImportPath = (data: SerializeImportData, styleImportData: string | undefined | null): string => { +export const serializeImportPath = ( + data: SerializeImportData, + styleImportData: string | undefined | null, + moduleSystem?: 'esm' | 'cjs', +): string => { let p = data.importeePath; if (isString(p)) { @@ -28,7 +33,7 @@ export const serializeImportPath = (data: SerializeImportData, styleImportData: p = './' + p; } - if (styleImportData === 'queryparams' || styleImportData === undefined) { + if (moduleSystem !== 'cjs' && (styleImportData === 'queryparams' || styleImportData === undefined)) { const paramData: ImportData = {}; if (isString(data.tag)) { paramData.tag = data.tag; diff --git a/src/compiler/transformers/style-imports.ts b/src/compiler/transformers/style-imports.ts index e5556444bdc7..248a9e21e46d 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 { getIdentifierFromResourceUrl, 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 _myComponentCssStyle from './my-component.css'; + * import _myComponentIosCssStyle 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, @@ -28,15 +65,12 @@ const updateEsmStyleImports = ( moduleFile.cmps.forEach((cmp) => { cmp.styles.forEach((style) => { + updateSourceFile = true; if (typeof style.styleIdentifier === 'string') { - updateSourceFile = true; - if (style.externalStyles.length > 0) { - // add style imports built from @Component() styleUrl option - styleImports.push(createEsmStyleImport(transformOpts, tsSourceFile, cmp, style)); - } else { - // update existing esm import of a style identifier - statements = updateEsmStyleImportPath(transformOpts, tsSourceFile, statements, cmp, style); - } + statements = updateEsmStyleImportPath(transformOpts, tsSourceFile, statements, cmp, style); + } else if (style.externalStyles.length > 0) { + // add style imports built from @Component() styleUrl option + styleImports.push(...createStyleImport(transformOpts, tsSourceFile, cmp, style, transformOpts.module as 'esm')); } }); }); @@ -86,22 +120,121 @@ 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[] = []; + const importedStyleIdentifiers: string[] = []; + + for (const externalStyle of style.externalStyles) { + /** + * 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 CMP_A_md__foo_bar_css from './foo/bar.css'; + * import CMP_A_ios__bar_foo_css 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 = getIdentifierFromResourceUrl(style.styleId + externalStyle.absolutePath); + + /** + * avoid to have duplicate style imports, e.g. if we have the following component: + * ```ts + * @Component({ + * styleUrls: ['./foo/bar.css', './foo/bar.css'], + * tag: 'cmp-a' + * }) + * ``` + */ + if (importedStyleIdentifiers.includes(styleIdentifier)) { + continue; + } + + importedStyleIdentifiers.push(styleIdentifier); + const importIdentifier = ts.factory.createIdentifier(styleIdentifier); + const importPath = getStyleImportPath(transformOpts, tsSourceFile, cmp, style, externalStyle.absolutePath); + + 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, @@ -111,9 +244,9 @@ const updateCjsStyleRequires = ( moduleFile.cmps.forEach((cmp) => { cmp.styles.forEach((style) => { - if (typeof style.styleIdentifier === 'string' && style.externalStyles.length > 0) { + if (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, transformOpts.module)); } }); }); @@ -125,35 +258,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, @@ -168,5 +272,5 @@ const getStyleImportPath = ( encapsulation: cmp.encapsulation, mode: style.modeName, }; - return serializeImportPath(importData, transformOpts.styleImportData); + return serializeImportPath(importData, transformOpts.styleImportData, transformOpts.module); }; diff --git a/src/compiler/transformers/test/lazy-component.spec.ts b/src/compiler/transformers/test/lazy-component.spec.ts index fdce3d70f51a..dad4095e0619 100644 --- a/src/compiler/transformers/test/lazy-component.spec.ts +++ b/src/compiler/transformers/test/lazy-component.spec.ts @@ -108,4 +108,137 @@ describe('lazy-component', () => { }`, ); }); + + describe('styling', () => { + function verifyStylingUsingComponent (code: string) { + return async (moduleType: string | undefined, expectedOutput: string) => { + const compilerCtx = mockCompilerCtx(); + const transformOpts: d.TransformOptions = { + coreImportPath: '@stencil/core', + componentExport: 'lazy', + componentMetadata: null, + currentDirectory: '/', + proxy: null, + style: 'static', + styleImportData: null, + }; + if (moduleType) { + transformOpts.module = moduleType as 'esm' | 'cjs'; + } + + const transformer = lazyComponentTransform(compilerCtx, transformOpts); + const t = transpileModule(code, null, compilerCtx, [], [transformer]); + expect(await formatCode(t.outputText)).toBe( + await formatCode(expectedOutput), + ); + } + } + + // eslint-disable-next-line jest/expect-expect + it.each([ + [ + 'esm', + `import { registerInstance as __stencil_registerInstance } from "@stencil/core"; + import CMP_Acmp_a_css from './cmp-a.css'; + export const CmpA = class { + constructor (hostRef) { + __stencil_registerInstance(this, hostRef); + } + }; + CmpA.style = CMP_Acmp_a_css;` + ], + [ + 'cjs', + `const CMP_Acmp_a_css = require('./cmp-a.css'); + export class CmpA { + constructor (hostRef) { + __stencil_registerInstance(this, hostRef); + } + }; + const { registerInstance: __stencil_registerInstance } = require('@stencil/core'); + CmpA.style = CMP_Acmp_a_css;` + ], + ])('using `styleUrl` parameter run in %s', verifyStylingUsingComponent(` + @Component({ + tag: 'cmp-a', + styleUrl: 'cmp-a.css' + }) + export class CmpA {} + `)) + + // eslint-disable-next-line jest/expect-expect + it.each([ + [ + 'esm', + ` + import { registerInstance as __stencil_registerInstance } from "@stencil/core"; + import CMP_A_barcmp_a_bar_css from './cmp-a.bar.css'; + import CMP_A_foocmp_a_foo_css from './cmp-a.foo.css'; + export const CmpA = class { + constructor (hostRef) { + __stencil_registerInstance(this, hostRef); + } + }; + CmpA.style = { bar: CMP_A_barcmp_a_bar_css, foo: CMP_A_foocmp_a_foo_css };` + ], + [ + 'cjs', + ` + const CMP_A_barcmp_a_bar_css = require('./cmp-a.bar.css'); + const CMP_A_foocmp_a_foo_css = require('./cmp-a.foo.css'); + const { registerInstance: __stencil_registerInstance } = require('@stencil/core'); + export class CmpA { + constructor (hostRef) { + __stencil_registerInstance(this, hostRef); + } + }; + CmpA.style = { bar: CMP_A_barcmp_a_bar_css, foo: CMP_A_foocmp_a_foo_css };` + ], + ])('using `styleUrls` parameter as object run in %s', verifyStylingUsingComponent(` + @Component({ + tag: 'cmp-a', + styleUrls: { + foo: 'cmp-a.foo.css', + bar: 'cmp-a.bar.css', + } + }) + export class CmpA {} + `)) + + // eslint-disable-next-line jest/expect-expect + it.each([ + [ + 'esm', + ` + import { registerInstance as __stencil_registerInstance } from "@stencil/core"; + import CMP_Acmp_a_foo_css from './cmp-a.foo.css'; + import CMP_Acmp_a_bar_css from './cmp-a.bar.css'; + export const CmpA = class { + constructor (hostRef) { + __stencil_registerInstance(this, hostRef); + } + }; + CmpA.style = CMP_Acmp_a_foo_css + CMP_Acmp_a_bar_css;` + ], + [ + 'cjs', + ` + const CMP_Acmp_a_foo_css = require('./cmp-a.foo.css'); + const CMP_Acmp_a_bar_css = require('./cmp-a.bar.css'); + const { registerInstance: __stencil_registerInstance } = require('@stencil/core'); + export class CmpA { + constructor (hostRef) { + __stencil_registerInstance(this, hostRef); + } + }; + CmpA.style = CMP_Acmp_a_foo_css + CMP_Acmp_a_bar_css;` + ], + ])('using `styleUrls` parameter as array run in %s', verifyStylingUsingComponent(` + @Component({ + tag: 'cmp-a', + styleUrls: ['cmp-a.foo.css', 'cmp-a.bar.css', 'cmp-a.foo.css'], + }) + export class CmpA {} + `)) + }); }); 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/test/transform-utils.spec.ts b/src/compiler/transformers/test/transform-utils.spec.ts index 5350f3ea4b3b..d0d9077454bb 100644 --- a/src/compiler/transformers/test/transform-utils.spec.ts +++ b/src/compiler/transformers/test/transform-utils.spec.ts @@ -1,6 +1,7 @@ import * as ts from 'typescript'; import { + getIdentifierFromResourceUrl, isMemberPrivate, mapJSDocTagInfo, retrieveModifierLike, @@ -9,6 +10,23 @@ import { } from '../transform-utils'; describe('transform-utils', () => { + it('getIdentifierFromResourceUrl', () => { + const testData = [ + ['/foo/bar.css', '_foo_bar_css'], + ['/foo/Bar.css', '_foo_Bar_css'], + ['/my-other-styles.css', '_my_other_styles_css'], + ['/my--other-styles.css', '_my__other_styles_css'], + ['C:\\foo\\bar.css?tag=my-component&encapsulation=shadow', 'C__foo_bar_css'], + [ + '/project/node_modules/@scope/foo/b_$%^&*(*())!@#a_r.css', + '_project_node_modules__scope_foo_b______________a_r_css', + ], + ]; + for (const [input, output] of testData) { + expect(getIdentifierFromResourceUrl(input)).toBe(output); + } + }); + it('flattens TypeScript JSDocTagInfo to Stencil JSDocTagInfo', () => { // tags corresponds to the following JSDoc /* diff --git a/src/compiler/transformers/transform-utils.ts b/src/compiler/transformers/transform-utils.ts index 28ef392ff82b..0262c1a164f5 100644 --- a/src/compiler/transformers/transform-utils.ts +++ b/src/compiler/transformers/transform-utils.ts @@ -1097,3 +1097,31 @@ export const tsPropDeclNameAsString = (node: ts.PropertyDeclaration, typeChecker return memberName; }; + +const SPECIAL_CHARS = /[\s~`!@#$%\^&*+=\-\[\]\\';,/{}|\\":<>\?()\._]/g; +/** + * transform any path to a valid identifier, e.g. + * - `/foo/bar/loo.css` -> `_foo_bar_loo_css` + * - `C:\\foo\bar\loo.css` -> `_C__foo_bar_loo_css` + * + * @param absolutePath windows or linux based path + * @returns a valid identifier to be used as variable name + */ +export const getIdentifierFromResourceUrl = (absolutePath: string): string => { + return ( + absolutePath + /** + * remove query params + */ + .split('?') + .shift() + /** + * replace special characters with `-` + */ + .replace(SPECIAL_CHARS, '-') + /** + * replace all `-` with `_` + */ + .replace(/-/g, '_') + ); +}; 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..62d2b12694c9 --- /dev/null +++ b/test/karma/test-app/style-plugin/bar.scss @@ -0,0 +1,3 @@ +.bar { + display: none; +} 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..be60bc327b98 --- /dev/null +++ b/test/karma/test-app/style-plugin/foo.scss @@ -0,0 +1,3 @@ +.foo { + display: none; +} 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..c0939d6cf6e2 100644 --- a/test/karma/test-app/style-plugin/karma.spec.ts +++ b/test/karma/test-app/style-plugin/karma.spec.ts @@ -38,4 +38,10 @@ 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 multipleStylesHost = app.querySelector('multiple-styles-cmp'); + const { width, height } = multipleStylesHost.getBoundingClientRect(); + expect({ width, height }).toEqual({ width: 0, height: 0 }) + }); }); 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..6a9f6e8fbb9c --- /dev/null +++ b/test/karma/test-app/style-plugin/multiple-styles.tsx @@ -0,0 +1,17 @@ +import { Component, h, Host } from '@stencil/core'; + +@Component({ + tag: 'multiple-styles-cmp', + styleUrls: ['foo.scss', 'bar.scss'], + shadow: true, +}) +export class SassCmp { + render() { + return ( + +
Foo
+
bar
+
+ ); + } +}