Repository: web-infra-dev/rsbuild (TypeScript)

Issue:
[Bug][1.0.0-alpha.2]: filename in multi-environments does not work
### Version

```sh
System:
    OS: macOS 14.5
    CPU: (10) arm64 Apple M1 Max
    Memory: 131.17 MB / 32.00 GB
    Shell: 5.9 - /bin/zsh
  Browsers:
    Chrome: 126.0.6478.127
    Chrome Canary: 128.0.6573.0
    Safari: 17.5
  npmPackages:
    @rsbuild/core: 1.0.0-alpha.0 => 1.0.0-alpha.0 
    @rsbuild/plugin-react: 1.0.0-alpha.0 => 1.0.0-alpha.0
```


### Details

```typescript
// rsbuild.config.ts
import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';

export default defineConfig({
  plugins: [pluginReact()],
  environments: {
    web: {
      output: {
        distPath: {
          root: 'doc_build',
        },
      },
    },
    node: {
      output: {
        target: 'node',
        distPath: {
          root: 'dist_ssr',
        },
        filename: {
          js: 'main.cjs',
        },
      },
    },
  },
});

```
### actual

```
├── dist_ssr
│   ├── index.js
│   └── index.js.LICENSE.txt
```

### expected

```
├── dist_ssr
│   ├── main.cjs
│   └── main.cjs.LICENSE.txt
```

### guess

The priority of merge config is wrong.

### Reproduce link

https://github.com/SoonIter/rsbuild-environment-filename.git

### Reproduce Steps

1. ni
2. nr build



