Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2aeeea0
fix(sdk): make isBrowserEnv tree-shakable
cursoragent Feb 16, 2026
fea3d21
chore: add changeset for isBrowserEnv
cursoragent Feb 17, 2026
577e6ff
chore: expand bundle size metrics
cursoragent Feb 17, 2026
82b5e2e
fix(sdk): keep isBrowserEnv API
cursoragent Feb 17, 2026
106578d
chore: tolerate bundle size assets
cursoragent Feb 17, 2026
1367002
chore: align bundle size ENV_TARGET
cursoragent Feb 17, 2026
3c9770c
chore: use rslib for bundle sizes
cursoragent Feb 17, 2026
680acb4
Merge branch 'main' into cursor/tree-shaking-coverage-5fd5
ScriptedAlchemy Feb 19, 2026
2dc915f
Merge branch 'main' into cursor/tree-shaking-coverage-5fd5
ScriptedAlchemy Feb 19, 2026
a850bd1
fix(sdk): recompute browser env at call time
ScriptedAlchemy Feb 24, 2026
d871a21
Merge remote-tracking branch 'origin/main' into cursor/tree-shaking-c…
ScriptedAlchemy Feb 24, 2026
193f7fe
fix(dts-plugin): align workspace entrypoints and RawSource typing
ScriptedAlchemy Feb 24, 2026
4d8ed5c
Merge branch 'main' into cursor/tree-shaking-coverage-5fd5
ScriptedAlchemy Feb 25, 2026
e03ea52
fix(sdk): align package entrypoints with emitted artifacts
ScriptedAlchemy Feb 25, 2026
bc68caa
Merge branch 'main' into cursor/tree-shaking-coverage-5fd5
ScriptedAlchemy Feb 25, 2026
d548490
Merge branch 'main' into cursor/tree-shaking-coverage-5fd5
ScriptedAlchemy Feb 26, 2026
c59c563
Merge branch 'main' into cursor/tree-shaking-coverage-5fd5
ScriptedAlchemy Feb 26, 2026
038bea0
Merge branch 'main' into cursor/tree-shaking-coverage-5fd5
ScriptedAlchemy Feb 27, 2026
c957914
Merge branch 'main' into cursor/tree-shaking-coverage-5fd5
ScriptedAlchemy Feb 27, 2026
e89d77c
Merge main into cursor/tree-shaking-coverage-5fd5
ScriptedAlchemy Feb 28, 2026
de8aa29
Merge branch 'main' into cursor/tree-shaking-coverage-5fd5
ScriptedAlchemy Feb 28, 2026
8e685af
Merge branch 'main' into cursor/tree-shaking-coverage-5fd5
ScriptedAlchemy Mar 2, 2026
66a47c6
Merge branch 'main' into cursor/tree-shaking-coverage-5fd5
ScriptedAlchemy Mar 3, 2026
5fc3639
Merge branch 'main' into cursor/tree-shaking-coverage-5fd5
ScriptedAlchemy Mar 5, 2026
db769ee
Merge branch 'main' into cursor/tree-shaking-coverage-5fd5
ScriptedAlchemy Mar 9, 2026
564a259
Merge branch 'main' into cursor/tree-shaking-coverage-5fd5
ScriptedAlchemy Mar 10, 2026
e953ab1
fix(metro,sdk): stabilize type import and env test mocks
ScriptedAlchemy Mar 10, 2026
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
8 changes: 8 additions & 0 deletions .changeset/breezy-walls-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@module-federation/sdk": minor
"@module-federation/runtime-core": minor
---

