Skip to content

Commit

Permalink
Extract, unit test, and improve getLabelFromStackTrace (#2534)
Browse files Browse the repository at this point in the history
* Extract getLabelFromStackTrace

[WIP]

* New tests

* Add Next.js playground

* Add Next.js tests

* Fix nextjs dependency incompatibilities

* yarn.lock

[skip ci]

* Add Safari stack traces

[skip ci]

* Add Safari stack traces for real this time

* Add cra-new

* Redo cra-new

* More test cases

* Create separate file for getLabelFromStackTrace tests

* Add ignored cra-new files

* Add more test cases

* Move cra-new to cra

* Remove broken razzle playground

* Finish test cases

* Parse stack traces

* Cleanup

* Add code examples to test

* Document how to use the old JSX transform

* Fix flow errors

* Remove eslint stuff from Next.js project

* Document Safari stack trace weirdness

* Fix some markdown in a comment

* Update docs & comments on Safari stack traces

* Remove unnecessary code from Next.js playground

* Only call getLabelFromStackTrace if label not already computed

* Add PURE annotation in get-label-by-stack-trace

* Remove "SSR & Safari" docs section

Since we plan to fix this one way or another.

* Add SSR test for classic runtime

* Remove unit tests of getFunctionNameFromStackTraceLine

* Revise changeset wording

* Remove commented console.log
  • Loading branch information
srmagura authored Nov 19, 2021
1 parent 2bac69b commit 57be9e8
Show file tree
Hide file tree
Showing 35 changed files with 4,975 additions and 11,502 deletions.
5 changes: 5 additions & 0 deletions .changeset/sweet-hotels-explain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@emotion/react': patch
---

Changed the implementation of the runtime label extraction in elements using the css prop (that only happens in development) to one that should yield more consistent results across browsers. This fixes some minor issues with React reporting hydration mismatches that wouldn't happen in production.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,6 @@
"react-native": "^0.63.2",
"react-primitives": "^0.8.1",
"react-router-dom": "^4.2.2",
"react-scripts": "1.1.5",
"react-test-renderer": "16.8.6",
"react18": "npm:react@alpha",
"react18-dom": "npm:react-dom@alpha",
Expand Down
553 changes: 553 additions & 0 deletions packages/react/__tests__/get-label-from-stack-trace.js

Large diffs are not rendered by default.

32 changes: 12 additions & 20 deletions packages/react/src/emotion-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@ import { ThemeContext } from './theming'
import { getRegisteredStyles, insertStyles } from '@emotion/utils'
import { hasOwnProperty, isBrowser } from './utils'
import { serializeStyles } from '@emotion/serialize'

// those identifiers come from error stacks, so they have to be valid JS identifiers
// thus we only need to replace what is a valid character for JS, but not for CSS
const sanitizeIdentifier = (identifier: string) =>
identifier.replace(/\$/g, '-')
import { getLabelFromStackTrace } from './get-label-from-stack-trace'

let typePropName = '__EMOTION_TYPE_PLEASE_DO_NOT_USE__'

Expand Down Expand Up @@ -37,21 +33,17 @@ export const createEmotionProps = (type: React.ElementType, props: Object) => {

newProps[typePropName] = type

if (process.env.NODE_ENV !== 'production') {
const error = new Error()
if (error.stack) {
// chrome
let match = error.stack.match(
/at (?:Object\.|Module\.|)(?:jsx|createEmotionProps).*\n\s+at (?:Object\.|)([A-Z][A-Za-z0-9$]+) /
)
if (!match) {
// safari and firefox
match = error.stack.match(/.*\n([A-Z][A-Za-z0-9$]+)@/)
}
if (match) {
newProps[labelPropName] = sanitizeIdentifier(match[1])
}
}
// For performance, only call getLabelFromStackTrace in development and when
// the label hasn't already been computed
if (
process.env.NODE_ENV !== 'production' &&
!!props.css &&
(typeof props.css !== 'object' ||
typeof props.css.name !== 'string' ||
props.css.name.indexOf('-') === -1)
) {
const label = getLabelFromStackTrace(new Error().stack)
if (label) newProps[labelPropName] = label
}

return newProps
Expand Down
53 changes: 53 additions & 0 deletions packages/react/src/get-label-from-stack-trace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// @flow

const getFunctionNameFromStackTraceLine = (line: string): ?string => {
// V8
let match = /^\s+at\s+([A-Za-z0-9$.]+)\s/.exec(line)

if (match) {
// The match may be something like 'Object.createEmotionProps'
const parts = match[1].split('.')
return parts[parts.length - 1]
}

// Safari / Firefox
match = /^([A-Za-z0-9$.]+)@/.exec(line)
if (match) return match[1]

return undefined
}

const internalReactFunctionNames = /* #__PURE__ */ new Set([
'renderWithHooks',
'processChild',
'finishClassComponent',
'renderToString'
])

// These identifiers come from error stacks, so they have to be valid JS
// identifiers, thus we only need to replace what is a valid character for JS,
// but not for CSS.
const sanitizeIdentifier = (identifier: string) =>
identifier.replace(/\$/g, '-')

export const getLabelFromStackTrace = (stackTrace: string): ?string => {
if (!stackTrace) return undefined

const lines = stackTrace.split('\n')

for (let i = 0; i < lines.length; i++) {
const functionName = getFunctionNameFromStackTraceLine(lines[i])

// The first line of V8 stack traces is just "Error"
if (!functionName) continue

// If we reach one of these, we have gone too far and should quit
if (internalReactFunctionNames.has(functionName)) break

// The component name is the first function in the stack that starts with an
// uppercase letter
if (/^[A-Z]/.test(functionName)) return sanitizeIdentifier(functionName)
}

return undefined
}
2 changes: 1 addition & 1 deletion playgrounds/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Emotion Development Playgrounds

These are intended to be places to experiment with behaviour that is hard to do in tests or a CodeSandbox. These are not intended to be perfect examples of how you would write emotion code with these other libraries as they will generally be focussed on edge cases.
These are intended to be places to experiment with behaviour that is hard to do in tests or a CodeSandbox. These are not intended to be perfect examples of how you would write emotion code with these other libraries as they will generally be focused on edge cases.

## Getting Started

Expand Down
8 changes: 8 additions & 0 deletions playgrounds/cra/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Necessary because we might have a different version of babel-jest, .etc than
# what react-scripts wants
SKIP_PREFLIGHT_CHECK=true

# Uncomment if you want to test stuff with the old JSX transform.
# You also need to change the `@jsxImportSource @emotion/react` line
# to `@jsx jsx` and import `jsx` from @emotion/react.
# DISABLE_NEW_JSX_TRANSFORM=true
23 changes: 23 additions & 0 deletions playgrounds/cra/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*
Loading

0 comments on commit 57be9e8

Please sign in to comment.