diff --git a/e2e/react-start/basic/src/routeTree.gen.ts b/e2e/react-start/basic/src/routeTree.gen.ts index 31543a1040d..56615ed3846 100644 --- a/e2e/react-start/basic/src/routeTree.gen.ts +++ b/e2e/react-start/basic/src/routeTree.gen.ts @@ -1017,3 +1017,12 @@ const rootRouteChildren: RootRouteChildren = { export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/labeler-config.yml b/labeler-config.yml index bd2b746e121..69529887a6e 100644 --- a/labeler-config.yml +++ b/labeler-config.yml @@ -1,9 +1,6 @@ 'package: arktype-adapter': - changed-files: - any-glob-to-any-file: 'packages/arktype-adapter/**/*' -'package: directive-functions-plugin': - - changed-files: - - any-glob-to-any-file: 'packages/directive-functions-plugin/**/*' 'package: eslint-plugin-router': - changed-files: - any-glob-to-any-file: 'packages/eslint-plugin-router/**/*' @@ -58,9 +55,6 @@ 'package: router-vite-plugin': - changed-files: - any-glob-to-any-file: 'packages/router-vite-plugin/**/*' -'package: server-functions-plugin': - - changed-files: - - any-glob-to-any-file: 'packages/server-functions-plugin/**/*' 'package: solid-router': - changed-files: - any-glob-to-any-file: 'packages/solid-router/**/*' diff --git a/package.json b/package.json index 8d7d840dc81..5d7fea83e7b 100644 --- a/package.json +++ b/package.json @@ -119,8 +119,6 @@ "@tanstack/vue-router": "workspace:*", "@tanstack/vue-router-devtools": "workspace:*", "@tanstack/eslint-plugin-router": "workspace:*", - "@tanstack/server-functions-plugin": "workspace:*", - "@tanstack/directive-functions-plugin": "workspace:*", "@tanstack/router-utils": "workspace:*", "@tanstack/start-static-server-functions": "workspace:*", "@tanstack/nitro-v2-vite-plugin": "workspace:*" diff --git a/packages/directive-functions-plugin/README.md b/packages/directive-functions-plugin/README.md deleted file mode 100644 index 687695557c9..00000000000 --- a/packages/directive-functions-plugin/README.md +++ /dev/null @@ -1,5 +0,0 @@ - - -# TanStack Directive Functions Plugin - -See https://tanstack.com/router/latest/docs/framework/react/routing/file-based-routing diff --git a/packages/directive-functions-plugin/eslint.config.js b/packages/directive-functions-plugin/eslint.config.js deleted file mode 100644 index 8ce6ad05fcd..00000000000 --- a/packages/directive-functions-plugin/eslint.config.js +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-check - -import rootConfig from '../../eslint.config.js' - -export default [...rootConfig] diff --git a/packages/directive-functions-plugin/package.json b/packages/directive-functions-plugin/package.json deleted file mode 100644 index f34ccd6a401..00000000000 --- a/packages/directive-functions-plugin/package.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "name": "@tanstack/directive-functions-plugin", - "version": "1.142.1", - "description": "Modern and scalable routing for React applications", - "author": "Tanner Linsley", - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/TanStack/router.git", - "directory": "packages/directive-functions-plugin" - }, - "homepage": "https://tanstack.com/start", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "keywords": [ - "react", - "location", - "router", - "routing", - "async", - "async router", - "typescript" - ], - "scripts": { - "clean": "rimraf ./dist && rimraf ./coverage", - "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", - "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", - "main": "dist/cjs/index.cjs", - "module": "dist/esm/index.js", - "exports": { - ".": { - "import": { - "types": "./dist/esm/index.d.ts", - "default": "./dist/esm/index.js" - }, - "require": { - "types": "./dist/cjs/index.d.cts", - "default": "./dist/cjs/index.cjs" - } - }, - "./package.json": "./package.json" - }, - "sideEffects": false, - "files": [ - "dist", - "src" - ], - "engines": { - "node": ">=12" - }, - "dependencies": { - "@babel/code-frame": "7.27.1", - "@babel/core": "^7.27.7", - "@babel/traverse": "^7.27.7", - "@babel/types": "^7.27.7", - "@tanstack/router-utils": "workspace:*", - "babel-dead-code-elimination": "^1.0.10", - "pathe": "^2.0.3", - "tiny-invariant": "^1.3.3" - }, - "devDependencies": { - "@types/babel__code-frame": "^7.0.6", - "@types/babel__core": "^7.20.5", - "@types/babel__traverse": "^7.20.7", - "dedent": "^1.6.0", - "vite": "^7.1.7" - }, - "peerDependencies": { - "vite": ">=6.0.0 || >=7.0.0" - } -} diff --git a/packages/directive-functions-plugin/src/compilers.ts b/packages/directive-functions-plugin/src/compilers.ts deleted file mode 100644 index a045dae88d9..00000000000 --- a/packages/directive-functions-plugin/src/compilers.ts +++ /dev/null @@ -1,660 +0,0 @@ -import * as babel from '@babel/core' -import { isIdentifier, isVariableDeclarator } from '@babel/types' -import { codeFrameColumns } from '@babel/code-frame' -import { - deadCodeElimination, - findReferencedIdentifiers, -} from 'babel-dead-code-elimination' -import path from 'pathe' -import { generateFromAst, parseAst } from '@tanstack/router-utils' -import type { GeneratorResult, ParseAstOptions } from '@tanstack/router-utils' - -export interface DirectiveFn { - nodePath: SupportedFunctionPath - functionName: string - functionId: string - extractedFilename: string - filename: string - chunkName: string - /** - * True when this function was discovered by the client build. - * Used to restrict HTTP access to only client-referenced functions. - */ - isClientReferenced?: boolean -} - -export type SupportedFunctionPath = - | babel.NodePath - | babel.NodePath - | babel.NodePath - -export type GenerateFunctionIdFn = (opts: { - filename: string - functionName: string - extractedFilename: string -}) => string - -export type ReplacerFn = (opts: { - fn: string - extractedFilename: string - filename: string - functionId: string - functionName: string - isSourceFn: boolean - /** - * True when this function was already discovered by a previous build (e.g., client). - * For SSR callers, this means the function is in the manifest and doesn't need - * an importer - the manifest lookup will find it. - */ - isClientReferenced: boolean -}) => string - -// const debug = process.env.TSR_VITE_DEBUG === 'true' - -export type CompileDirectivesOpts = ParseAstOptions & { - directive: string - getRuntimeCode?: (opts: { - directiveFnsById: Record - }) => string - generateFunctionId: GenerateFunctionIdFn - replacer: ReplacerFn - filename: string - root: string - isDirectiveSplitParam: boolean - directiveSplitParam: string - /** - * Previously discovered directive functions from other builds (e.g., client build). - * When provided, the compiler will use the canonical extractedFilename from this - * registry instead of computing it locally. This ensures SSR callers import from - * the same extracted file as the client. - */ - knownDirectiveFns?: Record - /** - * Whether the current environment is a client environment. - * Functions discovered in client environments are always client-referenced. - */ - isClientEnvironment?: boolean -} - -export function compileDirectives(opts: CompileDirectivesOpts): { - compiledResult: GeneratorResult - directiveFnsById: Record -} { - const ast = parseAst(opts) - const refIdents = findReferencedIdentifiers(ast) - const directiveFnsById = findDirectives(ast, { - ...opts, - directiveSplitParam: opts.directiveSplitParam, - isDirectiveSplitParam: opts.isDirectiveSplitParam, - }) - - // Add runtime code if there are directives - if (Object.keys(directiveFnsById).length > 0) { - if (opts.getRuntimeCode) { - const runtimeImport = babel.template.statement( - opts.getRuntimeCode({ directiveFnsById }), - )() - ast.program.body.unshift(runtimeImport) - } - } - - // If we are in the source file, we need to remove all exports - // then make sure that all of our functions are exported under their - // directive name - if (opts.isDirectiveSplitParam) { - safeRemoveExports(ast) - - // Export a single object with all of the functions - // e.g. export { directiveFn1, directiveFn2 } - ast.program.body.push( - babel.types.exportNamedDeclaration( - undefined, - Object.values(directiveFnsById).map((fn) => - babel.types.exportSpecifier( - babel.types.identifier(fn.functionName), - babel.types.identifier(fn.functionName), - ), - ), - ), - ) - } - - deadCodeElimination(ast, refIdents) - - const compiledResult = generateFromAst(ast, { - sourceMaps: true, - sourceFileName: opts.filename, - filename: opts.filename, - }) - - return { - compiledResult, - directiveFnsById, - } -} - -function findNearestVariableName( - path: babel.NodePath, - directive: string, -): string { - let currentPath: babel.NodePath | null = path - const nameParts: Array = [] - - while (currentPath) { - const name = (() => { - // Check for named function expression - if ( - babel.types.isFunctionExpression(currentPath.node) && - currentPath.node.id - ) { - return currentPath.node.id.name - } - - // Handle method chains - if (babel.types.isCallExpression(currentPath.node)) { - const current = currentPath.node.callee - const chainParts: Array = [] - - // Get the nearest method name (if it's a method call) - if (babel.types.isMemberExpression(current)) { - if (babel.types.isIdentifier(current.property)) { - chainParts.unshift(current.property.name) - } - - // Get the base callee - let base = current.object - while (!babel.types.isIdentifier(base)) { - if (babel.types.isCallExpression(base)) { - base = base.callee as babel.types.Expression - } else if (babel.types.isMemberExpression(base)) { - base = base.object - } else { - break - } - } - if (babel.types.isIdentifier(base)) { - chainParts.unshift(base.name) - } - } else if (babel.types.isIdentifier(current)) { - chainParts.unshift(current.name) - } - - if (chainParts.length > 0) { - return chainParts.join('_') - } - } - - // Rest of the existing checks... - if (babel.types.isFunctionDeclaration(currentPath.node)) { - return currentPath.node.id?.name - } - - if (babel.types.isIdentifier(currentPath.node)) { - return currentPath.node.name - } - - if ( - isVariableDeclarator(currentPath.node) && - isIdentifier(currentPath.node.id) - ) { - return currentPath.node.id.name - } - - if ( - babel.types.isClassMethod(currentPath.node) || - babel.types.isObjectMethod(currentPath.node) - ) { - throw new Error( - `"${directive}" in ClassMethod or ObjectMethod not supported`, - ) - } - - return '' - })() - - if (name) { - nameParts.unshift(name) - } - - currentPath = currentPath.parentPath - } - - return nameParts.length > 0 ? nameParts.join('_') : 'anonymous' -} - -function makeIdentifierSafe(identifier: string): string { - return identifier - .replace(/[^a-zA-Z0-9_$]/g, '_') // Replace unsafe chars with underscore - .replace(/^[0-9]/, '_$&') // Prefix leading number with underscore - .replace(/^\$/, '_$') // Prefix leading $ with underscore - .replace(/_{2,}/g, '_') // Collapse multiple underscores - .replace(/^_|_$/g, '') // Trim leading/trailing underscores -} - -export function findDirectives( - ast: babel.types.File, - opts: ParseAstOptions & { - directive: string - replacer?: ReplacerFn - generateFunctionId: GenerateFunctionIdFn - directiveSplitParam: string - filename: string - root: string - isDirectiveSplitParam: boolean - knownDirectiveFns?: Record - isClientEnvironment?: boolean - }, -): Record { - const directiveFnsById: Record = {} - const functionNameSet: Set = new Set() - - let programPath: babel.NodePath - - babel.traverse(ast, { - Program(path) { - programPath = path - }, - }) - - // Does the file have the directive in the program body? - const hasFileDirective = ast.program.directives.some( - (directive) => directive.value.value === opts.directive, - ) - const compileDirectiveOpts = { - isDirectiveSplitParam: opts.isDirectiveSplitParam, - } - - // If the entire file has a directive, we need to compile all of the functions that are - // exported by the file. - if (hasFileDirective) { - // Find all of the exported functions - // They must be either function declarations or const function/anonymous function declarations - babel.traverse(ast, { - ExportDefaultDeclaration(path) { - if (babel.types.isFunctionDeclaration(path.node.declaration)) { - compileDirective( - path.get('declaration') as SupportedFunctionPath, - compileDirectiveOpts, - ) - } - }, - ExportNamedDeclaration(path) { - if (babel.types.isFunctionDeclaration(path.node.declaration)) { - compileDirective( - path.get('declaration') as SupportedFunctionPath, - compileDirectiveOpts, - ) - } - }, - ExportDeclaration(path) { - if ( - babel.types.isExportNamedDeclaration(path.node) && - babel.types.isVariableDeclaration(path.node.declaration) && - (babel.types.isFunctionExpression( - path.node.declaration.declarations[0]?.init, - ) || - babel.types.isArrowFunctionExpression( - path.node.declaration.declarations[0]?.init, - )) - ) { - compileDirective( - path.get( - 'declaration.declarations.0.init', - ) as SupportedFunctionPath, - compileDirectiveOpts, - ) - } - }, - }) - } else { - // Find all directives - babel.traverse(ast, { - DirectiveLiteral(nodePath) { - if (nodePath.node.value === opts.directive) { - const directiveFn = nodePath.findParent((p) => p.isFunction()) - - if (!directiveFn) return - - // Handle class and object methods which are not supported - const isClassMethod = directiveFn.isClassMethod() - const isObjectMethod = directiveFn.isObjectMethod() - - if (isClassMethod || isObjectMethod) { - throw codeFrameError( - opts.code, - directiveFn.node.loc, - `"${opts.directive}" in ${isClassMethod ? 'class' : isObjectMethod ? 'object method' : ''} not supported`, - ) - } - - // If the function is inside another block that isn't the program, - // Error out. This is not supported. - const nearestBlock = directiveFn.findParent( - (p) => (p.isBlockStatement() || p.isScopable()) && !p.isProgram(), - ) - - if (nearestBlock) { - throw codeFrameError( - opts.code, - nearestBlock.node.loc, - `"${opts.directive}" cannot be nested in other blocks or functions`, - ) - } - - if ( - !directiveFn.isFunctionDeclaration() && - !directiveFn.isFunctionExpression() && - !( - directiveFn.isArrowFunctionExpression() && - babel.types.isBlockStatement(directiveFn.node.body) - ) - ) { - throw codeFrameError( - opts.code, - directiveFn.node.loc, - `"${opts.directive}" must be function declarations or function expressions`, - ) - } - - compileDirective(directiveFn, compileDirectiveOpts) - } - }, - }) - } - - return directiveFnsById - - function compileDirective( - directiveFn: SupportedFunctionPath, - compileDirectiveOpts: { - isDirectiveSplitParam: boolean - }, - ) { - // Move the function to program level while preserving its position - // in the program body - const programBody = programPath.node.body - - // Remove the directive directive from the function body - if ( - babel.types.isFunction(directiveFn.node) && - babel.types.isBlockStatement(directiveFn.node.body) - ) { - directiveFn.node.body.directives = - directiveFn.node.body.directives.filter( - (directive) => directive.value.value !== opts.directive, - ) - } - - // if the directive function is a top-level function, we need to create a const declaration - // using the same name as the function and replace the function with the variable declaration - // that points to the function - if (directiveFn.parentPath.isProgram()) { - if (!babel.types.isFunctionDeclaration(directiveFn.node)) { - throw new Error('Top level functions must be function declarations') - } - - const index = programBody.indexOf(directiveFn.node) - - // First get the name of the function - const originalFunctionName = directiveFn.node.id!.name - - // Now turn the function into an anonymous function - directiveFn.node.id = null - - const variableDeclaration = babel.types.variableDeclaration('const', [ - babel.types.variableDeclarator( - babel.types.identifier(originalFunctionName), - babel.types.toExpression(directiveFn.node as any), - ), - ]) - - directiveFn.replaceWith(variableDeclaration) - - directiveFn = programPath.get( - `body.${index}.declarations.0.init`, - ) as SupportedFunctionPath - } - - // Find the nearest variable name - let functionName = findNearestVariableName(directiveFn, opts.directive) - - const incrementFunctionNameVersion = (functionName: string) => { - const [realReferenceName, count] = functionName.split(/_(\d+)$/) - const resolvedCount = Number(count || '0') - const suffix = `_${resolvedCount + 1}` - return makeIdentifierSafe(realReferenceName!) + suffix - } - - while (functionNameSet.has(functionName)) { - functionName = incrementFunctionNameVersion(functionName) - } - - functionNameSet.add(functionName) - - while (programPath.scope.hasBinding(functionName)) { - functionName = incrementFunctionNameVersion(functionName) - programPath.scope.crawl() - } - - functionNameSet.add(functionName) - - // Use functionId to determine if this is a client-referenced function - const [baseFilename, ..._searchParams] = opts.filename.split('?') as [ - string, - ...Array, - ] - const searchParams = new URLSearchParams(_searchParams.join('&')) - searchParams.set(opts.directiveSplitParam, '') - - const localExtractedFilename = `${baseFilename}?${searchParams.toString()}` - - // Relative to have constant functionId regardless of the machine - // that we are executing - const relativeFilename = path.relative(opts.root, baseFilename) - const functionId = opts.generateFunctionId({ - filename: relativeFilename, - functionName, - extractedFilename: localExtractedFilename, - }) - - // Use the canonical extracted filename from knownDirectiveFns if available. - // This ensures SSR callers import from the same extracted file as the client, - // avoiding duplicate chunks with the same server function. - const knownFn = opts.knownDirectiveFns?.[functionId] - const extractedFilename = - knownFn?.extractedFilename ?? localExtractedFilename - // A function is client-referenced if: - // 1. It was discovered by the client environment (isClientEnvironment), OR - // 2. It was already known from a previous (client) build (knownFn exists) - const isClientReferenced = opts.isClientEnvironment || !!knownFn - - // For client-referenced functions in SSR caller files (not extracted files), - // we remove the second argument of .handler() because: - // 1. The RPC stub uses manifest lookup, which provides the implementation - // 2. The full implementation shouldn't be in the caller file - // We do this BEFORE hoisting/replacing to ensure we can still access the parent structure - if (isClientReferenced && !compileDirectiveOpts.isDirectiveSplitParam) { - // Find if this directive function is inside a .handler() call expression - // The structure is: .handler(directiveFn, originalImpl) - // We want to remove originalImpl (the second argument) - let currentPath: babel.NodePath | null = - directiveFn as babel.NodePath | null - while (currentPath && !currentPath.parentPath?.isProgram()) { - const parent: babel.NodePath | null = currentPath.parentPath - if ( - parent?.isCallExpression() && - babel.types.isMemberExpression(parent.node.callee) && - babel.types.isIdentifier(parent.node.callee.property) && - parent.node.callee.property.name === 'handler' && - parent.node.arguments.length === 2 - ) { - // Remove the second argument (the original implementation) - parent.node.arguments.pop() - break - } - currentPath = parent - } - } - - const topParent = - directiveFn.findParent((p) => !!p.parentPath?.isProgram()) || directiveFn - - const topParentIndex = programBody.indexOf(topParent.node as any) - - // If the function has a parent that isn't the program, - // we need to replace it with an identifier and - // hoist the function to the top level as a const declaration - if (directiveFn.parentPath.isProgram()) { - throw new Error( - 'Top level functions should have already been compiled to variable declarations by this point', - ) - } - // Then place the function at the top level - programBody.splice( - topParentIndex, - 0, - babel.types.variableDeclaration('const', [ - babel.types.variableDeclarator( - babel.types.identifier(functionName), - babel.types.toExpression(directiveFn.node as any), - ), - ]), - ) - - // If it's an exported named function, we need to swap it with an - // export const originalFunctionName = functionName - if ( - (babel.types.isExportNamedDeclaration(directiveFn.parentPath.node) || - (compileDirectiveOpts.isDirectiveSplitParam && - babel.types.isExportDefaultDeclaration( - directiveFn.parentPath.node, - ))) && - (babel.types.isFunctionDeclaration(directiveFn.node) || - babel.types.isFunctionExpression(directiveFn.node)) && - babel.types.isIdentifier(directiveFn.node.id) - ) { - const originalFunctionName = directiveFn.node.id.name - programBody.splice( - topParentIndex + 1, - 0, - babel.types.exportNamedDeclaration( - babel.types.variableDeclaration('const', [ - babel.types.variableDeclarator( - babel.types.identifier(originalFunctionName), - babel.types.identifier(functionName), - ), - ]), - ), - ) - - directiveFn.remove() - } else { - directiveFn.replaceWith(babel.types.identifier(functionName)) - } - - directiveFn = programPath.get( - `body.${topParentIndex}.declarations.0.init`, - ) as SupportedFunctionPath - - // If a replacer is provided, replace the function with the replacer - if (opts.replacer) { - const replacer = opts.replacer({ - fn: '$$fn$$', - extractedFilename, - filename: opts.filename, - functionId, - functionName, - isSourceFn: !!opts.directiveSplitParam, - isClientReferenced, - }) - - const replacement = babel.template.expression(replacer, { - placeholderPattern: false, - placeholderWhitelist: new Set(['$$fn$$']), - })({ - ...(replacer.includes('$$fn$$') - ? { $$fn$$: babel.types.toExpression(directiveFn.node) } - : {}), - }) - - directiveFn.replaceWith(replacement) - } - - // Finally register the directive to - // our map of directives - directiveFnsById[functionId] = { - nodePath: directiveFn, - functionName, - functionId, - extractedFilename, - filename: opts.filename, - chunkName: fileNameToChunkName(opts.root, extractedFilename), - isClientReferenced, - } - } -} - -function codeFrameError( - code: string, - loc: - | { - start: { line: number; column: number } - end: { line: number; column: number } - } - | undefined - | null, - message: string, -) { - if (!loc) { - return new Error(`${message} at unknown location`) - } - - const frame = codeFrameColumns( - code, - { - start: loc.start, - end: loc.end, - }, - { - highlightCode: true, - message, - }, - ) - - return new Error(frame) -} - -const safeRemoveExports = (ast: babel.types.File) => { - ast.program.body = ast.program.body.flatMap((node) => { - if ( - babel.types.isExportNamedDeclaration(node) || - babel.types.isExportDefaultDeclaration(node) - ) { - if ( - babel.types.isFunctionDeclaration(node.declaration) || - babel.types.isClassDeclaration(node.declaration) || - babel.types.isVariableDeclaration(node.declaration) - ) { - // do not remove export if it is an anonymous function / class, otherwise this would produce a syntax error - if ( - babel.types.isFunctionDeclaration(node.declaration) || - babel.types.isClassDeclaration(node.declaration) - ) { - if (!node.declaration.id) { - return node - } - } - return node.declaration - } else if (node.declaration === null) { - // remove e.g. `export { RouteComponent as component }` - return [] - } - } - return node - }) -} - -function fileNameToChunkName(root: string, fileName: string) { - // Replace anything that can't go into an import statement - return fileName.replace(root, '').replace(/[^a-zA-Z0-9_]/g, '_') -} diff --git a/packages/directive-functions-plugin/src/index.ts b/packages/directive-functions-plugin/src/index.ts deleted file mode 100644 index 62405cc1b97..00000000000 --- a/packages/directive-functions-plugin/src/index.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { fileURLToPath, pathToFileURL } from 'node:url' - -import { logDiff } from '@tanstack/router-utils' -import { compileDirectives } from './compilers' -import type { - CompileDirectivesOpts, - DirectiveFn, - GenerateFunctionIdFn, -} from './compilers' -import type { Plugin } from 'vite' - -const debug = - process.env.TSR_VITE_DEBUG && - ['true', 'directives-functions-plugin'].includes(process.env.TSR_VITE_DEBUG) - -export type { - DirectiveFn, - CompileDirectivesOpts, - ReplacerFn, - GenerateFunctionIdFn, -} from './compilers' - -export type DirectiveFunctionsViteEnvOptions = Pick< - CompileDirectivesOpts, - 'getRuntimeCode' | 'replacer' -> -export type DirectiveFunctionsViteOptions = DirectiveFunctionsViteEnvOptions & { - directive: string - onDirectiveFnsById?: (directiveFnsById: Record) => void - generateFunctionId: GenerateFunctionIdFn -} - -const createDirectiveRx = (directive: string) => - new RegExp(`"${directive}"|'${directive}'`, 'gm') - -export type DirectiveFunctionsVitePluginEnvOptions = { - directive: string - callers: Array< - DirectiveFunctionsViteEnvOptions & { - envName: string - envConsumer?: 'client' | 'server' - } - > - provider: DirectiveFunctionsViteEnvOptions & { envName: string } - onDirectiveFnsById?: (directiveFnsById: Record) => void - generateFunctionId: GenerateFunctionIdFn - /** - * Returns the currently known directive functions from previous builds. - * Used by server callers to look up canonical extracted filenames, - * ensuring they import from the same extracted file as the client. - */ - getKnownDirectiveFns?: () => Record -} - -function buildDirectiveSplitParam(directive: string) { - return `tsr-directive-${directive.replace(/[^a-zA-Z0-9]/g, '-')}` -} - -export function TanStackDirectiveFunctionsPluginEnv( - opts: DirectiveFunctionsVitePluginEnvOptions, -): Plugin { - let root: string = process.cwd() - - const directiveRx = createDirectiveRx(opts.directive) - - const appliedEnvironments = new Set([ - ...opts.callers.map((c) => c.envName), - opts.provider.envName, - ]) - - const directiveSplitParam = buildDirectiveSplitParam(opts.directive) - - return { - name: 'tanstack-start-directive-vite-plugin', - enforce: 'pre', - buildStart() { - root = this.environment.config.root - }, - applyToEnvironment(env) { - return appliedEnvironments.has(env.name) - }, - transform: { - filter: { - code: directiveRx, - }, - handler(code, id) { - const url = pathToFileURL(id) - url.searchParams.delete('v') - id = fileURLToPath(url).replace(/\\/g, '/') - - const isDirectiveSplitParam = id.includes(directiveSplitParam) - - let envOptions: DirectiveFunctionsViteEnvOptions & { - envName: string - envConsumer?: 'client' | 'server' - } - // Use provider options ONLY for extracted function files (files with directive split param) - // For all other files, use caller options even if we're in the provider environment - // This ensures route files reference extracted functions instead of duplicating implementations - if (isDirectiveSplitParam) { - envOptions = opts.provider - if (debug) - console.info( - `Compiling Directives for provider in environment ${envOptions.envName}: `, - id, - ) - } else { - // For non-extracted files, use caller options based on current environment - const callerOptions = opts.callers.find( - (e) => e.envName === this.environment.name, - ) - // If no caller is found for this environment (e.g., separate provider environment only processes extracted files), - // fall back to provider options - if (callerOptions) { - envOptions = callerOptions - if (debug) - console.info( - `Compiling Directives for caller in environment ${envOptions.envName}: `, - id, - ) - } else { - envOptions = opts.provider - if (debug) - console.info( - `Compiling Directives for provider (fallback) in environment ${opts.provider.envName}: `, - id, - ) - } - } - // Get known directive functions for looking up canonical extracted filenames - // This ensures SSR callers import from the same extracted file as the client - const knownDirectiveFns = opts.getKnownDirectiveFns?.() - - // Determine if this is a client environment - const isClientEnvironment = envOptions.envConsumer === 'client' - - const { compiledResult, directiveFnsById } = compileDirectives({ - directive: opts.directive, - getRuntimeCode: envOptions.getRuntimeCode, - generateFunctionId: opts.generateFunctionId, - replacer: envOptions.replacer, - code, - root, - filename: id, - directiveSplitParam, - isDirectiveSplitParam, - knownDirectiveFns, - isClientEnvironment, - }) - // when we process a file with a directive split param, we have already encountered the directives in that file - // (otherwise we wouldn't have gotten here) - if (!isDirectiveSplitParam) { - opts.onDirectiveFnsById?.(directiveFnsById) - } - - if (debug) { - logDiff(code, compiledResult.code) - console.log('Output:\n', compiledResult.code + '\n\n') - } - - return compiledResult - }, - }, - } -} diff --git a/packages/directive-functions-plugin/tests/compiler.test.ts b/packages/directive-functions-plugin/tests/compiler.test.ts deleted file mode 100644 index e6d993c9992..00000000000 --- a/packages/directive-functions-plugin/tests/compiler.test.ts +++ /dev/null @@ -1,978 +0,0 @@ -import { describe, expect, test } from 'vitest' - -import { compileDirectives } from '../src/compilers' -import type { CompileDirectivesOpts } from '../src/compilers' - -function makeFunctionIdUrlSafe(location: string): string { - return location - .replace(/[^a-zA-Z0-9-_]/g, '_') // Replace unsafe chars with underscore - .replace(/_{2,}/g, '_') // Collapse multiple underscores - .replace(/^_|_$/g, '') // Trim leading/trailing underscores - .replace(/_--/g, '--') // Clean up the joiner -} - -const generateFunctionId: CompileDirectivesOpts['generateFunctionId'] = ( - opts, -) => { - return makeFunctionIdUrlSafe(`${opts.filename}--${opts.functionName}`) -} - -const clientConfig: Omit = { - directive: 'use server', - root: './test-files', - filename: './test-files/test.ts', - getRuntimeCode: () => 'import { createClientRpc } from "my-rpc-lib-client"', - generateFunctionId, - replacer: (opts) => `createClientRpc(${JSON.stringify(opts.functionId)})`, - directiveSplitParam: 'tsr-directive-use-server', - isDirectiveSplitParam: false, -} - -const ssrConfig: Omit = { - directive: 'use server', - root: './test-files', - filename: './test-files/test.ts', - getRuntimeCode: () => 'import { createSsrRpc } from "my-rpc-lib-server"', - generateFunctionId, - replacer: (opts) => `createSsrRpc(${JSON.stringify(opts.functionId)})`, - directiveSplitParam: 'tsr-directive-use-server', - isDirectiveSplitParam: false, -} - -const serverConfig: Omit = { - directive: 'use server', - root: './test-files', - filename: './test-files/test.ts', - getRuntimeCode: () => 'import { createServerRpc } from "my-rpc-lib-server"', - generateFunctionId, - replacer: (opts) => - // On the server build, we need different code for the split function - // vs any other server functions the split function may reference - - // For the split function itself, we use the original function. - // For any other server functions the split function may reference, - // we use the splitImportFn which is a dynamic import of the split file. - `createServerRpc(${JSON.stringify(opts.functionId)}, ${opts.fn})`, - directiveSplitParam: 'tsr-directive-use-server', - isDirectiveSplitParam: true, -} - -describe('server function compilation', () => { - test('basic function declaration nested in other variable', () => { - const code = ` - export const namedFunction = createServerFn(function namedFunction() { - 'use server' - return 'hello' - }) - - export const namedGeneratorFunction = createServerFn(function* namedGeneratorFunction () { - 'use server' - yield 'hello' - return 'hello world' - }) - - export const arrowFunction = createServerFn(() => { - 'use server' - return 'hello' - }) - - export const anonymousFunction = createServerFn(function () { - 'use server' - return 'hello' - }) - - export const anonymousGeneratorFunction = createServerFn(function* () { - 'use server' - yield 'hello' - return 'hello world' - }) - - export const multipleDirectives = function multipleDirectives() { - 'use server' - 'use strict' - return 'hello' - } - - export const iife = (function () { - 'use server' - return 'hello' - })() - - export default function defaultExportFn() { - 'use server' - return 'hello' - } - - export function namedExportFn() { - 'use server' - return 'hello' - } - - export const exportedArrowFunction = wrapper(() => { - 'use server' - return 'hello' - }) - - export const namedExportConst = () => { - 'use server' - return usedFn() - } - - export const namedExportConstGenerator = function* () { - 'use server' - yield 'hello' - return usedFn() - } - - function usedFn() { - return 'hello' - } - - function unusedFn() { - return 'hello' - } - - const namedDefaultExport = 'namedDefaultExport' - export default namedDefaultExport - - const usedButNotExported = 'usedButNotExported' - - const namedExport = 'namedExport' - - export { - namedExport - } - ` - const client = compileDirectives({ - ...clientConfig, - code, - }) - const ssr = compileDirectives({ ...ssrConfig, code }) - - const server = compileDirectives({ - ...serverConfig, - code, - filename: `${ssr.directiveFnsById[Object.keys(ssr.directiveFnsById)[0]!]!.extractedFilename}`, - }) - - expect(client.compiledResult.code).toMatchInlineSnapshot(` - "import { createClientRpc } from "my-rpc-lib-client"; - const namedFunction_createServerFn_namedFunction = createClientRpc("test_ts--namedFunction_createServerFn_namedFunction"); - export const namedFunction = createServerFn(namedFunction_createServerFn_namedFunction); - const namedGeneratorFunction_createServerFn_namedGeneratorFunction = createClientRpc("test_ts--namedGeneratorFunction_createServerFn_namedGeneratorFunction"); - export const namedGeneratorFunction = createServerFn(namedGeneratorFunction_createServerFn_namedGeneratorFunction); - const arrowFunction_createServerFn = createClientRpc("test_ts--arrowFunction_createServerFn"); - export const arrowFunction = createServerFn(arrowFunction_createServerFn); - const anonymousFunction_createServerFn = createClientRpc("test_ts--anonymousFunction_createServerFn"); - export const anonymousFunction = createServerFn(anonymousFunction_createServerFn); - const anonymousGeneratorFunction_createServerFn = createClientRpc("test_ts--anonymousGeneratorFunction_createServerFn"); - export const anonymousGeneratorFunction = createServerFn(anonymousGeneratorFunction_createServerFn); - const multipleDirectives_multipleDirectives = createClientRpc("test_ts--multipleDirectives_multipleDirectives"); - export const multipleDirectives = multipleDirectives_multipleDirectives; - const iife_1 = createClientRpc("test_ts--iife_1"); - export const iife = iife_1(); - const defaultExportFn_1 = createClientRpc("test_ts--defaultExportFn_1"); - export default defaultExportFn_1; - const namedExportFn_1 = createClientRpc("test_ts--namedExportFn_1"); - export const namedExportFn = namedExportFn_1; - const exportedArrowFunction_wrapper = createClientRpc("test_ts--exportedArrowFunction_wrapper"); - export const exportedArrowFunction = wrapper(exportedArrowFunction_wrapper); - const namedExportConst_1 = createClientRpc("test_ts--namedExportConst_1"); - export const namedExportConst = namedExportConst_1; - const namedExportConstGenerator_1 = createClientRpc("test_ts--namedExportConstGenerator_1"); - export const namedExportConstGenerator = namedExportConstGenerator_1; - function unusedFn() { - return 'hello'; - } - const namedDefaultExport = 'namedDefaultExport'; - export default namedDefaultExport; - const usedButNotExported = 'usedButNotExported'; - const namedExport = 'namedExport'; - export { namedExport };" - `) - - expect(ssr.compiledResult.code).toMatchInlineSnapshot(` - "import { createSsrRpc } from "my-rpc-lib-server"; - const namedFunction_createServerFn_namedFunction = createSsrRpc("test_ts--namedFunction_createServerFn_namedFunction"); - export const namedFunction = createServerFn(namedFunction_createServerFn_namedFunction); - const namedGeneratorFunction_createServerFn_namedGeneratorFunction = createSsrRpc("test_ts--namedGeneratorFunction_createServerFn_namedGeneratorFunction"); - export const namedGeneratorFunction = createServerFn(namedGeneratorFunction_createServerFn_namedGeneratorFunction); - const arrowFunction_createServerFn = createSsrRpc("test_ts--arrowFunction_createServerFn"); - export const arrowFunction = createServerFn(arrowFunction_createServerFn); - const anonymousFunction_createServerFn = createSsrRpc("test_ts--anonymousFunction_createServerFn"); - export const anonymousFunction = createServerFn(anonymousFunction_createServerFn); - const anonymousGeneratorFunction_createServerFn = createSsrRpc("test_ts--anonymousGeneratorFunction_createServerFn"); - export const anonymousGeneratorFunction = createServerFn(anonymousGeneratorFunction_createServerFn); - const multipleDirectives_multipleDirectives = createSsrRpc("test_ts--multipleDirectives_multipleDirectives"); - export const multipleDirectives = multipleDirectives_multipleDirectives; - const iife_1 = createSsrRpc("test_ts--iife_1"); - export const iife = iife_1(); - const defaultExportFn_1 = createSsrRpc("test_ts--defaultExportFn_1"); - export default defaultExportFn_1; - const namedExportFn_1 = createSsrRpc("test_ts--namedExportFn_1"); - export const namedExportFn = namedExportFn_1; - const exportedArrowFunction_wrapper = createSsrRpc("test_ts--exportedArrowFunction_wrapper"); - export const exportedArrowFunction = wrapper(exportedArrowFunction_wrapper); - const namedExportConst_1 = createSsrRpc("test_ts--namedExportConst_1"); - export const namedExportConst = namedExportConst_1; - const namedExportConstGenerator_1 = createSsrRpc("test_ts--namedExportConstGenerator_1"); - export const namedExportConstGenerator = namedExportConstGenerator_1; - function unusedFn() { - return 'hello'; - } - const namedDefaultExport = 'namedDefaultExport'; - export default namedDefaultExport; - const usedButNotExported = 'usedButNotExported'; - const namedExport = 'namedExport'; - export { namedExport };" - `) - - expect(server.compiledResult.code).toMatchInlineSnapshot( - ` - "import { createServerRpc } from "my-rpc-lib-server"; - const namedFunction_createServerFn_namedFunction = createServerRpc("test_ts--namedFunction_createServerFn_namedFunction", function namedFunction() { - return 'hello'; - }); - const namedGeneratorFunction_createServerFn_namedGeneratorFunction = createServerRpc("test_ts--namedGeneratorFunction_createServerFn_namedGeneratorFunction", function* namedGeneratorFunction() { - yield 'hello'; - return 'hello world'; - }); - const arrowFunction_createServerFn = createServerRpc("test_ts--arrowFunction_createServerFn", () => { - return 'hello'; - }); - const anonymousFunction_createServerFn = createServerRpc("test_ts--anonymousFunction_createServerFn", function () { - return 'hello'; - }); - const anonymousGeneratorFunction_createServerFn = createServerRpc("test_ts--anonymousGeneratorFunction_createServerFn", function* () { - yield 'hello'; - return 'hello world'; - }); - const multipleDirectives_multipleDirectives = createServerRpc("test_ts--multipleDirectives_multipleDirectives", function multipleDirectives() { - 'use strict'; - - return 'hello'; - }); - const iife_1 = createServerRpc("test_ts--iife_1", function () { - return 'hello'; - }); - const defaultExportFn_1 = createServerRpc("test_ts--defaultExportFn_1", function defaultExportFn() { - return 'hello'; - }); - const defaultExportFn = defaultExportFn_1; - const namedExportFn_1 = createServerRpc("test_ts--namedExportFn_1", function namedExportFn() { - return 'hello'; - }); - const namedExportFn = namedExportFn_1; - const exportedArrowFunction_wrapper = createServerRpc("test_ts--exportedArrowFunction_wrapper", () => { - return 'hello'; - }); - const namedExportConst_1 = createServerRpc("test_ts--namedExportConst_1", () => { - return usedFn(); - }); - const namedExportConstGenerator_1 = createServerRpc("test_ts--namedExportConstGenerator_1", function* () { - yield 'hello'; - return usedFn(); - }); - function usedFn() { - return 'hello'; - } - function unusedFn() { - return 'hello'; - } - const namedDefaultExport = 'namedDefaultExport'; - export default namedDefaultExport; - const usedButNotExported = 'usedButNotExported'; - export { namedFunction_createServerFn_namedFunction, namedGeneratorFunction_createServerFn_namedGeneratorFunction, arrowFunction_createServerFn, anonymousFunction_createServerFn, anonymousGeneratorFunction_createServerFn, multipleDirectives_multipleDirectives, iife_1, defaultExportFn_1, namedExportFn_1, exportedArrowFunction_wrapper, namedExportConst_1, namedExportConstGenerator_1 };" - `, - ) - }) - - test('Does not support function declarations nested in other blocks', () => { - const code = ` - outer(() => { - function useServer() { - 'use server' - return 'hello' - } - }) - ` - - expect(() => compileDirectives({ ...clientConfig, code })).toThrow() - expect(() => compileDirectives({ ...serverConfig, code })).toThrow() - expect(() => - compileDirectives({ - ...serverConfig, - code, - filename: serverConfig.filename + `?tsr-serverfn-split=temp`, - }), - ).toThrow() - }) - - test('does not support class methods', () => { - const code = ` - class TestClass { - method() { - 'use server' - return 'hello' - } - - static staticMethod() { - 'use server' - return 'hello' - } - } - ` - - expect(() => compileDirectives({ ...clientConfig, code })).toThrow() - expect(() => compileDirectives({ ...serverConfig, code })).toThrow() - expect(() => - compileDirectives({ - ...serverConfig, - code, - filename: serverConfig.filename + `?tsr-serverfn-split=temp`, - }), - ).toThrow() - }) - - test('does not support object methods', () => { - const code = ` - const obj = { - method() { - 'use server' - return 'hello' - }, - } - ` - - expect(() => compileDirectives({ ...clientConfig, code })).toThrow() - expect(() => compileDirectives({ ...serverConfig, code })).toThrow() - expect(() => - compileDirectives({ - ...serverConfig, - code, - filename: serverConfig.filename + `?tsr-serverfn-split=temp`, - }), - ).toThrow() - }) - - test('multiple directiveFnsById', () => { - const code = ` - function multiDirective() { - 'use strict' - 'use server' - return 'hello' - } - - multiDirective() - ` - - const client = compileDirectives({ ...clientConfig, code }) - const ssr = compileDirectives({ ...ssrConfig, code }) - - const server = compileDirectives({ - ...serverConfig, - code, - filename: - ssr.directiveFnsById[Object.keys(ssr.directiveFnsById)[0]!]! - .extractedFilename, - }) - - expect(client.compiledResult.code).toMatchInlineSnapshot(` - "import { createClientRpc } from "my-rpc-lib-client"; - const multiDirective_1 = createClientRpc("test_ts--multiDirective_1"); - const multiDirective = multiDirective_1; - multiDirective();" - `) - expect(ssr.compiledResult.code).toMatchInlineSnapshot(` - "import { createSsrRpc } from "my-rpc-lib-server"; - const multiDirective_1 = createSsrRpc("test_ts--multiDirective_1"); - const multiDirective = multiDirective_1; - multiDirective();" - `) - expect(server.compiledResult.code).toMatchInlineSnapshot(` - "import { createServerRpc } from "my-rpc-lib-server"; - const multiDirective_1 = createServerRpc("test_ts--multiDirective_1", function () { - 'use strict'; - - return 'hello'; - }); - const multiDirective = multiDirective_1; - multiDirective(); - export { multiDirective_1 };" - `) - }) - - test('IIFE', () => { - const code = ` - export const iife = (function () { - 'use server' - return 'hello' - })() - ` - - const client = compileDirectives({ ...clientConfig, code }) - const ssr = compileDirectives({ ...ssrConfig, code }) - - const server = compileDirectives({ - ...serverConfig, - code, - filename: - ssr.directiveFnsById[Object.keys(ssr.directiveFnsById)[0]!]! - .extractedFilename, - }) - - expect(client.compiledResult.code).toMatchInlineSnapshot(` - "import { createClientRpc } from "my-rpc-lib-client"; - const iife_1 = createClientRpc("test_ts--iife_1"); - export const iife = iife_1();" - `) - - expect(ssr.compiledResult.code).toMatchInlineSnapshot(` - "import { createSsrRpc } from "my-rpc-lib-server"; - const iife_1 = createSsrRpc("test_ts--iife_1"); - export const iife = iife_1();" - `) - - expect(server.compiledResult.code).toMatchInlineSnapshot(` - "import { createServerRpc } from "my-rpc-lib-server"; - const iife_1 = createServerRpc("test_ts--iife_1", function () { - return 'hello'; - }); - export { iife_1 };" - `) - }) - - test('functions that might have the same functionId', () => { - const code = ` - outer(function useServer() { - 'use server' - return 'hello' - }) - - outer(function useServer() { - 'use server' - return 'hello' - }) - ` - - const client = compileDirectives({ ...clientConfig, code }) - const ssr = compileDirectives({ ...ssrConfig, code }) - - const server = compileDirectives({ - ...serverConfig, - code, - filename: - ssr.directiveFnsById[Object.keys(ssr.directiveFnsById)[0]!]! - .extractedFilename, - }) - - expect(client.compiledResult.code).toMatchInlineSnapshot(` - "import { createClientRpc } from "my-rpc-lib-client"; - const outer_useServer = createClientRpc("test_ts--outer_useServer"); - outer(outer_useServer); - const outer_useServer_1 = createClientRpc("test_ts--outer_useServer_1"); - outer(outer_useServer_1);" - `) - - expect(ssr.compiledResult.code).toMatchInlineSnapshot(` - "import { createSsrRpc } from "my-rpc-lib-server"; - const outer_useServer = createSsrRpc("test_ts--outer_useServer"); - outer(outer_useServer); - const outer_useServer_1 = createSsrRpc("test_ts--outer_useServer_1"); - outer(outer_useServer_1);" - `) - - expect(server.compiledResult.code).toMatchInlineSnapshot(` - "import { createServerRpc } from "my-rpc-lib-server"; - const outer_useServer = createServerRpc("test_ts--outer_useServer", function useServer() { - return 'hello'; - }); - outer(outer_useServer); - const outer_useServer_1 = createServerRpc("test_ts--outer_useServer_1", function useServer() { - return 'hello'; - }); - outer(outer_useServer_1); - export { outer_useServer, outer_useServer_1 };" - `) - }) - - test('use server directive in program body', () => { - const code = ` - 'use server' - - export function useServer() { - return usedInUseServer() - } - - function notExported() { - return 'hello' - } - - function usedInUseServer() { - return 'hello' - } - - export default function defaultExport() { - return 'hello' - } - ` - - const client = compileDirectives({ ...clientConfig, code }) - const ssr = compileDirectives({ ...ssrConfig, code }) - const server = compileDirectives({ - ...serverConfig, - code, - filename: - ssr.directiveFnsById[Object.keys(ssr.directiveFnsById)[0]!]! - .extractedFilename, - }) - - expect(client.compiledResult.code).toMatchInlineSnapshot(` - "'use server'; - - import { createClientRpc } from "my-rpc-lib-client"; - const useServer_1 = createClientRpc("test_ts--useServer_1"); - export const useServer = useServer_1; - function notExported() { - return 'hello'; - } - const defaultExport_1 = createClientRpc("test_ts--defaultExport_1"); - export default defaultExport_1;" - `) - expect(ssr.compiledResult.code).toMatchInlineSnapshot(` - "'use server'; - - import { createSsrRpc } from "my-rpc-lib-server"; - const useServer_1 = createSsrRpc("test_ts--useServer_1"); - export const useServer = useServer_1; - function notExported() { - return 'hello'; - } - const defaultExport_1 = createSsrRpc("test_ts--defaultExport_1"); - export default defaultExport_1;" - `) - expect(server.compiledResult.code).toMatchInlineSnapshot(` - "'use server'; - - import { createServerRpc } from "my-rpc-lib-server"; - const useServer_1 = createServerRpc("test_ts--useServer_1", function useServer() { - return usedInUseServer(); - }); - const useServer = useServer_1; - function notExported() { - return 'hello'; - } - function usedInUseServer() { - return 'hello'; - } - const defaultExport_1 = createServerRpc("test_ts--defaultExport_1", function defaultExport() { - return 'hello'; - }); - const defaultExport = defaultExport_1; - export { useServer_1, defaultExport_1 };" - `) - }) - - test('createServerFn with identifier', () => { - // The following code is the client output of the tanstack-start-plugin - // that compiles `createServerFn` calls to automatically add the `use server` - // directive in the right places. - const clientOrSsrCode = `import { createServerFn } from '@tanstack/react-start'; - export const myServerFn = createServerFn().handler(opts => { - "use server"; - - return myServerFn.__executeServer(opts); - }); - - export const myServerFn2 = createServerFn().handler(opts => { - "use server"; - - return myServerFn2.__executeServer(opts); - });` - - // The following code is the server output of the tanstack-start-plugin - // that compiles `createServerFn` calls to automatically add the `use server` - // directive in the right places. - const serverCode = `import { createServerFn } from '@tanstack/react-start'; - const myFunc = () => { - return 'hello from the server' - }; - export const myServerFn = createServerFn().handler(opts => { - "use server"; - - return myServerFn.__executeServer(opts); - }, myFunc); - - const myFunc2 = () => { - return myServerFn({ data: 'hello 2 from the server' }); - }; - export const myServerFn2 = createServerFn().handler(opts => { - "use server"; - - return myServerFn2.__executeServer(opts); - }, myFunc2);` - - const client = compileDirectives({ ...clientConfig, code: clientOrSsrCode }) - const ssr = compileDirectives({ ...ssrConfig, code: clientOrSsrCode }) - const server = compileDirectives({ - ...serverConfig, - code: serverCode, - filename: - ssr.directiveFnsById[Object.keys(ssr.directiveFnsById)[0]!]! - .extractedFilename, - }) - - expect(client.compiledResult.code).toMatchInlineSnapshot(` - "import { createClientRpc } from "my-rpc-lib-client"; - import { createServerFn } from '@tanstack/react-start'; - const myServerFn_createServerFn_handler = createClientRpc("test_ts--myServerFn_createServerFn_handler"); - export const myServerFn = createServerFn().handler(myServerFn_createServerFn_handler); - const myServerFn2_createServerFn_handler = createClientRpc("test_ts--myServerFn2_createServerFn_handler"); - export const myServerFn2 = createServerFn().handler(myServerFn2_createServerFn_handler);" - `) - - expect(ssr.compiledResult.code).toMatchInlineSnapshot(` - "import { createSsrRpc } from "my-rpc-lib-server"; - import { createServerFn } from '@tanstack/react-start'; - const myServerFn_createServerFn_handler = createSsrRpc("test_ts--myServerFn_createServerFn_handler"); - export const myServerFn = createServerFn().handler(myServerFn_createServerFn_handler); - const myServerFn2_createServerFn_handler = createSsrRpc("test_ts--myServerFn2_createServerFn_handler"); - export const myServerFn2 = createServerFn().handler(myServerFn2_createServerFn_handler);" - `) - - expect(server.compiledResult.code).toMatchInlineSnapshot(` - "import { createServerRpc } from "my-rpc-lib-server"; - import { createServerFn } from '@tanstack/react-start'; - const myFunc = () => { - return 'hello from the server'; - }; - const myServerFn_createServerFn_handler = createServerRpc("test_ts--myServerFn_createServerFn_handler", opts => { - return myServerFn.__executeServer(opts); - }); - const myServerFn = createServerFn().handler(myServerFn_createServerFn_handler, myFunc); - const myFunc2 = () => { - return myServerFn({ - data: 'hello 2 from the server' - }); - }; - const myServerFn2_createServerFn_handler = createServerRpc("test_ts--myServerFn2_createServerFn_handler", opts => { - return myServerFn2.__executeServer(opts); - }); - const myServerFn2 = createServerFn().handler(myServerFn2_createServerFn_handler, myFunc2); - export { myServerFn_createServerFn_handler, myServerFn2_createServerFn_handler };" - `) - }) - - test('async function with directive', () => { - const code = ` - async function bytesSignupServerFn({ email }: { email: string }) { - 'use server' - - return 'test' - } - - bytesSignupServerFn() - - ` - - const client = compileDirectives({ ...clientConfig, code }) - const ssr = compileDirectives({ ...ssrConfig, code }) - const server = compileDirectives({ - ...serverConfig, - code, - filename: - ssr.directiveFnsById[Object.keys(ssr.directiveFnsById)[0]!]! - .extractedFilename, - }) - - expect(client.compiledResult.code).toMatchInlineSnapshot(` - "import { createClientRpc } from "my-rpc-lib-client"; - const bytesSignupServerFn_1 = createClientRpc("test_ts--bytesSignupServerFn_1"); - const bytesSignupServerFn = bytesSignupServerFn_1; - bytesSignupServerFn();" - `) - expect(ssr.compiledResult.code).toMatchInlineSnapshot(` - "import { createSsrRpc } from "my-rpc-lib-server"; - const bytesSignupServerFn_1 = createSsrRpc("test_ts--bytesSignupServerFn_1"); - const bytesSignupServerFn = bytesSignupServerFn_1; - bytesSignupServerFn();" - `) - expect(server.compiledResult.code).toMatchInlineSnapshot(` - "import { createServerRpc } from "my-rpc-lib-server"; - const bytesSignupServerFn_1 = createServerRpc("test_ts--bytesSignupServerFn_1", async function ({ - email - }: { - email: string; - }) { - return 'test'; - }); - const bytesSignupServerFn = bytesSignupServerFn_1; - bytesSignupServerFn(); - export { bytesSignupServerFn_1 };" - `) - }) - - test('file-wide use server directive', () => { - const code = ` - 'use server' - - import { imported } from 'imported' - - export const serverFnConstWithImport = async () => { - return imported - } - - export function serverFnNamedWithImport () { - return imported - } - ` - - const client = compileDirectives({ ...clientConfig, code }) - const ssr = compileDirectives({ ...ssrConfig, code }) - const server = compileDirectives({ - ...serverConfig, - code, - filename: - ssr.directiveFnsById[Object.keys(ssr.directiveFnsById)[0]!]! - .extractedFilename, - }) - - expect(client.compiledResult.code).toMatchInlineSnapshot(` - "'use server'; - - import { createClientRpc } from "my-rpc-lib-client"; - const serverFnConstWithImport_1 = createClientRpc("test_ts--serverFnConstWithImport_1"); - export const serverFnConstWithImport = serverFnConstWithImport_1; - const serverFnNamedWithImport_1 = createClientRpc("test_ts--serverFnNamedWithImport_1"); - export const serverFnNamedWithImport = serverFnNamedWithImport_1;" - `) - - expect(ssr.compiledResult.code).toMatchInlineSnapshot(` - "'use server'; - - import { createSsrRpc } from "my-rpc-lib-server"; - const serverFnConstWithImport_1 = createSsrRpc("test_ts--serverFnConstWithImport_1"); - export const serverFnConstWithImport = serverFnConstWithImport_1; - const serverFnNamedWithImport_1 = createSsrRpc("test_ts--serverFnNamedWithImport_1"); - export const serverFnNamedWithImport = serverFnNamedWithImport_1;" - `) - - expect(server.compiledResult.code).toMatchInlineSnapshot(` - "'use server'; - - import { createServerRpc } from "my-rpc-lib-server"; - import { imported } from 'imported'; - const serverFnConstWithImport_1 = createServerRpc("test_ts--serverFnConstWithImport_1", async () => { - return imported; - }); - const serverFnNamedWithImport_1 = createServerRpc("test_ts--serverFnNamedWithImport_1", function serverFnNamedWithImport() { - return imported; - }); - const serverFnNamedWithImport = serverFnNamedWithImport_1; - export { serverFnConstWithImport_1, serverFnNamedWithImport_1 };" - `) - }) - test('async function with anonymous default export', () => { - const code = ` - async function bytesSignupServerFn({ email }: { email: string }) { - 'use server' - return 'test' - } - export default function () { - return null; - } - ` - - const client = compileDirectives({ ...clientConfig, code }) - const ssr = compileDirectives({ ...ssrConfig, code }) - const server = compileDirectives({ - ...serverConfig, - code, - filename: - ssr.directiveFnsById[Object.keys(ssr.directiveFnsById)[0]!]! - .extractedFilename, - }) - - expect(client.compiledResult.code).toMatchInlineSnapshot(` - "import { createClientRpc } from "my-rpc-lib-client"; - const bytesSignupServerFn_1 = createClientRpc("test_ts--bytesSignupServerFn_1"); - const bytesSignupServerFn = bytesSignupServerFn_1; - export default function () { - return null; - }" - `) - expect(ssr.compiledResult.code).toMatchInlineSnapshot(` - "import { createSsrRpc } from "my-rpc-lib-server"; - const bytesSignupServerFn_1 = createSsrRpc("test_ts--bytesSignupServerFn_1"); - const bytesSignupServerFn = bytesSignupServerFn_1; - export default function () { - return null; - }" - `) - expect(server.compiledResult.code).toMatchInlineSnapshot(` - "import { createServerRpc } from "my-rpc-lib-server"; - const bytesSignupServerFn_1 = createServerRpc("test_ts--bytesSignupServerFn_1", async function ({ - email - }: { - email: string; - }) { - return 'test'; - }); - const bytesSignupServerFn = bytesSignupServerFn_1; - export default function () { - return null; - } - export { bytesSignupServerFn_1 };" - `) - }) - - test('generator function', () => { - const code = ` - function* generator() { - 'use server' - yield 'hello' - return 'hello world' - } - ` - const client = compileDirectives({ - ...clientConfig, - code, - }) - const ssr = compileDirectives({ ...ssrConfig, code }) - - const server = compileDirectives({ - ...serverConfig, - code, - filename: `${ssr.directiveFnsById[Object.keys(ssr.directiveFnsById)[0]!]!.extractedFilename}`, - }) - - expect(client.compiledResult.code).toMatchInlineSnapshot(` - "import { createClientRpc } from "my-rpc-lib-client"; - const generator_1 = createClientRpc("test_ts--generator_1"); - const generator = generator_1;" - `) - - expect(ssr.compiledResult.code).toMatchInlineSnapshot(` - "import { createSsrRpc } from "my-rpc-lib-server"; - const generator_1 = createSsrRpc("test_ts--generator_1"); - const generator = generator_1;" - `) - - expect(server.compiledResult.code).toMatchInlineSnapshot(` - "import { createServerRpc } from "my-rpc-lib-server"; - const generator_1 = createServerRpc("test_ts--generator_1", function* () { - yield 'hello'; - return 'hello world'; - }); - const generator = generator_1; - export { generator_1 };" - `) - }) - test('async generator function', () => { - const code = ` - async function* asyncGenerator() { - 'use server' - yield 'hello' - return 'hello world' - } - ` - const client = compileDirectives({ - ...clientConfig, - code, - }) - const ssr = compileDirectives({ ...ssrConfig, code }) - - const server = compileDirectives({ - ...serverConfig, - code, - filename: `${ssr.directiveFnsById[Object.keys(ssr.directiveFnsById)[0]!]!.extractedFilename}`, - }) - - expect(client.compiledResult.code).toMatchInlineSnapshot(` - "import { createClientRpc } from "my-rpc-lib-client"; - const asyncGenerator_1 = createClientRpc("test_ts--asyncGenerator_1"); - const asyncGenerator = asyncGenerator_1;" - `) - - expect(ssr.compiledResult.code).toMatchInlineSnapshot(` - "import { createSsrRpc } from "my-rpc-lib-server"; - const asyncGenerator_1 = createSsrRpc("test_ts--asyncGenerator_1"); - const asyncGenerator = asyncGenerator_1;" - `) - - expect(server.compiledResult.code).toMatchInlineSnapshot(` - "import { createServerRpc } from "my-rpc-lib-server"; - const asyncGenerator_1 = createServerRpc("test_ts--asyncGenerator_1", async function* () { - yield 'hello'; - return 'hello world'; - }); - const asyncGenerator = asyncGenerator_1; - export { asyncGenerator_1 };" - `) - }) - - test('server function references exported value', () => { - const code = ` - - import z from "zod"; - - export const zExampleSchema = z.object({ - name: z.string(), - age: z.number(), - }); - - export const namedFunction = createServerFn(() => { - 'use server' - zExampleSchema.safeParse({ name: 'test', age: 123 }) - return 'hello' - }) - ` - - const client = compileDirectives({ ...clientConfig, code }) - const ssr = compileDirectives({ ...ssrConfig, code }) - const server = compileDirectives({ - ...serverConfig, - code, - filename: - ssr.directiveFnsById[Object.keys(ssr.directiveFnsById)[0]!]! - .extractedFilename, - }) - - expect(client.compiledResult.code).toMatchInlineSnapshot(` - "import { createClientRpc } from "my-rpc-lib-client"; - import z from "zod"; - export const zExampleSchema = z.object({ - name: z.string(), - age: z.number() - }); - const namedFunction_createServerFn = createClientRpc("test_ts--namedFunction_createServerFn"); - export const namedFunction = createServerFn(namedFunction_createServerFn);" - `) - expect(ssr.compiledResult.code).toMatchInlineSnapshot(` - "import { createSsrRpc } from "my-rpc-lib-server"; - import z from "zod"; - export const zExampleSchema = z.object({ - name: z.string(), - age: z.number() - }); - const namedFunction_createServerFn = createSsrRpc("test_ts--namedFunction_createServerFn"); - export const namedFunction = createServerFn(namedFunction_createServerFn);" - `) - expect(server.compiledResult.code).toMatchInlineSnapshot(` - "import { createServerRpc } from "my-rpc-lib-server"; - import z from "zod"; - const zExampleSchema = z.object({ - name: z.string(), - age: z.number() - }); - const namedFunction_createServerFn = createServerRpc("test_ts--namedFunction_createServerFn", () => { - zExampleSchema.safeParse({ - name: 'test', - age: 123 - }); - return 'hello'; - }); - export { namedFunction_createServerFn };" - `) - }) -}) diff --git a/packages/directive-functions-plugin/tsconfig.json b/packages/directive-functions-plugin/tsconfig.json deleted file mode 100644 index 37d21ef6cab..00000000000 --- a/packages/directive-functions-plugin/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "include": ["src", "vite.config.ts", "tests"], - "exclude": ["tests/**/test-files/**", "tests/**/snapshots/**"], - "compilerOptions": { - "jsx": "react-jsx" - } -} diff --git a/packages/directive-functions-plugin/vite.config.ts b/packages/directive-functions-plugin/vite.config.ts deleted file mode 100644 index 5389f0f7391..00000000000 --- a/packages/directive-functions-plugin/vite.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { defineConfig, mergeConfig } from 'vitest/config' -import { tanstackViteConfig } from '@tanstack/config/vite' -import packageJson from './package.json' - -const config = defineConfig({ - test: { - name: packageJson.name, - dir: './tests', - watch: false, - typecheck: { enabled: true }, - }, -}) - -export default mergeConfig( - config, - tanstackViteConfig({ - entry: './src/index.ts', - srcDir: './src', - }), -) diff --git a/packages/server-functions-plugin/README.md b/packages/server-functions-plugin/README.md deleted file mode 100644 index deb185341d4..00000000000 --- a/packages/server-functions-plugin/README.md +++ /dev/null @@ -1,75 +0,0 @@ - - -# TanStack Server Functions Plugin - -## Configuration - -Create a new instance of the plugin with the following options: - -```ts -TanStackServerFnPlugin({ - // This is the ID that will be available to look up and import - // our server function manifest and resolve its module - manifestVirtualImportId: 'tanstack:server-fn-manifest', - generateFunctionId: startPluginOpts?.serverFns?.generateFunctionId, - client: { - getRuntimeCode: () => - `import { createClientRpc } from '@tanstack/${corePluginOpts.framework}-start/client-rpc'`, - replacer: (d) => `createClientRpc('${d.functionId}')`, - envName: 'client', - }, - server: { - getRuntimeCode: () => - `import { createServerRpc } from '@tanstack/${corePluginOpts.framework}-start/server-rpc'`, - replacer: (d) => `createServerRpc('${d.functionId}', ${d.fn})`, - envName: 'ssr', - }, -}), -``` - -## Providing the wrapper implementations - -Each runtime replacement should be implemented by your framework. Generally, on the client runtime, you'll end up using a `fetch` call to call the server function your desired endpoint, like this: - -```ts -function createClientRpc(functionId: string) { - const url = `${process.env.YOUR_SERVER_BASE}/_serverFn/${functionId}` - - const fn = async (...args: any[]) => { - const res = await fetch(url, { - method: 'POST', - // You'll likely want to use a better serializer here - body: JSON.stringify(args), - }) - - return await res.json() - } - - // You can also assign any other properties you want to the function - // for things like form actions, or debugging - Object.assign(fn, { - url: url, - }) - - return fn -} -``` - -## Using the manifest - -In your server handler, you can import the manifest and use it to look up and dynamically import the server function you want to call. - -```ts -import { getServerFnById } from 'tanstack:server-fn-manifest' - -export const handler = async (req: Request) => { - const functionId = req.url.split('/').pop() - invariant(functionId, 'No function ID provided') - - const serverFn = await getServerFnById(functionId) - - const args = await req.json() - - return await serverFn(...args) -} -``` diff --git a/packages/server-functions-plugin/eslint.config.js b/packages/server-functions-plugin/eslint.config.js deleted file mode 100644 index 8ce6ad05fcd..00000000000 --- a/packages/server-functions-plugin/eslint.config.js +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-check - -import rootConfig from '../../eslint.config.js' - -export default [...rootConfig] diff --git a/packages/server-functions-plugin/package.json b/packages/server-functions-plugin/package.json deleted file mode 100644 index b5c9e0be500..00000000000 --- a/packages/server-functions-plugin/package.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "name": "@tanstack/server-functions-plugin", - "version": "1.142.1", - "description": "Modern and scalable routing for React applications", - "author": "Tanner Linsley", - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/TanStack/router.git", - "directory": "packages/server-functions-plugin" - }, - "homepage": "https://tanstack.com/start", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "keywords": [ - "react", - "location", - "router", - "routing", - "async", - "async router", - "typescript" - ], - "scripts": { - "clean": "rimraf ./dist && rimraf ./coverage", - "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", - "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", - "module": "dist/esm/index.js", - "exports": { - ".": { - "import": { - "types": "./dist/esm/index.d.ts", - "default": "./dist/esm/index.js" - } - }, - "./package.json": "./package.json" - }, - "sideEffects": false, - "files": [ - "dist", - "src" - ], - "engines": { - "node": ">=12" - }, - "dependencies": { - "@tanstack/directive-functions-plugin": "workspace:*", - "@babel/code-frame": "7.27.1", - "@babel/core": "^7.27.7", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.27.1", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.27.7", - "@babel/types": "^7.27.7", - "babel-dead-code-elimination": "^1.0.9", - "tiny-invariant": "^1.3.3" - }, - "devDependencies": { - "@types/babel__code-frame": "^7.0.6", - "@types/babel__core": "^7.20.5", - "@types/babel__template": "^7.4.4", - "@types/babel__traverse": "^7.20.7" - } -} diff --git a/packages/server-functions-plugin/src/index.ts b/packages/server-functions-plugin/src/index.ts deleted file mode 100644 index 38c612ebbec..00000000000 --- a/packages/server-functions-plugin/src/index.ts +++ /dev/null @@ -1,332 +0,0 @@ -/// -import crypto from 'node:crypto' -import assert from 'node:assert' -import { TanStackDirectiveFunctionsPluginEnv } from '@tanstack/directive-functions-plugin' -import type { Plugin } from 'vite' -import type { - DirectiveFn, - GenerateFunctionIdFn, - ReplacerFn, -} from '@tanstack/directive-functions-plugin' - -export type GenerateFunctionIdFnOptional = ( - opts: Omit[0], 'extractedFilename'>, -) => string | undefined - -export type TanStackServerFnPluginOpts = { - /** - * This virtual import ID will be used in the server build to import the manifest - * and its modules. - */ - manifestVirtualImportId: string - generateFunctionId?: GenerateFunctionIdFnOptional - callers: Array< - ServerFnPluginEnvOpts & { - envConsumer: 'client' | 'server' - /** - * Custom getServerFnById implementation for server callers. - * Required for server callers that need to load modules from a different - * environment. - */ - getServerFnById?: string - } - > - provider: ServerFnPluginEnvOpts - directive: string -} - -export type ServerFnPluginEnvOpts = { - getRuntimeCode: () => string - replacer: ReplacerFn - envName: string -} - -const debug = - process.env.TSR_VITE_DEBUG && - ['true', 'server-functions-plugin'].includes(process.env.TSR_VITE_DEBUG) - -const validateServerFnIdVirtualModule = `virtual:tanstack-start-validate-server-fn-id` - -function parseIdQuery(id: string): { - filename: string - query: { - [k: string]: string - } -} { - if (!id.includes('?')) return { filename: id, query: {} } - const [filename, rawQuery] = id.split(`?`, 2) as [string, string] - const query = Object.fromEntries(new URLSearchParams(rawQuery)) - return { filename, query } -} - -export function TanStackServerFnPlugin( - opts: TanStackServerFnPluginOpts, -): Array { - const directiveFnsById: Record = {} - - const onDirectiveFnsById = (d: Record) => { - if (debug) { - console.info(`onDirectiveFnsById received: `, d) - } - Object.assign(directiveFnsById, d) - if (debug) { - console.info(`directiveFnsById after update: `, directiveFnsById) - } - } - - const entryIdToFunctionId = new Map() - const functionIds = new Set() - - function withTrailingSlash(path: string): string { - if (path[path.length - 1] !== '/') { - return `${path}/` - } - return path - } - - let root = process.cwd() - let command: 'build' | 'serve' = 'build' - - const generateFunctionId: GenerateFunctionIdFn = ({ - extractedFilename, - functionName, - filename, - }) => { - if (command === 'serve') { - const rootWithTrailingSlash = withTrailingSlash(root) - let file = extractedFilename - if (extractedFilename.startsWith(rootWithTrailingSlash)) { - file = extractedFilename.slice(rootWithTrailingSlash.length) - } - file = `/@id/${file}` - - const serverFn: { - file: string - export: string - } = { - file, - export: functionName, - } - const base64 = Buffer.from(JSON.stringify(serverFn), 'utf8').toString( - 'base64url', - ) - return base64 - } - - // production build allows to override the function ID generation - const entryId = `${filename}--${functionName}` - let functionId = entryIdToFunctionId.get(entryId) - if (functionId === undefined) { - if (opts.generateFunctionId) { - functionId = opts.generateFunctionId({ - functionName, - filename, - }) - } - if (!functionId) { - functionId = crypto.createHash('sha256').update(entryId).digest('hex') - } - // Deduplicate in case the generated id conflicts with an existing id - if (functionIds.has(functionId)) { - let deduplicatedId - let iteration = 0 - do { - deduplicatedId = `${functionId}_${++iteration}` - } while (functionIds.has(deduplicatedId)) - functionId = deduplicatedId - } - entryIdToFunctionId.set(entryId, functionId) - functionIds.add(functionId) - } - return functionId - } - - const resolvedManifestVirtualImportId = resolveViteId( - opts.manifestVirtualImportId, - ) - - const appliedEnvironments = new Set([ - ...opts.callers - .filter((c) => c.envConsumer === 'server') - .map((c) => c.envName), - opts.provider.envName, - ]) - - const serverCallerEnvironments = new Map( - opts.callers - .filter((c) => c.envConsumer === 'server') - .map((c) => [c.envName, c]), - ) - - // SSR is the provider when the provider environment is also a server caller environment - // In this case, server-only-referenced functions won't be in the manifest (they're handled via direct imports) - // When SSR is NOT the provider, server-only-referenced functions ARE in the manifest and need isClientReferenced check - const ssrIsProvider = serverCallerEnvironments.has(opts.provider.envName) - - return [ - // The client plugin is used to compile the client directives - // and save them so we can create a manifest - TanStackDirectiveFunctionsPluginEnv({ - directive: opts.directive, - onDirectiveFnsById, - generateFunctionId, - provider: opts.provider, - callers: opts.callers, - // Provide access to known directive functions so SSR callers can use - // canonical extracted filenames from the client build - getKnownDirectiveFns: () => directiveFnsById, - }), - { - name: 'tanstack-start-server-fn-vite-plugin-validate-serverfn-id', - apply: 'serve', - load: { - filter: { - id: new RegExp(resolveViteId(validateServerFnIdVirtualModule)), - }, - handler(id) { - const parsed = parseIdQuery(id) - assert(parsed) - assert(parsed.query.id) - if (directiveFnsById[parsed.query.id]) { - return `export {}` - } - this.error(`Invalid server function ID: ${parsed.query.id}`) - }, - }, - }, - { - // On the server, we need to be able to read the server-function manifest from the client build. - // This is likely used in the handler for server functions, so we can find the server function - // by its ID, import it, and call it. - name: 'tanstack-start-server-fn-vite-plugin-manifest-server', - enforce: 'pre', - applyToEnvironment: (env) => { - return appliedEnvironments.has(env.name) - }, - configResolved(config) { - root = config.root - command = config.command - }, - resolveId: { - filter: { id: new RegExp(opts.manifestVirtualImportId) }, - handler() { - return resolvedManifestVirtualImportId - }, - }, - load: { - filter: { id: new RegExp(resolvedManifestVirtualImportId) }, - handler() { - // a different server side environment is used for e.g. SSR and server functions - if (this.environment.name !== opts.provider.envName) { - const getServerFnById = serverCallerEnvironments.get( - this.environment.name, - )?.getServerFnById - if (!getServerFnById) { - throw new Error( - `No getServerFnById implementation found for environment ${this.environment.name}`, - ) - } - - return getServerFnById - } - - if (this.environment.mode !== 'build') { - const mod = ` - export async function getServerFnById(id) { - const validateIdImport = ${JSON.stringify(validateServerFnIdVirtualModule)} + '?id=' + id - await import(/* @vite-ignore */ '/@id/__x00__' + validateIdImport) - const decoded = Buffer.from(id, 'base64url').toString('utf8') - const devServerFn = JSON.parse(decoded) - const mod = await import(/* @vite-ignore */ devServerFn.file) - return mod[devServerFn.export] - } - ` - return mod - } - - // When SSR is the provider, server-only-referenced functions aren't in the manifest, - // so no isClientReferenced check is needed. - // When SSR is NOT the provider (custom provider env), server-only-referenced - // functions ARE in the manifest and need the isClientReferenced check to - // block direct client HTTP requests to server-only-referenced functions. - const includeClientReferencedCheck = !ssrIsProvider - return generateManifestModule( - directiveFnsById, - includeClientReferencedCheck, - ) - }, - }, - }, - ] -} - -/** - * Generates the manifest module code for server functions. - * @param directiveFnsById - Map of function IDs to their directive function info - * @param includeClientReferencedCheck - Whether to include isClientReferenced flag and runtime check. - * This is needed when SSR is NOT the provider, so server-only-referenced functions in the manifest - * can be blocked from client HTTP requests. - */ -function generateManifestModule( - directiveFnsById: Record, - includeClientReferencedCheck: boolean, -): string { - const manifestEntries = Object.entries(directiveFnsById) - .map(([id, fn]) => { - const baseEntry = `'${id}': { - functionName: '${fn.functionName}', - importer: () => import(${JSON.stringify(fn.extractedFilename)})${ - includeClientReferencedCheck - ? `, - isClientReferenced: ${fn.isClientReferenced ?? true}` - : '' - } - }` - return baseEntry - }) - .join(',') - - const getServerFnByIdParams = includeClientReferencedCheck ? 'id, opts' : 'id' - const clientReferencedCheck = includeClientReferencedCheck - ? ` - // If called from client, only allow client-referenced functions - if (opts?.fromClient && !serverFnInfo.isClientReferenced) { - throw new Error('Server function not accessible from client: ' + id) - } -` - : '' - - return ` - const manifest = {${manifestEntries}} - - export async function getServerFnById(${getServerFnByIdParams}) { - const serverFnInfo = manifest[id] - if (!serverFnInfo) { - throw new Error('Server function info not found for ' + id) - } -${clientReferencedCheck} - const fnModule = await serverFnInfo.importer() - - if (!fnModule) { - console.info('serverFnInfo', serverFnInfo) - throw new Error('Server function module not resolved for ' + id) - } - - const action = fnModule[serverFnInfo.functionName] - - if (!action) { - console.info('serverFnInfo', serverFnInfo) - console.info('fnModule', fnModule) - - throw new Error( - \`Server function module export not resolved for serverFn ID: \${id}\`, - ) - } - return action - } - ` -} - -function resolveViteId(id: string) { - return `\0${id}` -} diff --git a/packages/server-functions-plugin/tests/index.test.ts b/packages/server-functions-plugin/tests/index.test.ts deleted file mode 100644 index a2d03d603e7..00000000000 --- a/packages/server-functions-plugin/tests/index.test.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { expect, test } from 'vitest' - -test('test', () => { - expect(1).toBe(1) -}) diff --git a/packages/server-functions-plugin/tsconfig.json b/packages/server-functions-plugin/tsconfig.json deleted file mode 100644 index 37d21ef6cab..00000000000 --- a/packages/server-functions-plugin/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "include": ["src", "vite.config.ts", "tests"], - "exclude": ["tests/**/test-files/**", "tests/**/snapshots/**"], - "compilerOptions": { - "jsx": "react-jsx" - } -} diff --git a/packages/server-functions-plugin/vite.config.ts b/packages/server-functions-plugin/vite.config.ts deleted file mode 100644 index ac06399a8f3..00000000000 --- a/packages/server-functions-plugin/vite.config.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { defineConfig, mergeConfig } from 'vitest/config' -import { tanstackViteConfig } from '@tanstack/config/vite' -import packageJson from './package.json' - -const config = defineConfig({ - test: { - name: packageJson.name, - dir: './tests', - watch: false, - typecheck: { enabled: true }, - }, -}) - -export default mergeConfig( - config, - tanstackViteConfig({ - entry: './src/index.ts', - srcDir: './src', - cjs: false, - }), -) diff --git a/packages/start-plugin-core/package.json b/packages/start-plugin-core/package.json index 9f70eb2e9a6..f21aec75ed6 100644 --- a/packages/start-plugin-core/package.json +++ b/packages/start-plugin-core/package.json @@ -69,7 +69,6 @@ "@tanstack/router-generator": "workspace:*", "@tanstack/router-plugin": "workspace:*", "@tanstack/router-utils": "workspace:*", - "@tanstack/server-functions-plugin": "workspace:*", "@tanstack/start-client-core": "workspace:*", "@tanstack/start-server-core": "workspace:*", "babel-dead-code-elimination": "^1.0.9", 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 deleted file mode 100644 index 6e2d7c6be92..00000000000 --- a/packages/start-plugin-core/src/create-server-fn-plugin/handleCreateIsomorphicFn.ts +++ /dev/null @@ -1,46 +0,0 @@ -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/handleCreateMiddleware.ts b/packages/start-plugin-core/src/create-server-fn-plugin/handleCreateMiddleware.ts deleted file mode 100644 index 25c23d0fb1c..00000000000 --- a/packages/start-plugin-core/src/create-server-fn-plugin/handleCreateMiddleware.ts +++ /dev/null @@ -1,45 +0,0 @@ -import * as t from '@babel/types' -import type { RewriteCandidate } from './types' - -/** - * Handles createMiddleware transformations. - * - * @param candidate - The rewrite candidate containing path and method chain - * @param opts - Options including the environment - */ -export function handleCreateMiddleware( - candidate: RewriteCandidate, - opts: { - env: 'client' | 'server' - }, -) { - if (opts.env === 'server') { - throw new Error('handleCreateMiddleware should not be called on the server') - } - - const { inputValidator, server } = candidate.methodChain - - if (inputValidator) { - const innerInputExpression = inputValidator.callPath.node.arguments[0] - - if (!innerInputExpression) { - throw new Error( - 'createMiddleware().inputValidator() must be called with a validator!', - ) - } - - // remove the validator call expression - if (t.isMemberExpression(inputValidator.callPath.node.callee)) { - inputValidator.callPath.replaceWith( - inputValidator.callPath.node.callee.object, - ) - } - } - - if (server) { - // remove the server call expression - if (t.isMemberExpression(server.callPath.node.callee)) { - server.callPath.replaceWith(server.callPath.node.callee.object) - } - } -} 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 deleted file mode 100644 index 112d99f420d..00000000000 --- a/packages/start-plugin-core/src/create-server-fn-plugin/handleCreateServerFn.ts +++ /dev/null @@ -1,145 +0,0 @@ -import * as t from '@babel/types' -import { codeFrameError } from './utils' -import type { RewriteCandidate } from './types' - -/** - * Handles createServerFn transformations. - * - * @param candidate - The rewrite candidate containing path and method chain - * @param opts - Options including the environment, code, directive, and provider file flag - */ -export function handleCreateServerFn( - candidate: RewriteCandidate, - opts: { - env: 'client' | 'server' - code: string - directive: string - /** - * Whether this file is a provider file (extracted server function file). - * Only provider files should have the handler implementation as a second argument. - */ - isProviderFile: boolean - }, -) { - const { path, methodChain } = candidate - const { inputValidator, handler } = methodChain - - // Check if the call is assigned to a variable - if (!path.parentPath.isVariableDeclarator()) { - throw new Error('createServerFn must be assigned to a variable!') - } - - // Get the identifier name of the variable - const variableDeclarator = path.parentPath.node - if (!t.isIdentifier(variableDeclarator.id)) { - throw codeFrameError( - opts.code, - variableDeclarator.id.loc!, - 'createServerFn must be assigned to a simple identifier, not a destructuring pattern', - ) - } - const existingVariableName = variableDeclarator.id.name - - if (inputValidator) { - const innerInputExpression = inputValidator.callPath.node.arguments[0] - - if (!innerInputExpression) { - throw new Error( - 'createServerFn().inputValidator() must be called with a validator!', - ) - } - - // If we're on the client, remove the validator call expression - if (opts.env === 'client') { - if (t.isMemberExpression(inputValidator.callPath.node.callee)) { - inputValidator.callPath.replaceWith( - inputValidator.callPath.node.callee.object, - ) - } - } - } - - // First, we need to move the handler function to a nested function call - // that is applied to the arguments passed to the server function. - - const handlerFnPath = handler?.firstArgPath - - if (!handler || !handlerFnPath?.node) { - throw codeFrameError( - opts.code, - path.node.callee.loc!, - `createServerFn must be called with a "handler" property!`, - ) - } - - // Validate the handler argument is an expression (not a SpreadElement, etc.) - if (!t.isExpression(handlerFnPath.node)) { - throw codeFrameError( - opts.code, - handlerFnPath.node.loc!, - `handler() must be called with an expression, not a ${handlerFnPath.node.type}`, - ) - } - - const handlerFn = handlerFnPath.node - - // So, the way we do this is we give the handler function a way - // to access the serverFn ctx on the server via function scope. - // The 'use server' extracted function will be called with the - // payload from the client, then use the scoped serverFn ctx - // to execute the handler function. - // This way, we can do things like data and middleware validation - // in the __execute function without having to AST transform the - // handler function too much itself. - - // .handler((optsOut, ctx) => { - // return ((optsIn) => { - // 'use server' - // ctx.__execute(handlerFn, optsIn) - // })(optsOut) - // }) - - // If the handler function is an identifier and we're on the client, we need to - // remove the bound function from the file. - // If we're on the server, you can leave it, since it will get referenced - // as a second argument. - - if (t.isIdentifier(handlerFn)) { - if (opts.env === 'client') { - // Find the binding for the handler function - const binding = handlerFnPath.scope.getBinding(handlerFn.name) - // Remove it - if (binding) { - binding.path.remove() - } - } - // If the env is server, just leave it alone - } - - handlerFnPath.replaceWith( - t.arrowFunctionExpression( - [t.identifier('opts'), t.identifier('signal')], - t.blockStatement( - // Everything in here is server-only, since the client - // will strip out anything in the 'use server' directive. - [ - t.returnStatement( - t.callExpression( - t.identifier(`${existingVariableName}.__executeServer`), - [t.identifier('opts'), t.identifier('signal')], - ), - ), - ], - [t.directive(t.directiveLiteral(opts.directive))], - ), - ), - ) - - // Add the serverFn as a second argument on the server side, - // but ONLY for provider files (extracted server function files). - // Caller files must NOT have the second argument because the implementation is already available in the extracted chunk - // and including it would duplicate code - if (opts.env === 'server' && opts.isProviderFile) { - handler.callPath.node.arguments.push(handlerFn) - } -} 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 deleted file mode 100644 index 206d13eab48..00000000000 --- a/packages/start-plugin-core/src/create-server-fn-plugin/handleEnvOnly.ts +++ /dev/null @@ -1,45 +0,0 @@ -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 deleted file mode 100644 index ee35e68849f..00000000000 --- a/packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { TRANSFORM_ID_REGEX } from '../constants' -import { - KindDetectionPatterns, - LookupKindsPerEnv, - ServerFnCompiler, - detectKindsInCode, -} from './compiler' -import type { CompileStartFrameworkOptions } from '../types' -import type { LookupConfig, LookupKind } from './compiler' -import type { PluginOption } from 'vite' - -function cleanId(id: string): string { - // Remove null byte prefix used by Vite/Rollup for virtual modules - if (id.startsWith('\0')) { - id = id.slice(1) - } - const queryIndex = id.indexOf('?') - return queryIndex === -1 ? id : id.substring(0, queryIndex) -} - -// Derive transform code filter from KindDetectionPatterns (single source of truth) -function getTransformCodeFilterForEnv(env: 'client' | 'server'): Array { - const validKinds = LookupKindsPerEnv[env] - const patterns: Array = [] - for (const [kind, pattern] of Object.entries(KindDetectionPatterns) as Array< - [LookupKind, RegExp] - >) { - if (validKinds.has(kind)) { - patterns.push(pattern) - } - } - return patterns -} - -const getLookupConfigurationsForEnv = ( - env: 'client' | 'server', - framework: CompileStartFrameworkOptions, -): Array => { - // Common configs for all environments - const commonConfigs: Array = [ - { - libName: `@tanstack/${framework}-start`, - rootExport: 'createServerFn', - kind: 'Root', - }, - { - libName: `@tanstack/${framework}-start`, - rootExport: 'createIsomorphicFn', - kind: 'IsomorphicFn', - }, - { - 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', - }, - ...commonConfigs, - ] - } else { - // Server-only: add ClientOnly JSX component lookup - return [ - ...commonConfigs, - { - libName: `@tanstack/${framework}-router`, - rootExport: 'ClientOnly', - kind: 'ClientOnlyJSX', - }, - ] - } -} -const SERVER_FN_LOOKUP = 'server-fn-module-lookup' - -function buildDirectiveSplitParam(directive: string) { - return `tsr-directive-${directive.replace(/[^a-zA-Z0-9]/g, '-')}` -} - -export function createServerFnPlugin(opts: { - framework: CompileStartFrameworkOptions - directive: string - environments: Array<{ name: string; type: 'client' | 'server' }> -}): PluginOption { - const compilers: Record = {} - const directiveSplitParam = buildDirectiveSplitParam(opts.directive) - - function perEnvServerFnPlugin(environment: { - name: string - type: 'client' | 'server' - }): PluginOption { - // Derive transform code filter from KindDetectionPatterns (single source of truth) - const transformCodeFilter = getTransformCodeFilterForEnv(environment.type) - - return { - name: `tanstack-start-core::server-fn:${environment.name}`, - enforce: 'pre', - applyToEnvironment(env) { - return env.name === environment.name - }, - transform: { - filter: { - id: { - exclude: new RegExp(`${SERVER_FN_LOOKUP}$`), - include: TRANSFORM_ID_REGEX, - }, - code: { - include: transformCodeFilter, - }, - }, - async handler(code, id) { - let compiler = compilers[this.environment.name] - if (!compiler) { - // Default to 'dev' mode for unknown environments (conservative: no caching) - const mode = - this.environment.mode === 'build' ? 'build' : ('dev' as const) - compiler = new ServerFnCompiler({ - env: environment.type, - directive: opts.directive, - lookupKinds: LookupKindsPerEnv[environment.type], - lookupConfigurations: getLookupConfigurationsForEnv( - environment.type, - opts.framework, - ), - mode, - loadModule: async (id: string) => { - if (this.environment.mode === 'build') { - const loaded = await this.load({ id }) - // Handle modules with no runtime code (e.g., type-only exports). - // After TypeScript compilation, these become empty modules. - // Create an empty module info instead of throwing. - const code = loaded.code ?? '' - compiler!.ingestModule({ code, id }) - } else if (this.environment.mode === 'dev') { - /** - * in dev, vite does not return code from `ctx.load()` - * so instead, we need to take a different approach - * we must force vite to load the module and run it through the vite plugin pipeline - * we can do this by using the `fetchModule` method - * the `captureServerFnModuleLookupPlugin` captures the module code via its transform hook and invokes analyzeModuleAST - */ - await this.environment.fetchModule( - id + '?' + SERVER_FN_LOOKUP, - ) - } else { - throw new Error( - `could not load module ${id}: unknown environment mode ${this.environment.mode}`, - ) - } - }, - resolveId: async (source: string, importer?: string) => { - const r = await this.resolve(source, importer) - if (r) { - if (!r.external) { - return cleanId(r.id) - } - } - return null - }, - }) - compilers[this.environment.name] = compiler - } - - const isProviderFile = id.includes(directiveSplitParam) - - // Detect which kinds are present in this file before parsing - const detectedKinds = detectKindsInCode(code, environment.type) - - id = cleanId(id) - const result = await compiler.compile({ - id, - code, - isProviderFile, - detectedKinds, - }) - return result - }, - }, - - hotUpdate(ctx) { - const compiler = compilers[this.environment.name] - - ctx.modules.forEach((m) => { - if (m.id) { - const deleted = compiler?.invalidateModule(m.id) - if (deleted) { - m.importers.forEach((importer) => { - if (importer.id) { - compiler?.invalidateModule(importer.id) - } - }) - } - } - }) - }, - } - } - - return [ - ...opts.environments.map(perEnvServerFnPlugin), - { - name: 'tanstack-start-core:capture-server-fn-module-lookup', - // we only need this plugin in dev mode - apply: 'serve', - applyToEnvironment(env) { - return !!opts.environments.find((e) => e.name === env.name) - }, - transform: { - filter: { - id: new RegExp(`${SERVER_FN_LOOKUP}$`), - }, - handler(code, id) { - const compiler = compilers[this.environment.name] - compiler?.ingestModule({ code, id: cleanId(id) }) - }, - }, - }, - ] -} 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 deleted file mode 100644 index 02886dc9fbc..00000000000 --- a/packages/start-plugin-core/src/create-server-fn-plugin/types.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type * as babel from '@babel/core' -import type * as t from '@babel/types' - -/** - * Info about a method call in the chain, including the call expression path - * and the path to its first argument (if any). - */ -export interface MethodCallInfo { - callPath: babel.NodePath - /** Path to the first argument, or null if no arguments */ - firstArgPath: babel.NodePath | null -} - -/** - * Pre-collected method chain paths for a root call expression. - * This avoids needing to traverse the AST again in handlers. - */ -export interface MethodChainPaths { - middleware: MethodCallInfo | null - inputValidator: MethodCallInfo | null - handler: MethodCallInfo | null - server: MethodCallInfo | null - client: MethodCallInfo | null -} - -export type MethodChainKey = keyof MethodChainPaths - -/** - * Information about a candidate that needs to be rewritten. - */ -export interface RewriteCandidate { - path: babel.NodePath - methodChain: MethodChainPaths -} 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 deleted file mode 100644 index 68d60a4fb12..00000000000 --- a/packages/start-plugin-core/src/create-server-fn-plugin/utils.ts +++ /dev/null @@ -1,24 +0,0 @@ -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 cfca4a12161..5147b96337c 100644 --- a/packages/start-plugin-core/src/plugin.ts +++ b/packages/start-plugin-core/src/plugin.ts @@ -1,6 +1,4 @@ import { joinPaths } from '@tanstack/router-core' -import { VIRTUAL_MODULES } from '@tanstack/start-server-core' -import { TanStackServerFnPlugin } from '@tanstack/server-functions-plugin' import * as vite from 'vite' import { crawlFrameworkPkgs } from 'vitefu' import { join } from 'pathe' @@ -18,7 +16,7 @@ import { getServerOutputDirectory, } from './output-directory' import { postServerBuild } from './post-server-build' -import { createServerFnPlugin } from './create-server-fn-plugin/plugin' +import { startCompilerPlugin } from './start-compiler-plugin/plugin' import type { GetConfigFn, ResolvedStartConfig, @@ -59,8 +57,6 @@ export function TanStackStartVitePluginCore( serverFnProviderEnv, } - const directive = corePluginOpts.serverFn?.directive ?? 'use server' - let startConfig: TanStackStartOutputConfig | null const getConfig: GetConfigFn = () => { if (!resolvedStartConfig.root) { @@ -356,55 +352,19 @@ export function TanStackStartVitePluginCore( }, }, }, - tanStackStartRouter(startPluginOpts, getConfig, corePluginOpts), - // N.B. Server function plugins must run BEFORE startCompilerPlugin because: - // 1. createServerFnPlugin transforms createServerFn().handler() to inject 'use server' directive - // 2. TanStackServerFnPlugin extracts 'use server' functions and registers them in the manifest - // 3. startCompilerPlugin handles createClientOnlyFn/createServerOnlyFn and runs DCE - // If startCompilerPlugin runs first, DCE may remove server function code before it can be registered - // (e.g., when a server function is only referenced inside a createClientOnlyFn callback) - createServerFnPlugin({ + // Server function plugin handles: + // 1. Identifying createServerFn().handler() calls + // 2. Extracting server functions to separate modules + // 3. Replacing call sites with RPC stubs + // 4. Generating the server function manifest + // Also handles createIsomorphicFn, createServerOnlyFn, createClientOnlyFn, createMiddleware + startCompilerPlugin({ framework: corePluginOpts.framework, - directive, environments, - }), - TanStackServerFnPlugin({ - // This is the ID that will be available to look up and import - // our server function manifest and resolve its module - manifestVirtualImportId: VIRTUAL_MODULES.serverFnManifest, - directive, generateFunctionId: startPluginOpts?.serverFns?.generateFunctionId, - callers: [ - { - envConsumer: 'client', - getRuntimeCode: () => - `import { createClientRpc } from '@tanstack/${corePluginOpts.framework}-start/client-rpc'`, - replacer: (d) => `createClientRpc('${d.functionId}')`, - envName: VITE_ENVIRONMENT_NAMES.client, - }, - { - envConsumer: 'server' as const, - getRuntimeCode: () => - `import { createSsrRpc } from '@tanstack/${corePluginOpts.framework}-start/ssr-rpc'`, - envName: VITE_ENVIRONMENT_NAMES.server, - replacer: (d: any) => - // When the function is client-referenced, it's in the manifest - use manifest lookup - // When SSR is NOT the provider, always use manifest lookup (no import() for different env) - // Otherwise, use the importer for functions only referenced on the server when SSR is the provider - d.isClientReferenced || !ssrIsProvider - ? `createSsrRpc('${d.functionId}')` - : `createSsrRpc('${d.functionId}', () => import(${JSON.stringify(d.extractedFilename)}).then(m => m['${d.functionName}']))`, - }, - ], - provider: { - getRuntimeCode: () => - `import { createServerRpc } from '@tanstack/${corePluginOpts.framework}-start/server-rpc'`, - replacer: (d) => `createServerRpc('${d.functionId}', ${d.fn})`, - envName: serverFnProviderEnv, - }, + providerEnvName: serverFnProviderEnv, }), - // Note: startCompilerPlugin functionality (createIsomorphicFn, createServerOnlyFn, createClientOnlyFn) - // is now merged into createServerFnPlugin above + tanStackStartRouter(startPluginOpts, getConfig, corePluginOpts), loadEnvPlugin(), startManifestPlugin({ getClientBundle: () => getBundle(VITE_ENVIRONMENT_NAMES.client), diff --git a/packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts b/packages/start-plugin-core/src/start-compiler-plugin/compiler.ts similarity index 87% rename from packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts rename to packages/start-plugin-core/src/start-compiler-plugin/compiler.ts index ff203523858..ee441008d6c 100644 --- a/packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts +++ b/packages/start-plugin-core/src/start-compiler-plugin/compiler.ts @@ -1,4 +1,5 @@ /* eslint-disable import/no-commonjs */ +import crypto from 'node:crypto' import * as t from '@babel/types' import { generateFromAst, parseAst } from '@tanstack/router-utils' import babel from '@babel/core' @@ -11,7 +12,13 @@ import { handleCreateMiddleware } from './handleCreateMiddleware' import { handleCreateIsomorphicFn } from './handleCreateIsomorphicFn' import { handleEnvOnlyFn } from './handleEnvOnly' import { handleClientOnlyJSX } from './handleClientOnlyJSX' -import type { MethodChainPaths, RewriteCandidate } from './types' +import type { + CompilationContext, + MethodChainPaths, + RewriteCandidate, + ServerFn, +} from './types' +import type { CompileStartFrameworkOptions } from '../types' type Binding = | { @@ -106,6 +113,32 @@ export const LookupKindsPerEnv: Record<'client' | 'server', Set> = { ] as const), } +/** + * Handler type for processing candidates of a specific kind. + * The kind is passed as the third argument to allow shared handlers (like handleEnvOnlyFn). + */ +type KindHandler = ( + candidates: Array, + context: CompilationContext, + kind: LookupKind, +) => void + +/** + * Registry mapping each LookupKind to its handler function. + * When adding a new kind, add its handler here. + */ +const KindHandlers: Record< + Exclude, + KindHandler +> = { + ServerFn: handleCreateServerFn, + Middleware: handleCreateMiddleware, + IsomorphicFn: handleCreateIsomorphicFn, + ServerOnlyFn: handleEnvOnlyFn, + ClientOnlyFn: handleEnvOnlyFn, + // ClientOnlyJSX is handled separately via JSX traversal, not here +} + /** * Detects which LookupKinds are present in the code using string matching. * This is a fast pre-scan before AST parsing to limit the work done during compilation. @@ -279,7 +312,7 @@ function isTopLevelDirectCallCandidate( return t.isProgram(path.parentPath.parentPath?.parent) } -export class ServerFnCompiler { +export class StartCompiler { private moduleCache = new Map() private initialized = false private validLookupKinds: Set @@ -292,10 +325,16 @@ export class ServerFnCompiler { // Maps: libName → (exportName → Kind) // This allows O(1) resolution for the common case without async resolveId calls private knownRootImports = new Map>() + + // For generating unique function IDs in production builds + private entryIdToFunctionId = new Map() + private functionIds = new Set() + constructor( private options: { env: 'client' | 'server' - directive: string + envName: string + root: string lookupConfigurations: Array lookupKinds: Set loadModule: (id: string) => Promise @@ -305,11 +344,92 @@ export class ServerFnCompiler { * In 'dev' mode (default), caching is disabled to avoid invalidation complexity with HMR. */ mode?: 'dev' | 'build' + /** + * The framework being used (e.g., 'react', 'solid'). + */ + framework: CompileStartFrameworkOptions + /** + * The Vite environment name for the server function provider. + */ + providerEnvName: string + /** + * Custom function ID generator (optional, defaults to hash-based). + */ + generateFunctionId?: (opts: { + filename: string + functionName: string + }) => string | undefined + /** + * Callback when server functions are discovered. + * Called after each file is compiled with its new functions. + */ + onServerFnsById?: (d: Record) => void + /** + * Returns the currently known server functions from previous builds. + * Used by server callers to look up canonical extracted filenames. + */ + getKnownServerFns?: () => Record }, ) { this.validLookupKinds = options.lookupKinds } + /** + * Generates a unique function ID for a server function. + * In dev mode, uses a base64-encoded JSON with file path and export name. + * In build mode, uses SHA256 hash or custom generator. + */ + private generateFunctionId(opts: { + filename: string + functionName: string + extractedFilename: string + }): string { + if (this.mode === 'dev') { + // In dev, encode the file path and export name for direct lookup + const rootWithTrailingSlash = this.options.root.endsWith('/') + ? this.options.root + : `${this.options.root}/` + let file = opts.extractedFilename + if (opts.extractedFilename.startsWith(rootWithTrailingSlash)) { + file = opts.extractedFilename.slice(rootWithTrailingSlash.length) + } + file = `/@id/${file}` + + const serverFn = { + file, + export: opts.functionName, + } + return Buffer.from(JSON.stringify(serverFn), 'utf8').toString('base64url') + } + + // Production build: use custom generator or hash + const entryId = `${opts.filename}--${opts.functionName}` + let functionId = this.entryIdToFunctionId.get(entryId) + if (functionId === undefined) { + if (this.options.generateFunctionId) { + functionId = this.options.generateFunctionId({ + filename: opts.filename, + functionName: opts.functionName, + }) + } + if (!functionId) { + functionId = crypto.createHash('sha256').update(entryId).digest('hex') + } + // Deduplicate in case the generated id conflicts with an existing id + if (this.functionIds.has(functionId)) { + let deduplicatedId + let iteration = 0 + do { + deduplicatedId = `${functionId}_${++iteration}` + } while (this.functionIds.has(deduplicatedId)) + functionId = deduplicatedId + } + this.entryIdToFunctionId.set(entryId, functionId) + this.functionIds.add(functionId) + } + return functionId + } + private get mode(): 'dev' | 'build' { return this.options.mode ?? 'dev' } @@ -536,12 +656,10 @@ export class ServerFnCompiler { public async compile({ code, id, - isProviderFile, detectedKinds, }: { code: string id: string - isProviderFile: boolean /** Pre-detected kinds present in this file. If not provided, all valid kinds are checked. */ detectedKinds?: Set }) { @@ -732,8 +850,11 @@ export class ServerFnCompiler { // Filter to valid candidates const validCandidates = resolvedCandidates.filter(({ kind }) => - this.validLookupKinds.has(kind as LookupKind), - ) as Array<{ path: babel.NodePath; kind: LookupKind }> + this.validLookupKinds.has(kind as Exclude), + ) as Array<{ + path: babel.NodePath + kind: Exclude + }> if (validCandidates.length === 0 && jsxCandidatePaths.length === 0) { return null @@ -742,7 +863,7 @@ export class ServerFnCompiler { // Process valid candidates to collect method chains const pathsToRewrite: Array<{ path: babel.NodePath - kind: LookupKind + kind: Exclude methodChain: MethodChainPaths }> = [] @@ -802,32 +923,43 @@ export class ServerFnCompiler { const refIdents = findReferencedIdentifiers(ast) - for (const { path, kind, methodChain } of pathsToRewrite) { - const candidate: RewriteCandidate = { path, methodChain } - if (kind === 'ServerFn') { - handleCreateServerFn(candidate, { - env: this.options.env, - code, - directive: this.options.directive, - isProviderFile, - }) - } else if (kind === 'Middleware') { - handleCreateMiddleware(candidate, { - env: this.options.env, - }) - } else if (kind === 'IsomorphicFn') { - handleCreateIsomorphicFn(candidate, { - env: this.options.env, - }) + const context: CompilationContext = { + ast, + id, + code, + env: this.options.env, + envName: this.options.envName, + root: this.options.root, + framework: this.options.framework, + providerEnvName: this.options.providerEnvName, + + generateFunctionId: (opts) => this.generateFunctionId(opts), + getKnownServerFns: () => this.options.getKnownServerFns?.() ?? {}, + onServerFnsById: this.options.onServerFnsById, + } + + // Group candidates by kind for batch processing + const candidatesByKind = new Map< + Exclude, + Array + >() + + for (const { path: candidatePath, kind, methodChain } of pathsToRewrite) { + const candidate: RewriteCandidate = { path: candidatePath, methodChain } + const existing = candidatesByKind.get(kind) + if (existing) { + existing.push(candidate) } else { - // ServerOnlyFn or ClientOnlyFn - handleEnvOnlyFn(candidate, { - env: this.options.env, - kind, - }) + candidatesByKind.set(kind, [candidate]) } } + // Process each kind using its registered handler + for (const [kind, candidates] of candidatesByKind) { + const handler = KindHandlers[kind] + handler(candidates, context, kind) + } + // Handle JSX candidates (e.g., ) // Note: We only reach here on the server (ClientOnlyJSX is only in LookupKindsPerEnv.server) // Verify import source using knownRootImports (same as function call resolution) diff --git a/packages/start-plugin-core/src/create-server-fn-plugin/handleClientOnlyJSX.ts b/packages/start-plugin-core/src/start-compiler-plugin/handleClientOnlyJSX.ts similarity index 100% rename from packages/start-plugin-core/src/create-server-fn-plugin/handleClientOnlyJSX.ts rename to packages/start-plugin-core/src/start-compiler-plugin/handleClientOnlyJSX.ts diff --git a/packages/start-plugin-core/src/start-compiler-plugin/handleCreateIsomorphicFn.ts b/packages/start-plugin-core/src/start-compiler-plugin/handleCreateIsomorphicFn.ts new file mode 100644 index 00000000000..83b7a69dde6 --- /dev/null +++ b/packages/start-plugin-core/src/start-compiler-plugin/handleCreateIsomorphicFn.ts @@ -0,0 +1,54 @@ +import * as t from '@babel/types' +import type { CompilationContext, RewriteCandidate } from './types' + +/** + * Handles createIsomorphicFn transformations for a batch of candidates. + * + * @param candidates - All IsomorphicFn candidates to process + * @param context - The compilation context + */ +export function handleCreateIsomorphicFn( + candidates: Array, + context: CompilationContext, +): void { + for (const candidate of candidates) { + const { path, methodChain } = candidate + + // Get the environment-specific call (.client() or .server()) + const envCallInfo = + context.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([]))) + continue + } + + if (!envCallInfo) { + // No implementation for this environment - replace with no-op + path.replaceWith(t.arrowFunctionExpression([], t.blockStatement([]))) + continue + } + + // Extract the function argument from the environment-specific call + const innerFn = envCallInfo.firstArgPath?.node + + if (!t.isExpression(innerFn)) { + throw new Error( + `createIsomorphicFn().${context.env}(func) must be called with a function!`, + ) + } + + path.replaceWith(innerFn) + } +} diff --git a/packages/start-plugin-core/src/start-compiler-plugin/handleCreateMiddleware.ts b/packages/start-plugin-core/src/start-compiler-plugin/handleCreateMiddleware.ts new file mode 100644 index 00000000000..5428ce1a88e --- /dev/null +++ b/packages/start-plugin-core/src/start-compiler-plugin/handleCreateMiddleware.ts @@ -0,0 +1,39 @@ +import { stripMethodCall } from './utils' +import type { CompilationContext, RewriteCandidate } from './types' + +/** + * Handles createMiddleware transformations for a batch of candidates. + * + * @param candidates - All Middleware candidates to process + * @param context - The compilation context + */ +export function handleCreateMiddleware( + candidates: Array, + context: CompilationContext, +): void { + if (context.env === 'server') { + throw new Error('handleCreateMiddleware should not be called on the server') + } + + for (const candidate of candidates) { + const { inputValidator, server } = candidate.methodChain + + if (inputValidator) { + const innerInputExpression = inputValidator.callPath.node.arguments[0] + + if (!innerInputExpression) { + throw new Error( + 'createMiddleware().inputValidator() must be called with a validator!', + ) + } + + // remove the validator call expression + stripMethodCall(inputValidator.callPath) + } + + if (server) { + // remove the server call expression + stripMethodCall(server.callPath) + } + } +} diff --git a/packages/start-plugin-core/src/start-compiler-plugin/handleCreateServerFn.ts b/packages/start-plugin-core/src/start-compiler-plugin/handleCreateServerFn.ts new file mode 100644 index 00000000000..b83c922b5bf --- /dev/null +++ b/packages/start-plugin-core/src/start-compiler-plugin/handleCreateServerFn.ts @@ -0,0 +1,491 @@ +import * as t from '@babel/types' +import babel from '@babel/core' +import path from 'pathe' +import { VITE_ENVIRONMENT_NAMES } from '../constants' +import { cleanId, codeFrameError, stripMethodCall } from './utils' +import type { CompilationContext, RewriteCandidate, ServerFn } from './types' +import type { CompileStartFrameworkOptions } from '../types' + +const TSS_SERVERFN_SPLIT_PARAM = 'tss-serverfn-split' + +// ============================================================================ +// Pre-compiled babel templates (compiled once at module load time) +// ============================================================================ + +// Template for provider files: createServerRpc('id', fn) +const serverRpcTemplate = babel.template.expression( + `createServerRpc(%%functionId%%, %%fn%%)`, +) + +// Template for client caller files: createClientRpc('id') +const clientRpcTemplate = babel.template.expression( + `createClientRpc(%%functionId%%)`, +) + +// Template for SSR caller files (manifest lookup): createSsrRpc('id') +const ssrRpcManifestTemplate = babel.template.expression( + `createSsrRpc(%%functionId%%)`, +) + +// Template for SSR caller files (with importer): createSsrRpc('id', () => import(...).then(m => m['name'])) +const ssrRpcImporterTemplate = babel.template.expression( + `createSsrRpc(%%functionId%%, () => import(%%extractedFilename%%).then(m => m[%%functionName%%]))`, +) + +// ============================================================================ +// Runtime code cache (cached per framework to avoid repeated AST generation) +// ============================================================================ + +type RuntimeCodeType = 'provider' | 'client' | 'ssr' +type FrameworkRuntimeCache = Record +const RuntimeCodeCache = new Map< + CompileStartFrameworkOptions, + FrameworkRuntimeCache +>() + +function getCachedRuntimeCode( + framework: CompileStartFrameworkOptions, + type: RuntimeCodeType, +): t.Statement { + let cache = RuntimeCodeCache.get(framework) + if (!cache) { + cache = { + provider: babel.template.ast( + `import { createServerRpc } from '@tanstack/${framework}-start/server-rpc'`, + { placeholderPattern: false }, + ) as t.Statement, + client: babel.template.ast( + `import { createClientRpc } from '@tanstack/${framework}-start/client-rpc'`, + { placeholderPattern: false }, + ) as t.Statement, + ssr: babel.template.ast( + `import { createSsrRpc } from '@tanstack/${framework}-start/ssr-rpc'`, + { placeholderPattern: false }, + ) as t.Statement, + } + RuntimeCodeCache.set(framework, cache) + } + return cache[type] +} + +/** + * Environment-specific configuration for server function transformation. + * This is computed internally based on the compilation context. + */ +interface EnvConfig { + /** Whether this environment is a client environment */ + isClientEnvironment: boolean + /** Whether SSR is the provider environment */ + ssrIsProvider: boolean + /** The runtime code type to use for imports */ + runtimeCodeType: RuntimeCodeType +} + +/** + * Gets the environment configuration for the current compilation context. + */ +function getEnvConfig( + context: CompilationContext, + isProviderFile: boolean, +): EnvConfig { + const { providerEnvName, env } = context + + // SSR is the provider when the provider environment is the default server environment + const ssrIsProvider = providerEnvName === VITE_ENVIRONMENT_NAMES.server + + if (isProviderFile) { + return { + isClientEnvironment: false, + ssrIsProvider, + runtimeCodeType: 'provider', + } + } + + if (env === 'client') { + return { + isClientEnvironment: true, + ssrIsProvider, + runtimeCodeType: 'client', + } + } + + // Server caller (SSR) + return { + isClientEnvironment: false, + ssrIsProvider, + runtimeCodeType: 'ssr', + } +} + +/** + * Generates the RPC stub expression for provider files. + * Uses pre-compiled template for performance. + */ +function generateProviderRpcStub( + functionId: string, + fn: t.Expression, +): t.Expression { + return serverRpcTemplate({ + functionId: t.stringLiteral(functionId), + fn, + }) +} + +/** + * Generates the RPC stub expression for caller files. + * Uses pre-compiled templates for performance. + */ +function generateCallerRpcStub( + functionId: string, + functionName: string, + extractedFilename: string, + isClientReferenced: boolean, + envConfig: EnvConfig, +): t.Expression { + if (envConfig.runtimeCodeType === 'client') { + return clientRpcTemplate({ + functionId: t.stringLiteral(functionId), + }) + } + + // SSR caller + // When the function is client-referenced, it's in the manifest - use manifest lookup + // When SSR is NOT the provider, always use manifest lookup (no import() for different env) + // Otherwise, use the importer for functions only referenced on the server when SSR is the provider + if (isClientReferenced || !envConfig.ssrIsProvider) { + return ssrRpcManifestTemplate({ + functionId: t.stringLiteral(functionId), + }) + } + + return ssrRpcImporterTemplate({ + functionId: t.stringLiteral(functionId), + extractedFilename: t.stringLiteral(extractedFilename), + functionName: t.stringLiteral(functionName), + }) +} + +/** + * Handles createServerFn transformations for a batch of candidates. + * + * This function performs extraction and replacement of server functions + * + * For caller files (non-provider): + * - Replaces the server function with an RPC stub + * - Does not include the handler function body + * + * For provider files: + * - Creates an extractedFn that calls __executeServer + * - Modifies .handler() to pass (extractedFn, serverFn) - two arguments + * + * @param candidates - All ServerFn candidates to process + * @param context - The compilation context with helpers and mutable state + * @returns Result containing runtime code to add, or null if no candidates processed + */ +export function handleCreateServerFn( + candidates: Array, + context: CompilationContext, +) { + if (candidates.length === 0) { + return + } + + const isProviderFile = context.id.includes(TSS_SERVERFN_SPLIT_PARAM) + // Get environment-specific configuration + const envConfig = getEnvConfig(context, isProviderFile) + + // Track function names to ensure uniqueness within this file + const functionNameSet = new Set() + + const exportNames = new Set() + const serverFnsById: Record = {} + + const [baseFilename] = context.id.split('?') as [string] + const extractedFilename = `${baseFilename}?${TSS_SERVERFN_SPLIT_PARAM}` + const relativeFilename = path.relative(context.root, baseFilename) + const knownFns = context.getKnownServerFns() + const cleanedContextId = cleanId(context.id) + + for (const candidate of candidates) { + const { path: candidatePath, methodChain } = candidate + const { inputValidator, handler } = methodChain + + // Check if the call is assigned to a variable + if (!candidatePath.parentPath.isVariableDeclarator()) { + throw new Error('createServerFn must be assigned to a variable!') + } + + // Get the identifier name of the variable + const variableDeclarator = candidatePath.parentPath.node + if (!t.isIdentifier(variableDeclarator.id)) { + throw codeFrameError( + context.code, + variableDeclarator.id.loc!, + 'createServerFn must be assigned to a simple identifier, not a destructuring pattern', + ) + } + const existingVariableName = variableDeclarator.id.name + + // Generate unique function name with _createServerFn_handler suffix + // The function name is derived from the variable name + let functionName = `${existingVariableName}_createServerFn_handler` + while (functionNameSet.has(functionName)) { + functionName = incrementFunctionNameVersion(functionName) + } + functionNameSet.add(functionName) + + // Generate function ID using pre-computed relative filename + const functionId = context.generateFunctionId({ + filename: relativeFilename, + functionName, + extractedFilename, + }) + + // Check if this function was already discovered by the client build + const knownFn = knownFns[functionId] + const isClientReferenced = envConfig.isClientEnvironment || !!knownFn + + // Use canonical extracted filename from known functions if available + const canonicalExtractedFilename = + knownFn?.extractedFilename ?? extractedFilename + + // Handle input validator - remove on client + if (inputValidator) { + const innerInputExpression = inputValidator.callPath.node.arguments[0] + + if (!innerInputExpression) { + throw new Error( + 'createServerFn().inputValidator() must be called with a validator!', + ) + } + + // If we're on the client, remove the validator call expression + if (context.env === 'client') { + stripMethodCall(inputValidator.callPath) + } + } + + const handlerFnPath = handler?.firstArgPath + + if (!handler || !handlerFnPath?.node) { + throw codeFrameError( + context.code, + candidatePath.node.callee.loc!, + `createServerFn must be called with a "handler" property!`, + ) + } + + // Validate the handler argument is an expression (not a SpreadElement, etc.) + if (!t.isExpression(handlerFnPath.node)) { + throw codeFrameError( + context.code, + handlerFnPath.node.loc!, + `handler() must be called with an expression, not a ${handlerFnPath.node.type}`, + ) + } + + const handlerFn = handlerFnPath.node + + // Register function only from caller files (not provider files) + // to avoid duplicates - provider files process the same functions + + if (!isProviderFile) { + serverFnsById[functionId] = { + functionName, + functionId, + filename: cleanedContextId, + extractedFilename: canonicalExtractedFilename, + isClientReferenced, + } + } + + if (isProviderFile) { + // PROVIDER FILE: This is the extracted file that contains the actual implementation + // We need to: + // 1. Create an extractedFn that calls __executeServer + // 2. Modify .handler() to pass (extractedFn, serverFn) - two arguments + // + // Expected output format: + // const extractedFn = createServerRpc("id", (opts) => varName.__executeServer(opts)); + // const varName = createServerFn().handler(extractedFn, originalHandler); + + // Build the arrow function: (opts, signal) => varName.__executeServer(opts, signal) + // The signal parameter is passed through to allow abort signal propagation + const executeServerArrowFn = t.arrowFunctionExpression( + [t.identifier('opts'), t.identifier('signal')], + t.callExpression( + t.memberExpression( + t.identifier(existingVariableName), + t.identifier('__executeServer'), + ), + [t.identifier('opts'), t.identifier('signal')], + ), + ) + + // Generate the replacement using pre-compiled template + const extractedFnInit = generateProviderRpcStub( + functionId, + executeServerArrowFn, + ) + + // Build the extracted function statement + const extractedFnStatement = t.variableDeclaration('const', [ + t.variableDeclarator(t.identifier(functionName), extractedFnInit), + ]) + + // Find the variable declaration statement containing our createServerFn + const variableDeclaration = candidatePath.parentPath.parentPath + if (!variableDeclaration.isVariableDeclaration()) { + throw new Error( + 'Expected createServerFn to be in a VariableDeclaration', + ) + } + + // Insert the extracted function statement before the variable declaration + variableDeclaration.insertBefore(extractedFnStatement) + + // Modify the .handler() call to pass two arguments: (extractedFn, serverFn) + // The handlerFnPath.node contains the original serverFn + const extractedFnIdentifier = t.identifier(functionName) + const serverFnNode = t.cloneNode(handlerFn, true) + + // Replace handler's arguments with [extractedFn, serverFn] + handler.callPath.node.arguments = [extractedFnIdentifier, serverFnNode] + + // Only export the extracted handler (e.g., myFn_createServerFn_handler) + // The manifest and all import paths only look up this suffixed name. + // The original variable (e.g., myFn) stays in the file but is not exported + // since it's only used internally. + exportNames.add(functionName) + } else { + // CALLER FILE: This file calls the server function but doesn't contain the implementation + // We need to: + // 1. Remove the handler function body (it will be in the provider file) + // 2. Replace the handler argument with an RPC stub + // + // IMPORTANT: We must keep the createServerFn().handler(extractedFn) structure + // so that the client middleware chain can unwrap the {result, error, context} response. + // + // Expected output format: + // const myFn = createServerFn().handler(createClientRpc("id")) + // or + // const myFn = createServerFn().handler(createSsrRpc("id", () => import(...))) + + // If the handler function is an identifier, we need to remove the bound function + // from the file since it won't be needed + if (t.isIdentifier(handlerFn)) { + const binding = handlerFnPath.scope.getBinding(handlerFn.name) + if (binding) { + binding.path.remove() + } + } + + // Generate the RPC stub using pre-compiled templates + const rpcStub = generateCallerRpcStub( + functionId, + functionName, + canonicalExtractedFilename, + isClientReferenced, + envConfig, + ) + + // Replace ONLY the handler argument with the RPC stub + // Keep the createServerFn().handler() wrapper intact for client middleware + handlerFnPath.replaceWith(rpcStub) + } + } + + // For provider files, add exports for all extracted functions + if (isProviderFile) { + // Remove all existing exports first + safeRemoveExports(context.ast) + + // Export all server function related variables from exportNames + // These were populated by handleCreateServerFn: + // 1. Extracted handlers: const fn_createServerFn_handler = createServerRpc(...) + // 2. Original variables: const fn = createServerFn().handler(...) + if (exportNames.size > 0) { + context.ast.program.body.push( + t.exportNamedDeclaration( + undefined, + Array.from(exportNames).map((name) => + t.exportSpecifier(t.identifier(name), t.identifier(name)), + ), + ), + ) + } + } + + // Notify about discovered functions (only for non-provider files) + if ( + !isProviderFile && + Object.keys(serverFnsById).length > 0 && + context.onServerFnsById + ) { + context.onServerFnsById(serverFnsById) + } + + // Add runtime import using cached AST node + const runtimeCode = getCachedRuntimeCode( + context.framework, + envConfig.runtimeCodeType, + ) + context.ast.program.body.unshift(t.cloneNode(runtimeCode)) +} + +/** + * Makes an identifier safe for use as a JavaScript identifier. + */ +function makeIdentifierSafe(identifier: string): string { + return identifier + .replace(/[^a-zA-Z0-9_$]/g, '_') // Replace unsafe chars with underscore + .replace(/^[0-9]/, '_$&') // Prefix leading number with underscore + .replace(/^\$/, '_$') // Prefix leading $ with underscore + .replace(/_{2,}/g, '_') // Collapse multiple underscores + .replace(/^_|_$/g, '') // Trim leading/trailing underscores +} + +/** + * Increments the version number suffix on a function name. + */ +function incrementFunctionNameVersion(functionName: string): string { + const [realReferenceName, count] = functionName.split(/_(\d+)$/) + const resolvedCount = Number(count || '0') + const suffix = `_${resolvedCount + 1}` + return makeIdentifierSafe(realReferenceName!) + suffix +} + +/** + * Removes all exports from the AST while preserving the declarations. + * Used for provider files where we want to re-export only the server functions. + */ +function safeRemoveExports(ast: t.File): void { + ast.program.body = ast.program.body.flatMap((node) => { + if ( + t.isExportNamedDeclaration(node) || + t.isExportDefaultDeclaration(node) + ) { + if ( + t.isFunctionDeclaration(node.declaration) || + t.isClassDeclaration(node.declaration) || + t.isVariableDeclaration(node.declaration) + ) { + // do not remove export if it is an anonymous function / class, + // otherwise this would produce a syntax error + if ( + t.isFunctionDeclaration(node.declaration) || + t.isClassDeclaration(node.declaration) + ) { + if (!node.declaration.id) { + return node + } + } + return node.declaration + } else if (node.declaration === null) { + // remove e.g. `export { RouteComponent as component }` + return [] + } + } + return node + }) +} diff --git a/packages/start-plugin-core/src/start-compiler-plugin/handleEnvOnly.ts b/packages/start-plugin-core/src/start-compiler-plugin/handleEnvOnly.ts new file mode 100644 index 00000000000..1fa6ab21cfb --- /dev/null +++ b/packages/start-plugin-core/src/start-compiler-plugin/handleEnvOnly.ts @@ -0,0 +1,56 @@ +import * as t from '@babel/types' +import type { CompilationContext, RewriteCandidate } from './types' +import type { LookupKind } from './compiler' + +function capitalize(str: string) { + if (!str) return '' + return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase() +} + +/** + * Handles serverOnly/clientOnly function transformations for a batch of candidates. + * + * @param candidates - All EnvOnly candidates to process (all same kind) + * @param context - The compilation context + * @param kind - The specific kind (ServerOnlyFn or ClientOnlyFn) + */ +export function handleEnvOnlyFn( + candidates: Array, + context: CompilationContext, + kind: LookupKind, +): void { + const targetEnv = kind === 'ClientOnlyFn' ? 'client' : 'server' + + for (const candidate of candidates) { + const { path } = candidate + + if (context.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/start-compiler-plugin/plugin.ts b/packages/start-plugin-core/src/start-compiler-plugin/plugin.ts new file mode 100644 index 00000000000..973b2b192b6 --- /dev/null +++ b/packages/start-plugin-core/src/start-compiler-plugin/plugin.ts @@ -0,0 +1,423 @@ +import { VIRTUAL_MODULES } from '@tanstack/start-server-core' +import { TRANSFORM_ID_REGEX, VITE_ENVIRONMENT_NAMES } from '../constants' +import { + KindDetectionPatterns, + LookupKindsPerEnv, + StartCompiler, + detectKindsInCode, +} from './compiler' +import { cleanId } from './utils' +import type { CompileStartFrameworkOptions } from '../types' +import type { LookupConfig, LookupKind } from './compiler' +import type { GenerateFunctionIdFnOptional, ServerFn } from './types' +import type { PluginOption } from 'vite' + +// Derive transform code filter from KindDetectionPatterns (single source of truth) +function getTransformCodeFilterForEnv(env: 'client' | 'server'): Array { + const validKinds = LookupKindsPerEnv[env] + const patterns: Array = [] + for (const [kind, pattern] of Object.entries(KindDetectionPatterns) as Array< + [LookupKind, RegExp] + >) { + if (validKinds.has(kind)) { + patterns.push(pattern) + } + } + return patterns +} + +const getLookupConfigurationsForEnv = ( + env: 'client' | 'server', + framework: CompileStartFrameworkOptions, +): Array => { + // Common configs for all environments + const commonConfigs: Array = [ + { + libName: `@tanstack/${framework}-start`, + rootExport: 'createServerFn', + kind: 'Root', + }, + { + libName: `@tanstack/${framework}-start`, + rootExport: 'createIsomorphicFn', + kind: 'IsomorphicFn', + }, + { + 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', + }, + ...commonConfigs, + ] + } else { + // Server-only: add ClientOnly JSX component lookup + return [ + ...commonConfigs, + { + libName: `@tanstack/${framework}-router`, + rootExport: 'ClientOnly', + kind: 'ClientOnlyJSX', + }, + ] + } +} +const SERVER_FN_LOOKUP = 'server-fn-module-lookup' + +function resolveViteId(id: string) { + return `\0${id}` +} + +const validateServerFnIdVirtualModule = `virtual:tanstack-start-validate-server-fn-id` + +function parseIdQuery(id: string): { + filename: string + query: { + [k: string]: string + } +} { + if (!id.includes('?')) return { filename: id, query: {} } + const [filename, rawQuery] = id.split(`?`, 2) as [string, string] + const query = Object.fromEntries(new URLSearchParams(rawQuery)) + return { filename, query } +} + +/** + * Generates the manifest module code for server functions. + * @param serverFnsById - Map of function IDs to their server function info + * @param includeClientReferencedCheck - Whether to include isClientReferenced flag and runtime check. + * This is needed when SSR is NOT the provider, so server-only-referenced functions in the manifest + * can be blocked from client HTTP requests. + */ +function generateManifestModule( + serverFnsById: Record, + includeClientReferencedCheck: boolean, +): string { + const manifestEntries = Object.entries(serverFnsById) + .map(([id, fn]) => { + const baseEntry = `'${id}': { + functionName: '${fn.functionName}', + importer: () => import(${JSON.stringify(fn.extractedFilename)})${ + includeClientReferencedCheck + ? `, + isClientReferenced: ${fn.isClientReferenced ?? true}` + : '' + } + }` + return baseEntry + }) + .join(',') + + const getServerFnByIdParams = includeClientReferencedCheck ? 'id, opts' : 'id' + const clientReferencedCheck = includeClientReferencedCheck + ? ` + // If called from client, only allow client-referenced functions + if (opts?.fromClient && !serverFnInfo.isClientReferenced) { + throw new Error('Server function not accessible from client: ' + id) + } +` + : '' + + return ` + const manifest = {${manifestEntries}} + + export async function getServerFnById(${getServerFnByIdParams}) { + const serverFnInfo = manifest[id] + if (!serverFnInfo) { + throw new Error('Server function info not found for ' + id) + } +${clientReferencedCheck} + const fnModule = await serverFnInfo.importer() + + if (!fnModule) { + console.info('serverFnInfo', serverFnInfo) + throw new Error('Server function module not resolved for ' + id) + } + + const action = fnModule[serverFnInfo.functionName] + + if (!action) { + console.info('serverFnInfo', serverFnInfo) + console.info('fnModule', fnModule) + + throw new Error( + \`Server function module export not resolved for serverFn ID: \${id}\`, + ) + } + return action + } + ` +} + +export interface StartCompilerPluginOptions { + framework: CompileStartFrameworkOptions + environments: Array<{ name: string; type: 'client' | 'server' }> + /** + * Custom function ID generator (optional). + */ + generateFunctionId?: GenerateFunctionIdFnOptional + /** + * The Vite environment name for the server function provider. + */ + providerEnvName: string +} + +export function startCompilerPlugin( + opts: StartCompilerPluginOptions, +): PluginOption { + const compilers: Record = {} + + // Shared registry of server functions across all environments + const serverFnsById: Record = {} + + const onServerFnsById = (d: Record) => { + Object.assign(serverFnsById, d) + } + + let root = process.cwd() + let command: 'build' | 'serve' = 'build' + + const resolvedResolverVirtualImportId = resolveViteId( + VIRTUAL_MODULES.serverFnResolver, + ) + + // Determine which environments need the resolver (getServerFnById) + // SSR environment always needs the resolver for server-side calls + // Provider environment needs it for the actual implementation + const ssrEnvName = VITE_ENVIRONMENT_NAMES.server + + // SSR is the provider when the provider environment is the default server environment + const ssrIsProvider = opts.providerEnvName === ssrEnvName + + // Environments that need the resolver: SSR (for server calls) and provider (for implementation) + const appliedResolverEnvironments = new Set( + ssrIsProvider ? [opts.providerEnvName] : [ssrEnvName, opts.providerEnvName], + ) + + function perEnvServerFnPlugin(environment: { + name: string + type: 'client' | 'server' + }): PluginOption { + // Derive transform code filter from KindDetectionPatterns (single source of truth) + const transformCodeFilter = getTransformCodeFilterForEnv(environment.type) + + return { + name: `tanstack-start-core::server-fn:${environment.name}`, + enforce: 'pre', + applyToEnvironment(env) { + return env.name === environment.name + }, + configResolved(config) { + root = config.root + command = config.command + }, + transform: { + filter: { + id: { + exclude: new RegExp(`${SERVER_FN_LOOKUP}$`), + include: TRANSFORM_ID_REGEX, + }, + code: { + include: transformCodeFilter, + }, + }, + async handler(code, id) { + let compiler = compilers[this.environment.name] + if (!compiler) { + // Default to 'dev' mode for unknown environments (conservative: no caching) + const mode = this.environment.mode === 'build' ? 'build' : 'dev' + compiler = new StartCompiler({ + env: environment.type, + envName: environment.name, + root, + lookupKinds: LookupKindsPerEnv[environment.type], + lookupConfigurations: getLookupConfigurationsForEnv( + environment.type, + opts.framework, + ), + mode, + framework: opts.framework, + providerEnvName: opts.providerEnvName, + generateFunctionId: opts.generateFunctionId, + onServerFnsById, + getKnownServerFns: () => serverFnsById, + loadModule: async (id: string) => { + if (this.environment.mode === 'build') { + const loaded = await this.load({ id }) + // Handle modules with no runtime code (e.g., type-only exports). + // After TypeScript compilation, these become empty modules. + // Create an empty module info instead of throwing. + const code = loaded.code ?? '' + compiler!.ingestModule({ code, id }) + } else if (this.environment.mode === 'dev') { + /** + * in dev, vite does not return code from `ctx.load()` + * so instead, we need to take a different approach + * we must force vite to load the module and run it through the vite plugin pipeline + * we can do this by using the `fetchModule` method + * the `captureServerFnModuleLookupPlugin` captures the module code via its transform hook and invokes analyzeModuleAST + */ + await this.environment.fetchModule( + id + '?' + SERVER_FN_LOOKUP, + ) + } else { + throw new Error( + `could not load module ${id}: unknown environment mode ${this.environment.mode}`, + ) + } + }, + resolveId: async (source: string, importer?: string) => { + const r = await this.resolve(source, importer) + if (r) { + if (!r.external) { + return cleanId(r.id) + } + } + return null + }, + }) + compilers[this.environment.name] = compiler + } + + // Detect which kinds are present in this file before parsing + const detectedKinds = detectKindsInCode(code, environment.type) + + const result = await compiler.compile({ + id, + code, + detectedKinds, + }) + return result + }, + }, + + hotUpdate(ctx) { + const compiler = compilers[this.environment.name] + + ctx.modules.forEach((m) => { + if (m.id) { + const deleted = compiler?.invalidateModule(m.id) + if (deleted) { + m.importers.forEach((importer) => { + if (importer.id) { + compiler?.invalidateModule(importer.id) + } + }) + } + } + }) + }, + } + } + + return [ + ...opts.environments.map(perEnvServerFnPlugin), + { + name: 'tanstack-start-core:capture-server-fn-module-lookup', + // we only need this plugin in dev mode + apply: 'serve', + applyToEnvironment(env) { + return !!opts.environments.find((e) => e.name === env.name) + }, + transform: { + filter: { + id: new RegExp(`${SERVER_FN_LOOKUP}$`), + }, + handler(code, id) { + const compiler = compilers[this.environment.name] + compiler?.ingestModule({ code, id: cleanId(id) }) + }, + }, + }, + // Validate server function ID in dev mode + { + name: 'tanstack-start-core:validate-server-fn-id', + apply: 'serve', + load: { + filter: { + id: new RegExp(resolveViteId(validateServerFnIdVirtualModule)), + }, + handler(id) { + const parsed = parseIdQuery(id) + if (parsed.query.id && serverFnsById[parsed.query.id]) { + return `export {}` + } + this.error(`Invalid server function ID: ${parsed.query.id}`) + }, + }, + }, + // Manifest plugin for server environments + { + name: 'tanstack-start-core:server-fn-resolver', + enforce: 'pre', + applyToEnvironment: (env) => { + return appliedResolverEnvironments.has(env.name) + }, + configResolved(config) { + root = config.root + command = config.command + }, + resolveId: { + filter: { id: new RegExp(VIRTUAL_MODULES.serverFnResolver) }, + handler() { + return resolvedResolverVirtualImportId + }, + }, + load: { + filter: { id: new RegExp(resolvedResolverVirtualImportId) }, + handler() { + // When SSR is not the provider, SSR callers need to use HTTP to call server functions + // since they can't directly import from the provider environment + if (this.environment.name !== opts.providerEnvName) { + // SSR caller: use HTTP-based getServerFnById + // This re-exports from the start-server-core package which handles HTTP calls + return `export { getServerFnById } from '@tanstack/start-server-core/server-fn-ssr-caller'` + } + + if (this.environment.mode !== 'build') { + const mod = ` + export async function getServerFnById(id) { + const validateIdImport = ${JSON.stringify(validateServerFnIdVirtualModule)} + '?id=' + id + await import(/* @vite-ignore */ '/@id/__x00__' + validateIdImport) + const decoded = Buffer.from(id, 'base64url').toString('utf8') + const devServerFn = JSON.parse(decoded) + const mod = await import(/* @vite-ignore */ devServerFn.file) + return mod[devServerFn.export] + } + ` + return mod + } + + // When SSR is the provider, server-only-referenced functions aren't in the manifest, + // so no isClientReferenced check is needed. + // When SSR is NOT the provider (custom provider env), server-only-referenced + // functions ARE in the manifest and need the isClientReferenced check to + // block direct client HTTP requests to server-only-referenced functions. + const includeClientReferencedCheck = !ssrIsProvider + return generateManifestModule( + serverFnsById, + includeClientReferencedCheck, + ) + }, + }, + }, + ] +} diff --git a/packages/start-plugin-core/src/start-compiler-plugin/types.ts b/packages/start-plugin-core/src/start-compiler-plugin/types.ts new file mode 100644 index 00000000000..141cb4e28fb --- /dev/null +++ b/packages/start-plugin-core/src/start-compiler-plugin/types.ts @@ -0,0 +1,133 @@ +import type * as babel from '@babel/core' +import type * as t from '@babel/types' +import type { CompileStartFrameworkOptions } from '../types' + +/** + * Context passed to all plugin handlers during compilation. + * Contains both read-only input data and mutable state that handlers update. + */ +export interface CompilationContext { + readonly ast: t.File + readonly code: string + readonly id: string + readonly env: 'client' | 'server' + readonly envName: string + readonly root: string + /** The framework being used (e.g., 'react', 'solid') */ + readonly framework: CompileStartFrameworkOptions + /** The Vite environment name for the server function provider */ + readonly providerEnvName: string + + /** Generate a unique function ID */ + generateFunctionId: GenerateFunctionIdFn + /** Get known server functions from previous builds (e.g., client build) */ + getKnownServerFns: () => Record + + /** + * Callback when server functions are discovered. + * Called after each file is compiled with its new functions. + */ + onServerFnsById: ((d: Record) => void) | undefined +} +/** + * Batched plugin handler signature. + * Receives ALL candidates of a specific kind in one call. + * Mutates the CompilationContext directly. + */ +export type BatchedPluginHandler = ( + candidates: Array, + context: CompilationContext, + opts: TOpts, +) => void + +/** + * Info about a method call in the chain, including the call expression path + * and the path to its first argument (if any). + */ +export interface MethodCallInfo { + callPath: babel.NodePath + /** Path to the first argument, or null if no arguments */ + firstArgPath: babel.NodePath | null +} + +/** + * Pre-collected method chain paths for a root call expression. + * This avoids needing to traverse the AST again in handlers. + */ +export interface MethodChainPaths { + middleware: MethodCallInfo | null + inputValidator: MethodCallInfo | null + handler: MethodCallInfo | null + server: MethodCallInfo | null + client: MethodCallInfo | null +} + +export type MethodChainKey = keyof MethodChainPaths + +/** + * Information about a candidate that needs to be rewritten. + */ +export interface RewriteCandidate { + path: babel.NodePath + methodChain: MethodChainPaths +} + +/** + * Represents an extracted server function that has been registered. + * Used for manifest generation and tracking function metadata. + */ +export interface ServerFn { + /** The unique name used to export this function */ + functionName: string + /** The unique ID for this function (used in RPC calls) */ + functionId: string + /** The filename with query param where the extracted implementation lives */ + extractedFilename: string + /** The original source filename */ + filename: string + /** + * True when this function was discovered by the client build. + * Used to restrict HTTP access to only client-referenced functions. + */ + isClientReferenced?: boolean +} + +/** + * Function type for generating unique function IDs. + */ +export type GenerateFunctionIdFn = (opts: { + filename: string + functionName: string + extractedFilename: string +}) => string + +/** + * Optional version that allows returning undefined to use default ID generation. + */ +export type GenerateFunctionIdFnOptional = ( + opts: Omit[0], 'extractedFilename'>, +) => string | undefined + +/** + * Function type for generating replacement code for server functions. + * Used internally by handleCreateServerFn. + */ +export type ReplacerFn = (opts: { + /** Placeholder for the original function expression */ + fn: string + /** The filename where the extracted implementation lives */ + extractedFilename: string + /** The original source filename */ + filename: string + /** The unique function ID */ + functionId: string + /** The export name for this function */ + functionName: string + /** True if this is the source/provider file (has the implementation) */ + isSourceFn: boolean + /** + * True when this function was already discovered by a previous build (e.g., client). + * For SSR callers, this means the function is in the manifest. + */ + isClientReferenced: boolean +}) => string diff --git a/packages/start-plugin-core/src/start-compiler-plugin/utils.ts b/packages/start-plugin-core/src/start-compiler-plugin/utils.ts new file mode 100644 index 00000000000..afc763627f2 --- /dev/null +++ b/packages/start-plugin-core/src/start-compiler-plugin/utils.ts @@ -0,0 +1,52 @@ +import { codeFrameColumns } from '@babel/code-frame' +import * as t from '@babel/types' +import type babel from '@babel/core' + +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) +} + +export function cleanId(id: string): string { + // Remove null byte prefix used by Vite/Rollup for virtual modules + if (id.startsWith('\0')) { + id = id.slice(1) + } + const queryIndex = id.indexOf('?') + return queryIndex === -1 ? id : id.substring(0, queryIndex) +} + +/** + * Strips a method call by replacing it with its callee object. + * E.g., `foo().bar()` -> `foo()` + * + * This is a common pattern used when removing method calls from chains + * (e.g., removing .server() from middleware on client, or .inputValidator() on client). + * + * @param callPath - The path to the CallExpression to strip + */ +export function stripMethodCall( + callPath: babel.NodePath, +): void { + if (t.isMemberExpression(callPath.node.callee)) { + callPath.replaceWith(callPath.node.callee.object) + } +} diff --git a/packages/start-plugin-core/src/types.ts b/packages/start-plugin-core/src/types.ts index b1e3e6a0f19..e2273d1a127 100644 --- a/packages/start-plugin-core/src/types.ts +++ b/packages/start-plugin-core/src/types.ts @@ -10,7 +10,6 @@ export interface TanStackStartVitePluginCoreOptions { start: string } serverFn?: { - directive?: string ssr?: { getServerFnById?: string } diff --git a/packages/start-plugin-core/tests/clientOnlyJSX/clientOnlyJSX.test.ts b/packages/start-plugin-core/tests/clientOnlyJSX/clientOnlyJSX.test.ts index 33239babc01..fedb3851f04 100644 --- a/packages/start-plugin-core/tests/clientOnlyJSX/clientOnlyJSX.test.ts +++ b/packages/start-plugin-core/tests/clientOnlyJSX/clientOnlyJSX.test.ts @@ -4,16 +4,28 @@ import { describe, expect, test } from 'vitest' import { detectKindsInCode, - ServerFnCompiler, -} from '../../src/create-server-fn-plugin/compiler' + StartCompiler, +} from '../../src/start-compiler-plugin/compiler' + +// Default test options for StartCompiler +function getDefaultTestOptions(env: 'client' | 'server') { + const envName = env === 'client' ? 'client' : 'ssr' + return { + envName, + root: '/test', + framework: 'react' as const, + providerEnvName: 'ssr', + } +} async function compile(opts: { env: 'client' | 'server' code: string id: string }) { - const compiler = new ServerFnCompiler({ + const compiler = new StartCompiler({ ...opts, + ...getDefaultTestOptions(opts.env), loadModule: async () => { // do nothing in test }, @@ -29,12 +41,10 @@ async function compile(opts: { resolveId: async (id) => { return id }, - directive: 'use server', }) const result = await compiler.compile({ code: opts.code, id: opts.id, - isProviderFile: false, }) return result } diff --git a/packages/start-plugin-core/tests/compiler.test.ts b/packages/start-plugin-core/tests/compiler.test.ts index 2ca0bf0d606..de92d56693c 100644 --- a/packages/start-plugin-core/tests/compiler.test.ts +++ b/packages/start-plugin-core/tests/compiler.test.ts @@ -1,12 +1,23 @@ import { describe, expect, test } from 'vitest' import { detectKindsInCode, - ServerFnCompiler, -} from '../src/create-server-fn-plugin/compiler' + StartCompiler, +} from '../src/start-compiler-plugin/compiler' import type { LookupConfig, LookupKind, -} from '../src/create-server-fn-plugin/compiler' +} from '../src/start-compiler-plugin/compiler' + +// Default test options for StartCompiler +function getDefaultTestOptions(env: 'client' | 'server') { + const envName = env === 'client' ? 'client' : 'ssr' + return { + envName, + root: '/test', + framework: 'react' as const, + providerEnvName: 'ssr', + } +} // Helper to create a compiler with all kinds enabled function createFullCompiler(env: 'client' | 'server') { @@ -49,9 +60,9 @@ function createFullCompiler(env: 'client' | 'server') { }, ] - return new ServerFnCompiler({ + return new StartCompiler({ env, - directive: 'use server', + ...getDefaultTestOptions(env), lookupKinds, lookupConfigurations, loadModule: async () => {}, @@ -214,11 +225,11 @@ describe('compiler handles multiple files with different kinds', () => { export const fn = createServerFn().handler(() => 'hello') `, id: 'file1.ts', - isProviderFile: false, + detectedKinds: new Set(['ServerFn']), }) expect(result1).not.toBeNull() - expect(result1!.code).toContain('__executeServer') // Client should have RPC stub + expect(result1!.code).toContain('createClientRpc') // Client should have RPC stub expect(result1!.code).not.toContain('createMiddleware') expect(result1!.code).not.toContain('createIsomorphicFn') @@ -232,7 +243,7 @@ describe('compiler handles multiple files with different kinds', () => { }) `, id: 'file2.ts', - isProviderFile: false, + detectedKinds: new Set(['Middleware']), }) expect(result2).not.toBeNull() @@ -249,7 +260,7 @@ describe('compiler handles multiple files with different kinds', () => { .server(() => 'server-value') `, id: 'file3.ts', - isProviderFile: false, + detectedKinds: new Set(['IsomorphicFn']), }) expect(result3).not.toBeNull() @@ -264,7 +275,7 @@ describe('compiler handles multiple files with different kinds', () => { export const fn = createServerOnlyFn(() => 'server only value') `, id: 'file4.ts', - isProviderFile: false, + detectedKinds: new Set(['ServerOnlyFn']), }) expect(result4).not.toBeNull() @@ -280,11 +291,11 @@ describe('compiler handles multiple files with different kinds', () => { export const isoFn = createIsomorphicFn().client(() => 'client-iso') `, id: 'file5.ts', - isProviderFile: false, + detectedKinds: new Set(['ServerFn', 'IsomorphicFn']), }) expect(result5).not.toBeNull() - expect(result5!.code).toContain('__executeServer') // ServerFn RPC + expect(result5!.code).toContain('createClientRpc') // ServerFn RPC expect(result5!.code).toContain('client-iso') // IsomorphicFn client impl }) @@ -298,7 +309,7 @@ describe('compiler handles multiple files with different kinds', () => { export const fn1 = createIsomorphicFn().client(() => 'first') `, id: 'first.ts', - isProviderFile: false, + detectedKinds: new Set(['IsomorphicFn']), }) expect(result1!.code).toContain('first') @@ -310,7 +321,7 @@ describe('compiler handles multiple files with different kinds', () => { export const fn2 = createIsomorphicFn().client(() => 'second') `, id: 'second.ts', - isProviderFile: false, + detectedKinds: new Set(['IsomorphicFn']), }) expect(result2!.code).toContain('second') @@ -327,7 +338,7 @@ describe('compiler handles multiple files with different kinds', () => { export const mw = createMiddleware().server(({ next }) => next()) `, id: 'middleware.ts', - isProviderFile: false, + // Intentionally including Middleware even though it's server env detectedKinds: new Set(['Middleware']), }) @@ -348,7 +359,7 @@ describe('edge cases for detectedKinds', () => { // .handler( in string `, id: 'empty.ts', - isProviderFile: false, + detectedKinds: new Set(), // Empty set }) @@ -365,7 +376,7 @@ describe('edge cases for detectedKinds', () => { export const fn = createIsomorphicFn().client(() => 'works') `, id: 'no-detected.ts', - isProviderFile: false, + // No detectedKinds provided }) @@ -383,7 +394,7 @@ describe('edge cases for detectedKinds', () => { export const fn = createIsomorphicFn().server(() => 'server-impl') `, id: 'filtered.ts', - isProviderFile: false, + detectedKinds: new Set(['Middleware', 'IsomorphicFn']), // Middleware should be filtered }) @@ -393,9 +404,9 @@ describe('edge cases for detectedKinds', () => { }) test('ingestModule handles empty code gracefully', () => { - const compiler = new ServerFnCompiler({ + const compiler = new StartCompiler({ env: 'client', - directive: 'use server', + ...getDefaultTestOptions('client'), lookupKinds: new Set(['ServerFn']), lookupConfigurations: [], loadModule: async () => {}, diff --git a/packages/start-plugin-core/tests/createIsomorphicFn/createIsomorphicFn.test.ts b/packages/start-plugin-core/tests/createIsomorphicFn/createIsomorphicFn.test.ts index 04d2946b063..c161b805860 100644 --- a/packages/start-plugin-core/tests/createIsomorphicFn/createIsomorphicFn.test.ts +++ b/packages/start-plugin-core/tests/createIsomorphicFn/createIsomorphicFn.test.ts @@ -2,15 +2,27 @@ import { readFile, readdir } from 'node:fs/promises' import path from 'node:path' import { afterAll, describe, expect, test, vi } from 'vitest' -import { ServerFnCompiler } from '../../src/create-server-fn-plugin/compiler' +import { StartCompiler } from '../../src/start-compiler-plugin/compiler' + +// Default test options for StartCompiler +function getDefaultTestOptions(env: 'client' | 'server') { + const envName = env === 'client' ? 'client' : 'ssr' + return { + envName, + root: '/test', + framework: 'react' as const, + providerEnvName: 'ssr', + } +} async function compile(opts: { env: 'client' | 'server' code: string id: string }) { - const compiler = new ServerFnCompiler({ + const compiler = new StartCompiler({ ...opts, + ...getDefaultTestOptions(opts.env), loadModule: async () => { // do nothing in test }, @@ -25,12 +37,10 @@ async function compile(opts: { resolveId: async (id) => { return id }, - directive: 'use server', }) const result = await compiler.compile({ code: opts.code, id: opts.id, - isProviderFile: false, }) return result } diff --git a/packages/start-plugin-core/tests/createMiddleware/createMiddleware.test.ts b/packages/start-plugin-core/tests/createMiddleware/createMiddleware.test.ts index fd45c6b3840..04e6d0d2197 100644 --- a/packages/start-plugin-core/tests/createMiddleware/createMiddleware.test.ts +++ b/packages/start-plugin-core/tests/createMiddleware/createMiddleware.test.ts @@ -1,7 +1,18 @@ import { readFile, readdir } from 'node:fs/promises' import path from 'node:path' import { describe, expect, test, vi } from 'vitest' -import { ServerFnCompiler } from '../../src/create-server-fn-plugin/compiler' +import { StartCompiler } from '../../src/start-compiler-plugin/compiler' + +// Default test options for StartCompiler +function getDefaultTestOptions(env: 'client' | 'server') { + const envName = env === 'client' ? 'client' : 'ssr' + return { + envName, + root: '/test', + framework: 'react' as const, + providerEnvName: 'ssr', + } +} async function getFilenames() { return await readdir(path.resolve(import.meta.dirname, './test-files')) @@ -12,8 +23,9 @@ async function compile(opts: { code: string id: string }) { - const compiler = new ServerFnCompiler({ + const compiler = new StartCompiler({ ...opts, + ...getDefaultTestOptions(opts.env), loadModule: async () => { // do nothing in test }, @@ -33,12 +45,10 @@ async function compile(opts: { resolveId: async (id) => { return id }, - directive: 'use server', }) const result = await compiler.compile({ code: opts.code, id: opts.id, - isProviderFile: false, }) return result } @@ -71,8 +81,9 @@ describe('createMiddleware compiles correctly', async () => { const resolveIdMock = vi.fn(async (id: string) => id) - const compiler = new ServerFnCompiler({ + const compiler = new StartCompiler({ env: 'client', + ...getDefaultTestOptions('client'), loadModule: async () => {}, lookupKinds: new Set(['Middleware']), lookupConfigurations: [ @@ -83,13 +94,11 @@ describe('createMiddleware compiles correctly', async () => { }, ], resolveId: resolveIdMock, - directive: 'use server', }) await compiler.compile({ code, id: 'test.ts', - isProviderFile: false, }) // resolveId should only be called once during init() for the library itself @@ -113,8 +122,9 @@ describe('createMiddleware compiles correctly', async () => { const resolveIdMock = vi.fn(async (id: string) => id) - const compiler = new ServerFnCompiler({ + const compiler = new StartCompiler({ env: 'client', + ...getDefaultTestOptions('client'), loadModule: async (id) => { // Simulate the factory module being loaded if (id === './factory') { @@ -136,13 +146,11 @@ describe('createMiddleware compiles correctly', async () => { }, ], resolveId: resolveIdMock, - directive: 'use server', }) await compiler.compile({ code: factoryCode, id: 'test.ts', - isProviderFile: false, }) // resolveId should be called exactly twice: diff --git a/packages/start-plugin-core/tests/createServerFn/createServerFn.test.ts b/packages/start-plugin-core/tests/createServerFn/createServerFn.test.ts index 2a8a5ff734e..a039321781f 100644 --- a/packages/start-plugin-core/tests/createServerFn/createServerFn.test.ts +++ b/packages/start-plugin-core/tests/createServerFn/createServerFn.test.ts @@ -1,20 +1,39 @@ import { readFile, readdir } from 'node:fs/promises' import path from 'node:path' import { describe, expect, test, vi } from 'vitest' -import { ServerFnCompiler } from '../../src/create-server-fn-plugin/compiler' +import { StartCompiler } from '../../src/start-compiler-plugin/compiler' + +// Default test options for StartCompiler +function getDefaultTestOptions(env: 'client' | 'server') { + const envName = env === 'client' ? 'client' : 'ssr' + return { + envName, + root: '/test', + framework: 'react' as const, + providerEnvName: 'ssr', + } +} async function getFilenames() { return await readdir(path.resolve(import.meta.dirname, './test-files')) } +const TSS_SERVERFN_SPLIT_PARAM = 'tss-serverfn-split' + async function compile(opts: { env: 'client' | 'server' code: string - id: string isProviderFile: boolean }) { - const compiler = new ServerFnCompiler({ + let id = 'test.ts' + + if (opts.isProviderFile) { + id += `?${TSS_SERVERFN_SPLIT_PARAM}` + } + + const compiler = new StartCompiler({ ...opts, + ...getDefaultTestOptions(opts.env), loadModule: async (id) => { // do nothing in test }, @@ -29,12 +48,10 @@ async function compile(opts: { resolveId: async (id) => { return id }, - directive: 'use server', }) const result = await compiler.compile({ code: opts.code, - id: opts.id, - isProviderFile: opts.isProviderFile, + id, }) return result } @@ -48,21 +65,27 @@ describe('createServerFn compiles correctly', async () => { ) const code = file.toString() - test.each(['client', 'server'] as const)( - `should compile for ${filename} %s`, - async (env) => { - const result = await compile({ - env, - code, - id: filename, - isProviderFile: env === 'server', - }) - - await expect(result!.code).toMatchFileSnapshot( - `./snapshots/${env}/${filename}`, - ) - }, - ) + test.each([ + { type: 'client', isProviderFile: false }, + { type: 'server', isProviderFile: false }, + { type: 'server', isProviderFile: true }, + ] as const)(`should compile for ${filename} %s`, async (env) => { + const result = await compile({ + env: env.type, + isProviderFile: env.isProviderFile, + code, + }) + + const folder = + env.type === 'client' + ? 'client' + : env.isProviderFile + ? 'server-provider' + : 'server-caller' + await expect(result!.code).toMatchFileSnapshot( + `./snapshots/${folder}/${filename}`, + ) + }) }) test('should work with identifiers of functions', async () => { @@ -74,60 +97,50 @@ describe('createServerFn compiles correctly', async () => { const myServerFn = createServerFn().handler(myFunc)` const compiledResultClient = await compile({ - id: 'test.ts', code, env: 'client', isProviderFile: false, }) - // Server caller (route file - no directive split param) + // Server caller (route file - no split param) // Should NOT have the second argument since implementation comes from extracted chunk const compiledResultServerCaller = await compile({ - id: 'test.ts', code, env: 'server', isProviderFile: false, }) - // Server provider (extracted file - has directive split param) + // Server provider (extracted file - has split param) // Should HAVE the second argument since this is the implementation file const compiledResultServerProvider = await compile({ - id: 'test.ts?tsr-directive-use-server', code, env: 'server', isProviderFile: true, }) expect(compiledResultClient!.code).toMatchInlineSnapshot(` - "import { createServerFn } from '@tanstack/react-start'; - const myServerFn = createServerFn().handler((opts, signal) => { - "use server"; - - return myServerFn.__executeServer(opts, signal); - });" + "import { createClientRpc } from '@tanstack/react-start/client-rpc'; + import { createServerFn } from '@tanstack/react-start'; + const myServerFn = createServerFn().handler(createClientRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6Im15U2VydmVyRm5fY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9"));" `) // Server caller: no second argument (implementation from extracted chunk) expect(compiledResultServerCaller!.code).toMatchInlineSnapshot(` - "import { createServerFn } from '@tanstack/react-start'; - const myServerFn = createServerFn().handler((opts, signal) => { - "use server"; - - return myServerFn.__executeServer(opts, signal); - });" + "import { createSsrRpc } from '@tanstack/react-start/ssr-rpc'; + import { createServerFn } from '@tanstack/react-start'; + const myServerFn = createServerFn().handler(createSsrRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6Im15U2VydmVyRm5fY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", () => import("test.ts?tss-serverfn-split").then(m => m["myServerFn_createServerFn_handler"])));" `) // Server provider: has second argument (this is the implementation file) expect(compiledResultServerProvider!.code).toMatchInlineSnapshot(` - "import { createServerFn } from '@tanstack/react-start'; + "import { createServerRpc } from '@tanstack/react-start/server-rpc'; + import { createServerFn } from '@tanstack/react-start'; const myFunc = () => { return 'hello from the server'; }; - const myServerFn = createServerFn().handler((opts, signal) => { - "use server"; - - return myServerFn.__executeServer(opts, signal); - }, myFunc);" + const myServerFn_createServerFn_handler = createServerRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6Im15U2VydmVyRm5fY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", (opts, signal) => myServerFn.__executeServer(opts, signal)); + const myServerFn = createServerFn().handler(myServerFn_createServerFn_handler, myFunc); + export { myServerFn_createServerFn_handler };" `) }) @@ -145,74 +158,53 @@ describe('createServerFn compiles correctly', async () => { // Client const compiledResult = await compile({ - id: 'test.ts', code, env: 'client', isProviderFile: false, }) expect(compiledResult!.code).toMatchInlineSnapshot(` - "import { createServerFn } from '@tanstack/react-start'; - export const exportedFn = createServerFn().handler((opts, signal) => { - "use server"; - - return exportedFn.__executeServer(opts, signal); - }); - const nonExportedFn = createServerFn().handler((opts, signal) => { - "use server"; - - return nonExportedFn.__executeServer(opts, signal); - });" + "import { createClientRpc } from '@tanstack/react-start/client-rpc'; + import { createServerFn } from '@tanstack/react-start'; + export const exportedFn = createServerFn().handler(createClientRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6ImV4cG9ydGVkRm5fY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9")); + const nonExportedFn = createServerFn().handler(createClientRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6Im5vbkV4cG9ydGVkRm5fY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9"));" `) // Server caller (route file) - no second argument const compiledResultServerCaller = await compile({ - id: 'test.ts', code, env: 'server', isProviderFile: false, }) expect(compiledResultServerCaller!.code).toMatchInlineSnapshot(` - "import { createServerFn } from '@tanstack/react-start'; - export const exportedFn = createServerFn().handler((opts, signal) => { - "use server"; - - return exportedFn.__executeServer(opts, signal); - }); - const nonExportedFn = createServerFn().handler((opts, signal) => { - "use server"; - - return nonExportedFn.__executeServer(opts, signal); - });" + "import { createSsrRpc } from '@tanstack/react-start/ssr-rpc'; + import { createServerFn } from '@tanstack/react-start'; + export const exportedFn = createServerFn().handler(createSsrRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6ImV4cG9ydGVkRm5fY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", () => import("test.ts?tss-serverfn-split").then(m => m["exportedFn_createServerFn_handler"]))); + const nonExportedFn = createServerFn().handler(createSsrRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6Im5vbkV4cG9ydGVkRm5fY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", () => import("test.ts?tss-serverfn-split").then(m => m["nonExportedFn_createServerFn_handler"])));" `) // Server provider (extracted file) - has second argument const compiledResultServerProvider = await compile({ - id: 'test.ts?tsr-directive-use-server', code, env: 'server', isProviderFile: true, }) expect(compiledResultServerProvider!.code).toMatchInlineSnapshot(` - "import { createServerFn } from '@tanstack/react-start'; + "import { createServerRpc } from '@tanstack/react-start/server-rpc'; + import { createServerFn } from '@tanstack/react-start'; const exportedVar = 'exported'; - export const exportedFn = createServerFn().handler((opts, signal) => { - "use server"; - - return exportedFn.__executeServer(opts, signal); - }, async () => { + const exportedFn_createServerFn_handler = createServerRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6ImV4cG9ydGVkRm5fY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", (opts, signal) => exportedFn.__executeServer(opts, signal)); + const exportedFn = createServerFn().handler(exportedFn_createServerFn_handler, async () => { return exportedVar; }); const nonExportedVar = 'non-exported'; - const nonExportedFn = createServerFn().handler((opts, signal) => { - "use server"; - - return nonExportedFn.__executeServer(opts, signal); - }, async () => { + const nonExportedFn_createServerFn_handler = createServerRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6Im5vbkV4cG9ydGVkRm5fY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", (opts, signal) => nonExportedFn.__executeServer(opts, signal)); + const nonExportedFn = createServerFn().handler(nonExportedFn_createServerFn_handler, async () => { return nonExportedVar; - });" + }); + export { exportedFn_createServerFn_handler, nonExportedFn_createServerFn_handler };" `) }) @@ -225,8 +217,9 @@ describe('createServerFn compiles correctly', async () => { const resolveIdMock = vi.fn(async (id: string) => id) - const compiler = new ServerFnCompiler({ + const compiler = new StartCompiler({ env: 'client', + ...getDefaultTestOptions('client'), loadModule: async () => {}, lookupKinds: new Set(['ServerFn']), lookupConfigurations: [ @@ -237,13 +230,11 @@ describe('createServerFn compiles correctly', async () => { }, ], resolveId: resolveIdMock, - directive: 'use server', }) await compiler.compile({ code, id: 'test.ts', - isProviderFile: false, }) // resolveId should only be called once during init() for the library itself @@ -266,8 +257,9 @@ describe('createServerFn compiles correctly', async () => { const resolveIdMock = vi.fn(async (id: string) => id) - const compiler = new ServerFnCompiler({ + const compiler = new StartCompiler({ env: 'client', + ...getDefaultTestOptions('client'), loadModule: async (id) => { // Simulate the factory module being loaded if (id === './factory') { @@ -289,13 +281,11 @@ describe('createServerFn compiles correctly', async () => { }, ], resolveId: resolveIdMock, - directive: 'use server', }) await compiler.compile({ code: factoryCode, id: 'test.ts', - isProviderFile: false, }) // resolveId should be called exactly twice: diff --git a/packages/start-plugin-core/tests/createServerFn/snapshots/client/createServerFnDestructured.tsx b/packages/start-plugin-core/tests/createServerFn/snapshots/client/createServerFnDestructured.tsx index c308ef05127..a26d8b38517 100644 --- a/packages/start-plugin-core/tests/createServerFn/snapshots/client/createServerFnDestructured.tsx +++ b/packages/start-plugin-core/tests/createServerFn/snapshots/client/createServerFnDestructured.tsx @@ -1,50 +1,23 @@ +import { createClientRpc } from '@tanstack/react-start/client-rpc'; import { createServerFn } from '@tanstack/react-start'; export const withUseServer = createServerFn({ method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withUseServer.__executeServer(opts, signal); -}); +}).handler(createClientRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhVc2VTZXJ2ZXJfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9")); export const withArrowFunction = createServerFn({ method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withArrowFunction.__executeServer(opts, signal); -}); +}).handler(createClientRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhBcnJvd0Z1bmN0aW9uX2NyZWF0ZVNlcnZlckZuX2hhbmRsZXIifQ")); export const withArrowFunctionAndFunction = createServerFn({ method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withArrowFunctionAndFunction.__executeServer(opts, signal); -}); +}).handler(createClientRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhBcnJvd0Z1bmN0aW9uQW5kRnVuY3Rpb25fY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9")); export const withoutUseServer = createServerFn({ method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withoutUseServer.__executeServer(opts, signal); -}); +}).handler(createClientRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhvdXRVc2VTZXJ2ZXJfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9")); export const withVariable = createServerFn({ method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withVariable.__executeServer(opts, signal); -}); +}).handler(createClientRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhWYXJpYWJsZV9jcmVhdGVTZXJ2ZXJGbl9oYW5kbGVyIn0")); export const withZodValidator = createServerFn({ method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withZodValidator.__executeServer(opts, signal); -}); +}).handler(createClientRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhab2RWYWxpZGF0b3JfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9")); export const withValidatorFn = createServerFn({ method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withValidatorFn.__executeServer(opts, signal); -}); \ No newline at end of file +}).handler(createClientRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhWYWxpZGF0b3JGbl9jcmVhdGVTZXJ2ZXJGbl9oYW5kbGVyIn0")); \ No newline at end of file diff --git a/packages/start-plugin-core/tests/createServerFn/snapshots/client/createServerFnDestructuredRename.tsx b/packages/start-plugin-core/tests/createServerFn/snapshots/client/createServerFnDestructuredRename.tsx index 2419360ce2a..4faeee53249 100644 --- a/packages/start-plugin-core/tests/createServerFn/snapshots/client/createServerFnDestructuredRename.tsx +++ b/packages/start-plugin-core/tests/createServerFn/snapshots/client/createServerFnDestructuredRename.tsx @@ -1,29 +1,14 @@ +import { createClientRpc } from '@tanstack/react-start/client-rpc'; import { createServerFn as serverFn } from '@tanstack/react-start'; export const withUseServer = serverFn({ method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withUseServer.__executeServer(opts, signal); -}); +}).handler(createClientRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhVc2VTZXJ2ZXJfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9")); export const withoutUseServer = serverFn({ method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withoutUseServer.__executeServer(opts, signal); -}); +}).handler(createClientRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhvdXRVc2VTZXJ2ZXJfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9")); export const withVariable = serverFn({ method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withVariable.__executeServer(opts, signal); -}); +}).handler(createClientRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhWYXJpYWJsZV9jcmVhdGVTZXJ2ZXJGbl9oYW5kbGVyIn0")); export const withZodValidator = serverFn({ method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withZodValidator.__executeServer(opts, signal); -}); \ No newline at end of file +}).handler(createClientRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhab2RWYWxpZGF0b3JfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9")); \ No newline at end of file diff --git a/packages/start-plugin-core/tests/createServerFn/snapshots/client/createServerFnStarImport.tsx b/packages/start-plugin-core/tests/createServerFn/snapshots/client/createServerFnStarImport.tsx index b308d505f25..a6bb257000a 100644 --- a/packages/start-plugin-core/tests/createServerFn/snapshots/client/createServerFnStarImport.tsx +++ b/packages/start-plugin-core/tests/createServerFn/snapshots/client/createServerFnStarImport.tsx @@ -1,29 +1,14 @@ +import { createClientRpc } from '@tanstack/react-start/client-rpc'; import * as TanStackStart from '@tanstack/react-start'; export const withUseServer = TanStackStart.createServerFn({ method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withUseServer.__executeServer(opts, signal); -}); +}).handler(createClientRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhVc2VTZXJ2ZXJfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9")); export const withoutUseServer = TanStackStart.createServerFn({ method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withoutUseServer.__executeServer(opts, signal); -}); +}).handler(createClientRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhvdXRVc2VTZXJ2ZXJfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9")); export const withVariable = TanStackStart.createServerFn({ method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withVariable.__executeServer(opts, signal); -}); +}).handler(createClientRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhWYXJpYWJsZV9jcmVhdGVTZXJ2ZXJGbl9oYW5kbGVyIn0")); export const withZodValidator = TanStackStart.createServerFn({ method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withZodValidator.__executeServer(opts, signal); -}); \ No newline at end of file +}).handler(createClientRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhab2RWYWxpZGF0b3JfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9")); \ No newline at end of file diff --git a/packages/start-plugin-core/tests/createServerFn/snapshots/client/createServerFnValidator.tsx b/packages/start-plugin-core/tests/createServerFn/snapshots/client/createServerFnValidator.tsx index 5896ad8c1df..a62a7b6e60b 100644 --- a/packages/start-plugin-core/tests/createServerFn/snapshots/client/createServerFnValidator.tsx +++ b/packages/start-plugin-core/tests/createServerFn/snapshots/client/createServerFnValidator.tsx @@ -1,8 +1,5 @@ +import { createClientRpc } from '@tanstack/react-start/client-rpc'; import { createServerFn } from '@tanstack/react-start'; export const withUseServer = createServerFn({ method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withUseServer.__executeServer(opts, signal); -}); \ No newline at end of file +}).handler(createClientRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhVc2VTZXJ2ZXJfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9")); \ No newline at end of file diff --git a/packages/start-plugin-core/tests/createServerFn/snapshots/client/factory.tsx b/packages/start-plugin-core/tests/createServerFn/snapshots/client/factory.tsx index fe95c7e47ee..d9b5f242e34 100644 --- a/packages/start-plugin-core/tests/createServerFn/snapshots/client/factory.tsx +++ b/packages/start-plugin-core/tests/createServerFn/snapshots/client/factory.tsx @@ -1,3 +1,4 @@ +import { createClientRpc } from '@tanstack/react-start/client-rpc'; import { createServerFn, createMiddleware } from '@tanstack/react-start'; const authMiddleware = createMiddleware({ type: 'function' @@ -23,16 +24,8 @@ const adminMiddleware = createMiddleware({ }); export const createAuthServerFn = createServerFn().middleware([authMiddleware]); const createAdminServerFn = createAuthServerFn().middleware([adminMiddleware]); -export const myAuthedFn = createAuthServerFn().handler((opts, signal) => { - "use server"; - - return myAuthedFn.__executeServer(opts, signal); -}); -export const deleteUserFn = createAdminServerFn().handler((opts, signal) => { - "use server"; - - return deleteUserFn.__executeServer(opts, signal); -}); +export const myAuthedFn = createAuthServerFn().handler(createClientRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6Im15QXV0aGVkRm5fY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9")); +export const deleteUserFn = createAdminServerFn().handler(createClientRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6ImRlbGV0ZVVzZXJGbl9jcmVhdGVTZXJ2ZXJGbl9oYW5kbGVyIn0")); function createFakeFn() { return { handler: cb => { diff --git a/packages/start-plugin-core/tests/createServerFn/snapshots/client/isomorphic-fns.tsx b/packages/start-plugin-core/tests/createServerFn/snapshots/client/isomorphic-fns.tsx index 1de861275b8..b1cd8f3f2ed 100644 --- a/packages/start-plugin-core/tests/createServerFn/snapshots/client/isomorphic-fns.tsx +++ b/packages/start-plugin-core/tests/createServerFn/snapshots/client/isomorphic-fns.tsx @@ -1,18 +1,11 @@ +import { createClientRpc } from '@tanstack/react-start/client-rpc'; import { createFileRoute } from '@tanstack/react-router'; import { createIsomorphicFn, createServerFn } from '@tanstack/react-start'; import { useState } from 'react'; const getEnv = createIsomorphicFn().server(() => 'server').client(() => 'client'); -const getServerEnv = createServerFn().handler((opts, signal) => { - "use server"; - - return getServerEnv.__executeServer(opts, signal); -}); +const getServerEnv = createServerFn().handler(createClientRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6ImdldFNlcnZlckVudl9jcmVhdGVTZXJ2ZXJGbl9oYW5kbGVyIn0")); const getEcho = createIsomorphicFn().server((input: string) => 'server received ' + input).client(input => 'client received ' + input); -const getServerEcho = createServerFn().handler((opts, signal) => { - "use server"; - - return getServerEcho.__executeServer(opts, signal); -}); +const getServerEcho = createServerFn().handler(createClientRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6ImdldFNlcnZlckVjaG9fY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9")); export const Route = createFileRoute('/isomorphic-fns')({ component: RouteComponent, loader() { diff --git a/packages/start-plugin-core/tests/createServerFn/snapshots/server-caller/createServerFnDestructured.tsx b/packages/start-plugin-core/tests/createServerFn/snapshots/server-caller/createServerFnDestructured.tsx new file mode 100644 index 00000000000..83f912ad8a0 --- /dev/null +++ b/packages/start-plugin-core/tests/createServerFn/snapshots/server-caller/createServerFnDestructured.tsx @@ -0,0 +1,24 @@ +import { createSsrRpc } from '@tanstack/react-start/ssr-rpc'; +import { createServerFn } from '@tanstack/react-start'; +import { z } from 'zod'; +export const withUseServer = createServerFn({ + method: 'GET' +}).handler(createSsrRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhVc2VTZXJ2ZXJfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", () => import("test.ts?tss-serverfn-split").then(m => m["withUseServer_createServerFn_handler"]))); +export const withArrowFunction = createServerFn({ + method: 'GET' +}).handler(createSsrRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhBcnJvd0Z1bmN0aW9uX2NyZWF0ZVNlcnZlckZuX2hhbmRsZXIifQ", () => import("test.ts?tss-serverfn-split").then(m => m["withArrowFunction_createServerFn_handler"]))); +export const withArrowFunctionAndFunction = createServerFn({ + method: 'GET' +}).handler(createSsrRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhBcnJvd0Z1bmN0aW9uQW5kRnVuY3Rpb25fY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", () => import("test.ts?tss-serverfn-split").then(m => m["withArrowFunctionAndFunction_createServerFn_handler"]))); +export const withoutUseServer = createServerFn({ + method: 'GET' +}).handler(createSsrRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhvdXRVc2VTZXJ2ZXJfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", () => import("test.ts?tss-serverfn-split").then(m => m["withoutUseServer_createServerFn_handler"]))); +export const withVariable = createServerFn({ + method: 'GET' +}).handler(createSsrRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhWYXJpYWJsZV9jcmVhdGVTZXJ2ZXJGbl9oYW5kbGVyIn0", () => import("test.ts?tss-serverfn-split").then(m => m["withVariable_createServerFn_handler"]))); +export const withZodValidator = createServerFn({ + method: 'GET' +}).handler(createSsrRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhab2RWYWxpZGF0b3JfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", () => import("test.ts?tss-serverfn-split").then(m => m["withZodValidator_createServerFn_handler"]))); +export const withValidatorFn = createServerFn({ + method: 'GET' +}).inputValidator(z.number()).handler(createSsrRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhWYWxpZGF0b3JGbl9jcmVhdGVTZXJ2ZXJGbl9oYW5kbGVyIn0", () => import("test.ts?tss-serverfn-split").then(m => m["withValidatorFn_createServerFn_handler"]))); \ No newline at end of file diff --git a/packages/start-plugin-core/tests/createServerFn/snapshots/server-caller/createServerFnDestructuredRename.tsx b/packages/start-plugin-core/tests/createServerFn/snapshots/server-caller/createServerFnDestructuredRename.tsx new file mode 100644 index 00000000000..bfaa169987d --- /dev/null +++ b/packages/start-plugin-core/tests/createServerFn/snapshots/server-caller/createServerFnDestructuredRename.tsx @@ -0,0 +1,14 @@ +import { createSsrRpc } from '@tanstack/react-start/ssr-rpc'; +import { createServerFn as serverFn } from '@tanstack/react-start'; +export const withUseServer = serverFn({ + method: 'GET' +}).handler(createSsrRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhVc2VTZXJ2ZXJfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", () => import("test.ts?tss-serverfn-split").then(m => m["withUseServer_createServerFn_handler"]))); +export const withoutUseServer = serverFn({ + method: 'GET' +}).handler(createSsrRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhvdXRVc2VTZXJ2ZXJfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", () => import("test.ts?tss-serverfn-split").then(m => m["withoutUseServer_createServerFn_handler"]))); +export const withVariable = serverFn({ + method: 'GET' +}).handler(createSsrRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhWYXJpYWJsZV9jcmVhdGVTZXJ2ZXJGbl9oYW5kbGVyIn0", () => import("test.ts?tss-serverfn-split").then(m => m["withVariable_createServerFn_handler"]))); +export const withZodValidator = serverFn({ + method: 'GET' +}).handler(createSsrRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhab2RWYWxpZGF0b3JfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", () => import("test.ts?tss-serverfn-split").then(m => m["withZodValidator_createServerFn_handler"]))); \ No newline at end of file diff --git a/packages/start-plugin-core/tests/createServerFn/snapshots/server-caller/createServerFnStarImport.tsx b/packages/start-plugin-core/tests/createServerFn/snapshots/server-caller/createServerFnStarImport.tsx new file mode 100644 index 00000000000..ab56b02c518 --- /dev/null +++ b/packages/start-plugin-core/tests/createServerFn/snapshots/server-caller/createServerFnStarImport.tsx @@ -0,0 +1,14 @@ +import { createSsrRpc } from '@tanstack/react-start/ssr-rpc'; +import * as TanStackStart from '@tanstack/react-start'; +export const withUseServer = TanStackStart.createServerFn({ + method: 'GET' +}).handler(createSsrRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhVc2VTZXJ2ZXJfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", () => import("test.ts?tss-serverfn-split").then(m => m["withUseServer_createServerFn_handler"]))); +export const withoutUseServer = TanStackStart.createServerFn({ + method: 'GET' +}).handler(createSsrRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhvdXRVc2VTZXJ2ZXJfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", () => import("test.ts?tss-serverfn-split").then(m => m["withoutUseServer_createServerFn_handler"]))); +export const withVariable = TanStackStart.createServerFn({ + method: 'GET' +}).handler(createSsrRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhWYXJpYWJsZV9jcmVhdGVTZXJ2ZXJGbl9oYW5kbGVyIn0", () => import("test.ts?tss-serverfn-split").then(m => m["withVariable_createServerFn_handler"]))); +export const withZodValidator = TanStackStart.createServerFn({ + method: 'GET' +}).handler(createSsrRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhab2RWYWxpZGF0b3JfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", () => import("test.ts?tss-serverfn-split").then(m => m["withZodValidator_createServerFn_handler"]))); \ No newline at end of file diff --git a/packages/start-plugin-core/tests/createServerFn/snapshots/server-caller/createServerFnValidator.tsx b/packages/start-plugin-core/tests/createServerFn/snapshots/server-caller/createServerFnValidator.tsx new file mode 100644 index 00000000000..a614a804095 --- /dev/null +++ b/packages/start-plugin-core/tests/createServerFn/snapshots/server-caller/createServerFnValidator.tsx @@ -0,0 +1,6 @@ +import { createSsrRpc } from '@tanstack/react-start/ssr-rpc'; +import { createServerFn } from '@tanstack/react-start'; +import { z } from 'zod'; +export const withUseServer = createServerFn({ + method: 'GET' +}).inputValidator(z.number()).handler(createSsrRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhVc2VTZXJ2ZXJfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", () => import("test.ts?tss-serverfn-split").then(m => m["withUseServer_createServerFn_handler"]))); \ No newline at end of file diff --git a/packages/start-plugin-core/tests/createServerFn/snapshots/server/factory.tsx b/packages/start-plugin-core/tests/createServerFn/snapshots/server-caller/factory.tsx similarity index 52% rename from packages/start-plugin-core/tests/createServerFn/snapshots/server/factory.tsx rename to packages/start-plugin-core/tests/createServerFn/snapshots/server-caller/factory.tsx index 388e9761c3b..372b97d5283 100644 --- a/packages/start-plugin-core/tests/createServerFn/snapshots/server/factory.tsx +++ b/packages/start-plugin-core/tests/createServerFn/snapshots/server-caller/factory.tsx @@ -1,3 +1,4 @@ +import { createSsrRpc } from '@tanstack/react-start/ssr-rpc'; import { createServerFn, createMiddleware } from '@tanstack/react-start'; const authMiddleware = createMiddleware({ type: 'function' @@ -23,20 +24,8 @@ const adminMiddleware = createMiddleware({ }); export const createAuthServerFn = createServerFn().middleware([authMiddleware]); const createAdminServerFn = createAuthServerFn().middleware([adminMiddleware]); -export const myAuthedFn = createAuthServerFn().handler((opts, signal) => { - "use server"; - - return myAuthedFn.__executeServer(opts, signal); -}, () => { - return 'myAuthedFn'; -}); -export const deleteUserFn = createAdminServerFn().handler((opts, signal) => { - "use server"; - - return deleteUserFn.__executeServer(opts, signal); -}, () => { - return 'deleteUserFn'; -}); +export const myAuthedFn = createAuthServerFn().handler(createSsrRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6Im15QXV0aGVkRm5fY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", () => import("test.ts?tss-serverfn-split").then(m => m["myAuthedFn_createServerFn_handler"]))); +export const deleteUserFn = createAdminServerFn().handler(createSsrRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6ImRlbGV0ZVVzZXJGbl9jcmVhdGVTZXJ2ZXJGbl9oYW5kbGVyIn0", () => import("test.ts?tss-serverfn-split").then(m => m["deleteUserFn_createServerFn_handler"]))); function createFakeFn() { return { handler: cb => { diff --git a/packages/start-plugin-core/tests/createServerFn/snapshots/server/isomorphic-fns.tsx b/packages/start-plugin-core/tests/createServerFn/snapshots/server-caller/isomorphic-fns.tsx similarity index 78% rename from packages/start-plugin-core/tests/createServerFn/snapshots/server/isomorphic-fns.tsx rename to packages/start-plugin-core/tests/createServerFn/snapshots/server-caller/isomorphic-fns.tsx index 08e55500c90..cd2823e6fe8 100644 --- a/packages/start-plugin-core/tests/createServerFn/snapshots/server/isomorphic-fns.tsx +++ b/packages/start-plugin-core/tests/createServerFn/snapshots/server-caller/isomorphic-fns.tsx @@ -1,20 +1,11 @@ +import { createSsrRpc } from '@tanstack/react-start/ssr-rpc'; import { createFileRoute } from '@tanstack/react-router'; import { createIsomorphicFn, createServerFn } from '@tanstack/react-start'; import { useState } from 'react'; const getEnv = createIsomorphicFn().server(() => 'server').client(() => 'client'); -const getServerEnv = createServerFn().handler((opts, signal) => { - "use server"; - - return getServerEnv.__executeServer(opts, signal); -}, () => getEnv()); +const getServerEnv = createServerFn().handler(createSsrRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6ImdldFNlcnZlckVudl9jcmVhdGVTZXJ2ZXJGbl9oYW5kbGVyIn0", () => import("test.ts?tss-serverfn-split").then(m => m["getServerEnv_createServerFn_handler"]))); const getEcho = createIsomorphicFn().server((input: string) => 'server received ' + input).client(input => 'client received ' + input); -const getServerEcho = createServerFn().inputValidator((input: string) => input).handler((opts, signal) => { - "use server"; - - return getServerEcho.__executeServer(opts, signal); -}, ({ - data -}) => getEcho(data)); +const getServerEcho = createServerFn().inputValidator((input: string) => input).handler(createSsrRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6ImdldFNlcnZlckVjaG9fY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", () => import("test.ts?tss-serverfn-split").then(m => m["getServerEcho_createServerFn_handler"]))); export const Route = createFileRoute('/isomorphic-fns')({ component: RouteComponent, loader() { diff --git a/packages/start-plugin-core/tests/createServerFn/snapshots/server-provider/createServerFnDestructured.tsx b/packages/start-plugin-core/tests/createServerFn/snapshots/server-provider/createServerFnDestructured.tsx new file mode 100644 index 00000000000..69fa019eb9f --- /dev/null +++ b/packages/start-plugin-core/tests/createServerFn/snapshots/server-provider/createServerFnDestructured.tsx @@ -0,0 +1,58 @@ +import { createServerRpc } from '@tanstack/react-start/server-rpc'; +import { createServerFn } from '@tanstack/react-start'; +import { z } from 'zod'; +const withUseServer_createServerFn_handler = createServerRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhVc2VTZXJ2ZXJfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", (opts, signal) => withUseServer.__executeServer(opts, signal)); +const withUseServer = createServerFn({ + method: 'GET' +}).handler(withUseServer_createServerFn_handler, async function () { + console.info('Fetching posts...'); + await new Promise(r => setTimeout(r, 500)); + return axios.get>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10)); +}); +const withArrowFunction_createServerFn_handler = createServerRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhBcnJvd0Z1bmN0aW9uX2NyZWF0ZVNlcnZlckZuX2hhbmRsZXIifQ", (opts, signal) => withArrowFunction.__executeServer(opts, signal)); +const withArrowFunction = createServerFn({ + method: 'GET' +}).handler(withArrowFunction_createServerFn_handler, async () => null); +const withArrowFunctionAndFunction_createServerFn_handler = createServerRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhBcnJvd0Z1bmN0aW9uQW5kRnVuY3Rpb25fY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", (opts, signal) => withArrowFunctionAndFunction.__executeServer(opts, signal)); +const withArrowFunctionAndFunction = createServerFn({ + method: 'GET' +}).handler(withArrowFunctionAndFunction_createServerFn_handler, async () => test()); +const withoutUseServer_createServerFn_handler = createServerRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhvdXRVc2VTZXJ2ZXJfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", (opts, signal) => withoutUseServer.__executeServer(opts, signal)); +const withoutUseServer = createServerFn({ + method: 'GET' +}).handler(withoutUseServer_createServerFn_handler, async () => { + console.info('Fetching posts...'); + await new Promise(r => setTimeout(r, 500)); + return axios.get>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10)); +}); +const withVariable_createServerFn_handler = createServerRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhWYXJpYWJsZV9jcmVhdGVTZXJ2ZXJGbl9oYW5kbGVyIn0", (opts, signal) => withVariable.__executeServer(opts, signal)); +const withVariable = createServerFn({ + method: 'GET' +}).handler(withVariable_createServerFn_handler, abstractedFunction); +async function abstractedFunction() { + console.info('Fetching posts...'); + await new Promise(r => setTimeout(r, 500)); + return axios.get>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10)); +} +function zodValidator(schema: TSchema, fn: (input: z.output) => TResult) { + return async (input: unknown) => { + return fn(schema.parse(input)); + }; +} +const withZodValidator_createServerFn_handler = createServerRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhab2RWYWxpZGF0b3JfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", (opts, signal) => withZodValidator.__executeServer(opts, signal)); +const withZodValidator = createServerFn({ + method: 'GET' +}).handler(withZodValidator_createServerFn_handler, zodValidator(z.number(), input => { + return { + 'you gave': input + }; +})); +const withValidatorFn_createServerFn_handler = createServerRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhWYWxpZGF0b3JGbl9jcmVhdGVTZXJ2ZXJGbl9oYW5kbGVyIn0", (opts, signal) => withValidatorFn.__executeServer(opts, signal)); +const withValidatorFn = createServerFn({ + method: 'GET' +}).inputValidator(z.number()).handler(withValidatorFn_createServerFn_handler, async ({ + input +}) => { + return null; +}); +export { withUseServer_createServerFn_handler, withArrowFunction_createServerFn_handler, withArrowFunctionAndFunction_createServerFn_handler, withoutUseServer_createServerFn_handler, withVariable_createServerFn_handler, withZodValidator_createServerFn_handler, withValidatorFn_createServerFn_handler }; \ No newline at end of file diff --git a/packages/start-plugin-core/tests/createServerFn/snapshots/server-provider/createServerFnDestructuredRename.tsx b/packages/start-plugin-core/tests/createServerFn/snapshots/server-provider/createServerFnDestructuredRename.tsx new file mode 100644 index 00000000000..cb14a1e1cde --- /dev/null +++ b/packages/start-plugin-core/tests/createServerFn/snapshots/server-provider/createServerFnDestructuredRename.tsx @@ -0,0 +1,42 @@ +import { createServerRpc } from '@tanstack/react-start/server-rpc'; +import { createServerFn as serverFn } from '@tanstack/react-start'; +import { z } from 'zod'; +const withUseServer_createServerFn_handler = createServerRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhVc2VTZXJ2ZXJfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", (opts, signal) => withUseServer.__executeServer(opts, signal)); +const withUseServer = serverFn({ + method: 'GET' +}).handler(withUseServer_createServerFn_handler, async function () { + console.info('Fetching posts...'); + await new Promise(r => setTimeout(r, 500)); + return axios.get>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10)); +}); +const withoutUseServer_createServerFn_handler = createServerRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhvdXRVc2VTZXJ2ZXJfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", (opts, signal) => withoutUseServer.__executeServer(opts, signal)); +const withoutUseServer = serverFn({ + method: 'GET' +}).handler(withoutUseServer_createServerFn_handler, async () => { + console.info('Fetching posts...'); + await new Promise(r => setTimeout(r, 500)); + return axios.get>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10)); +}); +const withVariable_createServerFn_handler = createServerRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhWYXJpYWJsZV9jcmVhdGVTZXJ2ZXJGbl9oYW5kbGVyIn0", (opts, signal) => withVariable.__executeServer(opts, signal)); +const withVariable = serverFn({ + method: 'GET' +}).handler(withVariable_createServerFn_handler, abstractedFunction); +async function abstractedFunction() { + console.info('Fetching posts...'); + await new Promise(r => setTimeout(r, 500)); + return axios.get>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10)); +} +function zodValidator(schema: TSchema, fn: (input: z.output) => TResult) { + return async (input: unknown) => { + return fn(schema.parse(input)); + }; +} +const withZodValidator_createServerFn_handler = createServerRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhab2RWYWxpZGF0b3JfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", (opts, signal) => withZodValidator.__executeServer(opts, signal)); +const withZodValidator = serverFn({ + method: 'GET' +}).handler(withZodValidator_createServerFn_handler, zodValidator(z.number(), input => { + return { + 'you gave': input + }; +})); +export { withUseServer_createServerFn_handler, withoutUseServer_createServerFn_handler, withVariable_createServerFn_handler, withZodValidator_createServerFn_handler }; \ No newline at end of file diff --git a/packages/start-plugin-core/tests/createServerFn/snapshots/server-provider/createServerFnStarImport.tsx b/packages/start-plugin-core/tests/createServerFn/snapshots/server-provider/createServerFnStarImport.tsx new file mode 100644 index 00000000000..ce3152eb083 --- /dev/null +++ b/packages/start-plugin-core/tests/createServerFn/snapshots/server-provider/createServerFnStarImport.tsx @@ -0,0 +1,44 @@ +import { createServerRpc } from '@tanstack/react-start/server-rpc'; +import * as TanStackStart from '@tanstack/react-start'; +import { z } from 'zod'; +const withUseServer_createServerFn_handler = createServerRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhVc2VTZXJ2ZXJfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", (opts, signal) => withUseServer.__executeServer(opts, signal)); +const withUseServer = TanStackStart.createServerFn({ + method: 'GET' +}).handler(withUseServer_createServerFn_handler, async function () { + 'use server'; + + console.info('Fetching posts...'); + await new Promise(r => setTimeout(r, 500)); + return axios.get>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10)); +}); +const withoutUseServer_createServerFn_handler = createServerRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhvdXRVc2VTZXJ2ZXJfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", (opts, signal) => withoutUseServer.__executeServer(opts, signal)); +const withoutUseServer = TanStackStart.createServerFn({ + method: 'GET' +}).handler(withoutUseServer_createServerFn_handler, async () => { + console.info('Fetching posts...'); + await new Promise(r => setTimeout(r, 500)); + return axios.get>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10)); +}); +const withVariable_createServerFn_handler = createServerRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhWYXJpYWJsZV9jcmVhdGVTZXJ2ZXJGbl9oYW5kbGVyIn0", (opts, signal) => withVariable.__executeServer(opts, signal)); +const withVariable = TanStackStart.createServerFn({ + method: 'GET' +}).handler(withVariable_createServerFn_handler, abstractedFunction); +async function abstractedFunction() { + console.info('Fetching posts...'); + await new Promise(r => setTimeout(r, 500)); + return axios.get>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10)); +} +function zodValidator(schema: TSchema, fn: (input: z.output) => TResult) { + return async (input: unknown) => { + return fn(schema.parse(input)); + }; +} +const withZodValidator_createServerFn_handler = createServerRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhab2RWYWxpZGF0b3JfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", (opts, signal) => withZodValidator.__executeServer(opts, signal)); +const withZodValidator = TanStackStart.createServerFn({ + method: 'GET' +}).handler(withZodValidator_createServerFn_handler, zodValidator(z.number(), input => { + return { + 'you gave': input + }; +})); +export { withUseServer_createServerFn_handler, withoutUseServer_createServerFn_handler, withVariable_createServerFn_handler, withZodValidator_createServerFn_handler }; \ No newline at end of file diff --git a/packages/start-plugin-core/tests/createServerFn/snapshots/server-provider/createServerFnValidator.tsx b/packages/start-plugin-core/tests/createServerFn/snapshots/server-provider/createServerFnValidator.tsx new file mode 100644 index 00000000000..8b353869ac3 --- /dev/null +++ b/packages/start-plugin-core/tests/createServerFn/snapshots/server-provider/createServerFnValidator.tsx @@ -0,0 +1,10 @@ +import { createServerRpc } from '@tanstack/react-start/server-rpc'; +import { createServerFn } from '@tanstack/react-start'; +import { z } from 'zod'; +const withUseServer_createServerFn_handler = createServerRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6IndpdGhVc2VTZXJ2ZXJfY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", (opts, signal) => withUseServer.__executeServer(opts, signal)); +const withUseServer = createServerFn({ + method: 'GET' +}).inputValidator(z.number()).handler(withUseServer_createServerFn_handler, ({ + input +}) => input + 1); +export { withUseServer_createServerFn_handler }; \ No newline at end of file diff --git a/packages/start-plugin-core/tests/createServerFn/snapshots/server-provider/factory.tsx b/packages/start-plugin-core/tests/createServerFn/snapshots/server-provider/factory.tsx new file mode 100644 index 00000000000..9443f0b1f49 --- /dev/null +++ b/packages/start-plugin-core/tests/createServerFn/snapshots/server-provider/factory.tsx @@ -0,0 +1,45 @@ +import { createServerRpc } from '@tanstack/react-start/server-rpc'; +import { createServerFn, createMiddleware } from '@tanstack/react-start'; +const authMiddleware = createMiddleware({ + type: 'function' +}).server(({ + next +}) => { + return next({ + context: { + auth: 'auth' + } + }); +}); +const adminMiddleware = createMiddleware({ + type: 'function' +}).server(({ + next +}) => { + return next({ + context: { + admin: 'admin' + } + }); +}); +const createAuthServerFn = createServerFn().middleware([authMiddleware]); +const createAdminServerFn = createAuthServerFn().middleware([adminMiddleware]); +const myAuthedFn_createServerFn_handler = createServerRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6Im15QXV0aGVkRm5fY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", (opts, signal) => myAuthedFn.__executeServer(opts, signal)); +const myAuthedFn = createAuthServerFn().handler(myAuthedFn_createServerFn_handler, () => { + return 'myAuthedFn'; +}); +const deleteUserFn_createServerFn_handler = createServerRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6ImRlbGV0ZVVzZXJGbl9jcmVhdGVTZXJ2ZXJGbl9oYW5kbGVyIn0", (opts, signal) => deleteUserFn.__executeServer(opts, signal)); +const deleteUserFn = createAdminServerFn().handler(deleteUserFn_createServerFn_handler, () => { + return 'deleteUserFn'; +}); +function createFakeFn() { + return { + handler: cb => { + return cb(); + } + }; +} +const x = createFakeFn().handler(() => { + return 'fakeFn'; +}); +export { myAuthedFn_createServerFn_handler, deleteUserFn_createServerFn_handler }; \ No newline at end of file diff --git a/packages/start-plugin-core/tests/createServerFn/snapshots/server-provider/isomorphic-fns.tsx b/packages/start-plugin-core/tests/createServerFn/snapshots/server-provider/isomorphic-fns.tsx new file mode 100644 index 00000000000..5dc29d66b5e --- /dev/null +++ b/packages/start-plugin-core/tests/createServerFn/snapshots/server-provider/isomorphic-fns.tsx @@ -0,0 +1,72 @@ +import { createServerRpc } from '@tanstack/react-start/server-rpc'; +import { createFileRoute } from '@tanstack/react-router'; +import { createIsomorphicFn, createServerFn } from '@tanstack/react-start'; +import { useState } from 'react'; +const getEnv = createIsomorphicFn().server(() => 'server').client(() => 'client'); +const getServerEnv_createServerFn_handler = createServerRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6ImdldFNlcnZlckVudl9jcmVhdGVTZXJ2ZXJGbl9oYW5kbGVyIn0", (opts, signal) => getServerEnv.__executeServer(opts, signal)); +const getServerEnv = createServerFn().handler(getServerEnv_createServerFn_handler, () => getEnv()); +const getEcho = createIsomorphicFn().server((input: string) => 'server received ' + input).client(input => 'client received ' + input); +const getServerEcho_createServerFn_handler = createServerRpc("eyJmaWxlIjoiL0BpZC90ZXN0LnRzP3Rzcy1zZXJ2ZXJmbi1zcGxpdCIsImV4cG9ydCI6ImdldFNlcnZlckVjaG9fY3JlYXRlU2VydmVyRm5faGFuZGxlciJ9", (opts, signal) => getServerEcho.__executeServer(opts, signal)); +const getServerEcho = createServerFn().inputValidator((input: string) => input).handler(getServerEcho_createServerFn_handler, ({ + data +}) => getEcho(data)); +const Route = createFileRoute('/isomorphic-fns')({ + component: RouteComponent, + loader() { + return { + envOnLoad: getEnv() + }; + } +}); +function RouteComponent() { + const { + envOnLoad + } = Route.useLoaderData(); + const [results, setResults] = useState>>(); + async function handleClick() { + const envOnClick = getEnv(); + const echo = getEcho('hello'); + const [serverEnv, serverEcho] = await Promise.all([getServerEnv(), getServerEcho({ + data: 'hello' + })]); + setResults({ + envOnClick, + echo, + serverEnv, + serverEcho + }); + } + const { + envOnClick, + echo, + serverEnv, + serverEcho + } = results || {}; + return
+ + {!!results &&
+

+ getEnv +

+ When we called the function on the server it returned: +
{JSON.stringify(serverEnv)}
+ When we called the function on the client it returned: +
{JSON.stringify(envOnClick)}
+ When we called the function during SSR it returned: +
{JSON.stringify(envOnLoad)}
+
+

+ echo +

+ When we called the function on the server it returned: +
+            {JSON.stringify(serverEcho)}
+          
+ When we called the function on the client it returned: +
{JSON.stringify(echo)}
+
} +
; +} +export { getServerEnv_createServerFn_handler, getServerEcho_createServerFn_handler }; \ No newline at end of file diff --git a/packages/start-plugin-core/tests/createServerFn/snapshots/server/createServerFnDestructured.tsx b/packages/start-plugin-core/tests/createServerFn/snapshots/server/createServerFnDestructured.tsx deleted file mode 100644 index 0f0c5f651dd..00000000000 --- a/packages/start-plugin-core/tests/createServerFn/snapshots/server/createServerFnDestructured.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { createServerFn } from '@tanstack/react-start'; -import { z } from 'zod'; -export const withUseServer = createServerFn({ - method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withUseServer.__executeServer(opts, signal); -}, async function () { - console.info('Fetching posts...'); - await new Promise(r => setTimeout(r, 500)); - return axios.get>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10)); -}); -export const withArrowFunction = createServerFn({ - method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withArrowFunction.__executeServer(opts, signal); -}, async () => null); -export const withArrowFunctionAndFunction = createServerFn({ - method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withArrowFunctionAndFunction.__executeServer(opts, signal); -}, async () => test()); -export const withoutUseServer = createServerFn({ - method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withoutUseServer.__executeServer(opts, signal); -}, async () => { - console.info('Fetching posts...'); - await new Promise(r => setTimeout(r, 500)); - return axios.get>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10)); -}); -export const withVariable = createServerFn({ - method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withVariable.__executeServer(opts, signal); -}, abstractedFunction); -async function abstractedFunction() { - console.info('Fetching posts...'); - await new Promise(r => setTimeout(r, 500)); - return axios.get>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10)); -} -function zodValidator(schema: TSchema, fn: (input: z.output) => TResult) { - return async (input: unknown) => { - return fn(schema.parse(input)); - }; -} -export const withZodValidator = createServerFn({ - method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withZodValidator.__executeServer(opts, signal); -}, zodValidator(z.number(), input => { - return { - 'you gave': input - }; -})); -export const withValidatorFn = createServerFn({ - method: 'GET' -}).inputValidator(z.number()).handler((opts, signal) => { - "use server"; - - return withValidatorFn.__executeServer(opts, signal); -}, async ({ - input -}) => { - return null; -}); \ No newline at end of file diff --git a/packages/start-plugin-core/tests/createServerFn/snapshots/server/createServerFnDestructuredRename.tsx b/packages/start-plugin-core/tests/createServerFn/snapshots/server/createServerFnDestructuredRename.tsx deleted file mode 100644 index 358c28448d0..00000000000 --- a/packages/start-plugin-core/tests/createServerFn/snapshots/server/createServerFnDestructuredRename.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { createServerFn as serverFn } from '@tanstack/react-start'; -import { z } from 'zod'; -export const withUseServer = serverFn({ - method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withUseServer.__executeServer(opts, signal); -}, async function () { - console.info('Fetching posts...'); - await new Promise(r => setTimeout(r, 500)); - return axios.get>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10)); -}); -export const withoutUseServer = serverFn({ - method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withoutUseServer.__executeServer(opts, signal); -}, async () => { - console.info('Fetching posts...'); - await new Promise(r => setTimeout(r, 500)); - return axios.get>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10)); -}); -export const withVariable = serverFn({ - method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withVariable.__executeServer(opts, signal); -}, abstractedFunction); -async function abstractedFunction() { - console.info('Fetching posts...'); - await new Promise(r => setTimeout(r, 500)); - return axios.get>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10)); -} -function zodValidator(schema: TSchema, fn: (input: z.output) => TResult) { - return async (input: unknown) => { - return fn(schema.parse(input)); - }; -} -export const withZodValidator = serverFn({ - method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withZodValidator.__executeServer(opts, signal); -}, zodValidator(z.number(), input => { - return { - 'you gave': input - }; -})); \ No newline at end of file diff --git a/packages/start-plugin-core/tests/createServerFn/snapshots/server/createServerFnStarImport.tsx b/packages/start-plugin-core/tests/createServerFn/snapshots/server/createServerFnStarImport.tsx deleted file mode 100644 index 44f67330725..00000000000 --- a/packages/start-plugin-core/tests/createServerFn/snapshots/server/createServerFnStarImport.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import * as TanStackStart from '@tanstack/react-start'; -import { z } from 'zod'; -export const withUseServer = TanStackStart.createServerFn({ - method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withUseServer.__executeServer(opts, signal); -}, async function () { - 'use server'; - - console.info('Fetching posts...'); - await new Promise(r => setTimeout(r, 500)); - return axios.get>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10)); -}); -export const withoutUseServer = TanStackStart.createServerFn({ - method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withoutUseServer.__executeServer(opts, signal); -}, async () => { - console.info('Fetching posts...'); - await new Promise(r => setTimeout(r, 500)); - return axios.get>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10)); -}); -export const withVariable = TanStackStart.createServerFn({ - method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withVariable.__executeServer(opts, signal); -}, abstractedFunction); -async function abstractedFunction() { - console.info('Fetching posts...'); - await new Promise(r => setTimeout(r, 500)); - return axios.get>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10)); -} -function zodValidator(schema: TSchema, fn: (input: z.output) => TResult) { - return async (input: unknown) => { - return fn(schema.parse(input)); - }; -} -export const withZodValidator = TanStackStart.createServerFn({ - method: 'GET' -}).handler((opts, signal) => { - "use server"; - - return withZodValidator.__executeServer(opts, signal); -}, zodValidator(z.number(), input => { - return { - 'you gave': input - }; -})); \ No newline at end of file diff --git a/packages/start-plugin-core/tests/createServerFn/snapshots/server/createServerFnValidator.tsx b/packages/start-plugin-core/tests/createServerFn/snapshots/server/createServerFnValidator.tsx deleted file mode 100644 index 1ca81b02603..00000000000 --- a/packages/start-plugin-core/tests/createServerFn/snapshots/server/createServerFnValidator.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createServerFn } from '@tanstack/react-start'; -import { z } from 'zod'; -export const withUseServer = createServerFn({ - method: 'GET' -}).inputValidator(z.number()).handler((opts, signal) => { - "use server"; - - return withUseServer.__executeServer(opts, signal); -}, ({ - input -}) => input + 1); \ No newline at end of file diff --git a/packages/start-plugin-core/tests/envOnly/envOnly.test.ts b/packages/start-plugin-core/tests/envOnly/envOnly.test.ts index f3818234ab4..67650b83e8a 100644 --- a/packages/start-plugin-core/tests/envOnly/envOnly.test.ts +++ b/packages/start-plugin-core/tests/envOnly/envOnly.test.ts @@ -2,15 +2,27 @@ import { readFile, readdir } from 'node:fs/promises' import path from 'node:path' import { describe, expect, test } from 'vitest' -import { ServerFnCompiler } from '../../src/create-server-fn-plugin/compiler' +import { StartCompiler } from '../../src/start-compiler-plugin/compiler' + +// Default test options for StartCompiler +function getDefaultTestOptions(env: 'client' | 'server') { + const envName = env === 'client' ? 'client' : 'ssr' + return { + envName, + root: '/test', + framework: 'react' as const, + providerEnvName: 'ssr', + } +} async function compile(opts: { env: 'client' | 'server' code: string id: string }) { - const compiler = new ServerFnCompiler({ + const compiler = new StartCompiler({ ...opts, + ...getDefaultTestOptions(opts.env), loadModule: async () => { // do nothing in test }, @@ -30,12 +42,10 @@ async function compile(opts: { resolveId: async (id) => { return id }, - directive: 'use server', }) const result = await compiler.compile({ code: opts.code, id: opts.id, - isProviderFile: false, }) return result } diff --git a/packages/start-server-core/package.json b/packages/start-server-core/package.json index c5b66498bf5..bca2b9847d2 100644 --- a/packages/start-server-core/package.json +++ b/packages/start-server-core/package.json @@ -62,8 +62,8 @@ "./package.json": "./package.json" }, "imports": { - "#tanstack-start-server-fn-manifest": { - "default": "./dist/esm/fake-start-server-fn-manifest.js" + "#tanstack-start-server-fn-resolver": { + "default": "./dist/esm/fake-start-server-fn-resolver.js" } }, "sideEffects": false, diff --git a/packages/start-server-core/src/createSsrRpc.ts b/packages/start-server-core/src/createSsrRpc.ts index 6a61cd3f0bd..59cf1b4bf2c 100644 --- a/packages/start-server-core/src/createSsrRpc.ts +++ b/packages/start-server-core/src/createSsrRpc.ts @@ -1,6 +1,6 @@ import { TSS_SERVER_FUNCTION } from '@tanstack/start-client-core' import { getServerFnById } from './getServerFnById' -import type { ServerFn } from '#tanstack-start-server-fn-manifest' +import type { ServerFn } from '#tanstack-start-server-fn-resolver' export type SsrRpcImporter = () => Promise diff --git a/packages/start-server-core/src/fake-start-server-fn-manifest.ts b/packages/start-server-core/src/fake-start-server-fn-resolver.ts similarity index 100% rename from packages/start-server-core/src/fake-start-server-fn-manifest.ts rename to packages/start-server-core/src/fake-start-server-fn-resolver.ts diff --git a/packages/start-server-core/src/getServerFnById.ts b/packages/start-server-core/src/getServerFnById.ts index 01a24846a3f..d3b57c88bb9 100644 --- a/packages/start-server-core/src/getServerFnById.ts +++ b/packages/start-server-core/src/getServerFnById.ts @@ -1 +1 @@ -export { getServerFnById } from '#tanstack-start-server-fn-manifest' +export { getServerFnById } from '#tanstack-start-server-fn-resolver' diff --git a/packages/start-server-core/src/tanstack-start.d.ts b/packages/start-server-core/src/tanstack-start.d.ts index 78c5c6538d7..106375b4da9 100644 --- a/packages/start-server-core/src/tanstack-start.d.ts +++ b/packages/start-server-core/src/tanstack-start.d.ts @@ -10,8 +10,8 @@ declare module 'tanstack-start-route-tree:v' { export const routeTree: AnyRoute | undefined } -declare module '#tanstack-start-server-fn-manifest' { - type ServerFn = (...args: Array) => Promise +declare module '#tanstack-start-server-fn-resolver' { + export type ServerFn = (...args: Array) => Promise export function getServerFnById( id: string, opts?: { fromClient?: boolean }, diff --git a/packages/start-server-core/src/virtual-modules.ts b/packages/start-server-core/src/virtual-modules.ts index 3f0b6b788eb..7280feacf26 100644 --- a/packages/start-server-core/src/virtual-modules.ts +++ b/packages/start-server-core/src/virtual-modules.ts @@ -1,5 +1,5 @@ export const VIRTUAL_MODULES = { startManifest: 'tanstack-start-manifest:v', injectedHeadScripts: 'tanstack-start-injected-head-scripts:v', - serverFnManifest: '#tanstack-start-server-fn-manifest', + serverFnResolver: '#tanstack-start-server-fn-resolver', } as const diff --git a/packages/start-server-core/vite.config.ts b/packages/start-server-core/vite.config.ts index f0ca2cc699f..b2c5589b575 100644 --- a/packages/start-server-core/vite.config.ts +++ b/packages/start-server-core/vite.config.ts @@ -22,13 +22,13 @@ export default mergeConfig( './src/index.tsx', './src/createServerRpc.ts', './src/createSsrRpc.ts', - './src/fake-start-server-fn-manifest.ts', + './src/fake-start-server-fn-resolver.ts', ], externalDeps: [ ...Object.values(VIRTUAL_MODULES), '#tanstack-start-entry', '#tanstack-router-entry', - '#tanstack-start-server-fn-manifest', + '#tanstack-start-server-fn-resolver', ], cjs: false, }), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc535e08424..84c7ce5445e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,8 +52,6 @@ overrides: '@tanstack/vue-router': workspace:* '@tanstack/vue-router-devtools': workspace:* '@tanstack/eslint-plugin-router': workspace:* - '@tanstack/server-functions-plugin': workspace:* - '@tanstack/directive-functions-plugin': workspace:* '@tanstack/router-utils': workspace:* '@tanstack/start-static-server-functions': workspace:* '@tanstack/nitro-v2-vite-plugin': workspace:* @@ -11112,49 +11110,6 @@ importers: specifier: ^19.2.0 version: 19.2.0(react@19.2.0) - packages/directive-functions-plugin: - dependencies: - '@babel/code-frame': - specifier: 7.27.1 - version: 7.27.1 - '@babel/core': - specifier: ^7.27.7 - version: 7.27.7 - '@babel/traverse': - specifier: ^7.27.7 - version: 7.27.7 - '@babel/types': - specifier: ^7.27.7 - version: 7.27.7 - '@tanstack/router-utils': - specifier: workspace:* - version: link:../router-utils - babel-dead-code-elimination: - specifier: ^1.0.10 - version: 1.0.10 - pathe: - specifier: ^2.0.3 - version: 2.0.3 - tiny-invariant: - specifier: ^1.3.3 - version: 1.3.3 - devDependencies: - '@types/babel__code-frame': - specifier: ^7.0.6 - version: 7.0.6 - '@types/babel__core': - specifier: ^7.20.5 - version: 7.20.5 - '@types/babel__traverse': - specifier: ^7.20.7 - version: 7.20.7 - dedent: - specifier: ^1.6.0 - version: 1.7.0(babel-plugin-macros@3.1.0) - vite: - specifier: ^7.1.7 - version: 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) - packages/eslint-plugin-router: dependencies: '@typescript-eslint/utils': @@ -11638,52 +11593,6 @@ importers: specifier: workspace:* version: link:../router-plugin - packages/server-functions-plugin: - dependencies: - '@babel/code-frame': - specifier: 7.27.1 - version: 7.27.1 - '@babel/core': - specifier: ^7.27.7 - version: 7.27.7 - '@babel/plugin-syntax-jsx': - specifier: ^7.27.1 - version: 7.27.1(@babel/core@7.27.7) - '@babel/plugin-syntax-typescript': - specifier: ^7.27.1 - version: 7.27.1(@babel/core@7.27.7) - '@babel/template': - specifier: ^7.27.2 - version: 7.27.2 - '@babel/traverse': - specifier: ^7.27.7 - version: 7.27.7 - '@babel/types': - specifier: ^7.27.7 - version: 7.27.7 - '@tanstack/directive-functions-plugin': - specifier: workspace:* - version: link:../directive-functions-plugin - babel-dead-code-elimination: - specifier: ^1.0.9 - version: 1.0.10 - tiny-invariant: - specifier: ^1.3.3 - version: 1.3.3 - devDependencies: - '@types/babel__code-frame': - specifier: ^7.0.6 - version: 7.0.6 - '@types/babel__core': - specifier: ^7.20.5 - version: 7.20.5 - '@types/babel__template': - specifier: ^7.4.4 - version: 7.4.4 - '@types/babel__traverse': - specifier: ^7.20.7 - version: 7.20.7 - packages/solid-router: dependencies: '@solid-devtools/logger': @@ -11928,9 +11837,6 @@ importers: '@tanstack/router-utils': specifier: workspace:* version: link:../router-utils - '@tanstack/server-functions-plugin': - specifier: workspace:* - version: link:../server-functions-plugin '@tanstack/start-client-core': specifier: workspace:* version: link:../start-client-core diff --git a/scripts/publish.js b/scripts/publish.js index c855c56b3b6..1196db777d9 100644 --- a/scripts/publish.js +++ b/scripts/publish.js @@ -96,14 +96,6 @@ await publish({ name: '@tanstack/router-vite-plugin', packageDir: 'packages/router-vite-plugin', }, - { - name: '@tanstack/directive-functions-plugin', - packageDir: 'packages/directive-functions-plugin', - }, - { - name: '@tanstack/server-functions-plugin', - packageDir: 'packages/server-functions-plugin', - }, { name: '@tanstack/eslint-plugin-router', packageDir: 'packages/eslint-plugin-router',