diff --git a/.changeset/great-vans-bathe.md b/.changeset/great-vans-bathe.md new file mode 100644 index 00000000000..e901530046d --- /dev/null +++ b/.changeset/great-vans-bathe.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +Add babel-plugin-dev-expression to transform warning calls in package bundle diff --git a/babel.config.js b/babel.config.js index 596c9d30212..0912aa5ab93 100644 --- a/babel.config.js +++ b/babel.config.js @@ -7,6 +7,7 @@ function replacementPlugin(env) { const sharedPlugins = [ 'macros', 'preval', + 'dev-expression', 'add-react-displayname', 'babel-plugin-styled-components', '@babel/plugin-proposal-nullish-coalescing-operator', diff --git a/package-lock.json b/package-lock.json index ff77f6b6817..27f616bebc7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -92,6 +92,7 @@ "babel-core": "7.0.0-bridge.0", "babel-loader": "^9.1.0", "babel-plugin-add-react-displayname": "0.0.5", + "babel-plugin-dev-expression": "0.2.3", "babel-plugin-macros": "3.1.0", "babel-plugin-open-source": "1.3.4", "babel-plugin-preval": "5.1.0", @@ -27620,6 +27621,15 @@ "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", "dev": true }, + "node_modules/babel-plugin-dev-expression": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dev-expression/-/babel-plugin-dev-expression-0.2.3.tgz", + "integrity": "sha512-rP5LK9QQTzCW61nVVzw88En1oK8t8gTsIeC6E61oelxNsU842yMjF0G1MxhvUpCkxCEIj7sE8/e5ieTheT//uw==", + "dev": true, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/babel-plugin-dynamic-import-node": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", @@ -76234,6 +76244,13 @@ } } }, + "babel-plugin-dev-expression": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dev-expression/-/babel-plugin-dev-expression-0.2.3.tgz", + "integrity": "sha512-rP5LK9QQTzCW61nVVzw88En1oK8t8gTsIeC6E61oelxNsU842yMjF0G1MxhvUpCkxCEIj7sE8/e5ieTheT//uw==", + "dev": true, + "requires": {} + }, "babel-plugin-dynamic-import-node": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", diff --git a/package.json b/package.json index 07698262bc5..a130b981837 100644 --- a/package.json +++ b/package.json @@ -170,6 +170,7 @@ "babel-core": "7.0.0-bridge.0", "babel-loader": "^9.1.0", "babel-plugin-add-react-displayname": "0.0.5", + "babel-plugin-dev-expression": "0.2.3", "babel-plugin-macros": "3.1.0", "babel-plugin-open-source": "1.3.4", "babel-plugin-preval": "5.1.0", diff --git a/rollup.config.js b/rollup.config.js index e18f208e78e..e6d054d3931 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -91,6 +91,7 @@ const baseConfig = { 'macros', 'preval', 'add-react-displayname', + 'dev-expression', 'babel-plugin-styled-components', '@babel/plugin-proposal-nullish-coalescing-operator', '@babel/plugin-proposal-optional-chaining', diff --git a/src/hooks/useControllableState.ts b/src/hooks/useControllableState.ts index de7dcb41896..5516a97d000 100644 --- a/src/hooks/useControllableState.ts +++ b/src/hooks/useControllableState.ts @@ -1,4 +1,5 @@ import React from 'react' +import {warning} from '../utils/warning' type ControllableStateOptions = { /** @@ -73,7 +74,8 @@ export function useControllableState({ // Uncontrolled -> Controlled // If the component prop is uncontrolled, the prop value should be undefined if (controlled.current === false && controlledValue) { - warn( + warning( + true, 'A component is changing an uncontrolled %s component to be controlled. ' + 'This is likely caused by the value changing to a defined value ' + 'from undefined. Decide between using a controlled or uncontrolled ' + @@ -86,7 +88,8 @@ export function useControllableState({ // Controlled -> Uncontrolled // If the component prop is controlled, the prop value should be defined if (controlled.current === true && !controlledValue) { - warn( + warning( + true, 'A component is changing a controlled %s component to be uncontrolled. ' + 'This is likely caused by the value changing to an undefined value ' + 'from a defined one. Decide between using a controlled or ' + @@ -103,16 +106,3 @@ export function useControllableState({ return [state, setState] } - -/** Warn when running in a development environment */ -const warn = __DEV__ - ? // eslint-disable-next-line @typescript-eslint/no-explicit-any - function warn(format: string, ...args: any[]) { - let index = 0 - const message = format.replace(/%s/g, () => { - return args[index++] - }) - // eslint-disable-next-line no-console - console.warn(`Warning: ${message}`) - } - : function emptyFunction() {} diff --git a/src/hooks/useMedia.tsx b/src/hooks/useMedia.tsx index 1671730c637..a394bffa36a 100644 --- a/src/hooks/useMedia.tsx +++ b/src/hooks/useMedia.tsx @@ -1,5 +1,6 @@ import React, {createContext, useContext, useState, useEffect} from 'react' import {canUseDOM} from '../utils/environment' +import {warning} from '../utils/warning' /** * `useMedia` will use the given `mediaQueryString` with `matchMedia` to @@ -31,12 +32,10 @@ export function useMedia(mediaQueryString: string, defaultState?: boolean) { } // A default value has not been provided, and you are rendering on the server, warn of a possible hydration mismatch when defaulting to false. - if (process.env.NODE_ENV !== 'production') { - // eslint-disable-next-line no-console - console.warn( - '`useMedia` When server side rendering, defaultState should be defined to prevent a hydration mismatches.', - ) - } + warning( + true, + '`useMedia` When server side rendering, defaultState should be defined to prevent a hydration mismatches.', + ) return false }) diff --git a/src/utils/__tests__/invariant.test.ts b/src/utils/__tests__/invariant.test.ts new file mode 100644 index 00000000000..a6bb7798e7b --- /dev/null +++ b/src/utils/__tests__/invariant.test.ts @@ -0,0 +1,19 @@ +import {invariant} from '../invariant' + +test('throws an error when the condition is `false`', () => { + expect(() => { + invariant(false, 'test') + }).toThrowError('test') +}) + +test('does not throw an error when the condition is `true`', () => { + expect(() => { + invariant(true, 'test') + }).not.toThrowError() +}) + +test('formats arguments into error string', () => { + expect(() => { + invariant(false, 'test %s %s %s', 1, 2, 3) + }).toThrowError('test 1 2 3') +}) diff --git a/src/utils/__tests__/warning.test.ts b/src/utils/__tests__/warning.test.ts new file mode 100644 index 00000000000..4f7e58bfee8 --- /dev/null +++ b/src/utils/__tests__/warning.test.ts @@ -0,0 +1,30 @@ +import {warning} from '../warning' + +test('emits a message to console.warn() when the condition is `true`', () => { + const spy = jest.spyOn(console, 'warn').mockImplementationOnce(() => {}) + + warning(true, 'test') + + expect(spy).toHaveBeenCalled() + expect(spy).toHaveBeenCalledWith('Warning:', 'test') + spy.mockRestore() +}) + +test('does not emit a message to console.warn() when the condition is `false`', () => { + const spy = jest.spyOn(console, 'warn').mockImplementationOnce(() => {}) + + warning(false, 'test') + + expect(spy).not.toHaveBeenCalled() + spy.mockRestore() +}) + +test('formats arguments into warning string', () => { + const spy = jest.spyOn(console, 'warn').mockImplementationOnce(() => {}) + + warning(true, 'test %s %s %s', 1, 2, 3) + + expect(spy).toHaveBeenCalled() + expect(spy).toHaveBeenCalledWith('Warning:', 'test 1 2 3') + spy.mockRestore() +}) diff --git a/src/utils/invariant.ts b/src/utils/invariant.ts new file mode 100644 index 00000000000..0ff91231636 --- /dev/null +++ b/src/utils/invariant.ts @@ -0,0 +1,31 @@ +function emptyFunction() {} + +// Inspired by invariant by fbjs +// @see https://github.com/facebook/fbjs/blob/main/packages/fbjs/src/__forks__/invariant.js +const invariant = __DEV__ + ? // eslint-disable-next-line @typescript-eslint/no-explicit-any + function invariant(condition: any, format?: string, ...args: Array) { + if (!condition) { + let error + + if (format === undefined) { + error = new Error( + 'Minified exception occurred; use the non-minified dev environment ' + + 'for the full error message and additional helpful warnings.', + ) + } else { + let index = 0 + const message = format.replace(/%s/g, () => { + return args[index++] + }) + + error = new Error(message) + error.name = 'Invariant Violation' + } + + throw error + } + } + : emptyFunction + +export {invariant} diff --git a/src/utils/warning.ts b/src/utils/warning.ts new file mode 100644 index 00000000000..e26aff1a345 --- /dev/null +++ b/src/utils/warning.ts @@ -0,0 +1,25 @@ +function emptyFunction() {} + +const warn = __DEV__ + ? function warn(message: string) { + // eslint-disable-next-line no-console + console.warn('Warning:', message) + } + : emptyFunction + +// Inspired by warning by fbjs +// @see https://github.com/facebook/fbjs/blob/main/packages/fbjs/src/__forks__/warning.js +const warning = __DEV__ + ? // eslint-disable-next-line @typescript-eslint/no-explicit-any + function warning(condition: any, format: string, ...args: Array) { + if (condition) { + let index = 0 + const message = format.replace(/%s/g, () => { + return args[index++] + }) + warn(message) + } + } + : emptyFunction + +export {warn, warning}