Skip to content

Commit 57be9e8

Browse files
authored
Extract, unit test, and improve getLabelFromStackTrace (#2534)
* 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
1 parent 2bac69b commit 57be9e8

35 files changed

+4975
-11502
lines changed

.changeset/sweet-hotels-explain.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@emotion/react': patch
3+
---
4+
5+
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.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,6 @@
256256
"react-native": "^0.63.2",
257257
"react-primitives": "^0.8.1",
258258
"react-router-dom": "^4.2.2",
259-
"react-scripts": "1.1.5",
260259
"react-test-renderer": "16.8.6",
261260
"react18": "npm:react@alpha",
262261
"react18-dom": "npm:react-dom@alpha",

packages/react/__tests__/get-label-from-stack-trace.js

Lines changed: 553 additions & 0 deletions
Large diffs are not rendered by default.

packages/react/src/emotion-element.js

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@ import { ThemeContext } from './theming'
55
import { getRegisteredStyles, insertStyles } from '@emotion/utils'
66
import { hasOwnProperty, isBrowser } from './utils'
77
import { serializeStyles } from '@emotion/serialize'
8-
9-
// those identifiers come from error stacks, so they have to be valid JS identifiers
10-
// thus we only need to replace what is a valid character for JS, but not for CSS
11-
const sanitizeIdentifier = (identifier: string) =>
12-
identifier.replace(/\$/g, '-')
8+
import { getLabelFromStackTrace } from './get-label-from-stack-trace'
139

1410
let typePropName = '__EMOTION_TYPE_PLEASE_DO_NOT_USE__'
1511

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

3834
newProps[typePropName] = type
3935

40-
if (process.env.NODE_ENV !== 'production') {
41-
const error = new Error()
42-
if (error.stack) {
43-
// chrome
44-
let match = error.stack.match(
45-
/at (?:Object\.|Module\.|)(?:jsx|createEmotionProps).*\n\s+at (?:Object\.|)([A-Z][A-Za-z0-9$]+) /
46-
)
47-
if (!match) {
48-
// safari and firefox
49-
match = error.stack.match(/.*\n([A-Z][A-Za-z0-9$]+)@/)
50-
}
51-
if (match) {
52-
newProps[labelPropName] = sanitizeIdentifier(match[1])
53-
}
54-
}
36+
// For performance, only call getLabelFromStackTrace in development and when
37+
// the label hasn't already been computed
38+
if (
39+
process.env.NODE_ENV !== 'production' &&
40+
!!props.css &&
41+
(typeof props.css !== 'object' ||
42+
typeof props.css.name !== 'string' ||
43+
props.css.name.indexOf('-') === -1)
44+
) {
45+
const label = getLabelFromStackTrace(new Error().stack)
46+
if (label) newProps[labelPropName] = label
5547
}
5648

5749
return newProps
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// @flow
2+
3+
const getFunctionNameFromStackTraceLine = (line: string): ?string => {
4+
// V8
5+
let match = /^\s+at\s+([A-Za-z0-9$.]+)\s/.exec(line)
6+
7+
if (match) {
8+
// The match may be something like 'Object.createEmotionProps'
9+
const parts = match[1].split('.')
10+
return parts[parts.length - 1]
11+
}
12+
13+
// Safari / Firefox
14+
match = /^([A-Za-z0-9$.]+)@/.exec(line)
15+
if (match) return match[1]
16+
17+
return undefined
18+
}
19+
20+
const internalReactFunctionNames = /* #__PURE__ */ new Set([
21+
'renderWithHooks',
22+
'processChild',
23+
'finishClassComponent',
24+
'renderToString'
25+
])
26+
27+
// These identifiers come from error stacks, so they have to be valid JS
28+
// identifiers, thus we only need to replace what is a valid character for JS,
29+
// but not for CSS.
30+
const sanitizeIdentifier = (identifier: string) =>
31+
identifier.replace(/\$/g, '-')
32+
33+
export const getLabelFromStackTrace = (stackTrace: string): ?string => {
34+
if (!stackTrace) return undefined
35+
36+
const lines = stackTrace.split('\n')
37+
38+
for (let i = 0; i < lines.length; i++) {
39+
const functionName = getFunctionNameFromStackTraceLine(lines[i])
40+
41+
// The first line of V8 stack traces is just "Error"
42+
if (!functionName) continue
43+
44+
// If we reach one of these, we have gone too far and should quit
45+
if (internalReactFunctionNames.has(functionName)) break
46+
47+
// The component name is the first function in the stack that starts with an
48+
// uppercase letter
49+
if (/^[A-Z]/.test(functionName)) return sanitizeIdentifier(functionName)
50+
}
51+
52+
return undefined
53+
}

playgrounds/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Emotion Development Playgrounds
22

3-
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.
3+
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.
44

55
## Getting Started
66

playgrounds/cra/.env

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Necessary because we might have a different version of babel-jest, .etc than
2+
# what react-scripts wants
3+
SKIP_PREFLIGHT_CHECK=true
4+
5+
# Uncomment if you want to test stuff with the old JSX transform.
6+
# You also need to change the `@jsxImportSource @emotion/react` line
7+
# to `@jsx jsx` and import `jsx` from @emotion/react.
8+
# DISABLE_NEW_JSX_TRANSFORM=true

playgrounds/cra/.gitignore

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# production
12+
/build
13+
14+
# misc
15+
.DS_Store
16+
.env.local
17+
.env.development.local
18+
.env.test.local
19+
.env.production.local
20+
21+
npm-debug.log*
22+
yarn-debug.log*
23+
yarn-error.log*

0 commit comments

Comments
 (0)