Repository: styled-components/babel-plugin-styled-components (js)

Issue:
Need better way to determine componentId
I was able to get duplicates with `create-react-app` setup because it uses babel-loader with cache enabled.

Considering other options.
- `path.hub.file.code`
- `path.hub.file.sourceFileName` and last part of `path.hub.file.sourceRoot`
- `path.hub.file.filename`

`code` hash is the most reliable, but it will break if we have two files with exactly the same source. For other options we'll have ask user to configure babel to provide `sourceRoot` and `fileName`

Pull request:
Make component id deterministic
fixes #24 

Patch:
diff --git a/src/visitors/displayNameAndId.js b/src/visitors/displayNameAndId.js
index 9aff0fa..ebf3838 100644
--- a/src/visitors/displayNameAndId.js
+++ b/src/visitors/displayNameAndId.js
@@ -2,6 +2,7 @@ import * as t from 'babel-types'
 import { useFileName, useDisplayName, useSSR } from '../utils/options'
 import getName from '../utils/getName'
 import path from 'path'
+import fs from 'fs'
 import hash from '../utils/hash'
 import { isStyled } from '../utils/detectors'
 
@@ -31,7 +32,8 @@ const addConfig = (path, displayName, componentId) => {
   )
 }
 
-const getDisplayName = (path, file) => {
+const getDisplayName = (path, state) => {
+  const { file } = state
   const componentName = getName(path)
   if (file) {
     return componentName ? `${blockName(file)}__${componentName}` : blockName(file)
@@ -40,21 +42,58 @@ const getDisplayName = (path, file) => {
   }
 }
 
-let id = 0
+const findModuleRoot = (filename) => {
+  if (!filename) {
+    return null
+  }
+  let dir = path.dirname(filename)
+  if (fs.existsSync(path.join(dir, 'package.json'))) {
+    return dir
+  } else if (dir !== filename) {
+    return findModuleRoot(dir)
+  } else {
+    return null
+  }
+}
+
+const FILE_HASH = 'styled-components-file-hash'
+const COMPONENT_POSITION = 'styled-components-component-position'
+
+const getFileHash = (state) => {
+  const { file } = state
+  // hash calculation is costly due to fs operations, so we'll cache it per file.
+  if (file.get(FILE_HASH)) {
+    return file.get(FILE_HASH)
+  }
+  const filename = file.opts.filename
+  // find module root directory
+  const moduleRoot = findModuleRoot(filename)
+  const filePath = moduleRoot && path.relative(moduleRoot, filename).replace(path.sep, '/')
+  const moduleName = moduleRoot && JSON.parse(fs.readFileSync(path.join(moduleRoot, 'package.json'))).name
+  const code = file.code
+
+  const fileHash = hash([moduleName, filePath, code].join(''))
+  file.set(FILE_HASH, fileHash)
+  return fileHash
+}
+
+const getNextId = (state) => {
+  const id = state.file.get(COMPONENT_POSITION) || 0
+  state.file.set(COMPONENT_POSITION, id + 1)
+  return id
+}
 
-const getComponentId = (displayName) => {
-  // Prefix the identifier with a character if no displayName exists because CSS classes cannot start with a number
-  return `${displayName || 's'}-${hash(`${id}${displayName}`)}`
+const getComponentId = (state) => {
+  // Prefix the identifier with a character because CSS classes cannot start with a number
+  return `${getFileHash(state).replace(/^(\d)/, 's$1')}-${getNextId(state)}`
 }
 
 export default (path, state) => {
   if (isStyled(path.node.tag, state)) {
-    const displayName = getDisplayName(path, useFileName(state) && state.file)
-    id++
     addConfig(
       path,
-      useDisplayName(state) && displayName,
-      useSSR(state) && getComponentId(displayName)
+      useDisplayName(state) && getDisplayName(path, useFileName(state) && state),
+      useSSR(state) && getComponentId(state)
     )
   }
 }


Test patch:
diff --git a/test/fixtures/02-add-identifier/after.js b/test/fixtures/02-add-identifier/after.js
index b2b7b3d..5096950 100644
--- a/test/fixtures/02-add-identifier/after.js
+++ b/test/fixtures/02-add-identifier/after.js
@@ -1,18 +1,18 @@
 const Test = styled.div.withConfig({
-  componentId: "Test-137hlza"
+  componentId: "f15zqz-0"
 })`width: 100%;`;
 const Test2 = true ? styled.div.withConfig({
-  componentId: "Test2-bjbly"
+  componentId: "f15zqz-1"
 })`` : styled.div.withConfig({
-  componentId: "Test2-3rtkrg"
+  componentId: "f15zqz-2"
 })``;
 const styles = { One: styled.div.withConfig({
-    componentId: "One-1sf086v"
+    componentId: "f15zqz-3"
   })`` };
 let Component;
 Component = styled.div.withConfig({
-  componentId: "Component-1uo0y5n"
+  componentId: "f15zqz-4"
 })``;
 const WrappedComponent = styled(Inner).withConfig({
-  componentId: "WrappedComponent-1hk71jk"
+  componentId: "f15zqz-5"
 })``;
diff --git a/test/fixtures/03-add-identifier-and-display-name/after.js b/test/fixtures/03-add-identifier-and-display-name/after.js
index dfbfa01..b60370a 100644
--- a/test/fixtures/03-add-identifier-and-display-name/after.js
+++ b/test/fixtures/03-add-identifier-and-display-name/after.js
@@ -1,24 +1,24 @@
 const Test = styled.div.withConfig({
   displayName: "Test",
-  componentId: "Test-18z24ew"
+  componentId: "s1j54k93-0"
 })`width: 100%;`;
 const Test2 = true ? styled.div.withConfig({
   displayName: "Test2",
-  componentId: "Test2-1hn2b5r"
+  componentId: "s1j54k93-1"
 })`` : styled.div.withConfig({
   displayName: "Test2",
-  componentId: "Test2-1hl3gfs"
+  componentId: "s1j54k93-2"
 })``;
 const styles = { One: styled.div.withConfig({
     displayName: "One",
-    componentId: "One-1r8uh7y"
+    componentId: "s1j54k93-3"
   })`` };
 let Component;
 Component = styled.div.withConfig({
   displayName: "Component",
-  componentId: "Component-1e47qp"
+  componentId: "s1j54k93-4"
 })``;
 const WrappedComponent = styled(Inner).withConfig({
   displayName: "WrappedComponent",
-  componentId: "WrappedComponent-kam0ab"
+  componentId: "s1j54k93-5"
 })``;
diff --git a/test/fixtures/04-track-the-imported-variable/after.js b/test/fixtures/04-track-the-imported-variable/after.js
index bea6215..c18be45 100644
--- a/test/fixtures/04-track-the-imported-variable/after.js
+++ b/test/fixtures/04-track-the-imported-variable/after.js
@@ -2,25 +2,25 @@ import s from "styled-components";
 
 const Test = s.div.withConfig({
   displayName: "Test",
-  componentId: "Test-62sgar"
+  componentId: "syqrfc-0"
 })`width: 100%;`;
 const Test2 = true ? s.div.withConfig({
   displayName: "Test2",
-  componentId: "Test2-1rmo90j"
+  componentId: "syqrfc-1"
 })`` : s.div.withConfig({
   displayName: "Test2",
-  componentId: "Test2-1e44ey5"
+  componentId: "syqrfc-2"
 })``;
 const styles = { One: s.div.withConfig({
     displayName: "One",
-    componentId: "One-amesrd"
+    componentId: "syqrfc-3"
   })`` };
 let Component;
 Component = s.div.withConfig({
   displayName: "Component",
-  componentId: "Component-ohabty"
+  componentId: "syqrfc-4"
 })``;
 const WrappedComponent = s(Inner).withConfig({
   displayName: "WrappedComponent",
-  componentId: "WrappedComponent-1f8r9pi"
+  componentId: "syqrfc-5"
 })``;
diff --git a/test/fixtures/05-use-file-name/after.js b/test/fixtures/05-use-file-name/after.js
index a7a420c..21b7b28 100644
--- a/test/fixtures/05-use-file-name/after.js
+++ b/test/fixtures/05-use-file-name/after.js
@@ -2,13 +2,13 @@ import styled from "styled-components";
 
 const Test = styled.div.withConfig({
   displayName: "before__Test",
-  componentId: "before__Test-102oti6"
+  componentId: "gl3p53-0"
 })`color: red;`;
 styled.div.withConfig({
   displayName: "before",
-  componentId: "before-hxqpa7"
+  componentId: "gl3p53-1"
 })``;
 export default styled.button.withConfig({
   displayName: "before",
-  componentId: "before-17r4qjq"
+  componentId: "gl3p53-2"
 })``;
diff --git a/test/fixtures/06-work-with-hoisted-default-as-import/after.js b/test/fixtures/06-work-with-hoisted-default-as-import/after.js
index 3af6fc8..22cbf69 100644
--- a/test/fixtures/06-work-with-hoisted-default-as-import/after.js
+++ b/test/fixtures/06-work-with-hoisted-default-as-import/after.js
@@ -1,5 +1,5 @@
 const Test = s.div.withConfig({
   displayName: 'before__Test',
-  componentId: 'before__Test-1ex1ta1'
+  componentId: 's2wuo7o-0'
 })`width: 100%;`;
 import { default as s, css } from 'styled-components';