Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions lru-cache-browser-issue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Issue: Top-level await with `node:diagnostics_channel` breaks browser builds in Webpack 5

## Description

The latest version of `lru-cache` (v11.x) uses top-level await to dynamically import `node:diagnostics_channel`:

```javascript
var C={hasSubscribers:!1},[S,W]=await import("node:diagnostics_channel")...
```

This causes issues in browser environments when building with Webpack 5, as Webpack doesn't handle the `node:` protocol scheme by default.

## Problem

1. **Top-level await**: The use of top-level await makes the entire module asynchronous
2. **Node.js protocol**: `node:diagnostics_channel` is not supported in browser environments
3. **Webpack 5 compatibility**: When Webpack encounters this, it either:
- Throws an `UnhandledSchemeError` for the `node:` protocol, or
- Causes the module export to become a Promise instead of the expected object

## Impact

When using `lru-cache` v11.x in a React application built with Webpack 5 (via Create React App / craco):

```javascript
// Expected behavior
import { LRUCache } from 'lru-cache';
const cache = new LRUCache({ max: 100 }); // Works

// Actual behavior
import * as LRUCacheModule from 'lru-cache';
console.log(LRUCacheModule); // Promise { <pending> } instead of module exports
```

This breaks downstream packages that depend on `lru-cache`, such as when used in `@kne/react-enum`, causing the entire module to become a Promise.

## Environment

- Node.js: v20.x
- Webpack: 5.x (via react-scripts 5.0.1)
- Build tool: craco 7.1.0
- Browser target: Modern browsers (Chrome, Firefox, Safari)

## Current Workaround

We can configure Webpack fallbacks, but this requires additional configuration:

```javascript
// In webpack config
webpackConfig.resolve.fallback = {
"diagnostics_channel": false
};

// Or create a mock module
webpackConfig.resolve.alias = {
"node:diagnostics_channel": path.resolve(__dirname, "mock-diagnostics-channel.js")
};
```

However, this doesn't solve the top-level await issue.

## Suggested Solution

Could you consider one of the following approaches:

1. **Conditional import**: Check for browser environment before importing `diagnostics_channel`:
```javascript
const hasDiagnostics = typeof process !== 'undefined' && process.versions?.node;
const diagnosticsChannel = hasDiagnostics ?
await import('node:diagnostics_channel') :
{ channel: () => ({ hasSubscribers: false }) };
```

2. **Browser bundle**: Provide a separate browser entry point that doesn't include Node.js-specific features

3. **Optional peer dependency**: Make `diagnostics_channel` an optional feature that can be safely omitted in browser builds

4. **Use synchronous import**: Avoid top-level await to prevent making the module async

## Version Info

- lru-cache version: 11.3.2
- Node.js: 20.x
- Webpack: 5.x

## Additional Context

This issue was discovered when upgrading from lru-cache v10.x to v11.x. Version 10.x works fine in browser environments because it doesn't use `node:diagnostics_channel` or top-level await.

Thank you for considering this issue! Let me know if you need any additional information or if I can help test a potential fix.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@kne-components/components-core",
"version": "0.4.68",
"version": "0.4.69",
"files": [
"build"
],
Expand Down Expand Up @@ -31,7 +31,7 @@
"@kne/info-page": "^0.2.9",
"@kne/is-empty": "^1.0.1",
"@kne/phone-number-input": "^0.1.5",
"@kne/react-enum": "^0.1.12",
"@kne/react-enum": "^0.1.14",
"@kne/react-fetch": "^1.5.5",
"@kne/react-file": "^0.1.35",
"@kne/react-file-type": "^1.0.5",
Expand All @@ -51,7 +51,7 @@
"@kne/use-simulation-blur": "^0.1.2",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"axios": "^1.13.5",
"axios": "1.13.5",
"classnames": "^2.3.2",
"color": "^4.2.3",
"cross-env": "^7.0.3",
Expand Down
24 changes: 13 additions & 11 deletions src/components/Navigation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,6 @@ const Navigation = withLocale(({
}
}
const windowResizeRef = useResize(callback);
useEffect(() => {
callback(windowResizeRef.current);
}, []);
const pathModuleName = location.pathname
.replace(new RegExp(`^${base}`), "")
.split("/")[1];
Expand Down Expand Up @@ -162,9 +159,11 @@ const Navigation = withLocale(({
trigger={['click']}
open={mobileMenuVisible}
onOpenChange={setMobileMenuVisible}
dropdownRender={(menu) => (<div className={style["mobile-dropdown-content"]}>
dropdownRender={(menu) => (<div
className={classnames(style["mobile-dropdown-content"], 'navigation-mobile-dropdown-content')}>
{menu}
{rightOptions && (<div className={style["mobile-dropdown-options"]}>
{rightOptions && (<div
className={classnames(style["mobile-dropdown-options"], 'navigation-mobile-dropdown-options')}>
{rightOptions}
</div>)}
</div>)}
Expand Down Expand Up @@ -200,7 +199,8 @@ const Navigation = withLocale(({
})],
}}
>
<div className={classnames(style["mobile-menu-trigger"], "mobile-menu-trigger")}>
<div
className={classnames(style["mobile-menu-trigger"], "navigation-mobile-menu-trigger")}>
<MenuOutlined/>
</div>
</Dropdown>
Expand Down Expand Up @@ -231,7 +231,7 @@ const Navigation = withLocale(({
<span>
{nameLabel || formatMessage({id: 'overflowedIndicator'})}
</span>
<span className={style["more-icon"]}>
<span className={classnames(style["more-icon"], 'navigation-more-icon')}>
<Icon type="icon-arrow-thin-down"/>
</span>
</Space>)}
Expand Down Expand Up @@ -269,10 +269,12 @@ const Navigation = withLocale(({
}),]}
/>
</Col>)}
{isMobile && (<Col className={style["navigation-mobile-title"]}>
{defaultTitle || formatMessage({id: 'defaultTitle'})}
</Col>)}
{!isMobile && <Col className={style["navigation-options"]}>{rightOptions}</Col>}
{isMobile && (
<Col className={classnames(style["navigation-mobile-title"], 'navigation-mobile-title')}>
{defaultTitle || formatMessage({id: 'defaultTitle'})}
</Col>)}
{!isMobile && <Col
className={classnames(style["navigation-options"], "navigation-options")}>{rightOptions}</Col>}
</Row>
</Header>
</div>
Expand Down
Loading