diff --git a/packages/start-plugin-core/src/rsbuild/planning.ts b/packages/start-plugin-core/src/rsbuild/planning.ts index e6518ad2e6..2f79d4d0c5 100644 --- a/packages/start-plugin-core/src/rsbuild/planning.ts +++ b/packages/start-plugin-core/src/rsbuild/planning.ts @@ -73,6 +73,7 @@ export function createRsbuildEnvironmentPlan(opts: { clientOutputDirectory: string serverOutputDirectory: string publicBase: string + assetPrefix?: string | undefined serverFnProviderEnv: string environmentOverrides?: RsbuildEnvironmentOverrides scriptFormat?: ScriptFormat @@ -106,6 +107,18 @@ export function createRsbuildEnvironmentPlan(opts: { ) } + // When the user set output.assetPrefix, forward it. When they set + // server.base (and not assetPrefix), forward that via publicBase for + // backwards compatibility with the existing serverBase-precedence path. + // Otherwise leave assetPrefix unset so downstream plugins (e.g. Module + // Federation) and rsbuild's own default can take effect. + const clientAssetPrefix = + typeof opts.assetPrefix === 'string' + ? opts.assetPrefix + : opts.publicBase !== '/' + ? opts.publicBase + : undefined + return { environments: { [RSBUILD_ENVIRONMENT_NAMES.client]: mergeRsbuildConfig( @@ -134,7 +147,7 @@ export function createRsbuildEnvironmentPlan(opts: { media: `${RSBUILD_CLIENT_ASSETS_DIR}/media`, assets: `${RSBUILD_CLIENT_ASSETS_DIR}/assets`, }, - assetPrefix: opts.publicBase, + assetPrefix: clientAssetPrefix, }, resolve: { alias, diff --git a/packages/start-plugin-core/src/rsbuild/plugin.ts b/packages/start-plugin-core/src/rsbuild/plugin.ts index 3de4ed8c2f..b59e330c9a 100644 --- a/packages/start-plugin-core/src/rsbuild/plugin.ts +++ b/packages/start-plugin-core/src/rsbuild/plugin.ts @@ -118,6 +118,16 @@ export function tanStackStartRsbuild( const serverBase = rsbuildConfig.server?.base const assetPrefix = rsbuildConfig.output?.assetPrefix + + // rsbuild normalizes output.assetPrefix to "/" when unset, same as + // server.base. Read the original config to tell "user set /" from + // "rsbuild filled in /" — when the user didn't set it, we forward + // undefined so rsbuild's own default and downstream plugins remain in + // control of the client environment's assetPrefix. + const originalConfig = api.getRsbuildConfig('original') + const userSetAssetPrefix = + originalConfig.output?.assetPrefix !== undefined + const publicBase = normalizePublicBase( typeof serverBase === 'string' ? serverBase @@ -171,6 +181,7 @@ export function tanStackStartRsbuild( clientOutputDirectory: resolvedStartConfig.outputDirectories.client, serverOutputDirectory: resolvedStartConfig.outputDirectories.server, publicBase: resolvedStartConfig.basePaths.publicBase, + assetPrefix: userSetAssetPrefix ? assetPrefix : undefined, serverFnProviderEnv, environmentOverrides: corePluginOpts.rsbuild?.environments, scriptFormat, diff --git a/packages/start-plugin-core/tests/rsbuild/output-directory.test.ts b/packages/start-plugin-core/tests/rsbuild/output-directory.test.ts index c0185a74eb..9a16ab00db 100644 --- a/packages/start-plugin-core/tests/rsbuild/output-directory.test.ts +++ b/packages/start-plugin-core/tests/rsbuild/output-directory.test.ts @@ -134,3 +134,74 @@ describe('createRsbuildEnvironmentPlan client output', () => { ) }) }) + +describe('createRsbuildEnvironmentPlan assetPrefix', () => { + const baseOptions = { + root: '/app', + entryAliases: { + client: '/app/src/client.tsx', + server: '/app/src/server.ts', + start: '/app/src/start.ts', + router: '/app/src/router.tsx', + alias: { + 'virtual:tanstack-start-client-entry': '/app/src/client.tsx', + 'virtual:tanstack-start-server-entry': '/app/src/server.ts', + '#tanstack-start-entry': '/app/src/start.ts', + '#tanstack-router-entry': '/app/src/router.tsx', + }, + }, + clientOutputDirectory: 'dist/client', + serverOutputDirectory: 'dist/server', + publicBase: '/app/', + serverFnProviderEnv: 'ssr', + } + + test('uses publicBase when assetPrefix is not provided and publicBase is not default', () => { + const plan = createRsbuildEnvironmentPlan(baseOptions) + expect(plan.environments.client!.output?.assetPrefix).toBe('/app/') + }) + + test('leaves assetPrefix undefined when neither configured and publicBase is default', () => { + const plan = createRsbuildEnvironmentPlan({ + ...baseOptions, + publicBase: '/', + }) + // Start forwards nothing so rsbuild's own default and downstream plugins + // (e.g. Module Federation gating on `=== undefined`) stay in control. + expect(plan.environments.client!.output?.assetPrefix).toBeUndefined() + }) + + test('forwards assetPrefix verbatim when explicitly set to "auto"', () => { + const plan = createRsbuildEnvironmentPlan({ + ...baseOptions, + assetPrefix: 'auto', + }) + expect(plan.environments.client!.output?.assetPrefix).toBe('auto') + }) + + test('uses assetPrefix when explicitly set to a path', () => { + const plan = createRsbuildEnvironmentPlan({ + ...baseOptions, + assetPrefix: '/cdn/', + }) + expect(plan.environments.client!.output?.assetPrefix).toBe('/cdn/') + }) + + test('uses assetPrefix when explicitly set to a full URL', () => { + const plan = createRsbuildEnvironmentPlan({ + ...baseOptions, + assetPrefix: 'https://cdn.example.com/', + }) + expect(plan.environments.client!.output?.assetPrefix).toBe( + 'https://cdn.example.com/', + ) + }) + + test('server environment never gets an assetPrefix', () => { + const plan = createRsbuildEnvironmentPlan({ + ...baseOptions, + assetPrefix: 'https://cdn.example.com/', + }) + expect(plan.environments.ssr!.output?.assetPrefix).toBeUndefined() + }) +})