Add `isBrowserEnvValue` as a tree-shakable ENV_TARGET-aware constant while
preserving the `isBrowserEnv()` function. Internal callers use the constant to
enable bundler dead-code elimination without breaking the public API.
7 changes: 5 additions & 2 deletions packages/bridge/bridge-react/src/lazy/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { isBrowserEnv, composeKeyWithSeparator } from '@module-federation/sdk';
import {
isBrowserEnvValue,
composeKeyWithSeparator,
} from '@module-federation/sdk';
import logger from './logger';
import {
DOWNGRADE_KEY,
Expand Down Expand Up @@ -153,7 +156,7 @@ export async function fetchData(
_id: id,
});
};
if (isBrowserEnv()) {
if (isBrowserEnvValue) {
const dataFetchItem = getDataFetchItem(id);
if (!dataFetchItem) {
throw new Error(`dataFetchItem not found, id: ${id}`);
Expand Down
4 changes: 2 additions & 2 deletions packages/runtime-core/src/core.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isBrowserEnv } from '@module-federation/sdk';
import { isBrowserEnvValue } from '@module-federation/sdk';
import type {
CreateScriptHookReturn,
GlobalModuleInfo,
Expand Down Expand Up @@ -185,7 +185,7 @@ export class ModuleFederation {
plugins,
remotes: [],
shared: {},
inBrowser: isBrowserEnv(),
inBrowser: isBrowserEnvValue,
};

this.name = userOptions.name;
Expand Down
4 changes: 2 additions & 2 deletions packages/runtime-core/src/plugins/generate-preload-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
ProviderModuleInfo,
isManifestProvider,
getResourceUrl,
isBrowserEnv,
isBrowserEnvValue,
} from '@module-federation/sdk';
import {
EntryAssets,
Expand Down Expand Up @@ -324,7 +324,7 @@ export const generatePreloadAssetsPlugin: () => ModuleFederationRuntimePlugin =
globalSnapshot,
remoteSnapshot,
} = args;
if (!isBrowserEnv()) {
if (!isBrowserEnvValue) {
return {
cssAssets: [],
jsAssetsWithoutEntry: [],
Expand Down
4 changes: 2 additions & 2 deletions packages/runtime-core/src/plugins/snapshot/SnapshotHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
ModuleInfo,
generateSnapshotFromManifest,
isManifestProvider,
isBrowserEnv,
isBrowserEnvValue,
} from '@module-federation/sdk';
import {
getShortErrorMsg,
Expand Down Expand Up @@ -187,7 +187,7 @@ export class SnapshotHandler {
// global snapshot includes manifest or module info includes manifest
if (globalRemoteSnapshot) {
if (isManifestProvider(globalRemoteSnapshot)) {
const remoteEntry = isBrowserEnv()
const remoteEntry = isBrowserEnvValue
? globalRemoteSnapshot.remoteEntry
: globalRemoteSnapshot.ssrRemoteEntry ||
globalRemoteSnapshot.remoteEntry ||
Expand Down
4 changes: 2 additions & 2 deletions packages/runtime-core/src/plugins/snapshot/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
ModuleInfo,
getResourceUrl,
isBrowserEnv,
isBrowserEnvValue,
} from '@module-federation/sdk';
import { ModuleFederationRuntimePlugin } from '../../type/plugin';
import {
Expand All @@ -26,7 +26,7 @@ export function assignRemoteInfo(

let entryUrl = getResourceUrl(remoteSnapshot, remoteEntryInfo.url);

if (!isBrowserEnv() && !entryUrl.startsWith('http')) {
if (!isBrowserEnvValue && !entryUrl.startsWith('http')) {
entryUrl = `https:${entryUrl}`;
}

Expand Down
4 changes: 2 additions & 2 deletions packages/runtime-core/src/remote/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
isBrowserEnv,
isBrowserEnvValue,
warn,
composeKeyWithSeparator,
ModuleInfo,
Expand Down Expand Up @@ -426,7 +426,7 @@ export class RemoteHandler {
}
// Set the remote entry to a complete path
if ('entry' in remote) {
if (isBrowserEnv() && !remote.entry.startsWith('http')) {
if (isBrowserEnvValue && !remote.entry.startsWith('http')) {
remote.entry = new URL(remote.entry, window.location.origin).href;
}
}
Expand Down
6 changes: 5 additions & 1 deletion packages/runtime-core/src/utils/env.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export { isBrowserEnv, isDebugMode } from '@module-federation/sdk';
export {
isBrowserEnv,
isBrowserEnvValue,
isDebugMode,
} from '@module-federation/sdk';

export function isDevelopmentMode(): boolean {
return true;
Expand Down
6 changes: 3 additions & 3 deletions packages/runtime-core/src/utils/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
loadScript,
loadScriptNode,
composeKeyWithSeparator,
isBrowserEnv,
isBrowserEnvValue,
} from '@module-federation/sdk';
import { DEFAULT_REMOTE_TYPE, DEFAULT_SCOPE } from '../constant';
import { ModuleFederation } from '../core';
Expand Down Expand Up @@ -265,11 +265,11 @@ export async function getRemoteEntry(params: {
if (res) {
return res;
}
// Use ENV_TARGET if defined, otherwise fallback to isBrowserEnv, must keep this
// Use ENV_TARGET if defined, otherwise fallback to isBrowserEnvValue
const isWebEnvironment =
typeof ENV_TARGET !== 'undefined'
? ENV_TARGET === 'web'
: isBrowserEnv();
: isBrowserEnvValue;

return isWebEnvironment
? loadEntryDom({
Expand Down
8 changes: 6 additions & 2 deletions packages/runtime-core/src/utils/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
RemoteWithEntry,
ModuleInfo,
RemoteEntryType,
isBrowserEnv,
isBrowserEnvValue,
isReactNativeEnv,
} from '@module-federation/sdk';
import { Remote, RemoteInfoOptionalVersion } from '../type';
Expand Down Expand Up @@ -89,7 +89,11 @@ export function getRemoteEntryInfoFromSnapshot(snapshot: ModuleInfo): {
type: 'global',
globalName: '',
};
if (isBrowserEnv() || isReactNativeEnv() || !('ssrRemoteEntry' in snapshot)) {
if (
isBrowserEnvValue ||
isReactNativeEnv() ||
!('ssrRemoteEntry' in snapshot)
) {
return 'remoteEntry' in snapshot
? {
url: snapshot.remoteEntry,
Expand Down
12 changes: 9 additions & 3 deletions packages/sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
// The SDK can be used to parse entry strings, encode and decode module names, and generate filenames for exposed modules and shared packages.
// It also includes a logger for debugging and environment detection utilities.
// Additionally, it provides a function to generate a snapshot from a manifest and environment detection utilities.
import { parseEntry, encodeName, decodeName, generateExposeFilename, generateShareFilename, createLogger, isBrowserEnv, isDebugMode, getProcessEnv, generateSnapshotFromManifest } from '@module-federation/sdk';
import { parseEntry, encodeName, decodeName, generateExposeFilename, generateShareFilename, createLogger, isBrowserEnv, isBrowserEnvValue, isDebugMode, getProcessEnv, generateSnapshotFromManifest } from '@module-federation/sdk';

// Parse an entry string into a RemoteEntryInfo object
parseEntry('entryString');
Expand All @@ -32,7 +32,8 @@ generateShareFilename('packageName', true);
const logger = createLogger('identifier');

// Check if the current environment is a browser
isBrowserEnv();
const inBrowser = isBrowserEnv();
const inBrowserStatic = isBrowserEnvValue;

// Check if the current environment is in debug mode
isDebugMode();
Expand Down Expand Up @@ -76,9 +77,14 @@ generateSnapshotFromManifest(manifest, options);

### isBrowserEnv

- Type: `isBrowserEnv()`
- Type: `isBrowserEnv(): boolean`
- Checks if the current environment is a browser.

### isBrowserEnvValue

- Type: `isBrowserEnvValue: boolean`
- Static browser environment flag (tree-shakable when ENV_TARGET is defined).

### isDebugMode

- Type: `isDebugMode()`
Expand Down
31 changes: 21 additions & 10 deletions packages/sdk/__tests__/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
import { getResourceUrl } from '../src/utils';
import { ModuleInfo } from '../src/types';
import { isBrowserEnv, isReactNativeEnv } from '../src/env';
import * as env from '../src/env';

jest.mock('../src/env', () => ({
isBrowserEnv: jest.fn(),
isReactNativeEnv: jest.fn(),
}));
jest.mock('../src/env', () => {
const mock = {
isBrowserEnvValue: false,
isBrowserEnv: jest.fn(() => mock.isBrowserEnvValue),
isReactNativeEnv: jest.fn(),
};
return mock;
});

const mockedEnv = env as unknown as {
isBrowserEnvValue: boolean;
isBrowserEnv: jest.Mock;
isReactNativeEnv: jest.Mock;
};

describe('getResourceUrl', () => {
let module: ModuleInfo;
let sourceUrl: string;

beforeEach(() => {
sourceUrl = 'test.js';
(isBrowserEnv as jest.Mock).mockReset();
(isReactNativeEnv as jest.Mock).mockReset();
mockedEnv.isBrowserEnvValue = false;
mockedEnv.isBrowserEnv.mockClear();
mockedEnv.isReactNativeEnv.mockReset();
});

test('should return url with getPublicPath', () => {
Expand All @@ -34,7 +45,7 @@ describe('getResourceUrl', () => {
test('should return url with publicPath in browser or RN env', () => {
const publicPath = 'https://public.com/';
module = { publicPath } as ModuleInfo;
(isBrowserEnv as jest.Mock).mockReturnValue(true);
mockedEnv.isBrowserEnvValue = true;
const result = getResourceUrl(module, sourceUrl);
expect(result).toBe('https://public.com/test.js');
});
Expand All @@ -43,8 +54,8 @@ describe('getResourceUrl', () => {
const publicPath = 'https://public.com/';
const ssrPublicPath = 'https://ssr.com/';
module = { publicPath, ssrPublicPath } as ModuleInfo;
(isBrowserEnv as jest.Mock).mockReturnValue(false);
(isReactNativeEnv as jest.Mock).mockReturnValue(false);
mockedEnv.isBrowserEnvValue = false;
mockedEnv.isReactNativeEnv.mockReturnValue(false);
const result = getResourceUrl(module, sourceUrl);
expect(result).toBe('https://ssr.com/test.js');
});
Expand Down
22 changes: 17 additions & 5 deletions packages/sdk/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@ declare global {
var FEDERATION_DEBUG: string | undefined;
}

// Declare the ENV_TARGET constant that will be defined by DefinePlugin
declare const ENV_TARGET: 'web' | 'node';

const isBrowserEnvValue =
typeof ENV_TARGET !== 'undefined'
? ENV_TARGET === 'web'
: typeof window !== 'undefined' && typeof window.document !== 'undefined';

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recompute browser detection at call time

This change memoizes browser detection at module evaluation (isBrowserEnvValue) and then has isBrowserEnv() return that cached value, so imports that happen before DOM globals are attached (for example in SSR/test harness bootstraps that set window later) will permanently report non-browser and route runtime code down node-only paths. Previously isBrowserEnv() re-checked window/document each call, so this is a behavior regression in dynamic initialization contexts.

Useful? React with 👍 / 👎.


function isBrowserEnv(): boolean {
return (
typeof window !== 'undefined' && typeof window.document !== 'undefined'
);
return isBrowserEnvValue;
}

function isReactNativeEnv(): boolean {
Expand All @@ -19,7 +25,7 @@ function isReactNativeEnv(): boolean {

function isBrowserDebug() {
try {
if (isBrowserEnv() && window.localStorage) {
if (isBrowserEnvValue && window.localStorage) {
return Boolean(localStorage.getItem(BROWSER_LOG_KEY));
}
} catch (error) {
Expand Down Expand Up @@ -48,4 +54,10 @@ const getProcessEnv = function (): Record<string, string | undefined> {
return typeof process !== 'undefined' && process.env ? process.env : {};
};

export { isBrowserEnv, isReactNativeEnv, isDebugMode, getProcessEnv };
export {
isBrowserEnv,
isBrowserEnvValue,
isReactNativeEnv,
isDebugMode,
getProcessEnv,
};
8 changes: 6 additions & 2 deletions packages/sdk/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
SEPARATOR,
MANIFEST_EXT,
} from './constant';
import { getProcessEnv, isBrowserEnv, isReactNativeEnv } from './env';
import { getProcessEnv, isBrowserEnvValue, isReactNativeEnv } from './env';

const LOG_CATEGORY = '[ Federation Runtime ]';

Expand Down Expand Up @@ -189,7 +189,11 @@ const getResourceUrl = (module: ModuleInfo, sourceUrl: string): string => {

return `${publicPath}${sourceUrl}`;
} else if ('publicPath' in module) {
if (!isBrowserEnv() && !isReactNativeEnv() && 'ssrPublicPath' in module) {
if (
!isBrowserEnvValue &&
!isReactNativeEnv() &&
'ssrPublicPath' in module
) {
return `${module.ssrPublicPath}${sourceUrl}`;
}
return `${module.publicPath}${sourceUrl}`;
Expand Down
Loading
Loading