diff --git a/packages/emotion/README.md b/packages/emotion/README.md index 6514800..0ac8370 100644 --- a/packages/emotion/README.md +++ b/packages/emotion/README.md @@ -45,14 +45,15 @@ Generate source maps for Emotion CSS. Source maps help trace styles back to thei ### `autoLabel` -- **Type:** `'never' | 'dev-only' | 'always'` +- **Type:** `'never' | 'dev-only' | 'always' | 'runtime'` - **Default:** `'dev-only'` -Controls when debug labels are added to styled components and `css` calls. +Controls when debug labels are added to styled components, `css`, and `keyframes` calls. - `'never'` — Never add labels - `'dev-only'` — Only add labels in development mode - `'always'` — Always add labels +- `'runtime'` — Add labels unless `process.env.NODE_ENV === "production"` at runtime ### `labelFormat` diff --git a/packages/emotion/src/index.ts b/packages/emotion/src/index.ts index 0b9192d..86e326b 100644 --- a/packages/emotion/src/index.ts +++ b/packages/emotion/src/index.ts @@ -18,6 +18,8 @@ import { createLabelWithInfo } from './label.js' import path from 'node:path' import hashString from '@emotion/hash' +const RUNTIME_PRODUCTION_CONDITION = 'process.env.NODE_ENV === "production"' + export type { EmotionPluginOptions } from './types.js' interface RecordData { @@ -83,6 +85,8 @@ export default function emotionPlugin(options: EmotionPluginOptions = {}): Plugi return false case 'dev-only': return isDev + case 'runtime': + return false default: autoLabel satisfies never return false @@ -112,6 +116,26 @@ export default function emotionPlugin(options: EmotionPluginOptions = {}): Plugi return label } + function createLabelProperty(context: string | null): string | null { + const label = escapeJSString(createLabel(context, false)) + if (autoLabel === 'runtime') { + return `...(${RUNTIME_PRODUCTION_CONDITION} ? {} : { label: "${label}" })` + } + return shouldAddLabel() ? `label: "${label}"` : null + } + + function createLabelArgument( + kind: ExprKind, + context: string | null, + terminateBeforeSourcemap: boolean, + ): string | null { + const label = escapeJSString(createRuntimeLabel(kind, context, terminateBeforeSourcemap)) + if (autoLabel === 'runtime') { + return `...(${RUNTIME_PRODUCTION_CONDITION} ? [] : ["${label}"])` + } + return shouldAddLabel() ? `"${label}"` : null + } + function makeSourceMap(offset: number): string | null { if (!(sourceMapEnabled ?? isDev)) return null const pos = getPos(sourceContent, offset) @@ -155,9 +179,9 @@ export default function emotionPlugin(options: EmotionPluginOptions = {}): Plugi // Add label and source map (unless in JSX element context) if (!inJsx) { - if (includeLabel && shouldAddLabel()) { - const label = createRuntimeLabel(kind, labelContext, true) - parts.push(`"${escapeJSString(label)}"`) + if (includeLabel) { + const labelArgument = createLabelArgument(kind, labelContext, true) + if (labelArgument) parts.push(labelArgument) } const sm = makeSourceMap(sourceMapOffset) if (sm) { @@ -369,10 +393,8 @@ export default function emotionPlugin(options: EmotionPluginOptions = {}): Plugi isFullReplace: true, apply: (getTarget) => { let labelObj = `target: "${getTarget()}"` - if (shouldAddLabel()) { - const label = createLabel(labelContext, false) - labelObj += `, label: "${escapeJSString(label)}"` - } + const labelProperty = createLabelProperty(labelContext) + if (labelProperty) labelObj += `, ${labelProperty}` const styledArgs = buildTaggedTemplateArgs( quasi, @@ -444,10 +466,8 @@ export default function emotionPlugin(options: EmotionPluginOptions = {}): Plugi const styledName = s.slice(tag.callee.start, tag.callee.end) const target = getTarget() let labelObj = `target: "${target}"` - if (shouldAddLabel()) { - const label = createLabel(labelContext, false) - labelObj += `, label: "${escapeJSString(label)}"` - } + const labelProperty = createLabelProperty(labelContext) + if (labelProperty) labelObj += `, ${labelProperty}` // Extract existing args from styled(Component, ...) const existingArgs = tag.arguments @@ -522,11 +542,11 @@ export default function emotionPlugin(options: EmotionPluginOptions = {}): Plugi apply: () => { s.appendLeft(node.start, '/* @__PURE__ */ ') let hasTrailingComma = checkTrailingCommaExistence(s.original, node.end - 1) - if (shouldAddLabel()) { - const label = createRuntimeLabel(kind, labelContext, false) + const labelArgument = createLabelArgument(kind, labelContext, false) + if (labelArgument) { s.appendRight( node.end - 1, - `${maybeComma(!hasTrailingComma)}"${escapeJSString(label)}"`, + `${maybeComma(!hasTrailingComma)}${labelArgument}`, ) hasTrailingComma = false } @@ -564,10 +584,8 @@ export default function emotionPlugin(options: EmotionPluginOptions = {}): Plugi apply: (getTarget) => { s.appendLeft(node.start, '/* @__PURE__ */ ') let labelObj = `target: "${getTarget()}"` - if (shouldAddLabel()) { - const label = createLabel(labelContext, false) - labelObj += `, label: "${escapeJSString(label)}"` - } + const labelProperty = createLabelProperty(labelContext) + if (labelProperty) labelObj += `, ${labelProperty}` // Add { target, label } as second arg to inner call if (callee.arguments.length === 1) { // Insert before inner call's closing ) @@ -646,10 +664,8 @@ export default function emotionPlugin(options: EmotionPluginOptions = {}): Plugi if (!wasInJsx) { labelObj += `target: "${getTarget()}"` s.appendLeft(node.start, '/* @__PURE__ */ ') - if (shouldAddLabel()) { - const label = createLabel(labelContext, false) - labelObj += `, label: "${escapeJSString(label)}"` - } + const labelProperty = createLabelProperty(labelContext) + if (labelProperty) labelObj += `, ${labelProperty}` } const styledName = s.slice(callee.object.start, callee.object.end) const propName = callee.property.name @@ -696,11 +712,11 @@ export default function emotionPlugin(options: EmotionPluginOptions = {}): Plugi apply: () => { s.appendLeft(node.start, '/* @__PURE__ */ ') let hasTrailingComma = checkTrailingCommaExistence(s.original, node.end - 1) - if (shouldAddLabel()) { - const label = createRuntimeLabel(kind, labelContext, false) + const labelArgument = createLabelArgument(kind, labelContext, false) + if (labelArgument) { s.appendRight( node.end - 1, - `${maybeComma(!hasTrailingComma)}"${escapeJSString(label)}"`, + `${maybeComma(!hasTrailingComma)}${labelArgument}`, ) hasTrailingComma = false } diff --git a/packages/emotion/src/types.ts b/packages/emotion/src/types.ts index 33c521f..47a3a27 100644 --- a/packages/emotion/src/types.ts +++ b/packages/emotion/src/types.ts @@ -26,13 +26,14 @@ export interface EmotionPluginOptions { sourceMap?: boolean /** - * When to add debug labels to styled components. + * When to add debug labels to styled components, `css`, and `keyframes`. * - 'never': Never add labels * - 'dev-only': Only add labels in development mode (default) * - 'always': Always add labels + * - 'runtime': Add labels unless `process.env.NODE_ENV` is 'production' at runtime * @default 'dev-only' */ - autoLabel?: 'never' | 'dev-only' | 'always' + autoLabel?: 'never' | 'dev-only' | 'always' | 'runtime' /** * Label format template. diff --git a/packages/emotion/tests/fixtures/runtime-labels/config.json b/packages/emotion/tests/fixtures/runtime-labels/config.json new file mode 100644 index 0000000..0d37834 --- /dev/null +++ b/packages/emotion/tests/fixtures/runtime-labels/config.json @@ -0,0 +1,5 @@ +{ + "autoLabel": "runtime", + "labelFormat": "[filename]-[local]", + "sourceMap": true +} diff --git a/packages/emotion/tests/fixtures/runtime-labels/input.tsx b/packages/emotion/tests/fixtures/runtime-labels/input.tsx new file mode 100644 index 0000000..a83fbec --- /dev/null +++ b/packages/emotion/tests/fixtures/runtime-labels/input.tsx @@ -0,0 +1,26 @@ +import styled from '@emotion/styled' +import { css, keyframes } from '@emotion/react' +import * as emotion from '@emotion/react' + +const Component = 'div' + +export const StyledMemberTag = styled.button`color: hotpink;` +export const StyledComponentTag = styled(Component)`color: hotpink;` +export const StyledNestedCall = styled('button', { + shouldForwardProp: () => true, +})({ color: 'hotpink' }) +export const StyledMemberCall = styled.button({ color: 'hotpink' }) +export const cssTag = css`color: hotpink;` +export const keyframesTag = keyframes`from { opacity: 0; } to { opacity: 1; }` +export const cssCall = css({ color: 'hotpink' }) +export const keyframesCall = keyframes({ from: { opacity: 0 }, to: { opacity: 1 } }) +export const namespaceCssTag = emotion.css`color: hotpink;` +export const namespaceKeyframesTag = emotion.keyframes` + from { opacity: 0; } + to { opacity: 1; } +` +export const namespaceCssCall = emotion.css({ color: 'hotpink' }) +export const namespaceKeyframesCall = emotion.keyframes({ + from: { opacity: 0 }, + to: { opacity: 1 }, +}) diff --git a/packages/emotion/tests/fixtures/runtime-labels/output.js b/packages/emotion/tests/fixtures/runtime-labels/output.js new file mode 100644 index 0000000..999de63 --- /dev/null +++ b/packages/emotion/tests/fixtures/runtime-labels/output.js @@ -0,0 +1,38 @@ +import styled from "@emotion/styled"; +import * as emotion from "@emotion/react"; +import { css, keyframes } from "@emotion/react"; +//#region virtual:entry.tsx +const Component = "div"; +const StyledMemberTag = /* @__PURE__ */ styled("button", { + target: "e1r3sr4i0", + ...process.env.NODE_ENV === "production" ? {} : { label: "virtual-entry-StyledMemberTag" } +})("color:hotpink;", "/*# sourceMappingURL=[sourcemap] */"); +const StyledComponentTag = /* @__PURE__ */ styled(Component, { + target: "e1r3sr4i1", + ...process.env.NODE_ENV === "production" ? {} : { label: "virtual-entry-StyledComponentTag" } +})("color:hotpink;", "/*# sourceMappingURL=[sourcemap] */"); +const StyledNestedCall = /* @__PURE__ */ styled("button", { + shouldForwardProp: () => true, + target: "e1r3sr4i2", + ...process.env.NODE_ENV === "production" ? {} : { label: "virtual-entry-StyledNestedCall" } +})({ color: "hotpink" }, "/*# sourceMappingURL=[sourcemap] */"); +const StyledMemberCall = /* @__PURE__ */ styled("button", { + target: "e1r3sr4i3", + ...process.env.NODE_ENV === "production" ? {} : { label: "virtual-entry-StyledMemberCall" } +})({ color: "hotpink" }, "/*# sourceMappingURL=[sourcemap] */"); +const cssTag = /* @__PURE__ */ css("color:hotpink;", ...process.env.NODE_ENV === "production" ? [] : ["label:virtual-entry-cssTag;"], "/*# sourceMappingURL=[sourcemap] */"); +const keyframesTag = /* @__PURE__ */ keyframes("from{opacity:0;}to{opacity:1;}", ...process.env.NODE_ENV === "production" ? [] : ["virtual-entry-keyframesTag"], "/*# sourceMappingURL=[sourcemap] */"); +const cssCall = /* @__PURE__ */ css({ color: "hotpink" }, ...process.env.NODE_ENV === "production" ? [] : ["label:virtual-entry-cssCall"], "/*# sourceMappingURL=[sourcemap] */"); +const keyframesCall = /* @__PURE__ */ keyframes({ + from: { opacity: 0 }, + to: { opacity: 1 } +}, ...process.env.NODE_ENV === "production" ? [] : ["virtual-entry-keyframesCall"], "/*# sourceMappingURL=[sourcemap] */"); +const namespaceCssTag = /* @__PURE__ */ emotion.css("color:hotpink;", ...process.env.NODE_ENV === "production" ? [] : ["label:virtual-entry-namespaceCssTag;"], "/*# sourceMappingURL=[sourcemap] */"); +const namespaceKeyframesTag = /* @__PURE__ */ emotion.keyframes("from{opacity:0;}to{opacity:1;}", ...process.env.NODE_ENV === "production" ? [] : ["virtual-entry-namespaceKeyframesTag"], "/*# sourceMappingURL=[sourcemap] */"); +const namespaceCssCall = /* @__PURE__ */ emotion.css({ color: "hotpink" }, ...process.env.NODE_ENV === "production" ? [] : ["label:virtual-entry-namespaceCssCall"], "/*# sourceMappingURL=[sourcemap] */"); +const namespaceKeyframesCall = /* @__PURE__ */ emotion.keyframes({ + from: { opacity: 0 }, + to: { opacity: 1 } +}, ...process.env.NODE_ENV === "production" ? [] : ["virtual-entry-namespaceKeyframesCall"], "/*# sourceMappingURL=[sourcemap] */"); +//#endregion +export { StyledComponentTag, StyledMemberCall, StyledMemberTag, StyledNestedCall, cssCall, cssTag, keyframesCall, keyframesTag, namespaceCssCall, namespaceCssTag, namespaceKeyframesCall, namespaceKeyframesTag }; diff --git a/packages/emotion/tests/transform.test.ts b/packages/emotion/tests/transform.test.ts index f5a0aac..2e55e03 100644 --- a/packages/emotion/tests/transform.test.ts +++ b/packages/emotion/tests/transform.test.ts @@ -79,6 +79,11 @@ async function transform( const build = await rolldown({ input: virtualEntry, + transform: { + define: { + 'process.env.NODE_ENV': 'process.env.NODE_ENV', + }, + }, plugins: [ { name: 'virtual',