Skip to content

Commit

Permalink
feat(project): add warning, invariant utilities and dev-expression pl…
Browse files Browse the repository at this point in the history
…ugin (#2901)

* chore(project): add warning utility and dev-expression plugin

* chore: address eslint violations

* feat: add invariant and warn helper

* chore: flip flag for useMedia warning

* chore: add changeset

* chore: add minified exception case for invariant

* chore: update type signature for invariant

* test: update test for warning helper

* refactor(hooks): update useControllableState warning usage

* test(warning): update test titles with flipped condition

---------

Co-authored-by: Josh Black <joshblack@users.noreply.github.com>
  • Loading branch information
joshblack and joshblack committed Mar 13, 2023
1 parent 418f316 commit 992f1ac
Show file tree
Hide file tree
Showing 11 changed files with 140 additions and 21 deletions.
5 changes: 5 additions & 0 deletions .changeset/great-vans-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

Add babel-plugin-dev-expression to transform warning calls in package bundle
1 change: 1 addition & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
17 changes: 17 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
20 changes: 5 additions & 15 deletions src/hooks/useControllableState.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react'
import {warning} from '../utils/warning'

type ControllableStateOptions<T> = {
/**
Expand Down Expand Up @@ -73,7 +74,8 @@ export function useControllableState<T>({
// 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 ' +
Expand All @@ -86,7 +88,8 @@ export function useControllableState<T>({
// 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 ' +
Expand All @@ -103,16 +106,3 @@ export function useControllableState<T>({

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() {}
11 changes: 5 additions & 6 deletions src/hooks/useMedia.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
})
Expand Down
19 changes: 19 additions & 0 deletions src/utils/__tests__/invariant.test.ts
Original file line number Diff line number Diff line change
@@ -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')
})
30 changes: 30 additions & 0 deletions src/utils/__tests__/warning.test.ts
Original file line number Diff line number Diff line change
@@ -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()
})
31 changes: 31 additions & 0 deletions src/utils/invariant.ts
Original file line number Diff line number Diff line change
@@ -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<any>) {
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}
25 changes: 25 additions & 0 deletions src/utils/warning.ts
Original file line number Diff line number Diff line change
@@ -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<any>) {
if (condition) {
let index = 0
const message = format.replace(/%s/g, () => {
return args[index++]
})
warn(message)
}
}
: emptyFunction

export {warn, warning}

0 comments on commit 992f1ac

Please sign in to comment.