diff --git a/.changeset/nice-students-matter.md b/.changeset/nice-students-matter.md
new file mode 100644
index 00000000000..bfac05665eb
--- /dev/null
+++ b/.changeset/nice-students-matter.md
@@ -0,0 +1,5 @@
+---
+'@itwin/itwinui-react': minor
+---
+
+DEV-only warnings will now only be properly excluded from the PROD bundle. This is done using a separate `"development"` entrypoint listed in `package.json#exports`.
diff --git a/packages/itwinui-react/.gitignore b/packages/itwinui-react/.gitignore
index 0d55298fe23..a98b46d78af 100644
--- a/packages/itwinui-react/.gitignore
+++ b/packages/itwinui-react/.gitignore
@@ -7,6 +7,8 @@ build
# Compiled Components
cjs
esm
+DEV-cjs
+DEV-esm
# Build outputs
react-table.d.ts
diff --git a/packages/itwinui-react/package.json b/packages/itwinui-react/package.json
index e93e023be7d..3d2d0cc3928 100644
--- a/packages/itwinui-react/package.json
+++ b/packages/itwinui-react/package.json
@@ -11,10 +11,12 @@
".": {
"import": {
"types": "./esm/index.d.ts",
+ "development": "./DEV-esm/index.js",
"default": "./esm/index.js"
},
"require": {
"types": "./cjs/index.d.ts",
+ "development": "./DEV-cjs/index.js",
"default": "./cjs/index.js"
}
},
@@ -27,8 +29,16 @@
}
},
"./styles.css": "./styles.css",
- "./cjs": "./cjs/index.js",
- "./esm": "./esm/index.js",
+ "./cjs": {
+ "types": "./cjs/index.d.ts",
+ "development": "./DEV-cjs/index.js",
+ "default": "./cjs/index.js"
+ },
+ "./esm": {
+ "types": "./esm/index.d.ts",
+ "development": "./DEV-esm/index.js",
+ "default": "./esm/index.js"
+ },
"./package.json": "./package.json"
},
"typesVersions": {
@@ -41,6 +51,8 @@
"files": [
"cjs",
"esm",
+ "DEV-esm",
+ "DEV-cjs",
"styles.css",
"CHANGELOG.md",
"LICENSE.md"
@@ -72,7 +84,7 @@
"build:types": "tsc -p tsconfig.build.json --outDir esm && tsc -p tsconfig.build.json --outDir cjs",
"build:styles": "vite build src/styles.js",
"build:post": "node ./scripts/postBuild.mjs",
- "clean:build": "rimraf esm && rimraf cjs && rimraf react-table.d.ts",
+ "clean:build": "rimraf esm && rimraf cjs && rimraf DEV-esm && rimraf DEV-cjs",
"clean:coverage": "rimraf coverage",
"clean": "rimraf .turbo && pnpm clean:coverage && pnpm clean:build && rimraf node_modules",
"test": "pnpm test:types && pnpm test:unit",
diff --git a/packages/itwinui-react/scripts/build.mjs b/packages/itwinui-react/scripts/build.mjs
index 9a173fd9110..a97aaefd239 100644
--- a/packages/itwinui-react/scripts/build.mjs
+++ b/packages/itwinui-react/scripts/build.mjs
@@ -26,6 +26,9 @@ const swcOptions = {
'jsc.target=es2020',
'jsc.minify.format.comments=false',
'jsc.externalHelpers=true',
+
+ 'jsc.minify.compress.defaults=false', // Disable default compress options
+ 'jsc.minify.compress.dead_code=true', // Remove dead code (useful for removing NODE_ENV checks in production)
].join(' -C '),
get esm() {
@@ -47,8 +50,24 @@ const swcOptions = {
},
};
-execSync(`pnpm swc src -d esm ${swcOptions.esm}`);
+// ----------------------------------------------------------------------------
+
+execSync(`pnpm swc src -d DEV-esm ${swcOptions.esm}`, {
+ env: { ...process.env, NODE_ENV: 'development' },
+});
+console.log('✓ Built esm (DEV).');
+
+execSync(`pnpm swc src -d DEV-cjs ${swcOptions.cjs}`, {
+ env: { ...process.env, NODE_ENV: 'development' },
+});
+console.log('✓ Built cjs (DEV).');
+
+execSync(`pnpm swc src -d esm ${swcOptions.esm}`, {
+ env: { ...process.env, NODE_ENV: 'production' },
+});
console.log('✓ Built esm.');
-execSync(`pnpm swc src -d cjs ${swcOptions.cjs}`);
+execSync(`pnpm swc src -d cjs ${swcOptions.cjs}`, {
+ env: { ...process.env, NODE_ENV: 'production' },
+});
console.log('✓ Built cjs.');
diff --git a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx
index b5096ec1a77..9f16c08bd24 100644
--- a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx
+++ b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx
@@ -15,7 +15,7 @@ import type { PolymorphicForwardRefComponent } from '../../utils/index.js';
import { Button } from '../Buttons/Button.js';
import { Anchor } from '../Typography/Anchor.js';
-const logWarningInDev = createWarningLogger();
+const logWarning = createWarningLogger();
type BreadcrumbsProps = {
/**
@@ -200,9 +200,11 @@ const ListItem = ({
children?.type === 'a' ||
children?.type === Button
) {
- logWarningInDev(
- 'Directly using Button/a/span as Breadcrumbs children is deprecated, please use `Breadcrumbs.Item` instead.',
- );
+ if (process.env.NODE_ENV === 'development') {
+ logWarning(
+ 'Directly using Button/a/span as Breadcrumbs children is deprecated, please use `Breadcrumbs.Item` instead.',
+ );
+ }
children = ;
}
diff --git a/packages/itwinui-react/src/core/Menu/MenuItem.tsx b/packages/itwinui-react/src/core/Menu/MenuItem.tsx
index 023752f0166..6a076b78429 100644
--- a/packages/itwinui-react/src/core/Menu/MenuItem.tsx
+++ b/packages/itwinui-react/src/core/Menu/MenuItem.tsx
@@ -15,7 +15,7 @@ import { ListItem } from '../List/ListItem.js';
import type { ListItemOwnProps } from '../List/ListItem.js';
import cx from 'classnames';
-const logWarningInDev = createWarningLogger();
+const logWarning = createWarningLogger();
export type MenuItemProps = {
/**
@@ -100,8 +100,12 @@ export const MenuItem = React.forwardRef((props, forwardedRef) => {
...rest
} = props;
- if (onClickProp != null && subMenuItems.length > 0) {
- logWarningInDev(
+ if (
+ process.env.NODE_ENV === 'development' &&
+ onClickProp != null &&
+ subMenuItems.length > 0
+ ) {
+ logWarning(
'Passing a non-empty submenuItems array and onClick to MenuItem at the same time is not supported. This is because when a non empty submenuItems array is passed, clicking the MenuItem toggles the submenu visibility.',
);
}
diff --git a/packages/itwinui-react/src/core/Table/Table.tsx b/packages/itwinui-react/src/core/Table/Table.tsx
index 96bcec6ee72..93bf72751bd 100644
--- a/packages/itwinui-react/src/core/Table/Table.tsx
+++ b/packages/itwinui-react/src/core/Table/Table.tsx
@@ -83,7 +83,7 @@ const COLUMN_MIN_WIDTHS = {
withExpander: 108, // expander column should be wider to accommodate the expander icon
};
-const logWarningInDev = createWarningLogger();
+const logWarning = createWarningLogger();
export type TablePaginatorRendererProps = {
/**
@@ -635,9 +635,11 @@ export const Table = <
if (columns.length === 1 && 'columns' in columns[0]) {
headerGroups = _headerGroups.slice(1);
- logWarningInDev(
- `Table's \`columns\` prop should not have a top-level \`Header\` or sub-columns. They are only allowed to be passed for backwards compatibility.\n See https://github.com/iTwin/iTwinUI/wiki/iTwinUI-react-v2-migration-guide#breaking-changes`,
- );
+ if (process.env.NODE_ENV === 'development') {
+ logWarning(
+ `Table's \`columns\` prop should not have a top-level \`Header\` or sub-columns. They are only allowed to be passed for backwards compatibility.\n See https://github.com/iTwin/iTwinUI/wiki/iTwinUI-react-v2-migration-guide#breaking-changes`,
+ );
+ }
}
const ariaDataAttributes = Object.entries(rest).reduce(
diff --git a/packages/itwinui-react/src/styles.js/vite.config.mjs b/packages/itwinui-react/src/styles.js/vite.config.mjs
index d2ae5b74065..6646523c6e4 100644
--- a/packages/itwinui-react/src/styles.js/vite.config.mjs
+++ b/packages/itwinui-react/src/styles.js/vite.config.mjs
@@ -95,6 +95,8 @@ const distEsmDir = path.join(distDir, 'esm');
const distCjsDir = path.join(distDir, 'cjs');
const outCjsDir = path.join(root, 'cjs');
const outEsmDir = path.join(root, 'esm');
+const outEsmDevDir = path.join(root, 'DEV-esm');
+const outCjsDevDir = path.join(root, 'DEV-cjs');
const copyBuildOutput = async () => {
// create cjs/ and esm/ directories if they don't exist
@@ -105,15 +107,23 @@ const copyBuildOutput = async () => {
await fs.promises.mkdir(outEsmDir);
}
- // copy styles.js from src/styles.js/dist/ into cjs/ and esm/
+ // copy styles.js from src/styles.js/dist/ into cjs/, esm/, DEV-cjs/, and DEV-esm/
await fs.promises.copyFile(
path.join(distEsmDir, 'styles.js'),
path.join(outEsmDir, 'styles.js'),
);
+ await fs.promises.copyFile(
+ path.join(distEsmDir, 'styles.js'),
+ path.join(outEsmDevDir, 'styles.js'),
+ );
await fs.promises.copyFile(
path.join(distCjsDir, 'styles.js'),
path.join(outCjsDir, 'styles.js'),
);
+ await fs.promises.copyFile(
+ path.join(distCjsDir, 'styles.js'),
+ path.join(outCjsDevDir, 'styles.js'),
+ );
// copy styles.css from src/styles.js/dist/ into /
await fs.promises.copyFile(
diff --git a/packages/itwinui-react/src/utils/functions/dev.ts b/packages/itwinui-react/src/utils/functions/dev.ts
index dea1ad432a4..0fe7c0271c5 100644
--- a/packages/itwinui-react/src/utils/functions/dev.ts
+++ b/packages/itwinui-react/src/utils/functions/dev.ts
@@ -14,30 +14,31 @@ const isVitest = typeof (globalThis as any).__vitest_index__ !== 'undefined';
const isUnitTest = isJest || isVitest || isMocha;
-let isDev = false;
-
-// wrapping in try-catch because process might be undefined
-try {
- isDev = process.env.NODE_ENV !== 'production' && !isUnitTest;
-} catch {}
-
/**
- * Logs message one time only in dev environments.
+ * Returns a function that can be used to log one-time warnings in dev environments.
+ *
+ * **Note**: The actual log call should be wrapped in a check against `process.env.NODE_ENV === 'development'`
+ * to ensure that it is removed from the production build output (by SWC).
+ * Read more about the [`NODE_ENV` convention](https://nodejs.org/en/learn/getting-started/nodejs-the-difference-between-development-and-production).
*
* @example
- * const logWarningInDev = createWarningLogger();
- * logWarningInDev("please don't use this")
+ * const logWarning = createWarningLogger();
+ *
+ * if (process.env.NODE_ENV === 'development') {
+ * logWarning("please don't use this")
+ * }
*/
-const createWarningLogger = !isDev
- ? () => () => {}
- : () => {
- let logged = false;
- return (message: string) => {
- if (!logged) {
- console.warn(message);
- logged = true;
- }
- };
- };
+const createWarningLogger =
+ process.env.NODE_ENV === 'development' && !isUnitTest
+ ? () => {
+ let logged = false;
+ return (message: string) => {
+ if (!logged) {
+ console.warn(message);
+ logged = true;
+ }
+ };
+ }
+ : () => () => {};
-export { isUnitTest, isDev, createWarningLogger };
+export { isUnitTest, createWarningLogger };
diff --git a/packages/itwinui-react/src/utils/hooks/useGlobals.ts b/packages/itwinui-react/src/utils/hooks/useGlobals.ts
index 0f9dc1c2bd8..b19b96b556c 100644
--- a/packages/itwinui-react/src/utils/hooks/useGlobals.ts
+++ b/packages/itwinui-react/src/utils/hooks/useGlobals.ts
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as React from 'react';
import { ThemeContext } from '../../core/ThemeProvider/ThemeContext.js';
-import { isDev } from '../functions/dev.js';
+import { isUnitTest } from '../functions/dev.js';
const didLogWarning = {
fontSize: false,
@@ -31,7 +31,12 @@ export const useThemeProviderWarning = (
themeContext: React.ContextType,
) => {
React.useEffect(() => {
- if (isDev && !didLogWarning.themeProvider && !themeContext) {
+ if (
+ process.env.NODE_ENV === 'development' &&
+ !isUnitTest &&
+ !didLogWarning.themeProvider &&
+ !themeContext
+ ) {
console.error(
'iTwinUI components must be used within a tree wrapped in a ThemeProvider.',
);
@@ -45,7 +50,11 @@ export const useThemeProviderWarning = (
/** Shows console error if the page changes the root font size */
const useRootFontSizeWarning = () => {
React.useEffect(() => {
- if (isDev && !didLogWarning.fontSize) {
+ if (
+ process.env.NODE_ENV === 'development' &&
+ !isUnitTest &&
+ !didLogWarning.fontSize
+ ) {
const rootFontSize = parseInt(
getComputedStyle(document.documentElement).fontSize,
);