Skip to content
Open
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
2 changes: 1 addition & 1 deletion e2e/react-start/custom-server-rsbuild/express-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ if (DEVELOPMENT) {
const { default: handler } =
(await import('./dist/server/index.js')) as FetchServerEntry
const nodeHandler = toNodeHandler(handler.fetch) as NodeHttp1Handler
app.use(express.static('dist/client'))
app.use('/static', express.static('dist/client'))
app.use(async (req, res, next) => {
try {
await nodeHandler(req, res)
Expand Down
14 changes: 6 additions & 8 deletions e2e/react-start/custom-server-rsbuild/rsbuild.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { tanstackStart } from '@tanstack/react-start/plugin/rsbuild'
// non-default client chunk layout. The combination that matters for the
// repro is:
//
// - `client.output: 'iife'` — emit the client entry as a self-executing
// - `client.output.module: false` — emit the client entry as a self-executing
// script. The manifest uses plain script tags and classic script preloads;
// setting IIFE here exercises that non-module asset path.
//
Expand All @@ -18,23 +18,19 @@ import { tanstackStart } from '@tanstack/react-start/plugin/rsbuild'
// the regression this fixture covers.
//
// - `client.distPath.root` + `distPath.js: ''` — flat layout, JS at the
// dist root. Matches the path `express-server.ts` serves via
// `express.static('dist/client')`.
// dist root mounted by `express-server.ts`.
//
// - `performance.buildCache: true` — exercise the rspack persistent
// cache, including warm-restart paths.
//
// - `output.assetPrefix: '/static/'` — force manifest URLs through an
// explicit prefix.
// - `output.assetPrefix: '/static/'` — force manifest URLs through the
// explicit prefix mounted by `express-server.ts`.
export default defineConfig({
plugins: [
pluginReact(),
tanstackStart({
rsbuild: {
installDevServerMiddleware: false,
client: {
output: 'iife',
},
},
}),
],
Expand All @@ -47,9 +43,11 @@ export default defineConfig({
environments: {
client: {
output: {
module: false,
distPath: {
root: path.resolve(__dirname, 'dist/client'),
js: '',
css: ''
},
},
tools: {
Expand Down
24 changes: 14 additions & 10 deletions packages/solid-start/src/plugin/rsbuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ import type {
TanStackStartRsbuildInputConfig,
TanStackStartRsbuildPluginCoreOptions,
} from '@tanstack/start-plugin-core/rsbuild'
import type { RsbuildPlugin } from '@rsbuild/core'
import type { RsbuildConfig, RsbuildPlugin } from '@rsbuild/core'

const frameworkDefaults = {
resolve: {
conditionNames: ['solid', '...'],
},
} satisfies RsbuildConfig

export function tanstackStart(
options?: TanStackStartRsbuildInputConfig,
Expand All @@ -17,22 +23,20 @@ export function tanstackStart(
defaultEntryPaths: solidStartDefaultEntryPaths,
providerEnvironmentName: RSBUILD_ENVIRONMENT_NAMES.server,
ssrIsProvider: true,
rsbuild: {
environments: {
all: {
resolve: {
conditionNames: ['solid', '...'],
},
},
},
},
}

const basePlugin = tanStackStartRsbuild(corePluginOpts, options)

return {
name: 'tanstack-solid-start-rsbuild',
setup(api) {
api.modifyRsbuildConfig({
order: 'pre',
handler(userConfig, { mergeRsbuildConfig }) {
return mergeRsbuildConfig(frameworkDefaults, userConfig)
},
})

basePlugin.setup(api)

api.modifyBundlerChain(async (chain, { CHAIN_ID, target }) => {
Expand Down
3 changes: 3 additions & 0 deletions packages/start-plugin-core/src/config-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import type { TanStackStartOutputConfig } from './schema'
import type {
GetConfigFn,
NormalizedBasePaths,
ResolvedStartConfig,
TanStackStartCoreOptions,
} from './types'
Expand Down Expand Up @@ -111,12 +112,14 @@ export function applyResolvedBaseAndOutput(opts: {
resolvedStartConfig: ResolvedStartConfig
root: string
publicBase: string
assetBase?: NormalizedBasePaths['assetBase']
clientOutputDirectory: string
serverOutputDirectory: string
}): void {
opts.resolvedStartConfig.root = opts.root
opts.resolvedStartConfig.basePaths = createNormalizedBasePaths({
publicBase: opts.publicBase,
assetBase: opts.assetBase,
})
opts.resolvedStartConfig.outputDirectories =
createNormalizedOutputDirectories({
Expand Down
3 changes: 2 additions & 1 deletion packages/start-plugin-core/src/planning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,11 @@ export function shouldRewriteDevBasepath(opts: {

export function createNormalizedBasePaths(opts: {
publicBase: string
assetBase?: NormalizedBasePaths['assetBase']
}): NormalizedBasePaths {
return {
publicBase: opts.publicBase,
assetBase: {
assetBase: opts.assetBase ?? {
dev: opts.publicBase,
build: opts.publicBase,
},
Expand Down
238 changes: 238 additions & 0 deletions packages/start-plugin-core/src/rsbuild/enforced-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
import { isDeepStrictEqual, styleText } from 'node:util'
import { mergeRsbuildConfig } from '@rsbuild/core'
import { ENTRY_POINTS } from '../constants'
import type { RsbuildConfig } from '@rsbuild/core'

interface EnforcedConfig {
[key: string]: EnforcedConfig | true
}

interface StartRsbuildEnforcedConfig {
global: EnforcedConfig
environments: {
client: EnforcedConfig
server: EnforcedConfig
}
}

const enforcedDefineConfig = {
'process.env.TSS_SERVER_FN_BASE': true,
'import.meta.env.TSS_SERVER_FN_BASE': true,
'process.env.TSS_ROUTER_BASEPATH': true,
'import.meta.env.TSS_ROUTER_BASEPATH': true,
'process.env.TSS_DEV_SERVER': true,
'import.meta.env.TSS_DEV_SERVER': true,
'process.env.TSS_DEV_SSR_STYLES_ENABLED': true,
'import.meta.env.TSS_DEV_SSR_STYLES_ENABLED': true,
'process.env.TSS_DEV_SSR_STYLES_BASEPATH': true,
'import.meta.env.TSS_DEV_SSR_STYLES_BASEPATH': true,
'process.env.TSS_INLINE_CSS_ENABLED': true,
'import.meta.env.TSS_INLINE_CSS_ENABLED': true,
'process.env.TSS_DISABLE_CSRF_MIDDLEWARE_WARNING': true,
'import.meta.env.TSS_DISABLE_CSRF_MIDDLEWARE_WARNING': true,
} satisfies EnforcedConfig

const commonEnvironmentConfig = {
source: {
define: enforcedDefineConfig,
entry: {
index: true,
},
},
resolve: {
alias: {
[ENTRY_POINTS.client]: true,
[ENTRY_POINTS.server]: true,
[ENTRY_POINTS.start]: true,
[ENTRY_POINTS.router]: true,
'react-server-dom-rspack/server$': true,
},
},
} satisfies EnforcedConfig

/**
* Rsbuild config fields that TanStack Start owns.
*
* A `true` leaf means that Start writes the final value for that field. Keep
* user-owned fields such as `server.base`, `dev.assetPrefix`,
* `output.assetPrefix`, and `output.distPath` out of this object: Start
* consumes those values but must not claim ownership of them.
*/
const enforcedConfig = {
global: {
source: {
define: enforcedDefineConfig,
},
server: {
compress: true,
htmlFallback: true,
},
dev: {
lazyCompilation: true,
liveReload: true,
},
},
environments: {
client: {
...commonEnvironmentConfig,
output: {
target: true,
},
},
server: {
...commonEnvironmentConfig,
output: {
target: true,
},
},
},
} satisfies StartRsbuildEnforcedConfig

function findOverriddenConfig(
config: unknown,
resolvedConfig: unknown,
enforced: EnforcedConfig,
path = '',
out: Array<string> = [],
): Array<string> {
if (!isObject(config) || !isObject(resolvedConfig)) {
return out
}

for (const key in enforced) {
if (!(key in config) || !(key in resolvedConfig)) {
continue
}

const rule = enforced[key]!
const configuredValue = config[key]
const resolvedValue = resolvedConfig[key]

if (rule === true) {
if (
!isDeepStrictEqual(
comparable(configuredValue),
comparable(resolvedValue),
)
) {
out.push(path + key)
}
} else {
findOverriddenConfig(
configuredValue,
resolvedValue,
rule,
`${path}${key}.`,
out,
)
}
}

return out
}

function findRsbuildOverriddenConfig(opts: {
originalConfig: RsbuildConfig
resolvedConfig: RsbuildConfig
clientEnvironmentName: string
serverEnvironmentName: string
providerEnvironmentName: string
}): Array<string> {
const overridden = findOverriddenConfig(
opts.originalConfig,
opts.resolvedConfig,
enforcedConfig.global,
)
const originalBaseConfig = { ...opts.originalConfig }
delete originalBaseConfig.environments
const resolvedBaseConfig = { ...opts.resolvedConfig }
delete resolvedBaseConfig.environments

const environmentNames = new Set([
opts.clientEnvironmentName,
opts.serverEnvironmentName,
opts.providerEnvironmentName,
])

for (const name of environmentNames) {
const explicitEnvironment = opts.originalConfig.environments?.[name]
const originalEnvironment = mergeRsbuildConfig(
originalBaseConfig,
explicitEnvironment,
)
const resolvedEnvironment = mergeRsbuildConfig(
resolvedBaseConfig,
opts.resolvedConfig.environments?.[name],
)

// Root source.define conflicts are reported once by enforcedConfig.global.
// Only compare defines explicitly written in this environment here, or an
// inherited root define would be reported again for every environment.
if (originalEnvironment.source) {
originalEnvironment.source.define = explicitEnvironment?.source?.define
}
const roleConfig =
name === opts.clientEnvironmentName
? enforcedConfig.environments.client
: name === opts.serverEnvironmentName ||
name === opts.providerEnvironmentName
? enforcedConfig.environments.server
: undefined

if (roleConfig) {
findOverriddenConfig(
originalEnvironment,
resolvedEnvironment,
roleConfig,
`environments.${name}.`,
overridden,
)
}
}

return [...new Set(overridden)]
}

export function warnOverriddenConfig(opts: {
originalConfig: RsbuildConfig
resolvedConfig: RsbuildConfig
clientEnvironmentName: string
serverEnvironmentName: string
providerEnvironmentName: string
}): void {
const overridden = findRsbuildOverriddenConfig(opts)

if (overridden.length === 0) {
return
}

console.error(
styleText(
['bold', 'red'],
'The following Rsbuild config options will be overridden by TanStack Start:',
) + overridden.map((key) => `\n - ${key}`).join(''),
)
}

function comparable(value: unknown): unknown {
if (typeof value === 'string') {
const normalized = value.replaceAll('\\', '/')
return process.platform === 'win32' ? normalized.toLowerCase() : normalized
}

if (Array.isArray(value)) {
return value.map(comparable)
}

if (isObject(value)) {
return Object.fromEntries(
Object.entries(value).map(([key, entry]) => [key, comparable(entry)]),
)
}

return value
}

function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null
}
Loading