From 2cd6c7a8e11060aaa367cdc7115b382b1b16c3b1 Mon Sep 17 00:00:00 2001 From: Manuel Schiller Date: Mon, 22 Dec 2025 00:42:30 +0100 Subject: [PATCH 1/9] fix: correctly match middleware expressions in compiler plugin filter fixes #6169 --- .../server-functions/src/routeTree.gen.ts | 22 +++++ .../src/routes/middleware/index.tsx | 8 ++ .../middleware/server-import-middleware.tsx | 82 +++++++++++++++++++ .../tests/server-functions.spec.ts | 20 +++++ .../src/create-server-fn-plugin/plugin.ts | 5 +- 5 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 e2e/react-start/server-functions/src/routes/middleware/server-import-middleware.tsx diff --git a/e2e/react-start/server-functions/src/routeTree.gen.ts b/e2e/react-start/server-functions/src/routeTree.gen.ts index 0f14d59ad7b..ad1492ba6d6 100644 --- a/e2e/react-start/server-functions/src/routeTree.gen.ts +++ b/e2e/react-start/server-functions/src/routeTree.gen.ts @@ -34,6 +34,7 @@ import { Route as CookiesIndexRouteImport } from './routes/cookies/index' import { Route as AbortSignalIndexRouteImport } from './routes/abort-signal/index' import { Route as RedirectTestTargetRouteImport } from './routes/redirect-test/target' import { Route as RedirectTestSsrTargetRouteImport } from './routes/redirect-test-ssr/target' +import { Route as MiddlewareServerImportMiddlewareRouteImport } from './routes/middleware/server-import-middleware' import { Route as MiddlewareSendServerFnRouteImport } from './routes/middleware/send-serverFn' import { Route as MiddlewareRequestMiddlewareRouteImport } from './routes/middleware/request-middleware' import { Route as MiddlewareClientMiddlewareRouterRouteImport } from './routes/middleware/client-middleware-router' @@ -166,6 +167,12 @@ const RedirectTestSsrTargetRoute = RedirectTestSsrTargetRouteImport.update({ path: '/redirect-test-ssr/target', getParentRoute: () => rootRouteImport, } as any) +const MiddlewareServerImportMiddlewareRoute = + MiddlewareServerImportMiddlewareRouteImport.update({ + id: '/middleware/server-import-middleware', + path: '/middleware/server-import-middleware', + getParentRoute: () => rootRouteImport, + } as any) const MiddlewareSendServerFnRoute = MiddlewareSendServerFnRouteImport.update({ id: '/middleware/send-serverFn', path: '/middleware/send-serverFn', @@ -221,6 +228,7 @@ export interface FileRoutesByFullPath { '/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute '/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute '/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute + '/middleware/server-import-middleware': typeof MiddlewareServerImportMiddlewareRoute '/redirect-test-ssr/target': typeof RedirectTestSsrTargetRoute '/redirect-test/target': typeof RedirectTestTargetRoute '/abort-signal': typeof AbortSignalIndexRoute @@ -254,6 +262,7 @@ export interface FileRoutesByTo { '/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute '/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute '/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute + '/middleware/server-import-middleware': typeof MiddlewareServerImportMiddlewareRoute '/redirect-test-ssr/target': typeof RedirectTestSsrTargetRoute '/redirect-test/target': typeof RedirectTestTargetRoute '/abort-signal': typeof AbortSignalIndexRoute @@ -288,6 +297,7 @@ export interface FileRoutesById { '/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute '/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute '/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute + '/middleware/server-import-middleware': typeof MiddlewareServerImportMiddlewareRoute '/redirect-test-ssr/target': typeof RedirectTestSsrTargetRoute '/redirect-test/target': typeof RedirectTestTargetRoute '/abort-signal/': typeof AbortSignalIndexRoute @@ -323,6 +333,7 @@ export interface FileRouteTypes { | '/middleware/client-middleware-router' | '/middleware/request-middleware' | '/middleware/send-serverFn' + | '/middleware/server-import-middleware' | '/redirect-test-ssr/target' | '/redirect-test/target' | '/abort-signal' @@ -356,6 +367,7 @@ export interface FileRouteTypes { | '/middleware/client-middleware-router' | '/middleware/request-middleware' | '/middleware/send-serverFn' + | '/middleware/server-import-middleware' | '/redirect-test-ssr/target' | '/redirect-test/target' | '/abort-signal' @@ -389,6 +401,7 @@ export interface FileRouteTypes { | '/middleware/client-middleware-router' | '/middleware/request-middleware' | '/middleware/send-serverFn' + | '/middleware/server-import-middleware' | '/redirect-test-ssr/target' | '/redirect-test/target' | '/abort-signal/' @@ -423,6 +436,7 @@ export interface RootRouteChildren { MiddlewareClientMiddlewareRouterRoute: typeof MiddlewareClientMiddlewareRouterRoute MiddlewareRequestMiddlewareRoute: typeof MiddlewareRequestMiddlewareRoute MiddlewareSendServerFnRoute: typeof MiddlewareSendServerFnRoute + MiddlewareServerImportMiddlewareRoute: typeof MiddlewareServerImportMiddlewareRoute RedirectTestSsrTargetRoute: typeof RedirectTestSsrTargetRoute RedirectTestTargetRoute: typeof RedirectTestTargetRoute AbortSignalIndexRoute: typeof AbortSignalIndexRoute @@ -613,6 +627,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof RedirectTestSsrTargetRouteImport parentRoute: typeof rootRouteImport } + '/middleware/server-import-middleware': { + id: '/middleware/server-import-middleware' + path: '/middleware/server-import-middleware' + fullPath: '/middleware/server-import-middleware' + preLoaderRoute: typeof MiddlewareServerImportMiddlewareRouteImport + parentRoute: typeof rootRouteImport + } '/middleware/send-serverFn': { id: '/middleware/send-serverFn' path: '/middleware/send-serverFn' @@ -679,6 +700,7 @@ const rootRouteChildren: RootRouteChildren = { MiddlewareClientMiddlewareRouterRoute: MiddlewareClientMiddlewareRouterRoute, MiddlewareRequestMiddlewareRoute: MiddlewareRequestMiddlewareRoute, MiddlewareSendServerFnRoute: MiddlewareSendServerFnRoute, + MiddlewareServerImportMiddlewareRoute: MiddlewareServerImportMiddlewareRoute, RedirectTestSsrTargetRoute: RedirectTestSsrTargetRoute, RedirectTestTargetRoute: RedirectTestTargetRoute, AbortSignalIndexRoute: AbortSignalIndexRoute, diff --git a/e2e/react-start/server-functions/src/routes/middleware/index.tsx b/e2e/react-start/server-functions/src/routes/middleware/index.tsx index 1119e6bfea9..060d16f6359 100644 --- a/e2e/react-start/server-functions/src/routes/middleware/index.tsx +++ b/e2e/react-start/server-functions/src/routes/middleware/index.tsx @@ -33,6 +33,14 @@ function RouteComponent() { Request Middleware in combination with server function +
  • + + Server imports in middleware are stripped from client build + +
  • ) diff --git a/e2e/react-start/server-functions/src/routes/middleware/server-import-middleware.tsx b/e2e/react-start/server-functions/src/routes/middleware/server-import-middleware.tsx new file mode 100644 index 00000000000..cdcb8966c6e --- /dev/null +++ b/e2e/react-start/server-functions/src/routes/middleware/server-import-middleware.tsx @@ -0,0 +1,82 @@ +import { createFileRoute } from '@tanstack/react-router' +import { createMiddleware, createServerFn } from '@tanstack/react-start' +import { getRequestHeaders } from '@tanstack/react-start/server' +import React from 'react' + +/** + * This test verifies that server-only imports (like getRequestHeaders from @tanstack/react-start/server) + * are properly removed from the client bundle when used inside createMiddleware().server(). + * + * If the .server() part is not stripped from the client build, this will fail with: + * "Module node:async_hooks has been externalized for browser compatibility" + * because @tanstack/react-start/server uses node:async_hooks internally. + */ +const serverImportMiddleware = createMiddleware({ type: 'function' }).server( + async ({ next }) => { + // Use a server-only import - this should be stripped from client build + const headers = getRequestHeaders() + const testHeader = headers.get('x-test-middleware') ?? 'missing' + + console.log('[server-import-middleware] X-Test-Middleware:', testHeader) + + return next({ + context: { + testHeader, + }, + }) + }, +) + +const serverFn = createServerFn() + .middleware([serverImportMiddleware]) + .handler(async ({ context }) => { + return { testHeader: context.testHeader } + }) + +export const Route = createFileRoute('/middleware/server-import-middleware')({ + component: RouteComponent, +}) + +function RouteComponent() { + const [result, setResult] = React.useState<{ testHeader: string } | null>( + null, + ) + const [error, setError] = React.useState(null) + + async function handleClick() { + try { + const data = await serverFn({ + headers: { 'x-test-middleware': 'test-header-value' }, + }) + setResult(data) + setError(null) + } catch (e) { + setResult(null) + setError(e instanceof Error ? e.message : String(e)) + } + } + + return ( +
    +

    Server Import in Middleware Test

    +

    + This test verifies that server-only imports (getRequestHeaders) inside + createMiddleware().server() are properly stripped from the client build. +

    + + {result && ( +
    + {result.testHeader} +
    + )} + {error && ( +
    Error: {error}
    + )} +
    + ) +} diff --git a/e2e/react-start/server-functions/tests/server-functions.spec.ts b/e2e/react-start/server-functions/tests/server-functions.spec.ts index 614e6d6b59e..5848f73a612 100644 --- a/e2e/react-start/server-functions/tests/server-functions.spec.ts +++ b/e2e/react-start/server-functions/tests/server-functions.spec.ts @@ -615,3 +615,23 @@ test('nested star re-exported server function factory middleware executes correc page.getByTestId('fn-comparison-nestedReexportedFactoryFn'), ).toContainText('equal') }) + +test('server-only imports in middleware.server() are stripped from client build', async ({ + page, +}) => { + // This test verifies that server-only imports (like getRequestHeaders from @tanstack/react-start/server) + // inside createMiddleware().server() are properly stripped from the client build. + // If the .server() part is not removed, the build would fail with node:async_hooks externalization errors. + // The fact that this page loads at all proves the server code was stripped correctly. + await page.goto('/middleware/server-import-middleware') + + await page.waitForLoadState('networkidle') + + // Click the button to call the server function with middleware + await page.getByTestId('test-server-import-middleware-btn').click() + + // Wait for the result - should contain our custom test header value + await expect( + page.getByTestId('server-import-middleware-result'), + ).toContainText('test-header-value') +}) diff --git a/packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts b/packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts index eebb000487b..790dcedb630 100644 --- a/packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts +++ b/packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts @@ -57,9 +57,12 @@ export function createServerFnPlugin(opts: { type: 'client' | 'server' }): PluginOption { // in server environments, we don't transform middleware calls + // for client environments, we need to match: + // - `.handler(` for createServerFn + // - `.server(` for createMiddleware const transformCodeFilter = environment.type === 'client' - ? [/\.\s*handler\(/, /\.\s*createMiddleware\(\)/] + ? [/\.\s*handler\(/, /\.\s*server\(/] : [/\.\s*handler\(/] return { From 460a2a168fec6bea7bfda4da642aeecc5697198d Mon Sep 17 00:00:00 2001 From: Manuel Schiller Date: Mon, 22 Dec 2025 09:33:02 +0100 Subject: [PATCH 2/9] unify into single compiler plugin --- .../src/create-server-fn-plugin/compiler.ts | 185 +++++++++++++++--- .../handleCreateIsomorphicFn.ts | 46 +++++ .../handleCreateServerFn.ts | 2 +- .../create-server-fn-plugin/handleEnvOnly.ts | 45 +++++ .../src/create-server-fn-plugin/plugin.ts | 74 +++++-- .../src/create-server-fn-plugin/utils.ts | 24 +++ packages/start-plugin-core/src/plugin.ts | 37 +--- packages/start-plugin-core/src/schema.ts | 2 +- .../src/start-compiler-plugin/compilers.ts | 176 ----------------- .../src/start-compiler-plugin/constants.ts | 5 - .../src/start-compiler-plugin/envOnly.ts | 58 ------ .../src/start-compiler-plugin/isomorphicFn.ts | 78 -------- .../src/start-compiler-plugin/plugin.ts | 111 ----------- .../src/start-compiler-plugin/utils.ts | 41 ---- .../src/start-manifest-plugin/plugin.ts | 2 +- .../src/start-router-plugin/plugin.ts | 2 +- packages/start-plugin-core/src/types.ts | 34 ++++ .../createIsomorphicFn.test.ts | 74 ++++--- .../client/createIsomorphicFnDestructured.tsx | 4 - .../createIsomorphicFnDestructuredRename.tsx | 4 - .../client/createIsomorphicFnStarImport.tsx | 4 - .../server/createIsomorphicFnDestructured.tsx | 4 - .../createIsomorphicFnDestructuredRename.tsx | 4 - .../server/createIsomorphicFnStarImport.tsx | 4 - .../createMiddleware.test.ts | 20 +- .../client/create-function-middleware.ts | 0 .../client/createMiddlewareDestructured.tsx | 0 .../createMiddlewareDestructuredRename.tsx | 0 .../client/createMiddlewareStarImport.tsx | 0 .../client/createMiddlewareValidator.tsx | 0 .../snapshots/client/createStart.tsx | 0 .../test-files/create-function-middleware.ts | 0 .../createMiddlewareDestructured.tsx | 0 .../createMiddlewareDestructuredRename.tsx | 0 .../test-files/createMiddlewareStarImport.tsx | 0 .../test-files/createMiddlewareValidator.tsx | 0 .../test-files/createStart.tsx | 0 .../createServerFn/createServerFn.test.ts | 14 +- .../tests/envOnly/envOnly.test.ts | 71 +++++-- .../snapshots/client/envOnlyDestructured.tsx | 1 - .../client/envOnlyDestructuredRename.tsx | 1 - .../snapshots/client/envOnlyStarImport.tsx | 1 - .../snapshots/server/envOnlyDestructured.tsx | 1 - .../server/envOnlyDestructuredRename.tsx | 1 - .../snapshots/server/envOnlyStarImport.tsx | 1 - 45 files changed, 489 insertions(+), 642 deletions(-) create mode 100644 packages/start-plugin-core/src/create-server-fn-plugin/handleCreateIsomorphicFn.ts create mode 100644 packages/start-plugin-core/src/create-server-fn-plugin/handleEnvOnly.ts create mode 100644 packages/start-plugin-core/src/create-server-fn-plugin/utils.ts delete mode 100644 packages/start-plugin-core/src/start-compiler-plugin/compilers.ts delete mode 100644 packages/start-plugin-core/src/start-compiler-plugin/constants.ts delete mode 100644 packages/start-plugin-core/src/start-compiler-plugin/envOnly.ts delete mode 100644 packages/start-plugin-core/src/start-compiler-plugin/isomorphicFn.ts delete mode 100644 packages/start-plugin-core/src/start-compiler-plugin/plugin.ts delete mode 100644 packages/start-plugin-core/src/start-compiler-plugin/utils.ts create mode 100644 packages/start-plugin-core/src/types.ts rename packages/start-plugin-core/tests/{createMiddleware-create-server-fn-plugin => createMiddleware}/createMiddleware.test.ts (90%) rename packages/start-plugin-core/tests/{createMiddleware-create-server-fn-plugin => createMiddleware}/snapshots/client/create-function-middleware.ts (100%) rename packages/start-plugin-core/tests/{createMiddleware-create-server-fn-plugin => createMiddleware}/snapshots/client/createMiddlewareDestructured.tsx (100%) rename packages/start-plugin-core/tests/{createMiddleware-create-server-fn-plugin => createMiddleware}/snapshots/client/createMiddlewareDestructuredRename.tsx (100%) rename packages/start-plugin-core/tests/{createMiddleware-create-server-fn-plugin => createMiddleware}/snapshots/client/createMiddlewareStarImport.tsx (100%) rename packages/start-plugin-core/tests/{createMiddleware-create-server-fn-plugin => createMiddleware}/snapshots/client/createMiddlewareValidator.tsx (100%) rename packages/start-plugin-core/tests/{createMiddleware-create-server-fn-plugin => createMiddleware}/snapshots/client/createStart.tsx (100%) rename packages/start-plugin-core/tests/{createMiddleware-create-server-fn-plugin => createMiddleware}/test-files/create-function-middleware.ts (100%) rename packages/start-plugin-core/tests/{createMiddleware-create-server-fn-plugin => createMiddleware}/test-files/createMiddlewareDestructured.tsx (100%) rename packages/start-plugin-core/tests/{createMiddleware-create-server-fn-plugin => createMiddleware}/test-files/createMiddlewareDestructuredRename.tsx (100%) rename packages/start-plugin-core/tests/{createMiddleware-create-server-fn-plugin => createMiddleware}/test-files/createMiddlewareStarImport.tsx (100%) rename packages/start-plugin-core/tests/{createMiddleware-create-server-fn-plugin => createMiddleware}/test-files/createMiddlewareValidator.tsx (100%) rename packages/start-plugin-core/tests/{createMiddleware-create-server-fn-plugin => createMiddleware}/test-files/createStart.tsx (100%) diff --git a/packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts b/packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts index f201d17a794..33f45a5bbf0 100644 --- a/packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts +++ b/packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts @@ -8,6 +8,8 @@ import { } from 'babel-dead-code-elimination' import { handleCreateServerFn } from './handleCreateServerFn' import { handleCreateMiddleware } from './handleCreateMiddleware' +import { handleCreateIsomorphicFn } from './handleCreateIsomorphicFn' +import { handleEnvOnlyFn } from './handleEnvOnly' import type { MethodChainPaths, RewriteCandidate } from './types' type Binding = @@ -30,31 +32,85 @@ type ExportEntry = type Kind = 'None' | `Root` | `Builder` | LookupKind -export type LookupKind = 'ServerFn' | 'Middleware' +export type LookupKind = + | 'ServerFn' + | 'Middleware' + | 'IsomorphicFn' + | 'ServerOnlyFn' + | 'ClientOnlyFn' + +// Detection strategy for each kind +type MethodChainSetup = { + type: 'methodChain' + candidateCallIdentifier: Set + // If true, a call to the root function (e.g., createIsomorphicFn()) is also a candidate + // even without chained method calls. This is used for IsomorphicFn which can be + // called without .client() or .server() (resulting in a no-op function). + allowRootAsCandidate?: boolean +} +type DirectCallSetup = { type: 'directCall' } -const LookupSetup: Record< - LookupKind, - { candidateCallIdentifier: Set } -> = { - ServerFn: { candidateCallIdentifier: new Set(['handler']) }, +const LookupSetup: Record = { + ServerFn: { + type: 'methodChain', + candidateCallIdentifier: new Set(['handler']), + }, Middleware: { + type: 'methodChain', candidateCallIdentifier: new Set(['server', 'client', 'createMiddlewares']), }, + IsomorphicFn: { + type: 'methodChain', + candidateCallIdentifier: new Set(['server', 'client']), + allowRootAsCandidate: true, // createIsomorphicFn() alone is valid (returns no-op) + }, + ServerOnlyFn: { type: 'directCall' }, + ClientOnlyFn: { type: 'directCall' }, } -// Pre-computed map: identifier name -> LookupKind for fast candidate detection -const IdentifierToKind = new Map() +// Pre-computed map: identifier name -> Set for fast candidate detection (method chain only) +// Multiple kinds can share the same identifier (e.g., 'server' and 'client' are used by both Middleware and IsomorphicFn) +const IdentifierToKinds = new Map>() for (const [kind, setup] of Object.entries(LookupSetup) as Array< - [LookupKind, { candidateCallIdentifier: Set }] + [LookupKind, MethodChainSetup | DirectCallSetup] >) { - for (const id of setup.candidateCallIdentifier) { - IdentifierToKind.set(id, kind) + if (setup.type === 'methodChain') { + for (const id of setup.candidateCallIdentifier) { + let kinds = IdentifierToKinds.get(id) + if (!kinds) { + kinds = new Set() + IdentifierToKinds.set(id, kinds) + } + kinds.add(kind) + } + } +} + +// Check if any of the valid lookup kinds use direct call pattern +function hasDirectCallKinds(lookupKinds: Set): boolean { + for (const kind of lookupKinds) { + if (LookupSetup[kind].type === 'directCall') { + return true + } + } + return false +} + +// Check if any of the valid lookup kinds allow root calls as candidates +function hasRootAsCandidateAllowed(lookupKinds: Set): boolean { + for (const kind of lookupKinds) { + const setup = LookupSetup[kind] + if (setup.type === 'methodChain' && setup.allowRootAsCandidate) { + return true + } } + return false } export type LookupConfig = { libName: string rootExport: string + kind: LookupKind | 'Root' // 'Root' for builder pattern, LookupKind for direct call } interface ModuleInfo { id: string @@ -87,10 +143,10 @@ export class ServerFnCompiler { this.validLookupKinds = options.lookupKinds } - private async init(id: string) { + private async init() { await Promise.all( this.options.lookupConfigurations.map(async (config) => { - const libId = await this.options.resolveId(config.libName, id) + const libId = await this.options.resolveId(config.libName) if (!libId) { throw new Error(`could not resolve "${config.libName}"`) } @@ -120,7 +176,7 @@ export class ServerFnCompiler { rootModule.bindings.set(config.rootExport, { type: 'var', init: t.identifier(config.rootExport), - resolvedKind: `Root` satisfies Kind, + resolvedKind: config.kind satisfies Kind, }) this.moduleCache.set(libId, rootModule) @@ -130,7 +186,7 @@ export class ServerFnCompiler { libExports = new Map() this.knownRootImports.set(config.libName, libExports) } - libExports.set(config.rootExport, 'Root') + libExports.set(config.rootExport, config.kind) }), ) @@ -259,7 +315,7 @@ export class ServerFnCompiler { isProviderFile: boolean }) { if (!this.initialized) { - await this.init(id) + await this.init() } const { bindings, ast } = this.ingestModule({ code, id }) const candidates = this.collectCandidates(bindings) @@ -378,10 +434,20 @@ export class ServerFnCompiler { directive: this.options.directive, isProviderFile, }) - } else { + } else if (kind === 'Middleware') { handleCreateMiddleware(candidate, { env: this.options.env, }) + } else if (kind === 'IsomorphicFn') { + handleCreateIsomorphicFn(candidate, { + env: this.options.env, + }) + } else { + // ServerOnlyFn or ClientOnlyFn + handleEnvOnlyFn(candidate, { + env: this.options.env, + kind, + }) } } @@ -397,15 +463,37 @@ export class ServerFnCompiler { // collects all candidate CallExpressions at top-level private collectCandidates(bindings: Map) { const candidates: Array = [] + const checkDirectCalls = hasDirectCallKinds(this.validLookupKinds) + const hasRootAsCandidateKinds = hasRootAsCandidateAllowed( + this.validLookupKinds, + ) for (const binding of bindings.values()) { - if (binding.type === 'var') { - const candidate = isCandidateCallExpression( + if (binding.type === 'var' && t.isCallExpression(binding.init)) { + // Pattern 1: Method chain pattern (.handler(), .server(), etc.) + const methodChainCandidate = isCandidateCallExpression( binding.init, this.validLookupKinds, ) - if (candidate) { - candidates.push(candidate) + if (methodChainCandidate) { + candidates.push(methodChainCandidate) + continue + } + + // Pattern 2: Direct call pattern + // Handles: + // - createServerOnlyFn(), createClientOnlyFn() (direct call kinds) + // - createIsomorphicFn() (root-as-candidate kinds) + // - TanStackStart.createServerOnlyFn() (namespace calls) + if (checkDirectCalls || hasRootAsCandidateKinds) { + if ( + t.isIdentifier(binding.init.callee) || + (t.isMemberExpression(binding.init.callee) && + t.isIdentifier(binding.init.callee.property)) + ) { + // Include as candidate - kind resolution will verify it's actually a known export + candidates.push(binding.init) + } } } } @@ -588,6 +676,19 @@ export class ServerFnCompiler { visited, ) if (calleeKind === 'Root' || calleeKind === 'Builder') { + // For kinds that allow root as candidate (e.g., IsomorphicFn), + // a call to the root function directly should return that kind. + // This handles both direct calls (createIsomorphicFn()) and + // namespace calls (TanStackStart.createIsomorphicFn()) + if (calleeKind === 'Root') { + for (const kind of this.validLookupKinds) { + const setup = LookupSetup[kind] + if (setup.type === 'methodChain' && setup.allowRootAsCandidate) { + // The callee already resolved to 'Root', so this kind allows root as candidate + return kind + } + } + } return 'Builder' } // Use direct Set.has() instead of iterating @@ -617,18 +718,25 @@ export class ServerFnCompiler { if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) { const prop = callee.property.name + // Check method chain patterns for each valid lookup kind + const serverFnSetup = LookupSetup['ServerFn'] if ( this.validLookupKinds.has('ServerFn') && - LookupSetup['ServerFn'].candidateCallIdentifier.has(prop) + serverFnSetup.type === 'methodChain' && + serverFnSetup.candidateCallIdentifier.has(prop) ) { const base = await this.resolveExprKind(callee.object, fileId, visited) if (base === 'Root' || base === 'Builder') { return 'ServerFn' } return 'None' - } else if ( + } + + const middlewareSetup = LookupSetup['Middleware'] + if ( this.validLookupKinds.has('Middleware') && - LookupSetup['Middleware'].candidateCallIdentifier.has(prop) + middlewareSetup.type === 'methodChain' && + middlewareSetup.candidateCallIdentifier.has(prop) ) { const base = await this.resolveExprKind(callee.object, fileId, visited) if (base === 'Root' || base === 'Builder' || base === 'Middleware') { @@ -636,6 +744,21 @@ export class ServerFnCompiler { } return 'None' } + + const isomorphicSetup = LookupSetup['IsomorphicFn'] + if ( + this.validLookupKinds.has('IsomorphicFn') && + isomorphicSetup.type === 'methodChain' && + isomorphicSetup.candidateCallIdentifier.has(prop) + ) { + const base = await this.resolveExprKind(callee.object, fileId, visited) + // Allow chaining: createIsomorphicFn().server().client() or .client().server() + if (base === 'Root' || base === 'Builder' || base === 'IsomorphicFn') { + return 'IsomorphicFn' + } + return 'None' + } + // Check if the object is a namespace import if (t.isIdentifier(callee.object)) { const info = await this.getModuleInfo(fileId) @@ -704,10 +827,16 @@ function isCandidateCallExpression( return undefined } - // Use pre-computed map for O(1) lookup instead of iterating over lookupKinds - const kind = IdentifierToKind.get(callee.property.name) - if (kind && lookupKinds.has(kind)) { - return node + // Use pre-computed map for O(1) lookup + // IdentifierToKinds maps identifier -> Set to handle shared identifiers + const possibleKinds = IdentifierToKinds.get(callee.property.name) + if (possibleKinds) { + // Check if any of the possible kinds are in the valid lookup kinds + for (const kind of possibleKinds) { + if (lookupKinds.has(kind)) { + return node + } + } } return undefined diff --git a/packages/start-plugin-core/src/create-server-fn-plugin/handleCreateIsomorphicFn.ts b/packages/start-plugin-core/src/create-server-fn-plugin/handleCreateIsomorphicFn.ts new file mode 100644 index 00000000000..6e2d7c6be92 --- /dev/null +++ b/packages/start-plugin-core/src/create-server-fn-plugin/handleCreateIsomorphicFn.ts @@ -0,0 +1,46 @@ +import * as t from '@babel/types' +import type { RewriteCandidate } from './types' + +export function handleCreateIsomorphicFn( + candidate: RewriteCandidate, + opts: { env: 'client' | 'server' }, +) { + const { path, methodChain } = candidate + + // Get the environment-specific call (.client() or .server()) + const envCallInfo = + opts.env === 'client' ? methodChain.client : methodChain.server + + // Check if we have any implementation at all + if (!methodChain.client && !methodChain.server) { + // No implementations provided - warn and replace with no-op + const variableId = path.parentPath.isVariableDeclarator() + ? path.parentPath.node.id + : null + console.warn( + 'createIsomorphicFn called without a client or server implementation!', + 'This will result in a no-op function.', + 'Variable name:', + t.isIdentifier(variableId) ? variableId.name : 'unknown', + ) + path.replaceWith(t.arrowFunctionExpression([], t.blockStatement([]))) + return + } + + if (!envCallInfo) { + // No implementation for this environment - replace with no-op + path.replaceWith(t.arrowFunctionExpression([], t.blockStatement([]))) + return + } + + // Extract the function argument from the environment-specific call + const innerFn = envCallInfo.firstArgPath?.node + + if (!t.isExpression(innerFn)) { + throw new Error( + `createIsomorphicFn().${opts.env}(func) must be called with a function!`, + ) + } + + path.replaceWith(innerFn) +} diff --git a/packages/start-plugin-core/src/create-server-fn-plugin/handleCreateServerFn.ts b/packages/start-plugin-core/src/create-server-fn-plugin/handleCreateServerFn.ts index 8837e93c7ad..112d99f420d 100644 --- a/packages/start-plugin-core/src/create-server-fn-plugin/handleCreateServerFn.ts +++ b/packages/start-plugin-core/src/create-server-fn-plugin/handleCreateServerFn.ts @@ -1,5 +1,5 @@ import * as t from '@babel/types' -import { codeFrameError } from '../start-compiler-plugin/utils' +import { codeFrameError } from './utils' import type { RewriteCandidate } from './types' /** diff --git a/packages/start-plugin-core/src/create-server-fn-plugin/handleEnvOnly.ts b/packages/start-plugin-core/src/create-server-fn-plugin/handleEnvOnly.ts new file mode 100644 index 00000000000..206d13eab48 --- /dev/null +++ b/packages/start-plugin-core/src/create-server-fn-plugin/handleEnvOnly.ts @@ -0,0 +1,45 @@ +import * as t from '@babel/types' +import type { RewriteCandidate } from './types' +import type { LookupKind } from './compiler' + +function capitalize(str: string) { + if (!str) return '' + return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase() +} + +export function handleEnvOnlyFn( + candidate: RewriteCandidate, + opts: { env: 'client' | 'server'; kind: LookupKind }, +) { + const { path } = candidate + const targetEnv = opts.kind === 'ClientOnlyFn' ? 'client' : 'server' + + if (opts.env === targetEnv) { + // Matching environment - extract the inner function + const innerFn = path.node.arguments[0] + + if (!t.isExpression(innerFn)) { + throw new Error( + `create${capitalize(targetEnv)}OnlyFn() must be called with a function!`, + ) + } + + path.replaceWith(innerFn) + } else { + // Wrong environment - replace with a function that throws an error + path.replaceWith( + t.arrowFunctionExpression( + [], + t.blockStatement([ + t.throwStatement( + t.newExpression(t.identifier('Error'), [ + t.stringLiteral( + `create${capitalize(targetEnv)}OnlyFn() functions can only be called on the ${targetEnv}!`, + ), + ]), + ), + ]), + ), + ) + } +} diff --git a/packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts b/packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts index 790dcedb630..9ff8573a61f 100644 --- a/packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts +++ b/packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts @@ -1,7 +1,7 @@ import { TRANSFORM_ID_REGEX } from '../constants' +import { CompileStartFrameworkOptions } from '../types' import { ServerFnCompiler } from './compiler' import type { LookupConfig, LookupKind } from './compiler' -import type { CompileStartFrameworkOptions } from '../start-compiler-plugin/compilers' import type { PluginOption } from 'vite' function cleanId(id: string): string { @@ -9,33 +9,65 @@ function cleanId(id: string): string { } const LookupKindsPerEnv: Record<'client' | 'server', Set> = { - client: new Set(['Middleware', 'ServerFn'] as const), - server: new Set(['ServerFn'] as const), + client: new Set([ + 'Middleware', + 'ServerFn', + 'IsomorphicFn', + 'ServerOnlyFn', + 'ClientOnlyFn', + ] as const), + server: new Set([ + 'ServerFn', + 'IsomorphicFn', + 'ServerOnlyFn', + 'ClientOnlyFn', + ] as const), } const getLookupConfigurationsForEnv = ( env: 'client' | 'server', framework: CompileStartFrameworkOptions, ): Array => { - const createServerFnConfig: LookupConfig = { - libName: `@tanstack/${framework}-start`, - rootExport: 'createServerFn', - } + // Common configs for all environments + const commonConfigs: Array = [ + { + libName: `@tanstack/${framework}-start`, + rootExport: 'createServerFn', + kind: 'Root', + }, + { + libName: `@tanstack/${framework}-start`, + rootExport: 'createIsomorphicFn', + kind: 'Root', + }, + { + libName: `@tanstack/${framework}-start`, + rootExport: 'createServerOnlyFn', + kind: 'ServerOnlyFn', + }, + { + libName: `@tanstack/${framework}-start`, + rootExport: 'createClientOnlyFn', + kind: 'ClientOnlyFn', + }, + ] + if (env === 'client') { return [ { libName: `@tanstack/${framework}-start`, rootExport: 'createMiddleware', + kind: 'Root', }, { libName: `@tanstack/${framework}-start`, rootExport: 'createStart', + kind: 'Root', }, - - createServerFnConfig, + ...commonConfigs, ] } else { - return [createServerFnConfig] + return commonConfigs } } const SERVER_FN_LOOKUP = 'server-fn-module-lookup' @@ -44,6 +76,13 @@ function buildDirectiveSplitParam(directive: string) { return `tsr-directive-${directive.replace(/[^a-zA-Z0-9]/g, '-')}` } + +const commonTransformCodeFilter = [ + /\.\s*handler\(/, + /createIsomorphicFn/, + /createServerOnlyFn/, + /createClientOnlyFn/, +] export function createServerFnPlugin(opts: { framework: CompileStartFrameworkOptions directive: string @@ -56,14 +95,19 @@ export function createServerFnPlugin(opts: { name: string type: 'client' | 'server' }): PluginOption { - // in server environments, we don't transform middleware calls - // for client environments, we need to match: + // Code filter patterns for transform functions: // - `.handler(` for createServerFn - // - `.server(` for createMiddleware + // - `createMiddleware(` for middleware (client only) + // - `createIsomorphicFn` for isomorphic functions + // - `createServerOnlyFn` for server-only functions + // - `createClientOnlyFn` for client-only functions const transformCodeFilter = environment.type === 'client' - ? [/\.\s*handler\(/, /\.\s*server\(/] - : [/\.\s*handler\(/] + ? [ + ...commonTransformCodeFilter, + /createMiddleware\s*\(/, + ] + : commonTransformCodeFilter return { name: `tanstack-start-core::server-fn:${environment.name}`, diff --git a/packages/start-plugin-core/src/create-server-fn-plugin/utils.ts b/packages/start-plugin-core/src/create-server-fn-plugin/utils.ts new file mode 100644 index 00000000000..68d60a4fb12 --- /dev/null +++ b/packages/start-plugin-core/src/create-server-fn-plugin/utils.ts @@ -0,0 +1,24 @@ +import { codeFrameColumns } from '@babel/code-frame' + +export function codeFrameError( + code: string, + loc: { + start: { line: number; column: number } + end: { line: number; column: number } + }, + message: string, +) { + const frame = codeFrameColumns( + code, + { + start: loc.start, + end: loc.end, + }, + { + highlightCode: true, + message, + }, + ) + + return new Error(frame) +} diff --git a/packages/start-plugin-core/src/plugin.ts b/packages/start-plugin-core/src/plugin.ts index 1a9a74045f1..8666803ce23 100644 --- a/packages/start-plugin-core/src/plugin.ts +++ b/packages/start-plugin-core/src/plugin.ts @@ -6,7 +6,6 @@ import { crawlFrameworkPkgs } from 'vitefu' import { join } from 'pathe' import { escapePath } from 'tinyglobby' import { startManifestPlugin } from './start-manifest-plugin/plugin' -import { startCompilerPlugin } from './start-compiler-plugin/plugin' import { ENTRY_POINTS, VITE_ENVIRONMENT_NAMES } from './constants' import { tanStackStartRouter } from './start-router-plugin/plugin' import { loadEnvPlugin } from './load-env-plugin/plugin' @@ -26,38 +25,7 @@ import type { TanStackStartOutputConfig, } from './schema' import type { PluginOption } from 'vite' -import type { CompileStartFrameworkOptions } from './start-compiler-plugin/compilers' - -export interface TanStackStartVitePluginCoreOptions { - framework: CompileStartFrameworkOptions - defaultEntryPaths: { - client: string - server: string - start: string - } - serverFn?: { - directive?: string - ssr?: { - getServerFnById?: string - } - providerEnv?: string - } -} - -export interface ResolvedStartConfig { - root: string - startFilePath: string | undefined - routerFilePath: string - srcDirectory: string - viteAppBase: string - serverFnProviderEnv: string -} - -export type GetConfigFn = () => { - startConfig: TanStackStartOutputConfig - resolvedStartConfig: ResolvedStartConfig - corePluginOpts: TanStackStartVitePluginCoreOptions -} +import { TanStackStartVitePluginCoreOptions, ResolvedStartConfig, GetConfigFn } from './types' function isFullUrl(str: string): boolean { try { @@ -431,7 +399,8 @@ export function TanStackStartVitePluginCore( envName: serverFnProviderEnv, }, }), - startCompilerPlugin({ framework: corePluginOpts.framework, environments }), + // Note: startCompilerPlugin functionality (createIsomorphicFn, createServerOnlyFn, createClientOnlyFn) + // is now merged into createServerFnPlugin above loadEnvPlugin(), startManifestPlugin({ getClientBundle: () => getBundle(VITE_ENVIRONMENT_NAMES.client), diff --git a/packages/start-plugin-core/src/schema.ts b/packages/start-plugin-core/src/schema.ts index 7dec32effda..f26dfdb0393 100644 --- a/packages/start-plugin-core/src/schema.ts +++ b/packages/start-plugin-core/src/schema.ts @@ -1,7 +1,7 @@ import path from 'node:path' import { z } from 'zod' import { configSchema, getConfig } from '@tanstack/router-plugin' -import type { TanStackStartVitePluginCoreOptions } from './plugin' +import { TanStackStartVitePluginCoreOptions } from './types' const tsrConfig = configSchema .omit({ autoCodeSplitting: true, target: true, verboseFileRoutes: true }) diff --git a/packages/start-plugin-core/src/start-compiler-plugin/compilers.ts b/packages/start-plugin-core/src/start-compiler-plugin/compilers.ts deleted file mode 100644 index df80fbf1608..00000000000 --- a/packages/start-plugin-core/src/start-compiler-plugin/compilers.ts +++ /dev/null @@ -1,176 +0,0 @@ -import * as babel from '@babel/core' -import * as t from '@babel/types' - -import { - deadCodeElimination, - findReferencedIdentifiers, -} from 'babel-dead-code-elimination' -import { generateFromAst, parseAst } from '@tanstack/router-utils' -import { transformFuncs } from './constants' -import { handleCreateIsomorphicFnCallExpression } from './isomorphicFn' -import { - handleCreateClientOnlyFnCallExpression, - handleCreateServerOnlyFnCallExpression, -} from './envOnly' -import type { GeneratorResult, ParseAstOptions } from '@tanstack/router-utils' - -export type CompileStartFrameworkOptions = 'react' | 'solid' | 'vue' - -type Identifiers = { [K in (typeof transformFuncs)[number]]: IdentifierConfig } - -export function compileStartOutputFactory( - framework: CompileStartFrameworkOptions, -) { - return function compileStartOutput(opts: CompileOptions): GeneratorResult { - const identifiers: Partial = { - createServerOnlyFn: { - name: 'createServerOnlyFn', - handleCallExpression: handleCreateServerOnlyFnCallExpression, - paths: [], - }, - createClientOnlyFn: { - name: 'createClientOnlyFn', - handleCallExpression: handleCreateClientOnlyFnCallExpression, - paths: [], - }, - createIsomorphicFn: { - name: 'createIsomorphicFn', - handleCallExpression: handleCreateIsomorphicFnCallExpression, - paths: [], - }, - } - - const ast = parseAst(opts) - - const doDce = opts.dce ?? true - // find referenced identifiers *before* we transform anything - const refIdents = doDce ? findReferencedIdentifiers(ast) : undefined - - const validImportSources = [ - `@tanstack/${framework}-start`, - '@tanstack/start-client-core', - ] - babel.traverse(ast, { - Program: { - enter(programPath) { - programPath.traverse({ - ImportDeclaration: (path) => { - if (!validImportSources.includes(path.node.source.value)) { - return - } - - // handle a destructured imports being renamed like "import { createServerFn as myCreateServerFn } from '@tanstack/react-start';" - path.node.specifiers.forEach((specifier) => { - transformFuncs.forEach((identifierKey) => { - const identifier = identifiers[identifierKey] - if (!identifier) { - return - } - if ( - specifier.type === 'ImportSpecifier' && - specifier.imported.type === 'Identifier' - ) { - if (specifier.imported.name === identifierKey) { - identifier.name = specifier.local.name - } - } - - // handle namespace imports like "import * as TanStackStart from '@tanstack/react-start';" - if (specifier.type === 'ImportNamespaceSpecifier') { - identifier.name = `${specifier.local.name}.${identifierKey}` - } - }) - }) - }, - CallExpression: (path) => { - transformFuncs.forEach((identifierKey) => { - const identifier = identifiers[identifierKey] - if (!identifier) { - return - } - // Check to see if the call expression is a call to the - // identifiers[identifierKey].name - if ( - t.isIdentifier(path.node.callee) && - path.node.callee.name === identifier.name - ) { - // The identifier could be a call to the original function - // in the source code. If this is case, we need to ignore it. - // Check the scope to see if the identifier is a function declaration. - // if it is, then we can ignore it. - - if ( - path.scope.getBinding(identifier.name)?.path.node.type === - 'FunctionDeclaration' - ) { - return - } - - return identifier.paths.push(path) - } - - // handle namespace imports like "import * as TanStackStart from '@tanstack/react-start';" - // which are then called like "TanStackStart.createServerFn()" - if (t.isMemberExpression(path.node.callee)) { - if ( - t.isIdentifier(path.node.callee.object) && - t.isIdentifier(path.node.callee.property) - ) { - const callname = [ - path.node.callee.object.name, - path.node.callee.property.name, - ].join('.') - - if (callname === identifier.name) { - identifier.paths.push(path) - } - } - } - - return - }) - }, - }) - - transformFuncs.forEach((identifierKey) => { - const identifier = identifiers[identifierKey] - if (!identifier) { - return - } - identifier.paths.forEach((path) => { - identifier.handleCallExpression( - path as babel.NodePath, - opts, - ) - }) - }) - }, - }, - }) - - if (doDce) { - deadCodeElimination(ast, refIdents) - } - - return generateFromAst(ast, { - sourceMaps: true, - sourceFileName: opts.filename, - filename: opts.filename, - }) - } -} - -export type CompileOptions = ParseAstOptions & { - env: 'server' | 'client' - dce?: boolean - filename: string -} - -export type IdentifierConfig = { - name: string - handleCallExpression: ( - path: babel.NodePath, - opts: CompileOptions, - ) => void - paths: Array -} diff --git a/packages/start-plugin-core/src/start-compiler-plugin/constants.ts b/packages/start-plugin-core/src/start-compiler-plugin/constants.ts deleted file mode 100644 index d5416a2c03e..00000000000 --- a/packages/start-plugin-core/src/start-compiler-plugin/constants.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const transformFuncs = [ - 'createServerOnlyFn', - 'createClientOnlyFn', - 'createIsomorphicFn', -] as const diff --git a/packages/start-plugin-core/src/start-compiler-plugin/envOnly.ts b/packages/start-plugin-core/src/start-compiler-plugin/envOnly.ts deleted file mode 100644 index c3c0a00f1cc..00000000000 --- a/packages/start-plugin-core/src/start-compiler-plugin/envOnly.ts +++ /dev/null @@ -1,58 +0,0 @@ -import * as t from '@babel/types' -import type * as babel from '@babel/core' - -import type { CompileOptions } from './compilers' - -function capitalize(str: string) { - if (!str) return '' - return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase() -} - -function buildEnvOnlyCallExpressionHandler(env: 'client' | 'server') { - return function envOnlyCallExpressionHandler( - path: babel.NodePath, - opts: CompileOptions, - ) { - // if (debug) - // console.info(`Handling ${env}Only call expression:`, path.toString()) - - const isEnvMatch = - env === 'client' ? opts.env === 'client' : opts.env === 'server' - - if (isEnvMatch) { - // extract the inner function from the call expression - const innerInputExpression = path.node.arguments[0] - - if (!t.isExpression(innerInputExpression)) { - throw new Error( - `${env}Only() functions must be called with a function!`, - ) - } - - path.replaceWith(innerInputExpression) - return - } - - // If we're on the wrong environment, replace the call expression - // with a function that always throws an error. - path.replaceWith( - t.arrowFunctionExpression( - [], - t.blockStatement([ - t.throwStatement( - t.newExpression(t.identifier('Error'), [ - t.stringLiteral( - `create${capitalize(env)}OnlyFn() functions can only be called on the ${env}!`, - ), - ]), - ), - ]), - ), - ) - } -} - -export const handleCreateServerOnlyFnCallExpression = - buildEnvOnlyCallExpressionHandler('server') -export const handleCreateClientOnlyFnCallExpression = - buildEnvOnlyCallExpressionHandler('client') diff --git a/packages/start-plugin-core/src/start-compiler-plugin/isomorphicFn.ts b/packages/start-plugin-core/src/start-compiler-plugin/isomorphicFn.ts deleted file mode 100644 index b9b68e678ac..00000000000 --- a/packages/start-plugin-core/src/start-compiler-plugin/isomorphicFn.ts +++ /dev/null @@ -1,78 +0,0 @@ -import * as t from '@babel/types' -import { getRootCallExpression } from './utils' -import type * as babel from '@babel/core' - -import type { CompileOptions } from './compilers' - -export function handleCreateIsomorphicFnCallExpression( - path: babel.NodePath, - opts: CompileOptions, -) { - const rootCallExpression = getRootCallExpression(path) - - // if (debug) - // console.info( - // 'Handling createIsomorphicFn call expression:', - // rootCallExpression.toString(), - // ) - - const callExpressionPaths = { - client: null as babel.NodePath | null, - server: null as babel.NodePath | null, - } - - const validMethods = Object.keys(callExpressionPaths) - - rootCallExpression.traverse({ - MemberExpression(memberExpressionPath) { - if (t.isIdentifier(memberExpressionPath.node.property)) { - const name = memberExpressionPath.node.property - .name as keyof typeof callExpressionPaths - - if ( - validMethods.includes(name) && - memberExpressionPath.parentPath.isCallExpression() - ) { - callExpressionPaths[name] = memberExpressionPath.parentPath - } - } - }, - }) - - if ( - validMethods.every( - (method) => - !callExpressionPaths[method as keyof typeof callExpressionPaths], - ) - ) { - const variableId = rootCallExpression.parentPath.isVariableDeclarator() - ? rootCallExpression.parentPath.node.id - : null - console.warn( - 'createIsomorphicFn called without a client or server implementation!', - 'This will result in a no-op function.', - 'Variable name:', - t.isIdentifier(variableId) ? variableId.name : 'unknown', - ) - } - - const envCallExpression = callExpressionPaths[opts.env] - - if (!envCallExpression) { - // if we don't have an implementation for this environment, default to a no-op - rootCallExpression.replaceWith( - t.arrowFunctionExpression([], t.blockStatement([])), - ) - return - } - - const innerInputExpression = envCallExpression.node.arguments[0] - - if (!t.isExpression(innerInputExpression)) { - throw new Error( - `createIsomorphicFn().${opts.env}(func) must be called with a function!`, - ) - } - - rootCallExpression.replaceWith(innerInputExpression) -} diff --git a/packages/start-plugin-core/src/start-compiler-plugin/plugin.ts b/packages/start-plugin-core/src/start-compiler-plugin/plugin.ts deleted file mode 100644 index 8d8ed6cd61e..00000000000 --- a/packages/start-plugin-core/src/start-compiler-plugin/plugin.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { fileURLToPath, pathToFileURL } from 'node:url' -import { createRequire } from 'node:module' -import { logDiff } from '@tanstack/router-utils' - -import { VIRTUAL_MODULES } from '@tanstack/start-server-core' -import { normalizePath } from 'vite' -import path from 'pathe' -import { makeIdFiltersToMatchWithQuery } from '@rolldown/pluginutils' -import { TRANSFORM_ID_REGEX } from '../constants' -import { compileStartOutputFactory } from './compilers' -import { transformFuncs } from './constants' -import type { Plugin, PluginOption } from 'vite' -import type { CompileStartFrameworkOptions } from './compilers' - -const debug = - process.env.TSR_VITE_DEBUG && - ['true', 'start-plugin'].includes(process.env.TSR_VITE_DEBUG) - -export type TanStackStartViteOptions = { - globalMiddlewareEntry: string -} - -const tokenRegex = new RegExp(transformFuncs.join('|')) - -const require = createRequire(import.meta.url) - -function resolveRuntimeFiles(opts: { package: string; files: Array }) { - const pkgRoot = resolvePackage(opts.package) - const basePath = path.join(pkgRoot, 'dist', 'esm') - return opts.files.map((file) => normalizePath(path.join(basePath, file))) -} - -function resolvePackage(packageName: string): string { - const pkgRoot = path.dirname(require.resolve(packageName + '/package.json')) - return pkgRoot -} - -const transformFilter = { - code: tokenRegex, - id: { - include: TRANSFORM_ID_REGEX, - exclude: [ - VIRTUAL_MODULES.serverFnManifest, - // N.B. the following files either just re-export or provide the runtime implementation of those functions - // we do not want to include them in the transformation - // however, those packages (especially start-client-core ATM) also USE these functions - // (namely `createIsomorphicFn` in `packages/start-client-core/src/getRouterInstance.ts`) and thus need to be transformed - ...makeIdFiltersToMatchWithQuery([ - ...resolveRuntimeFiles({ - package: '@tanstack/start-client-core', - files: [ - 'index.js', - 'createIsomorphicFn.js', - 'envOnly.js', - 'serverFnFetcher.js', - 'createStart.js', - 'createMiddleware.js', - ], - }), - ...resolveRuntimeFiles({ - package: '@tanstack/start-server-core', - files: ['index.js', 'server-functions-handler.js'], - }), - ]), - ], - }, -} - -export function startCompilerPlugin(opts: { - framework: CompileStartFrameworkOptions - environments: Array<{ name: string; type: 'client' | 'server' }> -}): PluginOption { - const compileStartOutput = compileStartOutputFactory(opts.framework) - - function perEnvCompilerPlugin(environment: { - name: string - type: 'client' | 'server' - }): Plugin { - return { - name: `tanstack-start-core:compiler:${environment.name}`, - enforce: 'pre', - applyToEnvironment(env) { - return env.name === environment.name - }, - transform: { - filter: transformFilter, - handler(code, id) { - const url = pathToFileURL(id) - url.searchParams.delete('v') - id = fileURLToPath(url).replace(/\\/g, '/') - - if (debug) console.info(`${environment.name} Compiling Start: `, id) - - const compiled = compileStartOutput({ - code, - filename: id, - env: environment.type, - }) - - if (debug) { - logDiff(code, compiled.code) - console.log('Output:\n', compiled.code + '\n\n') - } - - return compiled - }, - }, - } - } - return opts.environments.map(perEnvCompilerPlugin) -} diff --git a/packages/start-plugin-core/src/start-compiler-plugin/utils.ts b/packages/start-plugin-core/src/start-compiler-plugin/utils.ts deleted file mode 100644 index dcfa6014a28..00000000000 --- a/packages/start-plugin-core/src/start-compiler-plugin/utils.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { codeFrameColumns } from '@babel/code-frame' -import type * as t from '@babel/types' -import type * as babel from '@babel/core' - -export function getRootCallExpression(path: babel.NodePath) { - // Find the highest callExpression parent - let rootCallExpression: babel.NodePath = path - - // Traverse up the chain of CallExpressions - while (rootCallExpression.parentPath.isMemberExpression()) { - const parent = rootCallExpression.parentPath - if (parent.parentPath.isCallExpression()) { - rootCallExpression = parent.parentPath - } - } - - return rootCallExpression -} - -export function codeFrameError( - code: string, - loc: { - start: { line: number; column: number } - end: { line: number; column: number } - }, - message: string, -) { - const frame = codeFrameColumns( - code, - { - start: loc.start, - end: loc.end, - }, - { - highlightCode: true, - message, - }, - ) - - return new Error(frame) -} diff --git a/packages/start-plugin-core/src/start-manifest-plugin/plugin.ts b/packages/start-plugin-core/src/start-manifest-plugin/plugin.ts index 50fc50ad47e..f0272967099 100644 --- a/packages/start-plugin-core/src/start-manifest-plugin/plugin.ts +++ b/packages/start-plugin-core/src/start-manifest-plugin/plugin.ts @@ -4,9 +4,9 @@ import { VIRTUAL_MODULES } from '@tanstack/start-server-core' import { tsrSplit } from '@tanstack/router-plugin' import { resolveViteId } from '../utils' import { ENTRY_POINTS } from '../constants' -import type { GetConfigFn } from '../plugin' import type { PluginOption, Rollup } from 'vite' import type { Manifest, RouterManagedTag } from '@tanstack/router-core' +import { GetConfigFn } from '../types' const getCSSRecursively = ( chunk: Rollup.OutputChunk, diff --git a/packages/start-plugin-core/src/start-router-plugin/plugin.ts b/packages/start-plugin-core/src/start-router-plugin/plugin.ts index 4543eac384b..9f19ee47ffa 100644 --- a/packages/start-plugin-core/src/start-router-plugin/plugin.ts +++ b/packages/start-plugin-core/src/start-router-plugin/plugin.ts @@ -17,7 +17,7 @@ import type { } from '@tanstack/router-generator' import type { DevEnvironment, Plugin, PluginOption } from 'vite' import type { TanStackStartInputConfig } from '../schema' -import type { GetConfigFn, TanStackStartVitePluginCoreOptions } from '../plugin' +import { TanStackStartVitePluginCoreOptions, GetConfigFn } from '../types' function isServerOnlyNode(node: RouteNode | undefined) { if (!node?.createFileRouteProps) { diff --git a/packages/start-plugin-core/src/types.ts b/packages/start-plugin-core/src/types.ts new file mode 100644 index 00000000000..eecb07a1983 --- /dev/null +++ b/packages/start-plugin-core/src/types.ts @@ -0,0 +1,34 @@ +import { TanStackStartOutputConfig } from "./schema" + +export type CompileStartFrameworkOptions = 'react' | 'solid' | 'vue' + +export interface TanStackStartVitePluginCoreOptions { + framework: CompileStartFrameworkOptions + defaultEntryPaths: { + client: string + server: string + start: string + } + serverFn?: { + directive?: string + ssr?: { + getServerFnById?: string + } + providerEnv?: string + } +} + +export interface ResolvedStartConfig { + root: string + startFilePath: string | undefined + routerFilePath: string + srcDirectory: string + viteAppBase: string + serverFnProviderEnv: string +} + +export type GetConfigFn = () => { + startConfig: TanStackStartOutputConfig + resolvedStartConfig: ResolvedStartConfig + corePluginOpts: TanStackStartVitePluginCoreOptions +} \ No newline at end of file diff --git a/packages/start-plugin-core/tests/createIsomorphicFn/createIsomorphicFn.test.ts b/packages/start-plugin-core/tests/createIsomorphicFn/createIsomorphicFn.test.ts index ef65d23d0d7..a631cc11de9 100644 --- a/packages/start-plugin-core/tests/createIsomorphicFn/createIsomorphicFn.test.ts +++ b/packages/start-plugin-core/tests/createIsomorphicFn/createIsomorphicFn.test.ts @@ -2,9 +2,38 @@ import { readFile, readdir } from 'node:fs/promises' import path from 'node:path' import { afterAll, describe, expect, test, vi } from 'vitest' -import { compileStartOutputFactory } from '../../src/start-compiler-plugin/compilers' +import { ServerFnCompiler } from '../../src/create-server-fn-plugin/compiler' -const compileStartOutput = compileStartOutputFactory('react') +async function compile(opts: { + env: 'client' | 'server' + code: string + id: string +}) { + const compiler = new ServerFnCompiler({ + ...opts, + loadModule: async () => { + // do nothing in test + }, + lookupKinds: new Set(['IsomorphicFn']), + lookupConfigurations: [ + { + libName: `@tanstack/react-start`, + rootExport: 'createIsomorphicFn', + kind: 'Root', + }, + ], + resolveId: async (id) => { + return id + }, + directive: 'use server', + }) + const result = await compiler.compile({ + code: opts.code, + id: opts.id, + isProviderFile: false, + }) + return result +} async function getFilenames() { return await readdir(path.resolve(import.meta.dirname, './test-files')) @@ -38,49 +67,48 @@ describe('createIsomorphicFn compiles correctly', async () => { test.each(['client', 'server'] as const)( `should compile for ${filename} %s`, async (env) => { - const compiledResult = compileStartOutput({ + const compiledResult = await compile({ env, code, - filename, - dce: false, + id: filename, }) - await expect(compiledResult.code).toMatchFileSnapshot( + await expect(compiledResult!.code).toMatchFileSnapshot( `./snapshots/${env}/${filename}`, ) }, ) }) - test('should error if implementation not provided', () => { - expect(() => { - compileStartOutput({ + + test('should error if implementation not provided', async () => { + await expect( + compile({ env: 'client', code: ` import { createIsomorphicFn } from '@tanstack/react-start' const clientOnly = createIsomorphicFn().client()`, - filename: 'no-fn.ts', - dce: false, - }) - }).toThrowError() - expect(() => { - compileStartOutput({ + id: 'no-fn.ts', + }), + ).rejects.toThrowError() + + await expect( + compile({ env: 'server', code: ` import { createIsomorphicFn } from '@tanstack/react-start' const serverOnly = createIsomorphicFn().server()`, - filename: 'no-fn.ts', - dce: false, - }) - }).toThrowError() + id: 'no-fn.ts', + }), + ).rejects.toThrowError() }) - test('should warn to console if no implementations provided', () => { - compileStartOutput({ + + test('should warn to console if no implementations provided', async () => { + await compile({ env: 'client', code: ` import { createIsomorphicFn } from '@tanstack/react-start' const noImpl = createIsomorphicFn()`, - filename: 'no-fn.ts', - dce: false, + id: 'no-fn.ts', }) expect(consoleSpy).toHaveBeenCalledWith( noImplWarning, diff --git a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructured.tsx b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructured.tsx index 837195fb9c4..87eb2bba0ea 100644 --- a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructured.tsx +++ b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructured.tsx @@ -1,12 +1,8 @@ -import { createIsomorphicFn } from '@tanstack/react-start'; const noImpl = () => {}; const serverOnlyFn = () => {}; const clientOnlyFn = () => 'client'; const serverThenClientFn = () => 'client'; const clientThenServerFn = () => 'client'; -function abstractedServerFn() { - return 'server'; -} const serverOnlyFnAbstracted = () => {}; function abstractedClientFn() { return 'client'; diff --git a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructuredRename.tsx b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructuredRename.tsx index 4f0230416dc..87eb2bba0ea 100644 --- a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructuredRename.tsx +++ b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructuredRename.tsx @@ -1,12 +1,8 @@ -import { createIsomorphicFn as isomorphicFn } from '@tanstack/react-start'; const noImpl = () => {}; const serverOnlyFn = () => {}; const clientOnlyFn = () => 'client'; const serverThenClientFn = () => 'client'; const clientThenServerFn = () => 'client'; -function abstractedServerFn() { - return 'server'; -} const serverOnlyFnAbstracted = () => {}; function abstractedClientFn() { return 'client'; diff --git a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnStarImport.tsx b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnStarImport.tsx index 296ccdbe24b..87eb2bba0ea 100644 --- a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnStarImport.tsx +++ b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnStarImport.tsx @@ -1,12 +1,8 @@ -import * as TanStackStart from '@tanstack/react-start'; const noImpl = () => {}; const serverOnlyFn = () => {}; const clientOnlyFn = () => 'client'; const serverThenClientFn = () => 'client'; const clientThenServerFn = () => 'client'; -function abstractedServerFn() { - return 'server'; -} const serverOnlyFnAbstracted = () => {}; function abstractedClientFn() { return 'client'; diff --git a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructured.tsx b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructured.tsx index 1656889e535..f0b7d12af45 100644 --- a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructured.tsx +++ b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructured.tsx @@ -1,4 +1,3 @@ -import { createIsomorphicFn } from '@tanstack/react-start'; const noImpl = () => {}; const serverOnlyFn = () => 'server'; const clientOnlyFn = () => {}; @@ -8,9 +7,6 @@ function abstractedServerFn() { return 'server'; } const serverOnlyFnAbstracted = abstractedServerFn; -function abstractedClientFn() { - return 'client'; -} const clientOnlyFnAbstracted = () => {}; const serverThenClientFnAbstracted = abstractedServerFn; const clientThenServerFnAbstracted = abstractedServerFn; \ No newline at end of file diff --git a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructuredRename.tsx b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructuredRename.tsx index a46f061a451..f0b7d12af45 100644 --- a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructuredRename.tsx +++ b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructuredRename.tsx @@ -1,4 +1,3 @@ -import { createIsomorphicFn as isomorphicFn } from '@tanstack/react-start'; const noImpl = () => {}; const serverOnlyFn = () => 'server'; const clientOnlyFn = () => {}; @@ -8,9 +7,6 @@ function abstractedServerFn() { return 'server'; } const serverOnlyFnAbstracted = abstractedServerFn; -function abstractedClientFn() { - return 'client'; -} const clientOnlyFnAbstracted = () => {}; const serverThenClientFnAbstracted = abstractedServerFn; const clientThenServerFnAbstracted = abstractedServerFn; \ No newline at end of file diff --git a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnStarImport.tsx b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnStarImport.tsx index dc3ad9af048..f0b7d12af45 100644 --- a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnStarImport.tsx +++ b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnStarImport.tsx @@ -1,4 +1,3 @@ -import * as TanStackStart from '@tanstack/react-start'; const noImpl = () => {}; const serverOnlyFn = () => 'server'; const clientOnlyFn = () => {}; @@ -8,9 +7,6 @@ function abstractedServerFn() { return 'server'; } const serverOnlyFnAbstracted = abstractedServerFn; -function abstractedClientFn() { - return 'client'; -} const clientOnlyFnAbstracted = () => {}; const serverThenClientFnAbstracted = abstractedServerFn; const clientThenServerFnAbstracted = abstractedServerFn; \ No newline at end of file diff --git a/packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/createMiddleware.test.ts b/packages/start-plugin-core/tests/createMiddleware/createMiddleware.test.ts similarity index 90% rename from packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/createMiddleware.test.ts rename to packages/start-plugin-core/tests/createMiddleware/createMiddleware.test.ts index e26d3abeda5..2133e31cfa5 100644 --- a/packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/createMiddleware.test.ts +++ b/packages/start-plugin-core/tests/createMiddleware/createMiddleware.test.ts @@ -14,7 +14,7 @@ async function compile(opts: { }) { const compiler = new ServerFnCompiler({ ...opts, - loadModule: async (id) => { + loadModule: async () => { // do nothing in test }, lookupKinds: new Set(['Middleware']), @@ -22,10 +22,12 @@ async function compile(opts: { { libName: `@tanstack/react-start`, rootExport: 'createMiddleware', + kind: 'Root', }, { libName: `@tanstack/react-start`, rootExport: 'createStart', + kind: 'Root', }, ], resolveId: async (id) => { @@ -77,6 +79,7 @@ describe('createMiddleware compiles correctly', async () => { { libName: '@tanstack/react-start', rootExport: 'createMiddleware', + kind: 'Root', }, ], resolveId: resolveIdMock, @@ -92,11 +95,9 @@ describe('createMiddleware compiles correctly', async () => { // resolveId should only be called once during init() for the library itself // It should NOT be called again to resolve the import binding because // the fast path uses knownRootImports map for O(1) lookup + // Note: init() now resolves from project root, not from a specific file expect(resolveIdMock).toHaveBeenCalledTimes(1) - expect(resolveIdMock).toHaveBeenCalledWith( - '@tanstack/react-start', - 'test.ts', - ) + expect(resolveIdMock).toHaveBeenCalledWith('@tanstack/react-start') }) test('should use slow path for factory pattern (resolveId called for import resolution)', async () => { @@ -128,6 +129,7 @@ describe('createMiddleware compiles correctly', async () => { { libName: '@tanstack/react-start', rootExport: 'createMiddleware', + kind: 'Root', }, ], resolveId: resolveIdMock, @@ -141,17 +143,13 @@ describe('createMiddleware compiles correctly', async () => { }) // resolveId should be called exactly twice: - // 1. Once during init() for '@tanstack/react-start' + // 1. Once during init() for '@tanstack/react-start' (no importer - resolved from project root) // 2. Once to resolve './factory' import (slow path - not in knownRootImports) // // Note: The factory module's import from '@tanstack/react-start' ALSO uses // the fast path (knownRootImports), so no additional resolveId call is needed there. expect(resolveIdMock).toHaveBeenCalledTimes(2) - expect(resolveIdMock).toHaveBeenNthCalledWith( - 1, - '@tanstack/react-start', - 'test.ts', - ) + expect(resolveIdMock).toHaveBeenNthCalledWith(1, '@tanstack/react-start') expect(resolveIdMock).toHaveBeenNthCalledWith(2, './factory', 'test.ts') }) }) diff --git a/packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/snapshots/client/create-function-middleware.ts b/packages/start-plugin-core/tests/createMiddleware/snapshots/client/create-function-middleware.ts similarity index 100% rename from packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/snapshots/client/create-function-middleware.ts rename to packages/start-plugin-core/tests/createMiddleware/snapshots/client/create-function-middleware.ts diff --git a/packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/snapshots/client/createMiddlewareDestructured.tsx b/packages/start-plugin-core/tests/createMiddleware/snapshots/client/createMiddlewareDestructured.tsx similarity index 100% rename from packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/snapshots/client/createMiddlewareDestructured.tsx rename to packages/start-plugin-core/tests/createMiddleware/snapshots/client/createMiddlewareDestructured.tsx diff --git a/packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/snapshots/client/createMiddlewareDestructuredRename.tsx b/packages/start-plugin-core/tests/createMiddleware/snapshots/client/createMiddlewareDestructuredRename.tsx similarity index 100% rename from packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/snapshots/client/createMiddlewareDestructuredRename.tsx rename to packages/start-plugin-core/tests/createMiddleware/snapshots/client/createMiddlewareDestructuredRename.tsx diff --git a/packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/snapshots/client/createMiddlewareStarImport.tsx b/packages/start-plugin-core/tests/createMiddleware/snapshots/client/createMiddlewareStarImport.tsx similarity index 100% rename from packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/snapshots/client/createMiddlewareStarImport.tsx rename to packages/start-plugin-core/tests/createMiddleware/snapshots/client/createMiddlewareStarImport.tsx diff --git a/packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/snapshots/client/createMiddlewareValidator.tsx b/packages/start-plugin-core/tests/createMiddleware/snapshots/client/createMiddlewareValidator.tsx similarity index 100% rename from packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/snapshots/client/createMiddlewareValidator.tsx rename to packages/start-plugin-core/tests/createMiddleware/snapshots/client/createMiddlewareValidator.tsx diff --git a/packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/snapshots/client/createStart.tsx b/packages/start-plugin-core/tests/createMiddleware/snapshots/client/createStart.tsx similarity index 100% rename from packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/snapshots/client/createStart.tsx rename to packages/start-plugin-core/tests/createMiddleware/snapshots/client/createStart.tsx diff --git a/packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/test-files/create-function-middleware.ts b/packages/start-plugin-core/tests/createMiddleware/test-files/create-function-middleware.ts similarity index 100% rename from packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/test-files/create-function-middleware.ts rename to packages/start-plugin-core/tests/createMiddleware/test-files/create-function-middleware.ts diff --git a/packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/test-files/createMiddlewareDestructured.tsx b/packages/start-plugin-core/tests/createMiddleware/test-files/createMiddlewareDestructured.tsx similarity index 100% rename from packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/test-files/createMiddlewareDestructured.tsx rename to packages/start-plugin-core/tests/createMiddleware/test-files/createMiddlewareDestructured.tsx diff --git a/packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/test-files/createMiddlewareDestructuredRename.tsx b/packages/start-plugin-core/tests/createMiddleware/test-files/createMiddlewareDestructuredRename.tsx similarity index 100% rename from packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/test-files/createMiddlewareDestructuredRename.tsx rename to packages/start-plugin-core/tests/createMiddleware/test-files/createMiddlewareDestructuredRename.tsx diff --git a/packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/test-files/createMiddlewareStarImport.tsx b/packages/start-plugin-core/tests/createMiddleware/test-files/createMiddlewareStarImport.tsx similarity index 100% rename from packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/test-files/createMiddlewareStarImport.tsx rename to packages/start-plugin-core/tests/createMiddleware/test-files/createMiddlewareStarImport.tsx diff --git a/packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/test-files/createMiddlewareValidator.tsx b/packages/start-plugin-core/tests/createMiddleware/test-files/createMiddlewareValidator.tsx similarity index 100% rename from packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/test-files/createMiddlewareValidator.tsx rename to packages/start-plugin-core/tests/createMiddleware/test-files/createMiddlewareValidator.tsx diff --git a/packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/test-files/createStart.tsx b/packages/start-plugin-core/tests/createMiddleware/test-files/createStart.tsx similarity index 100% rename from packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/test-files/createStart.tsx rename to packages/start-plugin-core/tests/createMiddleware/test-files/createStart.tsx diff --git a/packages/start-plugin-core/tests/createServerFn/createServerFn.test.ts b/packages/start-plugin-core/tests/createServerFn/createServerFn.test.ts index 758a61a94eb..f6161ccfaa9 100644 --- a/packages/start-plugin-core/tests/createServerFn/createServerFn.test.ts +++ b/packages/start-plugin-core/tests/createServerFn/createServerFn.test.ts @@ -23,6 +23,7 @@ async function compile(opts: { { libName: `@tanstack/react-start`, rootExport: 'createServerFn', + kind: 'Root', }, ], resolveId: async (id) => { @@ -232,6 +233,7 @@ describe('createServerFn compiles correctly', async () => { { libName: '@tanstack/react-start', rootExport: 'createServerFn', + kind: 'Root', }, ], resolveId: resolveIdMock, @@ -248,10 +250,7 @@ describe('createServerFn compiles correctly', async () => { // It should NOT be called again to resolve the import binding because // the fast path uses knownRootImports map for O(1) lookup expect(resolveIdMock).toHaveBeenCalledTimes(1) - expect(resolveIdMock).toHaveBeenCalledWith( - '@tanstack/react-start', - 'test.ts', - ) + expect(resolveIdMock).toHaveBeenCalledWith('@tanstack/react-start') }) test('should use slow path for factory pattern (resolveId called for import resolution)', async () => { @@ -283,6 +282,7 @@ describe('createServerFn compiles correctly', async () => { { libName: '@tanstack/react-start', rootExport: 'createServerFn', + kind: 'Root', }, ], resolveId: resolveIdMock, @@ -302,11 +302,7 @@ describe('createServerFn compiles correctly', async () => { // Note: The factory module's import from '@tanstack/react-start' ALSO uses // the fast path (knownRootImports), so no additional resolveId call is needed there. expect(resolveIdMock).toHaveBeenCalledTimes(2) - expect(resolveIdMock).toHaveBeenNthCalledWith( - 1, - '@tanstack/react-start', - 'test.ts', - ) + expect(resolveIdMock).toHaveBeenNthCalledWith(1, '@tanstack/react-start') expect(resolveIdMock).toHaveBeenNthCalledWith(2, './factory', 'test.ts') }) }) diff --git a/packages/start-plugin-core/tests/envOnly/envOnly.test.ts b/packages/start-plugin-core/tests/envOnly/envOnly.test.ts index d68a546fb91..f3818234ab4 100644 --- a/packages/start-plugin-core/tests/envOnly/envOnly.test.ts +++ b/packages/start-plugin-core/tests/envOnly/envOnly.test.ts @@ -2,9 +2,43 @@ import { readFile, readdir } from 'node:fs/promises' import path from 'node:path' import { describe, expect, test } from 'vitest' -import { compileStartOutputFactory } from '../../src/start-compiler-plugin/compilers' +import { ServerFnCompiler } from '../../src/create-server-fn-plugin/compiler' -const compileStartOutput = compileStartOutputFactory('react') +async function compile(opts: { + env: 'client' | 'server' + code: string + id: string +}) { + const compiler = new ServerFnCompiler({ + ...opts, + loadModule: async () => { + // do nothing in test + }, + lookupKinds: new Set(['ServerOnlyFn', 'ClientOnlyFn']), + lookupConfigurations: [ + { + libName: `@tanstack/react-start`, + rootExport: 'createServerOnlyFn', + kind: 'ServerOnlyFn', + }, + { + libName: `@tanstack/react-start`, + rootExport: 'createClientOnlyFn', + kind: 'ClientOnlyFn', + }, + ], + resolveId: async (id) => { + return id + }, + directive: 'use server', + }) + const result = await compiler.compile({ + code: opts.code, + id: opts.id, + isProviderFile: false, + }) + return result +} async function getFilenames() { return await readdir(path.resolve(import.meta.dirname, './test-files')) @@ -22,39 +56,38 @@ describe('envOnly functions compile correctly', async () => { test.each(['client', 'server'] as const)( `should compile for ${filename} %s`, async (env) => { - const compiledResult = compileStartOutput({ + const compiledResult = await compile({ env, code, - filename, - dce: false, + id: filename, }) - await expect(compiledResult.code).toMatchFileSnapshot( + await expect(compiledResult!.code).toMatchFileSnapshot( `./snapshots/${env}/${filename}`, ) }, ) }) - test('should error if implementation not provided', () => { - expect(() => { - compileStartOutput({ + + test('should error if implementation not provided', async () => { + await expect( + compile({ env: 'client', code: ` import { createClientOnlyFn } from '@tanstack/react-start' const fn = createClientOnlyFn()`, - filename: 'no-fn.ts', - dce: false, - }) - }).toThrowError() - expect(() => { - compileStartOutput({ + id: 'no-fn.ts', + }), + ).rejects.toThrowError() + + await expect( + compile({ env: 'server', code: ` import { createServerOnlyFn } from '@tanstack/react-start' const fn = createServerOnlyFn()`, - filename: 'no-fn.ts', - dce: false, - }) - }).toThrowError() + id: 'no-fn.ts', + }), + ).rejects.toThrowError() }) }) diff --git a/packages/start-plugin-core/tests/envOnly/snapshots/client/envOnlyDestructured.tsx b/packages/start-plugin-core/tests/envOnly/snapshots/client/envOnlyDestructured.tsx index d94dcb06dd5..d56ad1f374f 100644 --- a/packages/start-plugin-core/tests/envOnly/snapshots/client/envOnlyDestructured.tsx +++ b/packages/start-plugin-core/tests/envOnly/snapshots/client/envOnlyDestructured.tsx @@ -1,4 +1,3 @@ -import { createServerOnlyFn, createClientOnlyFn } from '@tanstack/react-start'; const serverFunc = () => { throw new Error("createServerOnlyFn() functions can only be called on the server!"); }; diff --git a/packages/start-plugin-core/tests/envOnly/snapshots/client/envOnlyDestructuredRename.tsx b/packages/start-plugin-core/tests/envOnly/snapshots/client/envOnlyDestructuredRename.tsx index c3c0e21f6a0..d56ad1f374f 100644 --- a/packages/start-plugin-core/tests/envOnly/snapshots/client/envOnlyDestructuredRename.tsx +++ b/packages/start-plugin-core/tests/envOnly/snapshots/client/envOnlyDestructuredRename.tsx @@ -1,4 +1,3 @@ -import { createServerOnlyFn as serverFn, createClientOnlyFn as clientFn } from '@tanstack/react-start'; const serverFunc = () => { throw new Error("createServerOnlyFn() functions can only be called on the server!"); }; diff --git a/packages/start-plugin-core/tests/envOnly/snapshots/client/envOnlyStarImport.tsx b/packages/start-plugin-core/tests/envOnly/snapshots/client/envOnlyStarImport.tsx index adafc67ab16..d56ad1f374f 100644 --- a/packages/start-plugin-core/tests/envOnly/snapshots/client/envOnlyStarImport.tsx +++ b/packages/start-plugin-core/tests/envOnly/snapshots/client/envOnlyStarImport.tsx @@ -1,4 +1,3 @@ -import * as TanstackStart from '@tanstack/react-start'; const serverFunc = () => { throw new Error("createServerOnlyFn() functions can only be called on the server!"); }; diff --git a/packages/start-plugin-core/tests/envOnly/snapshots/server/envOnlyDestructured.tsx b/packages/start-plugin-core/tests/envOnly/snapshots/server/envOnlyDestructured.tsx index 7feb328db6b..5373578295e 100644 --- a/packages/start-plugin-core/tests/envOnly/snapshots/server/envOnlyDestructured.tsx +++ b/packages/start-plugin-core/tests/envOnly/snapshots/server/envOnlyDestructured.tsx @@ -1,4 +1,3 @@ -import { createServerOnlyFn, createClientOnlyFn } from '@tanstack/react-start'; const serverFunc = () => 'server'; const clientFunc = () => { throw new Error("createClientOnlyFn() functions can only be called on the client!"); diff --git a/packages/start-plugin-core/tests/envOnly/snapshots/server/envOnlyDestructuredRename.tsx b/packages/start-plugin-core/tests/envOnly/snapshots/server/envOnlyDestructuredRename.tsx index 8d8bdac72ce..5373578295e 100644 --- a/packages/start-plugin-core/tests/envOnly/snapshots/server/envOnlyDestructuredRename.tsx +++ b/packages/start-plugin-core/tests/envOnly/snapshots/server/envOnlyDestructuredRename.tsx @@ -1,4 +1,3 @@ -import { createServerOnlyFn as serverFn, createClientOnlyFn as clientFn } from '@tanstack/react-start'; const serverFunc = () => 'server'; const clientFunc = () => { throw new Error("createClientOnlyFn() functions can only be called on the client!"); diff --git a/packages/start-plugin-core/tests/envOnly/snapshots/server/envOnlyStarImport.tsx b/packages/start-plugin-core/tests/envOnly/snapshots/server/envOnlyStarImport.tsx index ce13e851258..5373578295e 100644 --- a/packages/start-plugin-core/tests/envOnly/snapshots/server/envOnlyStarImport.tsx +++ b/packages/start-plugin-core/tests/envOnly/snapshots/server/envOnlyStarImport.tsx @@ -1,4 +1,3 @@ -import * as TanstackStart from '@tanstack/react-start'; const serverFunc = () => 'server'; const clientFunc = () => { throw new Error("createClientOnlyFn() functions can only be called on the client!"); From 7fb11d08f55e9d1a890a280041ea8b60a341c30b Mon Sep 17 00:00:00 2001 From: Manuel Schiller Date: Mon, 22 Dec 2025 09:47:49 +0100 Subject: [PATCH 3/9] cleanup --- .../src/create-server-fn-plugin/compiler.ts | 69 +++++++------------ .../src/create-server-fn-plugin/plugin.ts | 11 ++- .../src/create-server-fn-plugin/types.ts | 8 --- 3 files changed, 29 insertions(+), 59 deletions(-) diff --git a/packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts b/packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts index 33f45a5bbf0..9a260e65f91 100644 --- a/packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts +++ b/packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts @@ -86,27 +86,6 @@ for (const [kind, setup] of Object.entries(LookupSetup) as Array< } } -// Check if any of the valid lookup kinds use direct call pattern -function hasDirectCallKinds(lookupKinds: Set): boolean { - for (const kind of lookupKinds) { - if (LookupSetup[kind].type === 'directCall') { - return true - } - } - return false -} - -// Check if any of the valid lookup kinds allow root calls as candidates -function hasRootAsCandidateAllowed(lookupKinds: Set): boolean { - for (const kind of lookupKinds) { - const setup = LookupSetup[kind] - if (setup.type === 'methodChain' && setup.allowRootAsCandidate) { - return true - } - } - return false -} - export type LookupConfig = { libName: string rootExport: string @@ -114,8 +93,6 @@ export type LookupConfig = { } interface ModuleInfo { id: string - code: string - ast: ReturnType bindings: Map exports: Map // Track `export * from './module'` declarations for re-export resolution @@ -126,6 +103,11 @@ export class ServerFnCompiler { private moduleCache = new Map() private initialized = false private validLookupKinds: Set + // Precomputed flags for candidate detection (avoid recomputing on each collectCandidates call) + private hasDirectCallKinds: boolean + private hasRootAsCandidateKinds: boolean + // For IsomorphicFn, we need to know which kind allows root as candidate (precomputed) + private rootAsCandidateKind: LookupKind | null = null // Fast lookup for direct imports from known libraries (e.g., '@tanstack/react-start') // Maps: libName → (exportName → Kind) // This allows O(1) resolution for the common case without async resolveId calls @@ -141,6 +123,19 @@ export class ServerFnCompiler { }, ) { this.validLookupKinds = options.lookupKinds + + // Precompute flags for candidate detection + this.hasDirectCallKinds = false + this.hasRootAsCandidateKinds = false + for (const kind of options.lookupKinds) { + const setup = LookupSetup[kind] + if (setup.type === 'directCall') { + this.hasDirectCallKinds = true + } else if (setup.allowRootAsCandidate) { + this.hasRootAsCandidateKinds = true + this.rootAsCandidateKind = kind + } + } } private async init() { @@ -154,10 +149,8 @@ export class ServerFnCompiler { if (!rootModule) { // insert root binding rootModule = { - ast: null as any, bindings: new Map(), exports: new Map(), - code: '', id: libId, reExportAllSources: [], } @@ -175,7 +168,7 @@ export class ServerFnCompiler { }) rootModule.bindings.set(config.rootExport, { type: 'var', - init: t.identifier(config.rootExport), + init: null, // Not needed since resolvedKind is set resolvedKind: config.kind satisfies Kind, }) this.moduleCache.set(libId, rootModule) @@ -290,15 +283,13 @@ export class ServerFnCompiler { } const info: ModuleInfo = { - code, id, - ast, bindings, exports, reExportAllSources, } this.moduleCache.set(id, info) - return info + return { info, ast } } public invalidateModule(id: string) { @@ -317,8 +308,8 @@ export class ServerFnCompiler { if (!this.initialized) { await this.init() } - const { bindings, ast } = this.ingestModule({ code, id }) - const candidates = this.collectCandidates(bindings) + const { info, ast } = this.ingestModule({ code, id }) + const candidates = this.collectCandidates(info.bindings) if (candidates.length === 0) { // this hook will only be invoked if there is `.handler(` | `.server(` | `.client(` in the code, // so not discovering a handler candidate is rather unlikely, but maybe possible? @@ -463,10 +454,6 @@ export class ServerFnCompiler { // collects all candidate CallExpressions at top-level private collectCandidates(bindings: Map) { const candidates: Array = [] - const checkDirectCalls = hasDirectCallKinds(this.validLookupKinds) - const hasRootAsCandidateKinds = hasRootAsCandidateAllowed( - this.validLookupKinds, - ) for (const binding of bindings.values()) { if (binding.type === 'var' && t.isCallExpression(binding.init)) { @@ -485,7 +472,7 @@ export class ServerFnCompiler { // - createServerOnlyFn(), createClientOnlyFn() (direct call kinds) // - createIsomorphicFn() (root-as-candidate kinds) // - TanStackStart.createServerOnlyFn() (namespace calls) - if (checkDirectCalls || hasRootAsCandidateKinds) { + if (this.hasDirectCallKinds || this.hasRootAsCandidateKinds) { if ( t.isIdentifier(binding.init.callee) || (t.isMemberExpression(binding.init.callee) && @@ -680,14 +667,8 @@ export class ServerFnCompiler { // a call to the root function directly should return that kind. // This handles both direct calls (createIsomorphicFn()) and // namespace calls (TanStackStart.createIsomorphicFn()) - if (calleeKind === 'Root') { - for (const kind of this.validLookupKinds) { - const setup = LookupSetup[kind] - if (setup.type === 'methodChain' && setup.allowRootAsCandidate) { - // The callee already resolved to 'Root', so this kind allows root as candidate - return kind - } - } + if (calleeKind === 'Root' && this.rootAsCandidateKind) { + return this.rootAsCandidateKind } return 'Builder' } diff --git a/packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts b/packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts index 9ff8573a61f..d76b2fbb7f5 100644 --- a/packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts +++ b/packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts @@ -1,11 +1,12 @@ import { TRANSFORM_ID_REGEX } from '../constants' -import { CompileStartFrameworkOptions } from '../types' import { ServerFnCompiler } from './compiler' +import type { CompileStartFrameworkOptions } from '../types' import type { LookupConfig, LookupKind } from './compiler' import type { PluginOption } from 'vite' function cleanId(id: string): string { - return id.split('?')[0]! + const queryIndex = id.indexOf('?') + return queryIndex === -1 ? id : id.substring(0, queryIndex) } const LookupKindsPerEnv: Record<'client' | 'server', Set> = { @@ -76,7 +77,6 @@ function buildDirectiveSplitParam(directive: string) { return `tsr-directive-${directive.replace(/[^a-zA-Z0-9]/g, '-')}` } - const commonTransformCodeFilter = [ /\.\s*handler\(/, /createIsomorphicFn/, @@ -103,10 +103,7 @@ export function createServerFnPlugin(opts: { // - `createClientOnlyFn` for client-only functions const transformCodeFilter = environment.type === 'client' - ? [ - ...commonTransformCodeFilter, - /createMiddleware\s*\(/, - ] + ? [...commonTransformCodeFilter, /createMiddleware\s*\(/] : commonTransformCodeFilter return { diff --git a/packages/start-plugin-core/src/create-server-fn-plugin/types.ts b/packages/start-plugin-core/src/create-server-fn-plugin/types.ts index 598c11c8c72..02886dc9fbc 100644 --- a/packages/start-plugin-core/src/create-server-fn-plugin/types.ts +++ b/packages/start-plugin-core/src/create-server-fn-plugin/types.ts @@ -25,14 +25,6 @@ export interface MethodChainPaths { export type MethodChainKey = keyof MethodChainPaths -export const METHOD_CHAIN_KEYS: ReadonlyArray = [ - 'middleware', - 'inputValidator', - 'handler', - 'server', - 'client', -] as const - /** * Information about a candidate that needs to be rewritten. */ From 1948064c68d8836b1543ea561a2a132ab51a8bc2 Mon Sep 17 00:00:00 2001 From: Manuel Schiller Date: Mon, 22 Dec 2025 13:48:43 +0100 Subject: [PATCH 4/9] move into separate package --- packages/start-client-core/package.json | 1 + .../src/getGlobalStartContext.ts | 2 +- .../src/getRouterInstance.ts | 2 +- .../src/getStartContextServerOnly.ts | 2 +- .../start-client-core/src/getStartOptions.ts | 2 +- packages/start-client-core/src/index.tsx | 5 +- packages/start-fn-stubs/eslint.config.js | 14 +++ packages/start-fn-stubs/package.json | 57 ++++++++++++ packages/start-fn-stubs/src/index.ts | 51 ++++++++++ packages/start-fn-stubs/tsconfig.json | 7 ++ packages/start-fn-stubs/vite.config.ts | 20 ++++ .../src/create-server-fn-plugin/compiler.ts | 92 ++++++++++--------- pnpm-lock.yaml | 29 +++--- scripts/publish.js | 4 + 14 files changed, 227 insertions(+), 61 deletions(-) create mode 100644 packages/start-fn-stubs/eslint.config.js create mode 100644 packages/start-fn-stubs/package.json create mode 100644 packages/start-fn-stubs/src/index.ts create mode 100644 packages/start-fn-stubs/tsconfig.json create mode 100644 packages/start-fn-stubs/vite.config.ts diff --git a/packages/start-client-core/package.json b/packages/start-client-core/package.json index 02d233566a2..d860d14da7c 100644 --- a/packages/start-client-core/package.json +++ b/packages/start-client-core/package.json @@ -80,6 +80,7 @@ }, "dependencies": { "@tanstack/router-core": "workspace:*", + "@tanstack/start-fn-stubs": "workspace:*", "@tanstack/start-storage-context": "workspace:*", "seroval": "^1.4.1", "tiny-invariant": "^1.3.3", diff --git a/packages/start-client-core/src/getGlobalStartContext.ts b/packages/start-client-core/src/getGlobalStartContext.ts index 20c1bcd6da3..3e1af51361a 100644 --- a/packages/start-client-core/src/getGlobalStartContext.ts +++ b/packages/start-client-core/src/getGlobalStartContext.ts @@ -1,5 +1,5 @@ import { getStartContext } from '@tanstack/start-storage-context' -import { createIsomorphicFn } from './createIsomorphicFn' +import { createIsomorphicFn } from '@tanstack/start-fn-stubs' import type { AssignAllServerRequestContext } from './createMiddleware' import type { Expand, Register } from '@tanstack/router-core' diff --git a/packages/start-client-core/src/getRouterInstance.ts b/packages/start-client-core/src/getRouterInstance.ts index 501970ed0b0..d1fc9d6301c 100644 --- a/packages/start-client-core/src/getRouterInstance.ts +++ b/packages/start-client-core/src/getRouterInstance.ts @@ -1,5 +1,5 @@ import { getStartContext } from '@tanstack/start-storage-context' -import { createIsomorphicFn } from './createIsomorphicFn' +import { createIsomorphicFn } from '@tanstack/start-fn-stubs' import type { Awaitable, RegisteredRouter } from '@tanstack/router-core' export const getRouterInstance: () => Awaitable = diff --git a/packages/start-client-core/src/getStartContextServerOnly.ts b/packages/start-client-core/src/getStartContextServerOnly.ts index 78787a49825..4b1ec43c04d 100644 --- a/packages/start-client-core/src/getStartContextServerOnly.ts +++ b/packages/start-client-core/src/getStartContextServerOnly.ts @@ -1,4 +1,4 @@ import { getStartContext } from '@tanstack/start-storage-context' -import { createServerOnlyFn } from './envOnly' +import { createServerOnlyFn } from '@tanstack/start-fn-stubs' export const getStartContextServerOnly = createServerOnlyFn(getStartContext) diff --git a/packages/start-client-core/src/getStartOptions.ts b/packages/start-client-core/src/getStartOptions.ts index 7c08e1f49a6..d290934b5fb 100644 --- a/packages/start-client-core/src/getStartOptions.ts +++ b/packages/start-client-core/src/getStartOptions.ts @@ -1,5 +1,5 @@ import { getStartContext } from '@tanstack/start-storage-context' -import { createIsomorphicFn } from './createIsomorphicFn' +import { createIsomorphicFn } from '@tanstack/start-fn-stubs' import type { AnyStartInstanceOptions } from './createStart' export const getStartOptions: () => AnyStartInstanceOptions | undefined = diff --git a/packages/start-client-core/src/index.tsx b/packages/start-client-core/src/index.tsx index 015ef25987d..eb5f7eb8f0e 100644 --- a/packages/start-client-core/src/index.tsx +++ b/packages/start-client-core/src/index.tsx @@ -4,12 +4,13 @@ export { hydrate, json, mergeHeaders } from '@tanstack/router-core/ssr/client' export { createIsomorphicFn, + createServerOnlyFn, + createClientOnlyFn, type IsomorphicFn, type ServerOnlyFn, type ClientOnlyFn, type IsomorphicFnBase, -} from './createIsomorphicFn' -export { createServerOnlyFn, createClientOnlyFn } from './envOnly' +} from '@tanstack/start-fn-stubs' export { createServerFn } from './createServerFn' export { createMiddleware, diff --git a/packages/start-fn-stubs/eslint.config.js b/packages/start-fn-stubs/eslint.config.js new file mode 100644 index 00000000000..f4c18ebca2f --- /dev/null +++ b/packages/start-fn-stubs/eslint.config.js @@ -0,0 +1,14 @@ +// @ts-check + +import rootConfig from '../../eslint.config.js' + +export default [ + ...rootConfig, + { + files: ['**/*.{ts,tsx}'], + }, + { + plugins: {}, + rules: {}, + }, +] diff --git a/packages/start-fn-stubs/package.json b/packages/start-fn-stubs/package.json new file mode 100644 index 00000000000..31c73ebc872 --- /dev/null +++ b/packages/start-fn-stubs/package.json @@ -0,0 +1,57 @@ +{ + "name": "@tanstack/start-fn-stubs", + "version": "1.142.8", + "description": "Stub functions for TanStack Start isomorphic and environment-specific functions", + "author": "Tanner Linsley", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/TanStack/router.git", + "directory": "packages/start-fn-stubs" + }, + "homepage": "https://tanstack.com/start", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "keywords": [ + "tanstack", + "start", + "isomorphic", + "server", + "client" + ], + "scripts": { + "clean": "rimraf ./dist && rimraf ./coverage", + "test": "pnpm test:eslint && pnpm test:types && pnpm test:build", + "test:eslint": "eslint ./src", + "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"", + "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js", + "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js", + "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js", + "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js", + "test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js", + "test:types:ts59": "tsc", + "test:build": "publint --strict && attw --ignore-rules no-resolution --pack .", + "build": "vite build" + }, + "type": "module", + "types": "dist/esm/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + } + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "files": [ + "dist", + "src" + ], + "engines": { + "node": ">=22.12.0" + } +} diff --git a/packages/start-fn-stubs/src/index.ts b/packages/start-fn-stubs/src/index.ts new file mode 100644 index 00000000000..16f4fbe27d9 --- /dev/null +++ b/packages/start-fn-stubs/src/index.ts @@ -0,0 +1,51 @@ +// a function that can have different implementations on the client and server. +// implementations not provided will default to a no-op function. + +export type IsomorphicFn< + TArgs extends Array = [], + TServer = undefined, + TClient = undefined, +> = (...args: TArgs) => TServer | TClient + +export interface ServerOnlyFn, TServer> + extends IsomorphicFn { + client: ( + clientImpl: (...args: TArgs) => TClient, + ) => IsomorphicFn +} + +export interface ClientOnlyFn, TClient> + extends IsomorphicFn { + server: ( + serverImpl: (...args: TArgs) => TServer, + ) => IsomorphicFn +} + +export interface IsomorphicFnBase extends IsomorphicFn { + server: , TServer>( + serverImpl: (...args: TArgs) => TServer, + ) => ServerOnlyFn + client: , TClient>( + clientImpl: (...args: TArgs) => TClient, + ) => ClientOnlyFn +} + +// this is a dummy function, it will be replaced by the transformer +// if we use `createIsomorphicFn` in this library itself, vite tries to execute it before the transformer runs +// therefore we must return a dummy function that allows calling `server` and `client` method chains. +export function createIsomorphicFn(): IsomorphicFnBase { + return { + server: () => ({ client: () => () => {} }), + client: () => ({ server: () => () => {} }), + } as any +} + +type EnvOnlyFn = ) => any>(fn: TFn) => TFn + +// A function that will only be available in the server build +// If called on the client, it will throw an error +export const createServerOnlyFn: EnvOnlyFn = (fn) => fn + +// A function that will only be available in the client build +// If called on the server, it will throw an error +export const createClientOnlyFn: EnvOnlyFn = (fn) => fn diff --git a/packages/start-fn-stubs/tsconfig.json b/packages/start-fn-stubs/tsconfig.json new file mode 100644 index 00000000000..0484835e6b5 --- /dev/null +++ b/packages/start-fn-stubs/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "module": "esnext" + }, + "include": ["src", "vite.config.ts"] +} diff --git a/packages/start-fn-stubs/vite.config.ts b/packages/start-fn-stubs/vite.config.ts new file mode 100644 index 00000000000..3c6a120dafc --- /dev/null +++ b/packages/start-fn-stubs/vite.config.ts @@ -0,0 +1,20 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import { tanstackViteConfig } from '@tanstack/config/vite' +import packageJson from './package.json' + +const config = defineConfig({ + test: { + typecheck: { enabled: true }, + name: packageJson.name, + watch: false, + }, +}) + +export default mergeConfig( + config, + tanstackViteConfig({ + srcDir: './src', + entry: './src/index.ts', + cjs: false, + }), +) diff --git a/packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts b/packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts index 9a260e65f91..1ee85e39ad7 100644 --- a/packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts +++ b/packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts @@ -91,6 +91,7 @@ export type LookupConfig = { rootExport: string kind: LookupKind | 'Root' // 'Root' for builder pattern, LookupKind for direct call } + interface ModuleInfo { id: string bindings: Map @@ -139,8 +140,28 @@ export class ServerFnCompiler { } private async init() { + // Register internal stub package exports for recognition. + // These don't need module resolution - only the knownRootImports fast path. + this.knownRootImports.set( + '@tanstack/start-fn-stubs', + new Map([ + ['createIsomorphicFn', 'Root'], + ['createServerOnlyFn', 'ServerOnlyFn'], + ['createClientOnlyFn', 'ClientOnlyFn'], + ]), + ) + await Promise.all( this.options.lookupConfigurations.map(async (config) => { + // Populate the fast lookup map for direct imports (by package name) + // This allows O(1) recognition of imports from known packages. + let libExports = this.knownRootImports.get(config.libName) + if (!libExports) { + libExports = new Map() + this.knownRootImports.set(config.libName, libExports) + } + libExports.set(config.rootExport, config.kind) + const libId = await this.options.resolveId(config.libName) if (!libId) { throw new Error(`could not resolve "${config.libName}"`) @@ -172,14 +193,6 @@ export class ServerFnCompiler { resolvedKind: config.kind satisfies Kind, }) this.moduleCache.set(libId, rootModule) - - // Also populate the fast lookup map for direct imports - let libExports = this.knownRootImports.get(config.libName) - if (!libExports) { - libExports = new Map() - this.knownRootImports.set(config.libName, libExports) - } - libExports.set(config.rootExport, config.kind) }), ) @@ -699,45 +712,38 @@ export class ServerFnCompiler { if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) { const prop = callee.property.name - // Check method chain patterns for each valid lookup kind - const serverFnSetup = LookupSetup['ServerFn'] - if ( - this.validLookupKinds.has('ServerFn') && - serverFnSetup.type === 'methodChain' && - serverFnSetup.candidateCallIdentifier.has(prop) - ) { + // Check if this property matches any method chain pattern + const possibleKinds = IdentifierToKinds.get(prop) + if (possibleKinds) { + // Resolve base expression ONCE and reuse for all pattern checks const base = await this.resolveExprKind(callee.object, fileId, visited) - if (base === 'Root' || base === 'Builder') { - return 'ServerFn' - } - return 'None' - } - const middlewareSetup = LookupSetup['Middleware'] - if ( - this.validLookupKinds.has('Middleware') && - middlewareSetup.type === 'methodChain' && - middlewareSetup.candidateCallIdentifier.has(prop) - ) { - const base = await this.resolveExprKind(callee.object, fileId, visited) - if (base === 'Root' || base === 'Builder' || base === 'Middleware') { - return 'Middleware' - } - return 'None' - } + // Check each possible kind that uses this identifier + for (const kind of possibleKinds) { + if (!this.validLookupKinds.has(kind)) continue - const isomorphicSetup = LookupSetup['IsomorphicFn'] - if ( - this.validLookupKinds.has('IsomorphicFn') && - isomorphicSetup.type === 'methodChain' && - isomorphicSetup.candidateCallIdentifier.has(prop) - ) { - const base = await this.resolveExprKind(callee.object, fileId, visited) - // Allow chaining: createIsomorphicFn().server().client() or .client().server() - if (base === 'Root' || base === 'Builder' || base === 'IsomorphicFn') { - return 'IsomorphicFn' + if (kind === 'ServerFn') { + if (base === 'Root' || base === 'Builder') { + return 'ServerFn' + } + } else if (kind === 'Middleware') { + if ( + base === 'Root' || + base === 'Builder' || + base === 'Middleware' + ) { + return 'Middleware' + } + } else if (kind === 'IsomorphicFn') { + if ( + base === 'Root' || + base === 'Builder' || + base === 'IsomorphicFn' + ) { + return 'IsomorphicFn' + } + } } - return 'None' } // Check if the object is a namespace import diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fffbd8ea37e..08f4c4a642e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10167,7 +10167,7 @@ importers: devDependencies: '@netlify/vite-plugin-tanstack-start': specifier: ^1.1.4 - version: 1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + version: 1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) '@tailwindcss/vite': specifier: ^4.1.18 version: 4.1.18(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) @@ -11780,6 +11780,9 @@ importers: '@tanstack/router-core': specifier: workspace:* version: link:../router-core + '@tanstack/start-fn-stubs': + specifier: workspace:* + version: link:../start-fn-stubs '@tanstack/start-storage-context': specifier: workspace:* version: link:../start-storage-context @@ -11793,6 +11796,8 @@ importers: specifier: ^1.0.3 version: 1.0.3 + packages/start-fn-stubs: {} + packages/start-plugin-core: dependencies: '@babel/code-frame': @@ -26390,13 +26395,13 @@ snapshots: uuid: 11.1.0 write-file-atomic: 5.0.1 - '@netlify/dev@4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)': + '@netlify/dev@4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)': dependencies: '@netlify/blobs': 10.1.0 '@netlify/config': 23.2.0 '@netlify/dev-utils': 4.3.0 '@netlify/edge-functions-dev': 1.0.0 - '@netlify/functions-dev': 1.0.0(rollup@4.52.5) + '@netlify/functions-dev': 1.0.0(encoding@0.1.13)(rollup@4.52.5) '@netlify/headers': 2.1.0 '@netlify/images': 1.3.0(@netlify/blobs@10.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0) '@netlify/redirects': 3.1.0 @@ -26464,12 +26469,12 @@ snapshots: dependencies: '@netlify/types': 2.1.0 - '@netlify/functions-dev@1.0.0(rollup@4.52.5)': + '@netlify/functions-dev@1.0.0(encoding@0.1.13)(rollup@4.52.5)': dependencies: '@netlify/blobs': 10.1.0 '@netlify/dev-utils': 4.3.0 '@netlify/functions': 5.0.0 - '@netlify/zip-it-and-ship-it': 14.1.11(rollup@4.52.5) + '@netlify/zip-it-and-ship-it': 14.1.11(encoding@0.1.13)(rollup@4.52.5) cron-parser: 4.9.0 decache: 4.6.2 extract-zip: 2.0.1 @@ -26559,9 +26564,9 @@ snapshots: '@netlify/types@2.1.0': {} - '@netlify/vite-plugin-tanstack-start@1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))': + '@netlify/vite-plugin-tanstack-start@1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))': dependencies: - '@netlify/vite-plugin': 2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + '@netlify/vite-plugin': 2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) optionalDependencies: '@tanstack/solid-start': link:packages/solid-start @@ -26589,9 +26594,9 @@ snapshots: - supports-color - uploadthing - '@netlify/vite-plugin@2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))': + '@netlify/vite-plugin@2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))': dependencies: - '@netlify/dev': 4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5) + '@netlify/dev': 4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5) '@netlify/dev-utils': 4.3.0 dedent: 1.7.0(babel-plugin-macros@3.1.0) vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) @@ -26619,13 +26624,13 @@ snapshots: - supports-color - uploadthing - '@netlify/zip-it-and-ship-it@14.1.11(rollup@4.52.5)': + '@netlify/zip-it-and-ship-it@14.1.11(encoding@0.1.13)(rollup@4.52.5)': dependencies: '@babel/parser': 7.28.5 '@babel/types': 7.28.4 '@netlify/binary-info': 1.0.0 '@netlify/serverless-functions-api': 2.7.1 - '@vercel/nft': 0.29.4(rollup@4.52.5) + '@vercel/nft': 0.29.4(encoding@0.1.13)(rollup@4.52.5) archiver: 7.0.1 common-path-prefix: 3.0.0 copy-file: 11.1.0 @@ -29726,7 +29731,7 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vercel/nft@0.29.4(rollup@4.52.5)': + '@vercel/nft@0.29.4(encoding@0.1.13)(rollup@4.52.5)': dependencies: '@mapbox/node-pre-gyp': 2.0.0(encoding@0.1.13) '@rollup/pluginutils': 5.1.4(rollup@4.52.5) diff --git a/scripts/publish.js b/scripts/publish.js index 2760c5a87c4..c855c56b3b6 100644 --- a/scripts/publish.js +++ b/scripts/publish.js @@ -144,6 +144,10 @@ await publish({ name: '@tanstack/start-storage-context', packageDir: 'packages/start-storage-context', }, + { + name: '@tanstack/start-fn-stubs', + packageDir: 'packages/start-fn-stubs', + }, { name: '@tanstack/react-start', packageDir: 'packages/react-start', From bc0aae10492584c375fa341fd6ed07f831b34df6 Mon Sep 17 00:00:00 2001 From: Manuel Schiller Date: Mon, 22 Dec 2025 14:00:04 +0100 Subject: [PATCH 5/9] move --- packages/start-fn-stubs/package.json | 4 +- .../src/createIsomorphicFn.ts | 0 .../src/envOnly.ts | 0 packages/start-fn-stubs/src/index.ts | 60 +++---------------- .../tests/createIsomorphicFn.test-d.ts | 2 +- .../tests/envOnly.test-d.ts | 2 +- 6 files changed, 14 insertions(+), 54 deletions(-) rename packages/{start-client-core => start-fn-stubs}/src/createIsomorphicFn.ts (100%) rename packages/{start-client-core => start-fn-stubs}/src/envOnly.ts (100%) rename packages/{start-client-core/src => start-fn-stubs}/tests/createIsomorphicFn.test-d.ts (97%) rename packages/{start-client-core/src => start-fn-stubs}/tests/envOnly.test-d.ts (94%) diff --git a/packages/start-fn-stubs/package.json b/packages/start-fn-stubs/package.json index 31c73ebc872..432c0ee95bf 100644 --- a/packages/start-fn-stubs/package.json +++ b/packages/start-fn-stubs/package.json @@ -23,7 +23,9 @@ ], "scripts": { "clean": "rimraf ./dist && rimraf ./coverage", - "test": "pnpm test:eslint && pnpm test:types && pnpm test:build", + "test": "pnpm test:eslint && pnpm test:types && pnpm test:build && pnpm test:unit", + "test:unit": "vitest", + "test:unit:dev": "vitest --watch", "test:eslint": "eslint ./src", "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"", "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js", diff --git a/packages/start-client-core/src/createIsomorphicFn.ts b/packages/start-fn-stubs/src/createIsomorphicFn.ts similarity index 100% rename from packages/start-client-core/src/createIsomorphicFn.ts rename to packages/start-fn-stubs/src/createIsomorphicFn.ts diff --git a/packages/start-client-core/src/envOnly.ts b/packages/start-fn-stubs/src/envOnly.ts similarity index 100% rename from packages/start-client-core/src/envOnly.ts rename to packages/start-fn-stubs/src/envOnly.ts diff --git a/packages/start-fn-stubs/src/index.ts b/packages/start-fn-stubs/src/index.ts index 16f4fbe27d9..5ca646d9d1c 100644 --- a/packages/start-fn-stubs/src/index.ts +++ b/packages/start-fn-stubs/src/index.ts @@ -1,51 +1,9 @@ -// a function that can have different implementations on the client and server. -// implementations not provided will default to a no-op function. - -export type IsomorphicFn< - TArgs extends Array = [], - TServer = undefined, - TClient = undefined, -> = (...args: TArgs) => TServer | TClient - -export interface ServerOnlyFn, TServer> - extends IsomorphicFn { - client: ( - clientImpl: (...args: TArgs) => TClient, - ) => IsomorphicFn -} - -export interface ClientOnlyFn, TClient> - extends IsomorphicFn { - server: ( - serverImpl: (...args: TArgs) => TServer, - ) => IsomorphicFn -} - -export interface IsomorphicFnBase extends IsomorphicFn { - server: , TServer>( - serverImpl: (...args: TArgs) => TServer, - ) => ServerOnlyFn - client: , TClient>( - clientImpl: (...args: TArgs) => TClient, - ) => ClientOnlyFn -} - -// this is a dummy function, it will be replaced by the transformer -// if we use `createIsomorphicFn` in this library itself, vite tries to execute it before the transformer runs -// therefore we must return a dummy function that allows calling `server` and `client` method chains. -export function createIsomorphicFn(): IsomorphicFnBase { - return { - server: () => ({ client: () => () => {} }), - client: () => ({ server: () => () => {} }), - } as any -} - -type EnvOnlyFn = ) => any>(fn: TFn) => TFn - -// A function that will only be available in the server build -// If called on the client, it will throw an error -export const createServerOnlyFn: EnvOnlyFn = (fn) => fn - -// A function that will only be available in the client build -// If called on the server, it will throw an error -export const createClientOnlyFn: EnvOnlyFn = (fn) => fn +export { + createIsomorphicFn, + type IsomorphicFn, + type ServerOnlyFn, + type ClientOnlyFn, + type IsomorphicFnBase, +} from './createIsomorphicFn' + +export { createServerOnlyFn, createClientOnlyFn } from './envOnly' diff --git a/packages/start-client-core/src/tests/createIsomorphicFn.test-d.ts b/packages/start-fn-stubs/tests/createIsomorphicFn.test-d.ts similarity index 97% rename from packages/start-client-core/src/tests/createIsomorphicFn.test-d.ts rename to packages/start-fn-stubs/tests/createIsomorphicFn.test-d.ts index 89f427d8c64..7c6c3b040a0 100644 --- a/packages/start-client-core/src/tests/createIsomorphicFn.test-d.ts +++ b/packages/start-fn-stubs/tests/createIsomorphicFn.test-d.ts @@ -1,5 +1,5 @@ import { expectTypeOf, test } from 'vitest' -import { createIsomorphicFn } from '../createIsomorphicFn' +import { createIsomorphicFn } from '../src/createIsomorphicFn' test('createIsomorphicFn with no implementations', () => { const fn = createIsomorphicFn() diff --git a/packages/start-client-core/src/tests/envOnly.test-d.ts b/packages/start-fn-stubs/tests/envOnly.test-d.ts similarity index 94% rename from packages/start-client-core/src/tests/envOnly.test-d.ts rename to packages/start-fn-stubs/tests/envOnly.test-d.ts index 4a47ddf946b..dd83e4f5617 100644 --- a/packages/start-client-core/src/tests/envOnly.test-d.ts +++ b/packages/start-fn-stubs/tests/envOnly.test-d.ts @@ -1,5 +1,5 @@ import { expectTypeOf, test } from 'vitest' -import { createClientOnlyFn, createServerOnlyFn } from '../envOnly' +import { createClientOnlyFn, createServerOnlyFn } from '../src/envOnly' const inputFn = () => 'output' From f7876b580a3bee7b9fc5daa4aa21500bfeee612c Mon Sep 17 00:00:00 2001 From: Manuel Schiller Date: Mon, 22 Dec 2025 14:05:12 +0100 Subject: [PATCH 6/9] lint --- packages/start-plugin-core/src/plugin.ts | 2 +- packages/start-plugin-core/src/schema.ts | 2 +- packages/start-plugin-core/src/start-manifest-plugin/plugin.ts | 2 +- packages/start-plugin-core/src/start-router-plugin/plugin.ts | 2 +- packages/start-plugin-core/src/types.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/start-plugin-core/src/plugin.ts b/packages/start-plugin-core/src/plugin.ts index 8666803ce23..3de3c3e357c 100644 --- a/packages/start-plugin-core/src/plugin.ts +++ b/packages/start-plugin-core/src/plugin.ts @@ -19,13 +19,13 @@ import { } from './output-directory' import { postServerBuild } from './post-server-build' import { createServerFnPlugin } from './create-server-fn-plugin/plugin' +import type { GetConfigFn, ResolvedStartConfig, TanStackStartVitePluginCoreOptions } from './types' import type { ViteEnvironmentNames } from './constants' import type { TanStackStartInputConfig, TanStackStartOutputConfig, } from './schema' import type { PluginOption } from 'vite' -import { TanStackStartVitePluginCoreOptions, ResolvedStartConfig, GetConfigFn } from './types' function isFullUrl(str: string): boolean { try { diff --git a/packages/start-plugin-core/src/schema.ts b/packages/start-plugin-core/src/schema.ts index f26dfdb0393..235796a1290 100644 --- a/packages/start-plugin-core/src/schema.ts +++ b/packages/start-plugin-core/src/schema.ts @@ -1,7 +1,7 @@ import path from 'node:path' import { z } from 'zod' import { configSchema, getConfig } from '@tanstack/router-plugin' -import { TanStackStartVitePluginCoreOptions } from './types' +import type { TanStackStartVitePluginCoreOptions } from './types' const tsrConfig = configSchema .omit({ autoCodeSplitting: true, target: true, verboseFileRoutes: true }) diff --git a/packages/start-plugin-core/src/start-manifest-plugin/plugin.ts b/packages/start-plugin-core/src/start-manifest-plugin/plugin.ts index f0272967099..3f28bea375c 100644 --- a/packages/start-plugin-core/src/start-manifest-plugin/plugin.ts +++ b/packages/start-plugin-core/src/start-manifest-plugin/plugin.ts @@ -4,9 +4,9 @@ import { VIRTUAL_MODULES } from '@tanstack/start-server-core' import { tsrSplit } from '@tanstack/router-plugin' import { resolveViteId } from '../utils' import { ENTRY_POINTS } from '../constants' +import type { GetConfigFn } from '../types' import type { PluginOption, Rollup } from 'vite' import type { Manifest, RouterManagedTag } from '@tanstack/router-core' -import { GetConfigFn } from '../types' const getCSSRecursively = ( chunk: Rollup.OutputChunk, diff --git a/packages/start-plugin-core/src/start-router-plugin/plugin.ts b/packages/start-plugin-core/src/start-router-plugin/plugin.ts index 9f19ee47ffa..1536d19c91e 100644 --- a/packages/start-plugin-core/src/start-router-plugin/plugin.ts +++ b/packages/start-plugin-core/src/start-router-plugin/plugin.ts @@ -10,6 +10,7 @@ import { routesManifestPlugin } from './generator-plugins/routes-manifest-plugin import { prerenderRoutesPlugin } from './generator-plugins/prerender-routes-plugin' import { pruneServerOnlySubtrees } from './pruneServerOnlySubtrees' import { SERVER_PROP } from './constants' +import type { GetConfigFn, TanStackStartVitePluginCoreOptions } from '../types' import type { Generator, GeneratorPlugin, @@ -17,7 +18,6 @@ import type { } from '@tanstack/router-generator' import type { DevEnvironment, Plugin, PluginOption } from 'vite' import type { TanStackStartInputConfig } from '../schema' -import { TanStackStartVitePluginCoreOptions, GetConfigFn } from '../types' function isServerOnlyNode(node: RouteNode | undefined) { if (!node?.createFileRouteProps) { diff --git a/packages/start-plugin-core/src/types.ts b/packages/start-plugin-core/src/types.ts index eecb07a1983..e4262b9329b 100644 --- a/packages/start-plugin-core/src/types.ts +++ b/packages/start-plugin-core/src/types.ts @@ -1,4 +1,4 @@ -import { TanStackStartOutputConfig } from "./schema" +import type { TanStackStartOutputConfig } from "./schema" export type CompileStartFrameworkOptions = 'react' | 'solid' | 'vue' From 400bb105a264f327b8d1afd1453a3c3af249ac09 Mon Sep 17 00:00:00 2001 From: Manuel Schiller Date: Mon, 22 Dec 2025 14:33:52 +0100 Subject: [PATCH 7/9] fix --- .../src/create-server-fn-plugin/compiler.ts | 12 +----------- .../src/create-server-fn-plugin/plugin.ts | 2 +- .../createIsomorphicFn/createIsomorphicFn.test.ts | 2 +- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts b/packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts index 1ee85e39ad7..9ad4c4e5aa8 100644 --- a/packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts +++ b/packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts @@ -107,8 +107,6 @@ export class ServerFnCompiler { // Precomputed flags for candidate detection (avoid recomputing on each collectCandidates call) private hasDirectCallKinds: boolean private hasRootAsCandidateKinds: boolean - // For IsomorphicFn, we need to know which kind allows root as candidate (precomputed) - private rootAsCandidateKind: LookupKind | null = null // Fast lookup for direct imports from known libraries (e.g., '@tanstack/react-start') // Maps: libName → (exportName → Kind) // This allows O(1) resolution for the common case without async resolveId calls @@ -134,7 +132,6 @@ export class ServerFnCompiler { this.hasDirectCallKinds = true } else if (setup.allowRootAsCandidate) { this.hasRootAsCandidateKinds = true - this.rootAsCandidateKind = kind } } } @@ -145,7 +142,7 @@ export class ServerFnCompiler { this.knownRootImports.set( '@tanstack/start-fn-stubs', new Map([ - ['createIsomorphicFn', 'Root'], + ['createIsomorphicFn', 'IsomorphicFn'], ['createServerOnlyFn', 'ServerOnlyFn'], ['createClientOnlyFn', 'ClientOnlyFn'], ]), @@ -676,13 +673,6 @@ export class ServerFnCompiler { visited, ) if (calleeKind === 'Root' || calleeKind === 'Builder') { - // For kinds that allow root as candidate (e.g., IsomorphicFn), - // a call to the root function directly should return that kind. - // This handles both direct calls (createIsomorphicFn()) and - // namespace calls (TanStackStart.createIsomorphicFn()) - if (calleeKind === 'Root' && this.rootAsCandidateKind) { - return this.rootAsCandidateKind - } return 'Builder' } // Use direct Set.has() instead of iterating diff --git a/packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts b/packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts index d76b2fbb7f5..f19ffbda102 100644 --- a/packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts +++ b/packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts @@ -39,7 +39,7 @@ const getLookupConfigurationsForEnv = ( { libName: `@tanstack/${framework}-start`, rootExport: 'createIsomorphicFn', - kind: 'Root', + kind: 'IsomorphicFn', }, { libName: `@tanstack/${framework}-start`, diff --git a/packages/start-plugin-core/tests/createIsomorphicFn/createIsomorphicFn.test.ts b/packages/start-plugin-core/tests/createIsomorphicFn/createIsomorphicFn.test.ts index a631cc11de9..04d2946b063 100644 --- a/packages/start-plugin-core/tests/createIsomorphicFn/createIsomorphicFn.test.ts +++ b/packages/start-plugin-core/tests/createIsomorphicFn/createIsomorphicFn.test.ts @@ -19,7 +19,7 @@ async function compile(opts: { { libName: `@tanstack/react-start`, rootExport: 'createIsomorphicFn', - kind: 'Root', + kind: 'IsomorphicFn', }, ], resolveId: async (id) => { From 14179dc14fe14e1fc9613acfcfbe8e6a162d968c Mon Sep 17 00:00:00 2001 From: Manuel Schiller Date: Mon, 22 Dec 2025 14:41:20 +0100 Subject: [PATCH 8/9] format --- packages/start-plugin-core/src/plugin.ts | 6 +++++- packages/start-plugin-core/src/types.ts | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/start-plugin-core/src/plugin.ts b/packages/start-plugin-core/src/plugin.ts index 3de3c3e357c..cfca4a12161 100644 --- a/packages/start-plugin-core/src/plugin.ts +++ b/packages/start-plugin-core/src/plugin.ts @@ -19,7 +19,11 @@ import { } from './output-directory' import { postServerBuild } from './post-server-build' import { createServerFnPlugin } from './create-server-fn-plugin/plugin' -import type { GetConfigFn, ResolvedStartConfig, TanStackStartVitePluginCoreOptions } from './types' +import type { + GetConfigFn, + ResolvedStartConfig, + TanStackStartVitePluginCoreOptions, +} from './types' import type { ViteEnvironmentNames } from './constants' import type { TanStackStartInputConfig, diff --git a/packages/start-plugin-core/src/types.ts b/packages/start-plugin-core/src/types.ts index e4262b9329b..b1e3e6a0f19 100644 --- a/packages/start-plugin-core/src/types.ts +++ b/packages/start-plugin-core/src/types.ts @@ -1,4 +1,4 @@ -import type { TanStackStartOutputConfig } from "./schema" +import type { TanStackStartOutputConfig } from './schema' export type CompileStartFrameworkOptions = 'react' | 'solid' | 'vue' @@ -31,4 +31,4 @@ export type GetConfigFn = () => { startConfig: TanStackStartOutputConfig resolvedStartConfig: ResolvedStartConfig corePluginOpts: TanStackStartVitePluginCoreOptions -} \ No newline at end of file +} From 5ff68a162199c90213ff3efd53141b47fa9cfca4 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 22 Dec 2025 13:42:27 +0000 Subject: [PATCH 9/9] ci: apply automated fixes --- labeler-config.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/labeler-config.yml b/labeler-config.yml index d09a5367098..bd2b746e121 100644 --- a/labeler-config.yml +++ b/labeler-config.yml @@ -82,6 +82,9 @@ 'package: start-client-core': - changed-files: - any-glob-to-any-file: 'packages/start-client-core/**/*' +'package: start-fn-stubs': + - changed-files: + - any-glob-to-any-file: 'packages/start-fn-stubs/**/*' 'package: start-plugin-core': - changed-files: - any-glob-to-any-file: 'packages/start-plugin-core/**/*'