Repository: teleporthq/teleport-code-generators
Pull request #336 (2019-08-07 09:35:14)

refactor(css-modules): classname generation optimization and tests for multiple cases
Solves the problem of generating classes like: `className: {styles['container']}`

codecov[bot]: # [Codecov](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/336?src=pr&el=h1) Report
> Merging [#336](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/336?src=pr&el=desc) into [master](https://codecov.io/gh/teleporthq/teleport-code-generators/commit/bc6ce26a287a7d7e0a054bbc387a0c651db6b622?src=pr&el=desc) will **increase** coverage by `0.18%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/336/graphs/tree.svg?width=650&token=LI2WPEFm1R&height=150&src=pr)](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/336?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##           master    #336      +/-   ##
=========================================
+ Coverage   75.42%   75.6%   +0.18%     
=========================================
  Files         101     101              
  Lines        3365    3366       +1     
  Branches      711     712       +1     
=========================================
+ Hits         2538    2545       +7     
+ Misses        800     795       -5     
+ Partials       27      26       -1
```


| [Impacted Files](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/336?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [...ges/teleport-plugin-react-css-modules/src/index.ts](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/336/diff?src=pr&el=tree#diff-cGFja2FnZXMvdGVsZXBvcnQtcGx1Z2luLXJlYWN0LWNzcy1tb2R1bGVzL3NyYy9pbmRleC50cw==) | `98.07% <100%> (+11.8%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/336?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/336?src=pr&el=footer). Last update [bc6ce26...27d6277](https://codecov.io/gh/teleporthq/teleport-code-generators/pull/336?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/packages/teleport-plugin-react-css-modules/src/index.ts b/packages/teleport-plugin-react-css-modules/src/index.ts
index 81239fb6..09c442ca 100644
--- a/packages/teleport-plugin-react-css-modules/src/index.ts
+++ b/packages/teleport-plugin-react-css-modules/src/index.ts
@@ -35,7 +35,7 @@ const defaultConfigProps = {
   styleChunkName: 'css-modules',
   styleObjectImportName: 'styles',
   camelCaseClassNames: false,
-  moduleExtension: 'false',
+  moduleExtension: false,
   classAttributeName: 'className',
 }
 
@@ -76,13 +76,16 @@ export const createPlugin: ComponentPluginFactory<CSSModulesConfig> = (config =
 
         if (Object.keys(staticStyles).length > 0) {
           const className = camelCaseToDashCase(key)
-          const classNameInJS = dashCaseToCamelCase(className)
+          const jsFriendlyClassName = dashCaseToCamelCase(className)
 
           cssClasses.push(createCSSClass(className, getContentOfStyleObject(staticStyles)))
 
-          const classReferenceIdentifier = camelCaseClassNames
-            ? `styles.${classNameInJS}`
-            : `styles['${className}']`
+          // When the className is equal to the jsFriendlyClassName, it can be safely addressed with `styles.<className>`
+          const classNameIsJSFriendly = className === jsFriendlyClassName
+          const classReferenceIdentifier =
+            camelCaseClassNames || classNameIsJSFriendly
+              ? `styles.${jsFriendlyClassName}`
+              : `styles['${className}']`
 
           addDynamicAttributeToJSXTag(root, classAttributeName, classReferenceIdentifier)
         }


Test patch:
diff --git a/packages/teleport-plugin-react-css-modules/__tests__/index.ts b/packages/teleport-plugin-react-css-modules/__tests__/index.ts
index 08eccc09..63d60bc6 100644
--- a/packages/teleport-plugin-react-css-modules/__tests__/index.ts
+++ b/packages/teleport-plugin-react-css-modules/__tests__/index.ts
@@ -1,43 +1,23 @@
+import * as types from '@babel/types'
 import {
   component,
   elementNode,
+  dynamicNode,
   staticNode,
 } from '@teleporthq/teleport-shared/dist/cjs/builders/uidl-builders'
-import { ComponentStructure, ChunkDefinition } from '@teleporthq/teleport-types'
+import { ComponentStructure } from '@teleporthq/teleport-types'
 import { createPlugin } from '../src/index'
-import { CHUNK_TYPE, FILE_TYPE } from '@teleporthq/teleport-shared/dist/cjs/constants'
+import { createComponentChunk, setupPluginStructure } from './mocks'
+import { FILE_TYPE } from '@teleporthq/teleport-shared/dist/cjs/constants'
 
 describe('plugin-react-css-modules', () => {
-  const plugin = createPlugin()
-  const componentChunk: ChunkDefinition = {
-    name: 'jsx-component',
-    meta: {
-      nodesLookup: {
-        container: {
-          openingElement: {
-            name: {
-              name: '',
-            },
-            attributes: [],
-          },
-        },
-      },
-      dynamicRefPrefix: {
-        prop: 'props.',
-      },
-    },
-    type: CHUNK_TYPE.AST,
-    fileType: FILE_TYPE.JS,
-    linkAfter: ['import-local'],
-    content: {},
-  }
-
   it('generates no chunk if no styles exist', async () => {
+    const plugin = createPlugin()
     const uidlSample = component('CSSModules', elementNode('container'))
     const structure: ComponentStructure = {
       uidl: uidlSample,
       options: {},
-      chunks: [componentChunk],
+      chunks: [createComponentChunk()],
       dependencies: {},
     }
 
@@ -47,31 +27,138 @@ describe('plugin-react-css-modules', () => {
   })
 
   it('generates a string chunk out of the styles and adds the className', async () => {
-    const style = {
-      height: staticNode('100px'),
-    }
-    const element = elementNode('container', {}, [], { type: 'package' }, style)
-    element.content.key = 'container'
-    const uidlSample = component('CSSModules', element)
+    const plugin = createPlugin()
+    const structure = setupPluginStructure()
+    const { chunks } = await plugin(structure)
 
-    const structure: ComponentStructure = {
-      uidl: uidlSample,
-      options: {},
-      chunks: [componentChunk],
-      dependencies: {},
-    }
+    expect(chunks.length).toBe(2)
+    expect(chunks[1].type).toBe('string')
+    expect(chunks[1].fileType).toBe(FILE_TYPE.CSS)
+    expect(chunks[1].content).toContain('height: 100px;')
 
+    const nodeReference = chunks[0].meta.nodesLookup.container
+    expect(nodeReference.openingElement.attributes.length).toBe(1)
+
+    const classNameAttr = nodeReference.openingElement.attributes[0]
+    expect(classNameAttr.name.name).toBe('className')
+    expect(classNameAttr.value.expression.name).toBe('styles.container')
+  })
+
+  it('generates a string chunk out of the styles and adds the className between brackets', async () => {
+    const plugin = createPlugin()
+    const structure = setupPluginStructure('list-container')
     const { chunks } = await plugin(structure)
 
     expect(chunks.length).toBe(2)
     expect(chunks[1].type).toBe('string')
+    expect(chunks[1].fileType).toBe(FILE_TYPE.CSS)
     expect(chunks[1].content).toContain('height: 100px;')
 
-    const nodeReference = componentChunk.meta.nodesLookup.container
+    const nodeReference = chunks[0].meta.nodesLookup['list-container']
     expect(nodeReference.openingElement.attributes.length).toBe(1)
 
     const classNameAttr = nodeReference.openingElement.attributes[0]
     expect(classNameAttr.name.name).toBe('className')
-    expect(classNameAttr.value.expression.name).toBe("styles['container']")
+    expect(classNameAttr.value.expression.name).toBe("styles['list-container']")
+  })
+
+  it('generates a string chunk out of the styles and adds the className in camel case', async () => {
+    const plugin = createPlugin({ camelCaseClassNames: true })
+    const structure = setupPluginStructure('list-container')
+    const { chunks } = await plugin(structure)
+
+    expect(chunks.length).toBe(2)
+    expect(chunks[1].type).toBe('string')
+    expect(chunks[1].fileType).toBe(FILE_TYPE.CSS)
+    expect(chunks[1].content).toContain('height: 100px;')
+
+    const nodeReference = chunks[0].meta.nodesLookup['list-container']
+    expect(nodeReference.openingElement.attributes.length).toBe(1)
+
+    const classNameAttr = nodeReference.openingElement.attributes[0]
+    expect(classNameAttr.name.name).toBe('className')
+    expect(classNameAttr.value.expression.name).toBe('styles.listContainer')
+  })
+
+  it('generates a string chunk out of the styles and adds the class attribute', async () => {
+    const plugin = createPlugin({ classAttributeName: 'class' })
+    const structure = setupPluginStructure('list-container')
+    const { chunks } = await plugin(structure)
+
+    expect(chunks.length).toBe(2)
+    expect(chunks[1].type).toBe('string')
+    expect(chunks[1].fileType).toBe(FILE_TYPE.CSS)
+    expect(chunks[1].content).toContain('height: 100px;')
+
+    const nodeReference = chunks[0].meta.nodesLookup['list-container']
+    expect(nodeReference.openingElement.attributes.length).toBe(1)
+
+    const classNameAttr = nodeReference.openingElement.attributes[0]
+    expect(classNameAttr.name.name).toBe('class')
+    expect(classNameAttr.value.expression.name).toBe("styles['list-container']")
+  })
+
+  it('generates a string chunk of type CSSMODULE', async () => {
+    const plugin = createPlugin({ moduleExtension: true })
+    const structure = setupPluginStructure('list-container')
+    const { chunks } = await plugin(structure)
+
+    expect(chunks.length).toBe(2)
+    expect(chunks[1].type).toBe('string')
+    expect(chunks[1].fileType).toBe(FILE_TYPE.CSSMODULE)
+    expect(chunks[1].content).toContain('height: 100px;')
+  })
+
+  it('inlines dynamic style and does not generate a new chunk if no static styles are present', async () => {
+    const plugin = createPlugin()
+    const structure = setupPluginStructure('container', {
+      height: dynamicNode('prop', 'height'),
+    })
+
+    const { chunks } = await plugin(structure)
+    expect(chunks.length).toBe(1)
+
+    const nodeReference = chunks[0].meta.nodesLookup.container
+    expect(nodeReference.openingElement.attributes.length).toBe(1)
+
+    const styleAttr = nodeReference.openingElement.attributes[0]
+    expect(styleAttr.name.name).toBe('style')
+
+    const dynamicStyleObject = styleAttr.value.expression as types.ObjectExpression
+    const heightProperty = dynamicStyleObject.properties[0] as types.ObjectProperty
+    expect(heightProperty.key.value).toBe('height')
+    expect((heightProperty.value as types.MemberExpression).object.name).toBe('props.')
+    expect((heightProperty.value as types.MemberExpression).property.name).toBe('height')
+  })
+
+  it('inlines dynamic style and generates a new chunk with the static styles', async () => {
+    const plugin = createPlugin()
+    const structure = setupPluginStructure('container', {
+      height: dynamicNode('prop', 'height'),
+      width: staticNode('auto'),
+    })
+
+    const { chunks } = await plugin(structure)
+    expect(chunks.length).toBe(2)
+
+    expect(chunks[1].type).toBe('string')
+    expect(chunks[1].fileType).toBe(FILE_TYPE.CSS)
+    expect(chunks[1].content).toContain('width: auto;')
+
+    const nodeReference = chunks[0].meta.nodesLookup.container
+    expect(nodeReference.openingElement.attributes.length).toBe(2)
+
+    const classNameAttr = nodeReference.openingElement.attributes[0]
+    expect(classNameAttr.name.name).toBe('className')
+    expect(classNameAttr.value.expression.name).toBe('styles.container')
+
+    const styleAttr = nodeReference.openingElement.attributes[1]
+    expect(styleAttr.name.name).toBe('style')
+
+    const dynamicStyleObject = styleAttr.value.expression as types.ObjectExpression
+    const heightProperty = dynamicStyleObject.properties[0] as types.ObjectProperty
+    expect(heightProperty.key.value).toBe('height')
+    expect((heightProperty.value as types.MemberExpression).object.name).toBe('props.')
+    expect((heightProperty.value as types.MemberExpression).property.name).toBe('height')
   })
 })
diff --git a/packages/teleport-plugin-react-css-modules/__tests__/mocks.ts b/packages/teleport-plugin-react-css-modules/__tests__/mocks.ts
new file mode 100644
index 00000000..52392b8c
--- /dev/null
+++ b/packages/teleport-plugin-react-css-modules/__tests__/mocks.ts
@@ -0,0 +1,59 @@
+import {
+  ChunkDefinition,
+  ComponentStructure,
+  UIDLStyleDefinitions,
+} from '@teleporthq/teleport-types'
+import { CHUNK_TYPE, FILE_TYPE } from '@teleporthq/teleport-shared/dist/cjs/constants'
+import {
+  component,
+  elementNode,
+  staticNode,
+} from '@teleporthq/teleport-shared/dist/cjs/builders/uidl-builders'
+
+export const createComponentChunk = (elementKey: string = 'container') => {
+  const componentChunk: ChunkDefinition = {
+    name: 'jsx-component',
+    meta: {
+      nodesLookup: {
+        [elementKey]: {
+          openingElement: {
+            name: {
+              name: '',
+            },
+            attributes: [],
+          },
+        },
+      },
+      dynamicRefPrefix: {
+        prop: 'props.',
+      },
+    },
+    type: CHUNK_TYPE.AST,
+    fileType: FILE_TYPE.JS,
+    linkAfter: ['import-local'],
+    content: {},
+  }
+
+  return componentChunk
+}
+
+export const setupPluginStructure = (
+  elementKey: string = 'container',
+  styleDefinition: UIDLStyleDefinitions = null
+) => {
+  const style = styleDefinition || {
+    height: staticNode('100px'),
+  }
+  const element = elementNode('container', {}, [], null, style)
+  element.content.key = elementKey
+  const uidlSample = component('CSSModules', element)
+
+  const structure: ComponentStructure = {
+    uidl: uidlSample,
+    options: {},
+    chunks: [createComponentChunk(elementKey)],
+    dependencies: {},
+  }
+
+  return structure
+}