Fix (patch):
diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts
index c599fa7241..bdfe236893 100644
--- a/packages/core/src/config.ts
+++ b/packages/core/src/config.ts
@@ -32,7 +32,6 @@ import {
   HMR_SOCKET_PATH,
   HTML_DIST_DIR,
   IMAGE_DIST_DIR,
-  JS_DIST_DIR,
   MEDIA_DIST_DIR,
   ROOT_DIST_DIR,
   SVG_DIST_DIR,
@@ -130,7 +129,6 @@ const getDefaultOutputConfig = (): NormalizedOutputConfig => ({
   target: 'web',
   distPath: {
     root: ROOT_DIST_DIR,
-    js: JS_DIST_DIR,
     css: CSS_DIST_DIR,
     svg: SVG_DIST_DIR,
     font: FONT_DIST_DIR,
diff --git a/packages/core/src/helpers.ts b/packages/core/src/helpers.ts
index 39b8b10211..de8306684f 100644
--- a/packages/core/src/helpers.ts
+++ b/packages/core/src/helpers.ts
@@ -432,16 +432,19 @@ export function getFilename(
   config: NormalizedConfig | NormalizedEnvironmentConfig,
   type: 'js',
   isProd: boolean,
+  isServer?: boolean,
 ): NonNullable<FilenameConfig['js']>;
 export function getFilename(
   config: NormalizedConfig | NormalizedEnvironmentConfig,
   type: Exclude<keyof FilenameConfig, 'js'>,
   isProd: boolean,
+  isServer?: boolean,
 ): string;
 export function getFilename(
   config: NormalizedConfig | NormalizedEnvironmentConfig,
   type: keyof FilenameConfig,
   isProd: boolean,
+  isServer?: boolean,
 ) {
   const { filename, filenameHash } = config.output;
 
@@ -456,7 +459,7 @@ export function getFilename(
 
   switch (type) {
     case 'js':
-      return filename.js ?? `[name]${isProd ? hash : ''}.js`;
+      return filename.js ?? `[name]${isProd && !isServer ? hash : ''}.js`;
     case 'css':
       return filename.css ?? `[name]${isProd ? hash : ''}.css`;
     case 'svg':
diff --git a/packages/core/src/plugins/output.ts b/packages/core/src/plugins/output.ts
index 939205fb14..09dadc6cf7 100644
--- a/packages/core/src/plugins/output.ts
+++ b/packages/core/src/plugins/output.ts
@@ -52,6 +52,21 @@ function getPublicPath({
   return formatPublicPath(publicPath);
 }
 
+const getJsAsyncPath = (
+  jsPath: string,
+  isServer: boolean,
+  jsAsync?: string,
+) => {
+  if (jsAsync !== undefined) {
+    return jsAsync;
+  }
+  if (isServer) {
+    return jsPath;
+  }
+
+  return jsPath ? `${jsPath}/async` : 'async';
+};
+
 export const pluginOutput = (): RsbuildPlugin => ({
   name: 'rsbuild:output',
 
@@ -68,10 +83,12 @@ export const pluginOutput = (): RsbuildPlugin => ({
 
         // js output
         const jsPath = config.output.distPath.js;
-        const jsAsyncPath =
-          config.output.distPath.jsAsync ??
-          (jsPath ? `${jsPath}/async` : 'async');
-        const jsFilename = getFilename(config, 'js', isProd);
+        const jsAsyncPath = getJsAsyncPath(
+          jsPath,
+          isServer,
+          config.output.distPath.jsAsync,
+        );
+        const jsFilename = getFilename(config, 'js', isProd, isServer);
         const isJsFilenameFn = typeof jsFilename === 'function';
 
         chain.output
@@ -102,13 +119,10 @@ export const pluginOutput = (): RsbuildPlugin => ({
           .hashFunction('xxhash64');
 
         if (isServer) {
-          chain.output
-            .filename('[name].js')
-            .chunkFilename('[name].js')
-            .library({
-              ...(chain.output.get('library') || {}),
-              type: 'commonjs2',
-            });
+          chain.output.library({
+            type: 'commonjs2',
+            ...(chain.output.get('library') || {}),
+          });
         }
 
         if (config.output.copy && api.context.bundlerType === 'rspack') {
diff --git a/packages/core/src/provider/initConfigs.ts b/packages/core/src/provider/initConfigs.ts
index bce513b7f6..9d0940b83a 100644
--- a/packages/core/src/provider/initConfigs.ts
+++ b/packages/core/src/provider/initConfigs.ts
@@ -8,6 +8,7 @@ import type {
   RspackConfig,
 } from '@rsbuild/shared';
 import { getDefaultEntry, normalizeConfig } from '../config';
+import { JS_DIST_DIR } from '../constants';
 import {
   updateContextByNormalizedConfig,
   updateEnvironmentContext,
@@ -73,6 +74,19 @@ const initEnvironmentConfigs = (
     normalizedConfig;
   const { assetPrefix, lazyCompilation } = dev;
 
+  const applyEnvironmentDefaultConfig = (config: MergedEnvironmentConfig) => {
+    if (!config.source.entry) {
+      config.source.entry = getDefaultEntryWithMemo();
+    }
+
+    const isServer = config.output.target === 'node';
+    if (config.output.distPath.js === undefined) {
+      config.output.distPath.js = isServer ? '' : JS_DIST_DIR;
+    }
+
+    return config;
+  };
+
   if (environments && Object.keys(environments).length) {
     return Object.fromEntries(
       Object.entries(environments).map(([name, config]) => {
@@ -89,27 +103,20 @@ const initEnvironmentConfigs = (
           ) as unknown as MergedEnvironmentConfig),
         };
 
-        if (!environmentConfig.source.entry) {
-          environmentConfig.source.entry = getDefaultEntryWithMemo();
-        }
-
-        return [name, environmentConfig];
+        return [name, applyEnvironmentDefaultConfig(environmentConfig)];
       }),
     );
   }
 
   return {
-    [camelCase(rsbuildSharedConfig.output.target)]: {
-      ...rsbuildSharedConfig,
-      source: {
-        ...rsbuildSharedConfig.source,
-        entry: rsbuildSharedConfig.source.entry ?? getDefaultEntryWithMemo(),
-      },
-      dev: {
-        assetPrefix,
-        lazyCompilation,
-      },
-    } as MergedEnvironmentConfig,
+    [camelCase(rsbuildSharedConfig.output.target)]:
+      applyEnvironmentDefaultConfig({
+        ...rsbuildSharedConfig,
+        dev: {
+          assetPrefix,
+          lazyCompilation,
+        },
+      } as MergedEnvironmentConfig),
   };
 };
 
diff --git a/packages/shared/src/types/config/index.ts b/packages/shared/src/types/config/index.ts
index 639cb287c1..39ad5161e5 100644
--- a/packages/shared/src/types/config/index.ts
+++ b/packages/shared/src/types/config/index.ts
@@ -3,7 +3,11 @@ import type { RsbuildPlugins } from '..';
 import type { DeepReadonly } from '../utils';
 import type { DevConfig, NormalizedDevConfig } from './dev';
 import type { HtmlConfig, NormalizedHtmlConfig } from './html';
-import type { NormalizedOutputConfig, OutputConfig } from './output';
+import type {
+  DistPathConfig,
+  NormalizedOutputConfig,
+  OutputConfig,
+} from './output';
 import type {
   NormalizedPerformanceConfig,
   PerformanceConfig,
@@ -102,7 +106,12 @@ export type MergedEnvironmentConfig = {
   html: NormalizedHtmlConfig;
   tools: NormalizedToolsConfig;
   source: NormalizedSourceConfig;
-  output: NormalizedOutputConfig;
+  output: Omit<NormalizedOutputConfig, 'distPath'> & {
+    distPath: Omit<Required<DistPathConfig>, 'jsAsync' | 'cssAsync'> & {
+      jsAsync?: string;
+      cssAsync?: string;
+    };
+  };
   plugins?: RsbuildPlugins;
   security: NormalizedSecurityConfig;
   performance: NormalizedPerformanceConfig;
@@ -116,7 +125,7 @@ export type NormalizedEnvironmentConfig = DeepReadonly<{
   tools: NormalizedToolsConfig;
   source: NormalizedSourceConfig;
   server: NormalizedServerConfig;
-  output: NormalizedOutputConfig;
+  output: MergedEnvironmentConfig['output'];
   plugins?: RsbuildPlugins;
   security: NormalizedSecurityConfig;
   performance: NormalizedPerformanceConfig;
diff --git a/packages/shared/src/types/config/output.ts b/packages/shared/src/types/config/output.ts
index 271c320e20..bd9538d57b 100644
--- a/packages/shared/src/types/config/output.ts
+++ b/packages/shared/src/types/config/output.ts
@@ -300,7 +300,8 @@ export interface OutputConfig {
 export interface NormalizedOutputConfig extends OutputConfig {
   target: RsbuildTarget;
   filename: FilenameConfig;
-  distPath: Omit<Required<DistPathConfig>, 'jsAsync' | 'cssAsync'> & {
+  distPath: Omit<Required<DistPathConfig>, 'jsAsync' | 'cssAsync' | 'js'> & {
+    js?: string;
     jsAsync?: string;
     cssAsync?: string;
   };
diff --git a/website/docs/en/config/output/dist-path.mdx b/website/docs/en/config/output/dist-path.mdx
index 41ab50fb83..167712f286 100644
--- a/website/docs/en/config/output/dist-path.mdx
+++ b/website/docs/en/config/output/dist-path.mdx
@@ -24,8 +24,8 @@ type DistPathConfig = {
 const defaultDistPath = {
   root: 'dist',
   html: '/',
-  js: 'static/js',
-  jsAsync: 'static/js/async',
+  js: isServer ? '' : 'static/js',
+  jsAsync: isServer ? '' : 'static/js/async',
   css: 'static/css',
   cssAsync: 'static/css/async',
   svg: 'static/svg',
diff --git a/website/docs/en/config/output/filename.mdx b/website/docs/en/config/output/filename.mdx
index 463cf3170a..13b7071224 100644
--- a/website/docs/en/config/output/filename.mdx
+++ b/website/docs/en/config/output/filename.mdx
@@ -30,7 +30,7 @@ const devDefaultFilename = {
 
 // Production
 const prodDefaultFilename = {
-  js: '[name].[contenthash:8].js',
+  js: isServer ? '[name].js' : '[name].[contenthash:8].js',
   css: '[name].[contenthash:8].css',
   svg: '[name].[contenthash:8].svg',
   font: '[name].[contenthash:8][ext]',
diff --git a/website/docs/zh/config/output/dist-path.mdx b/website/docs/zh/config/output/dist-path.mdx
index b7d108c7bc..83763acbd7 100644
--- a/website/docs/zh/config/output/dist-path.mdx
+++ b/website/docs/zh/config/output/dist-path.mdx
@@ -24,8 +24,8 @@ type DistPathConfig = {
 const defaultDistPath = {
   root: 'dist',
   html: '/',
-  js: 'static/js',
-  jsAsync: 'static/js/async',
+  js: isServer ? '' : 'static/js',
+  jsAsync: isServer ? '' : 'static/js/async',
   css: 'static/css',
   cssAsync: 'static/css/async',
   svg: 'static/svg',
diff --git a/website/docs/zh/config/output/filename.mdx b/website/docs/zh/config/output/filename.mdx
index ba20cdc111..8076eeb21f 100644
--- a/website/docs/zh/config/output/filename.mdx
+++ b/website/docs/zh/config/output/filename.mdx
@@ -30,7 +30,7 @@ const devDefaultFilename = {
 
 // 生产环境
 const prodDefaultFilename = {
-  js: '[name].[contenthash:8].js',
+  js: isServer ? '[name].js' : '[name].[contenthash:8].js',
   css: '[name].[contenthash:8].css',
   svg: '[name].[contenthash:8].svg',
   font: '[name].[contenthash:8][ext]',