diff --git a/src/compiler/transformers/add-static-style.ts b/src/compiler/transformers/add-static-style.ts index b7a08229472..eb4d341dcac 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,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 = createStyleIdentifierFromUrl(style.externalStyles); const propUrlIdentifier = ts.factory.createPropertyAssignment(style.modeName, styleUrlIdentifier); styleModes.push(propUrlIdentifier); } @@ -118,7 +118,7 @@ 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); + return createStyleIdentifierFromUrl(style.externalStyles); } return null; @@ -134,16 +134,46 @@ 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'] + * }) + * export class MyComponent { + * // ... + * } + * ``` + * + * it would generate the following expression: + * + * ```ts + * import _myComponentCssStyle from './my-component.css'; + * import _myComponentIosCssStyle from './my-component.ios.css'; + * export class MyComponent { + * // ... + * } + * MyComponent.style = _myComponentCssStyle + _myComponentIosCssStyle; + * ``` + * + * Note: style imports are made in [`createEsmStyleImport`](src/compiler/transformers/style-imports.ts). + * + * @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 = ( + externalStyles: d.ExternalStyleCompiler[], +): ts.Identifier | ts.BinaryExpression => { + if (externalStyles.length === 1) { + return ts.factory.createIdentifier(getIdentifierFromResourceUrl(externalStyles[0].absolutePath)); } - style.styleIdentifier += 'Style'; - style.externalStyles = [style.externalStyles[0]]; - - return ts.factory.createIdentifier(style.styleIdentifier); + const firstExternalStyle = externalStyles[0]; + return ts.factory.createBinaryExpression( + createStyleIdentifierFromUrl([firstExternalStyle]), + ts.SyntaxKind.PlusToken, + createStyleIdentifierFromUrl(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 b6d787630f0..7dd65fc36a3 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,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 = createStyleIdentifierFromUrl(style.externalStyles); 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 = createStyleIdentifierFromUrl(style.externalStyles); 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/stencil-import-path.ts b/src/compiler/transformers/stencil-import-path.ts index 65c28e41629..b02166ecae9 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 e5556444bdc..2c5c37f336c 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(...createEsmStyleImport(transformOpts, tsSourceFile, cmp, style)); } }); }); @@ -92,16 +126,38 @@ const createEsmStyleImport = ( 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.createImportDeclaration( - undefined, - ts.factory.createImportClause(false, importName, undefined), - ts.factory.createStringLiteral(importPath), - ); + const imports: ts.ImportDeclaration[] = []; + for (const externalStyle of style.externalStyles) { + /** + * Add import statement for each style + * e.g. `const _ImportPathStyle = require('./import-path.css');` + * + * Attention: if you make changes to the import identifier (e.g. `_ImportPathStyle`), + * you also need to update the identifier in [`createStyleIdentifierFromUrl`](`src/compiler/transformers/add-static-style.ts`). + */ + const importIdentifier = ts.factory.createIdentifier(getIdentifierFromResourceUrl(externalStyle.absolutePath)); + const importPath = getStyleImportPath(transformOpts, tsSourceFile, cmp, style, externalStyle.absolutePath); + + imports.push( + ts.factory.createImportDeclaration( + undefined, + ts.factory.createImportClause(false, importIdentifier, undefined), + ts.factory.createStringLiteral(importPath), + ), + ); + } + + 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 +167,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(...createCjsStyleRequire(transformOpts, tsSourceFile, cmp, style)); } }); }); @@ -131,27 +187,41 @@ const createCjsStyleRequire = ( 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)], - ), + const imports: ts.VariableStatement[] = []; + for (const externalStyle of style.externalStyles) { + /** + * Add import statement for each style + * e.g. `import _ImportPathStyle from './import-path.css';` + * + * Attention: if you make changes to the import identifier (e.g. `_ImportPathStyle`), + * you also need to update the identifier in [`createStyleIdentifierFromUrl`](`src/compiler/transformers/add-static-style.ts`). + */ + const importIdentifier = ts.factory.createIdentifier(getIdentifierFromResourceUrl(externalStyle.absolutePath)); + const importPath = getStyleImportPath(transformOpts, tsSourceFile, cmp, style, externalStyle.absolutePath); + + 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, ), - ], - ts.NodeFlags.Const, - ), - ); + ), + ); + } + + return imports; }; const getStyleImportPath = ( @@ -168,5 +238,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 fdce3d70f51..75485b9f58f 100644 --- a/src/compiler/transformers/test/lazy-component.spec.ts +++ b/src/compiler/transformers/test/lazy-component.spec.ts @@ -108,4 +108,107 @@ describe('lazy-component', () => { }`, ); }); + + it('allows to define multiple styleUrls', async () => { + const compilerCtx = mockCompilerCtx(); + const transformOpts: d.TransformOptions = { + coreImportPath: '@stencil/core', + componentExport: 'lazy', + componentMetadata: null, + currentDirectory: '/', + proxy: null, + style: 'static', + styleImportData: null, + }; + const code = ` + @Component({ + styleUrls: ['./foo/bar.css', './bar/foo.css'], + tag: 'cmp-a' + }) + export class CmpA {} + `; + const transformer = lazyComponentTransform(compilerCtx, transformOpts); + const t = transpileModule(code, null, compilerCtx, [], [transformer]); + expect(await formatCode(t.outputText)).toBe( + await c`import { registerInstance as __stencil_registerInstance } from "@stencil/core"; + import __foo_bar_css from './foo/bar.css'; + import __bar_foo_css from './bar/foo.css'; + export const CmpA = class { + constructor(hostRef) { + __stencil_registerInstance(this, hostRef); + } + } + CmpA.style = __foo_bar_css + __bar_foo_css ;`, + ); + }); + + it('allows to define multiple styleUrls in CJS', async () => { + const compilerCtx = mockCompilerCtx(); + const transformOpts: d.TransformOptions = { + coreImportPath: '@stencil/core', + componentExport: 'lazy', + componentMetadata: null, + currentDirectory: '/', + proxy: null, + module: 'cjs', + style: 'static', + styleImportData: null, + }; + const code = ` + @Component({ + styleUrls: ['./foo/bar.css', './bar/foo.css'], + tag: 'cmp-a' + }) + export class CmpA {} + `; + const transformer = lazyComponentTransform(compilerCtx, transformOpts); + const t = transpileModule(code, null, compilerCtx, [], [transformer]); + expect(await formatCode(t.outputText)).toBe( + await c`const __foo_bar_css = require('./foo/bar.css'); + const __bar_foo_css = require('./bar/foo.css'); + const { registerInstance: __stencil_registerInstance } = require('@stencil/core'); + export class CmpA { + constructor(hostRef) { + __stencil_registerInstance(this, hostRef); + } + }; + CmpA.style = __foo_bar_css + __bar_foo_css ;`, + ); + }); + + it('allows to define multiple platform styles', async () => { + const compilerCtx = mockCompilerCtx(); + const transformOpts: d.TransformOptions = { + coreImportPath: '@stencil/core', + componentExport: 'lazy', + componentMetadata: null, + currentDirectory: '/', + proxy: null, + style: 'static', + styleImportData: null, + }; + const code = ` + @Component({ + styleUrls: { + foo: './foo/bar.css', + bar: './bar/foo.css' + }, + tag: 'cmp-a' + }) + export class CmpA {} + `; + const transformer = lazyComponentTransform(compilerCtx, transformOpts); + const t = transpileModule(code, null, compilerCtx, [], [transformer]); + expect(await formatCode(t.outputText)).toBe( + await c`import { registerInstance as __stencil_registerInstance } from "@stencil/core"; + import __bar_foo_css from './bar/foo.css'; + import __foo_bar_css from './foo/bar.css'; + export const CmpA = class { + constructor(hostRef) { + __stencil_registerInstance(this, hostRef); + } + } + CmpA.style = { bar: __bar_foo_css , foo: __foo_bar_css }`, + ); + }); }); diff --git a/src/compiler/transformers/test/parse-styles.spec.ts b/src/compiler/transformers/test/parse-styles.spec.ts index 674dc3f8de4..c75d0f295ae 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 5350f3ea4b3..d0d9077454b 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 28ef392ff82..0262c1a164f 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, '_') + ); +};