From 1663de672eef4bbcf7c36cbb50b519b4a9823887 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Wed, 11 Dec 2024 13:12:26 -0700 Subject: [PATCH 01/38] checkpoint --- .../server-functions-vite-plugin/README.md | 5 + .../eslint.config.js | 5 + .../server-functions-vite-plugin/package.json | 84 ++ .../server-functions-vite-plugin/src/ast.ts | 44 ++ .../src/compilers.ts | 715 ++++++++++++++++++ .../server-functions-vite-plugin/src/index.ts | 55 ++ .../createIsomorphicFn.test.ts | 90 +++ .../client/createIsomorphicFnDestructured.tsx | 16 + .../createIsomorphicFnDestructuredRename.tsx | 16 + .../client/createIsomorphicFnStarImport.tsx | 16 + .../server/createIsomorphicFnDestructured.tsx | 16 + .../createIsomorphicFnDestructuredRename.tsx | 16 + .../server/createIsomorphicFnStarImport.tsx | 16 + .../createIsomorphicFnDestructured.tsx | 35 + .../createIsomorphicFnDestructuredRename.tsx | 35 + .../createIsomorphicFnStarImport.tsx | 37 + .../createMiddleware/createMiddleware.test.ts | 36 + .../client/createMiddlewareDestructured.tsx | 24 + .../createMiddlewareDestructuredRename.tsx | 24 + .../client/createMiddlewareStarImport.tsx | 24 + .../client/createMiddlewareValidator.tsx | 5 + .../server/createMiddlewareDestructured.tsx | 36 + .../createMiddlewareDestructuredRename.tsx | 36 + .../server/createMiddlewareStarImport.tsx | 38 + .../server/createMiddlewareValidator.tsx | 7 + .../createMiddlewareDestructured.tsx | 51 ++ .../createMiddlewareDestructuredRename.tsx | 51 ++ .../test-files/createMiddlewareStarImport.tsx | 52 ++ .../test-files/createMiddlewareValidator.tsx | 8 + .../createServerFn/createServerFn.test.ts | 62 ++ .../client/createServerFnDestructured.tsx | 61 ++ .../createServerFnDestructuredRename.tsx | 40 + .../client/createServerFnStarImport.tsx | 40 + .../client/createServerFnValidator.tsx | 9 + .../server/createServerFnDestructured.tsx | 77 ++ .../createServerFnDestructuredRename.tsx | 52 ++ .../server/createServerFnStarImport.tsx | 52 ++ .../server/createServerFnValidator.tsx | 11 + .../test-files/createServerFnDestructured.tsx | 67 ++ .../createServerFnDestructuredRename.tsx | 51 ++ .../test-files/createServerFnStarImport.tsx | 52 ++ .../test-files/createServerFnValidator.tsx | 8 + .../tests/envOnly/envOnly.test.ts | 58 ++ .../snapshots/client/envOnlyDestructured.tsx | 5 + .../client/envOnlyDestructuredRename.tsx | 5 + .../snapshots/client/envOnlyStarImport.tsx | 5 + .../snapshots/server/envOnlyDestructured.tsx | 5 + .../server/envOnlyDestructuredRename.tsx | 5 + .../snapshots/server/envOnlyStarImport.tsx | 5 + .../test-files/envOnlyDestructured.tsx | 5 + .../test-files/envOnlyDestructuredRename.tsx | 5 + .../envOnly/test-files/envOnlyStarImport.tsx | 5 + .../tsconfig.json | 8 + .../vite.config.ts | 20 + packages/start/package.json | 11 +- packages/start/src/client-runtime/index.tsx | 22 +- packages/start/src/config/index.ts | 31 +- .../start/src/react-server-runtime/index.tsx | 14 - packages/start/src/server-runtime/index.tsx | 172 +---- packages/start/src/ssr-runtime/index.tsx | 161 ++++ packages/start/tsconfig.json | 2 +- packages/start/tsconfigs/config.tsconfig.json | 1 + .../tsconfigs/router-manifest.tsconfig.json | 1 + .../server-functions-plugin.tsconfig.json | 10 + packages/start/vite.config.ts | 4 +- 65 files changed, 2531 insertions(+), 204 deletions(-) create mode 100644 packages/server-functions-vite-plugin/README.md create mode 100644 packages/server-functions-vite-plugin/eslint.config.js create mode 100644 packages/server-functions-vite-plugin/package.json create mode 100644 packages/server-functions-vite-plugin/src/ast.ts create mode 100644 packages/server-functions-vite-plugin/src/compilers.ts create mode 100644 packages/server-functions-vite-plugin/src/index.ts create mode 100644 packages/server-functions-vite-plugin/tests/createIsomorphicFn/createIsomorphicFn.test.ts create mode 100644 packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructured.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructuredRename.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnStarImport.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructured.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructuredRename.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnStarImport.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnDestructured.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnDestructuredRename.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnStarImport.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createMiddleware/createMiddleware.test.ts create mode 100644 packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareDestructured.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareDestructuredRename.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareStarImport.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareValidator.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareDestructured.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareDestructuredRename.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareStarImport.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareValidator.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareDestructured.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareDestructuredRename.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareStarImport.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareValidator.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createServerFn/createServerFn.test.ts create mode 100644 packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnDestructured.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnDestructuredRename.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnStarImport.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnValidator.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnDestructured.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnDestructuredRename.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnStarImport.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnValidator.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnDestructured.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnDestructuredRename.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnStarImport.tsx create mode 100644 packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnValidator.tsx create mode 100644 packages/server-functions-vite-plugin/tests/envOnly/envOnly.test.ts create mode 100644 packages/server-functions-vite-plugin/tests/envOnly/snapshots/client/envOnlyDestructured.tsx create mode 100644 packages/server-functions-vite-plugin/tests/envOnly/snapshots/client/envOnlyDestructuredRename.tsx create mode 100644 packages/server-functions-vite-plugin/tests/envOnly/snapshots/client/envOnlyStarImport.tsx create mode 100644 packages/server-functions-vite-plugin/tests/envOnly/snapshots/server/envOnlyDestructured.tsx create mode 100644 packages/server-functions-vite-plugin/tests/envOnly/snapshots/server/envOnlyDestructuredRename.tsx create mode 100644 packages/server-functions-vite-plugin/tests/envOnly/snapshots/server/envOnlyStarImport.tsx create mode 100644 packages/server-functions-vite-plugin/tests/envOnly/test-files/envOnlyDestructured.tsx create mode 100644 packages/server-functions-vite-plugin/tests/envOnly/test-files/envOnlyDestructuredRename.tsx create mode 100644 packages/server-functions-vite-plugin/tests/envOnly/test-files/envOnlyStarImport.tsx create mode 100644 packages/server-functions-vite-plugin/tsconfig.json create mode 100644 packages/server-functions-vite-plugin/vite.config.ts delete mode 100644 packages/start/src/react-server-runtime/index.tsx create mode 100644 packages/start/src/ssr-runtime/index.tsx create mode 100644 packages/start/tsconfigs/server-functions-plugin.tsconfig.json diff --git a/packages/server-functions-vite-plugin/README.md b/packages/server-functions-vite-plugin/README.md new file mode 100644 index 0000000000..798fa094a3 --- /dev/null +++ b/packages/server-functions-vite-plugin/README.md @@ -0,0 +1,5 @@ + + +# TanStack Start Vite Plugin + +See https://tanstack.com/router/latest/docs/framework/react/guide/file-based-routing diff --git a/packages/server-functions-vite-plugin/eslint.config.js b/packages/server-functions-vite-plugin/eslint.config.js new file mode 100644 index 0000000000..8ce6ad05fc --- /dev/null +++ b/packages/server-functions-vite-plugin/eslint.config.js @@ -0,0 +1,5 @@ +// @ts-check + +import rootConfig from '../../eslint.config.js' + +export default [...rootConfig] diff --git a/packages/server-functions-vite-plugin/package.json b/packages/server-functions-vite-plugin/package.json new file mode 100644 index 0000000000..41d0915a16 --- /dev/null +++ b/packages/server-functions-vite-plugin/package.json @@ -0,0 +1,84 @@ +{ + "name": "@tanstack/start-vite-plugin", + "version": "1.87.3", + "description": "Modern and scalable routing for React applications", + "author": "Tanner Linsley", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/TanStack/router.git", + "directory": "packages/start-vite-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:eslint": "eslint ./src", + "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"", + "test:types:ts52": "node ../../node_modules/typescript52/lib/tsc.js", + "test:types:ts53": "node ../../node_modules/typescript53/lib/tsc.js", + "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": "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/core": "^7.26.0", + "@babel/code-frame": "7.26.2", + "@babel/generator": "^7.26.3", + "@babel/parser": "^7.26.3", + "@babel/plugin-syntax-jsx": "^7.25.9", + "@babel/plugin-syntax-typescript": "^7.25.9", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.26.4", + "@babel/types": "^7.26.3", + "@types/babel__core": "^7.20.5", + "@types/babel__generator": "^7.6.8", + "@types/babel__code-frame": "^7.0.6", + "@types/babel__template": "^7.4.4", + "@types/babel__traverse": "^7.20.6", + "tiny-invariant": "^1.3.3", + "babel-dead-code-elimination": "^1.0.6" + } +} diff --git a/packages/server-functions-vite-plugin/src/ast.ts b/packages/server-functions-vite-plugin/src/ast.ts new file mode 100644 index 0000000000..6062993cfd --- /dev/null +++ b/packages/server-functions-vite-plugin/src/ast.ts @@ -0,0 +1,44 @@ +import * as babel from '@babel/core' +import '@babel/parser' +// @ts-expect-error +import _babelPluginJsx from '@babel/plugin-syntax-jsx' +// @ts-expect-error +import _babelPluginTypeScript from '@babel/plugin-syntax-typescript' + +let babelPluginJsx = _babelPluginJsx +let babelPluginTypeScript = _babelPluginTypeScript + +if (babelPluginJsx.default) { + babelPluginJsx = babelPluginJsx.default +} + +if (babelPluginTypeScript.default) { + babelPluginTypeScript = babelPluginTypeScript.default +} + +export type ParseAstOptions = { + code: string + filename: string + root: string + env: 'server' | 'client' +} + +export function parseAst(opts: ParseAstOptions) { + const babelPlugins: Array = [ + babelPluginJsx, + [ + babelPluginTypeScript, + { + isTSX: true, + }, + ], + ] + + return babel.parse(opts.code, { + plugins: babelPlugins, + root: opts.root, + filename: opts.filename, + sourceMaps: true, + sourceType: 'module', + }) +} diff --git a/packages/server-functions-vite-plugin/src/compilers.ts b/packages/server-functions-vite-plugin/src/compilers.ts new file mode 100644 index 0000000000..a5d8c49b11 --- /dev/null +++ b/packages/server-functions-vite-plugin/src/compilers.ts @@ -0,0 +1,715 @@ +import * as babel from '@babel/core' +import * as t from '@babel/types' +import _generate from '@babel/generator' +import { codeFrameColumns } from '@babel/code-frame' +import { deadCodeElimination } from 'babel-dead-code-elimination' + +import { parseAst } from './ast' +import type { ParseAstOptions } from './ast' + +// Babel is a CJS package and uses `default` as named binding (`exports.default =`). +// https://github.com/babel/babel/issues/15269. +let generate = (_generate as any)['default'] as typeof _generate + +// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition +if (!generate) { + generate = _generate +} + +export function compileEliminateDeadCode(opts: ParseAstOptions) { + const ast = parseAst(opts) + if (!ast) { + throw new Error( + `Failed to compile ast for compileEliminateDeadCode() for the file: ${opts.filename}`, + ) + } + deadCodeElimination(ast) + + return generate(ast, { + sourceMaps: true, + }) +} + +const debug = process.env.TSR_VITE_DEBUG === 'true' + +// build these once and reuse them +const handleServerOnlyCallExpression = + buildEnvOnlyCallExpressionHandler('server') +const handleClientOnlyCallExpression = + buildEnvOnlyCallExpressionHandler('client') + +type IdentifierConfig = { + name: string + type: 'ImportSpecifier' | 'ImportNamespaceSpecifier' + namespaceId: string + handleCallExpression: ( + path: babel.NodePath, + opts: ParseAstOptions, + ) => void + paths: Array +} + +export function compileStartOutput(opts: ParseAstOptions) { + const ast = parseAst(opts) + + if (!ast) { + throw new Error( + `Failed to compile ast for compileStartOutput() for the file: ${opts.filename}`, + ) + } + + babel.traverse(ast, { + Program: { + enter(programPath) { + const identifiers: { + createServerFn: IdentifierConfig + createMiddleware: IdentifierConfig + serverOnly: IdentifierConfig + clientOnly: IdentifierConfig + createIsomorphicFn: IdentifierConfig + } = { + createServerFn: { + name: 'createServerFn', + type: 'ImportSpecifier', + namespaceId: '', + handleCallExpression: handleCreateServerFnCallExpression, + paths: [], + }, + createMiddleware: { + name: 'createMiddleware', + type: 'ImportSpecifier', + namespaceId: '', + handleCallExpression: handleCreateMiddlewareCallExpression, + paths: [], + }, + serverOnly: { + name: 'serverOnly', + type: 'ImportSpecifier', + namespaceId: '', + handleCallExpression: handleServerOnlyCallExpression, + paths: [], + }, + clientOnly: { + name: 'clientOnly', + type: 'ImportSpecifier', + namespaceId: '', + handleCallExpression: handleClientOnlyCallExpression, + paths: [], + }, + createIsomorphicFn: { + name: 'createIsomorphicFn', + type: 'ImportSpecifier', + namespaceId: '', + handleCallExpression: handleCreateIsomorphicFnCallExpression, + paths: [], + }, + } + + const identifierKeys = Object.keys(identifiers) as Array< + keyof typeof identifiers + > + + programPath.traverse({ + ImportDeclaration: (path) => { + if (path.node.source.value !== '@tanstack/start') { + return + } + + // handle a destructured imports being renamed like "import { createServerFn as myCreateServerFn } from '@tanstack/start';" + path.node.specifiers.forEach((specifier) => { + identifierKeys.forEach((identifierKey) => { + const identifier = identifiers[identifierKey] + + if ( + specifier.type === 'ImportSpecifier' && + specifier.imported.type === 'Identifier' + ) { + if (specifier.imported.name === identifierKey) { + identifier.name = specifier.local.name + identifier.type = 'ImportSpecifier' + } + } + + // handle namespace imports like "import * as TanStackStart from '@tanstack/start';" + if (specifier.type === 'ImportNamespaceSpecifier') { + identifier.type = 'ImportNamespaceSpecifier' + identifier.namespaceId = specifier.local.name + identifier.name = `${identifier.namespaceId}.${identifierKey}` + } + }) + }) + }, + CallExpression: (path) => { + identifierKeys.forEach((identifierKey) => { + // Check to see if the call expression is a call to the + // identifiers[identifierKey].name + if ( + t.isIdentifier(path.node.callee) && + path.node.callee.name === identifiers[identifierKey].name + ) { + // The identifier could be a call to the original function + // in the source code. If this is case, we need to ignore it. + // Check the scope to see if the identifier is a function declaration. + // if it is, then we can ignore it. + + if ( + path.scope.getBinding(identifiers[identifierKey].name)?.path + .node.type === 'FunctionDeclaration' + ) { + return + } + + return identifiers[identifierKey].paths.push(path) + } + + if (t.isMemberExpression(path.node.callee)) { + if ( + t.isIdentifier(path.node.callee.object) && + t.isIdentifier(path.node.callee.property) + ) { + const callname = [ + path.node.callee.object.name, + path.node.callee.property.name, + ].join('.') + + if (callname === identifiers[identifierKey].name) { + identifiers[identifierKey].paths.push(path) + } + } + } + + return + }) + }, + }) + + identifierKeys.forEach((identifierKey) => { + identifiers[identifierKey].paths.forEach((path) => { + identifiers[identifierKey].handleCallExpression( + path as babel.NodePath, + opts, + ) + }) + }) + }, + }, + }) + + return generate(ast, { + sourceMaps: true, + minified: process.env.NODE_ENV === 'production', + }) +} + +function handleCreateServerFnCallExpression( + path: babel.NodePath, + opts: ParseAstOptions, +) { + // The function is the 'fn' property of the object passed to createServerFn + + // const firstArg = path.node.arguments[0] + // if (t.isObjectExpression(firstArg)) { + // // Was called with some options + // } + + // Traverse the member expression and find the call expressions for + // the validator, handler, and middleware methods. Check to make sure they + // are children of the createServerFn call expression. + + const calledOptions = path.node.arguments[0] + ? (path.get('arguments.0') as babel.NodePath) + : null + + const shouldValidateClient = !!calledOptions?.node.properties.find((prop) => { + return ( + t.isObjectProperty(prop) && + t.isIdentifier(prop.key) && + prop.key.name === 'validateClient' && + t.isBooleanLiteral(prop.value) && + prop.value.value === true + ) + }) + + const callExpressionPaths = { + middleware: null as babel.NodePath | null, + validator: null as babel.NodePath | null, + handler: null as babel.NodePath | null, + } + + const validMethods = Object.keys(callExpressionPaths) + + const rootCallExpression = getRootCallExpression(path) + + if (debug) + console.info( + 'Handling createServerFn call expression:', + rootCallExpression.toString(), + ) + + // Check if the call is assigned to a variable + if (!rootCallExpression.parentPath.isVariableDeclarator()) { + throw new Error('createServerFn must be assigned to a variable!') + } + + // Get the identifier name of the variable + const variableDeclarator = rootCallExpression.parentPath.node + const existingVariableName = (variableDeclarator.id as t.Identifier).name + + rootCallExpression.traverse({ + MemberExpression(memberExpressionPath) { + if (t.isIdentifier(memberExpressionPath.node.property)) { + const name = memberExpressionPath.node.property + .name as keyof typeof callExpressionPaths + + if ( + validMethods.includes(name) && + memberExpressionPath.parentPath.isCallExpression() + ) { + callExpressionPaths[name] = memberExpressionPath.parentPath + } + } + }, + }) + + if (callExpressionPaths.validator) { + const innerInputExpression = callExpressionPaths.validator.node.arguments[0] + + if (!innerInputExpression) { + throw new Error( + 'createServerFn().validator() must be called with a validator!', + ) + } + + // If we're on the client, and we're not validating the client, remove the validator call expression + if ( + opts.env === 'client' && + !shouldValidateClient && + t.isMemberExpression(callExpressionPaths.validator.node.callee) + ) { + callExpressionPaths.validator.replaceWith( + callExpressionPaths.validator.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 = callExpressionPaths.handler?.get( + 'arguments.0', + ) as babel.NodePath + + if (!callExpressionPaths.handler || !handlerFnPath.node) { + throw codeFrameError( + opts.code, + path.node.callee.loc!, + `createServerFn must be called with a "handler" property!`, + ) + } + + 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) + // }) + + removeUseServerDirective(handlerFnPath) + + handlerFnPath.replaceWith( + t.arrowFunctionExpression( + [t.identifier('opts')], + 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.directive(t.directiveLiteral('use server'))], + ), + ), + ) + + if (opts.env === 'server') { + callExpressionPaths.handler.node.arguments.push(handlerFn) + } +} + +function removeUseServerDirective(path: babel.NodePath) { + path.traverse({ + Directive(path) { + if (path.node.value.value === 'use server') { + path.remove() + } + }, + }) +} + +function handleCreateMiddlewareCallExpression( + path: babel.NodePath, + opts: ParseAstOptions, +) { + // const firstArg = path.node.arguments[0] + + // if (!t.isObjectExpression(firstArg)) { + // throw new Error( + // 'createMiddleware must be called with an object of options!', + // ) + // } + + // const idProperty = firstArg.properties.find((prop) => { + // return ( + // t.isObjectProperty(prop) && + // t.isIdentifier(prop.key) && + // prop.key.name === 'id' + // ) + // }) + + // if ( + // !idProperty || + // !t.isObjectProperty(idProperty) || + // !t.isStringLiteral(idProperty.value) + // ) { + // throw new Error( + // 'createMiddleware must be called with an "id" property!', + // ) + // } + + const rootCallExpression = getRootCallExpression(path) + + if (debug) + console.info( + 'Handling createMiddleware call expression:', + rootCallExpression.toString(), + ) + + // Check if the call is assigned to a variable + // if (!rootCallExpression.parentPath.isVariableDeclarator()) { + // TODO: move this logic out to eslint or something like + // the router generator code that can do autofixes on save. + + // // If not assigned to a variable, wrap the call in a variable declaration + // const variableDeclaration = t.variableDeclaration('const', [ + // t.variableDeclarator(t.identifier(middlewareName), path.node), + // ]) + + // // The parent could be an expression statement, if it is, we need to replace + // // it with the variable declaration + // if (path.parentPath.isExpressionStatement()) { + // path.parentPath.replaceWith(variableDeclaration) + // } else { + // // If the parent is not an expression statement, then it is a statement + // // that is not an expression, like a variable declaration or a return statement. + // // In this case, we need to insert the variable declaration before the statement + // path.parentPath.insertBefore(variableDeclaration) + // } + + // // Now we need to export it. Just add an export statement + // // to the program body + // path.findParent((parentPath) => { + // if (parentPath.isProgram()) { + // parentPath.node.body.push( + // t.exportNamedDeclaration(null, [ + // t.exportSpecifier( + // t.identifier(middlewareName), + // t.identifier(middlewareName), + // ), + // ]), + // ) + // } + // return false + // }) + + // throw new Error( + // 'createMiddleware must be assigned to a variable and exported!', + // ) + // } + + // const variableDeclarator = rootCallExpression.parentPath.node + // const existingVariableName = (variableDeclarator.id as t.Identifier).name + + // const program = rootCallExpression.findParent((parentPath) => { + // return parentPath.isProgram() + // }) as babel.NodePath + + // let isExported = false as boolean + + // program.traverse({ + // ExportNamedDeclaration: (path) => { + // if ( + // path.isExportNamedDeclaration() && + // path.node.declaration && + // t.isVariableDeclaration(path.node.declaration) && + // path.node.declaration.declarations.some((decl) => { + // return ( + // t.isVariableDeclarator(decl) && + // t.isIdentifier(decl.id) && + // decl.id.name === existingVariableName + // ) + // }) + // ) { + // isExported = true + // } + // }, + // }) + + // If not exported, export it + // if (!isExported) { + // TODO: move this logic out to eslint or something like + // the router generator code that can do autofixes on save. + + // path.parentPath.parentPath.insertAfter( + // t.exportNamedDeclaration(null, [ + // t.exportSpecifier( + // t.identifier(existingVariableName), + // t.identifier(existingVariableName), + // ), + // ]), + // ) + + // throw new Error( + // 'createMiddleware must be exported as a named export!', + // ) + // } + + // The function is the 'fn' property of the object passed to createMiddleware + + // const firstArg = path.node.arguments[0] + // if (t.isObjectExpression(firstArg)) { + // // Was called with some options + // } + + // Traverse the member expression and find the call expressions for + // the validator, handler, and middleware methods. Check to make sure they + // are children of the createMiddleware call expression. + + const callExpressionPaths = { + middleware: null as babel.NodePath | null, + validator: null as babel.NodePath | null, + client: null as babel.NodePath | null, + server: null as babel.NodePath | null, + } + + const validMethods = Object.keys(callExpressionPaths) + + rootCallExpression.traverse({ + MemberExpression(memberExpressionPath) { + if (t.isIdentifier(memberExpressionPath.node.property)) { + const name = memberExpressionPath.node.property + .name as keyof typeof callExpressionPaths + + if ( + validMethods.includes(name) && + memberExpressionPath.parentPath.isCallExpression() + ) { + callExpressionPaths[name] = memberExpressionPath.parentPath + } + } + }, + }) + + if (callExpressionPaths.validator) { + const innerInputExpression = callExpressionPaths.validator.node.arguments[0] + + if (!innerInputExpression) { + throw new Error( + 'createMiddleware().validator() must be called with a validator!', + ) + } + + // If we're on the client, remove the validator call expression + if (opts.env === 'client') { + if (t.isMemberExpression(callExpressionPaths.validator.node.callee)) { + callExpressionPaths.validator.replaceWith( + callExpressionPaths.validator.node.callee.object, + ) + } + } + } + + const useFnPath = callExpressionPaths.server?.get( + 'arguments.0', + ) as babel.NodePath + + if (!callExpressionPaths.server || !useFnPath.node) { + throw new Error('createMiddleware must be called with a "use" property!') + } + + // If we're on the client, remove the use call expression + + if (opts.env === 'client') { + if (t.isMemberExpression(callExpressionPaths.server.node.callee)) { + callExpressionPaths.server.replaceWith( + callExpressionPaths.server.node.callee.object, + ) + } + } +} + +function buildEnvOnlyCallExpressionHandler(env: 'client' | 'server') { + return function envOnlyCallExpressionHandler( + path: babel.NodePath, + opts: ParseAstOptions, + ) { + if (debug) + console.info(`Handling ${env}Only call expression:`, path.toString()) + + if (opts.env === env) { + // extract the inner function from the call expression + const innerInputExpression = path.node.arguments[0] + + if (!t.isExpression(innerInputExpression)) { + throw new Error( + `${env}Only() functions must be called with a function!`, + ) + } + + path.replaceWith(innerInputExpression) + return + } + + // If we're on the wrong environment, replace the call expression + // with a function that always throws an error. + path.replaceWith( + t.arrowFunctionExpression( + [], + t.blockStatement([ + t.throwStatement( + t.newExpression(t.identifier('Error'), [ + t.stringLiteral( + `${env}Only() functions can only be called on the ${env}!`, + ), + ]), + ), + ]), + ), + ) + } +} + +function handleCreateIsomorphicFnCallExpression( + path: babel.NodePath, + opts: ParseAstOptions, +) { + const rootCallExpression = getRootCallExpression(path) + + if (debug) + console.info( + 'Handling createIsomorphicFn call expression:', + rootCallExpression.toString(), + ) + + const callExpressionPaths = { + client: null as babel.NodePath | null, + server: null as babel.NodePath | null, + } + + const validMethods = Object.keys(callExpressionPaths) + + rootCallExpression.traverse({ + MemberExpression(memberExpressionPath) { + if (t.isIdentifier(memberExpressionPath.node.property)) { + const name = memberExpressionPath.node.property + .name as keyof typeof callExpressionPaths + + if ( + validMethods.includes(name) && + memberExpressionPath.parentPath.isCallExpression() + ) { + callExpressionPaths[name] = memberExpressionPath.parentPath + } + } + }, + }) + + if ( + validMethods.every( + (method) => + !callExpressionPaths[method as keyof typeof callExpressionPaths], + ) + ) { + const variableId = rootCallExpression.parentPath.isVariableDeclarator() + ? rootCallExpression.parentPath.node.id + : null + console.warn( + 'createIsomorphicFn called without a client or server implementation!', + 'This will result in a no-op function.', + 'Variable name:', + t.isIdentifier(variableId) ? variableId.name : 'unknown', + ) + } + + const envCallExpression = callExpressionPaths[opts.env] + + if (!envCallExpression) { + // if we don't have an implementation for this environment, default to a no-op + rootCallExpression.replaceWith( + t.arrowFunctionExpression([], t.blockStatement([])), + ) + return + } + + const innerInputExpression = envCallExpression.node.arguments[0] + + if (!t.isExpression(innerInputExpression)) { + throw new Error( + `createIsomorphicFn().${opts.env}(func) must be called with a function!`, + ) + } + + rootCallExpression.replaceWith(innerInputExpression) +} + +function getRootCallExpression(path: babel.NodePath) { + // Find the highest callExpression parent + let rootCallExpression: babel.NodePath = path + + // Traverse up the chain of CallExpressions + while (rootCallExpression.parentPath.isMemberExpression()) { + const parent = rootCallExpression.parentPath + if (parent.parentPath.isCallExpression()) { + rootCallExpression = parent.parentPath + } + } + + return rootCallExpression +} + +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/server-functions-vite-plugin/src/index.ts b/packages/server-functions-vite-plugin/src/index.ts new file mode 100644 index 0000000000..6067851fd3 --- /dev/null +++ b/packages/server-functions-vite-plugin/src/index.ts @@ -0,0 +1,55 @@ +import { fileURLToPath, pathToFileURL } from 'node:url' + +import type { Plugin } from 'vite' + +const debug = Boolean(process.env.TSR_VITE_DEBUG) + +export type ServerFunctionsViteOptions = {} + +const useServerRx = /"use server"|'use server'/ + +export function TanStackStartViteServerFn(): Array { + // opts: ServerFunctionsViteOptions, + let ROOT: string = process.cwd() + + return [ + { + name: 'tanstack-start-server-fn-client-vite-plugin', + enforce: 'pre', + configResolved: (config) => { + ROOT = config.root + }, + transform(code, id) { + const url = pathToFileURL(id) + url.searchParams.delete('v') + id = fileURLToPath(url).replace(/\\/g, '/') + + if (!useServerRx.test(code)) { + return null + } + + const { compiledCode, serverFns } = compileServerFnServer({ + code, + root: ROOT, + filename: id, + }) + + if (debug) console.info('') + if (debug) console.info('Compiled createServerFn Output') + if (debug) console.info('') + if (debug) console.info(compiledCode) + if (debug) console.info('') + if (debug) console.info('') + if (debug) console.info('') + + return compiledCode + }, + }, + { + name: 'tanstack-start-server-fn-server-vite-plugin', + transform(code, id) { + return null + }, + }, + ] +} diff --git a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/createIsomorphicFn.test.ts b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/createIsomorphicFn.test.ts new file mode 100644 index 0000000000..28f29510a7 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/createIsomorphicFn.test.ts @@ -0,0 +1,90 @@ +import { readFile, readdir } from 'node:fs/promises' +import path from 'node:path' +import { afterAll, describe, expect, test, vi } from 'vitest' + +import { compileStartOutput } from '../../src/compilers' + +async function getFilenames() { + return await readdir(path.resolve(import.meta.dirname, './test-files')) +} + +describe('createIsomorphicFn compiles correctly', async () => { + const noImplWarning = + 'createIsomorphicFn called without a client or server implementation!' + + const originalConsoleWarn = console.warn + const consoleSpy = vi.spyOn(console, 'warn').mockImplementation((...args) => { + // we want to avoid sending this warning to the console, we know about it + if (args[0] === noImplWarning) { + return + } + originalConsoleWarn(...args) + }) + + afterAll(() => { + consoleSpy.mockRestore() + }) + + const filenames = await getFilenames() + + describe.each(filenames)('should handle "%s"', async (filename) => { + const file = await readFile( + path.resolve(import.meta.dirname, `./test-files/${filename}`), + ) + const code = file.toString() + + test.each(['client', 'server'] as const)( + `should compile for ${filename} %s`, + async (env) => { + const compiledResult = compileStartOutput({ + env, + code, + root: './test-files', + filename, + }) + + await expect(compiledResult.code).toMatchFileSnapshot( + `./snapshots/${env}/${filename}`, + ) + }, + ) + }) + test('should error if implementation not provided', () => { + expect(() => { + compileStartOutput({ + env: 'client', + code: ` + import { createIsomorphicFn } from '@tanstack/start' + const clientOnly = createIsomorphicFn().client()`, + root: './test-files', + filename: 'no-fn.ts', + }) + }).toThrowError() + expect(() => { + compileStartOutput({ + env: 'server', + code: ` + import { createIsomorphicFn } from '@tanstack/start' + const serverOnly = createIsomorphicFn().server()`, + root: './test-files', + filename: 'no-fn.ts', + }) + }).toThrowError() + }) + test('should warn to console if no implementations provided', () => { + compileStartOutput({ + env: 'client', + code: ` + import { createIsomorphicFn } from '@tanstack/start' + const noImpl = createIsomorphicFn()`, + root: './test-files', + filename: 'no-fn.ts', + }) + expect(consoleSpy).toHaveBeenCalledWith( + noImplWarning, + 'This will result in a no-op function.', + 'Variable name:', + 'noImpl', + ) + }) +}) diff --git a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructured.tsx b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructured.tsx new file mode 100644 index 0000000000..6fcb30691c --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructured.tsx @@ -0,0 +1,16 @@ +import { createIsomorphicFn } from '@tanstack/start'; +const noImpl = () => {}; +const serverOnlyFn = () => {}; +const clientOnlyFn = () => 'client'; +const serverThenClientFn = () => 'client'; +const clientThenServerFn = () => 'client'; +function abstractedServerFn() { + return 'server'; +} +const serverOnlyFnAbstracted = () => {}; +function abstractedClientFn() { + return 'client'; +} +const clientOnlyFnAbstracted = abstractedClientFn; +const serverThenClientFnAbstracted = abstractedClientFn; +const clientThenServerFnAbstracted = abstractedClientFn; \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructuredRename.tsx b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructuredRename.tsx new file mode 100644 index 0000000000..d273a49f40 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructuredRename.tsx @@ -0,0 +1,16 @@ +import { createIsomorphicFn as isomorphicFn } from '@tanstack/start'; +const noImpl = () => {}; +const serverOnlyFn = () => {}; +const clientOnlyFn = () => 'client'; +const serverThenClientFn = () => 'client'; +const clientThenServerFn = () => 'client'; +function abstractedServerFn() { + return 'server'; +} +const serverOnlyFnAbstracted = () => {}; +function abstractedClientFn() { + return 'client'; +} +const clientOnlyFnAbstracted = abstractedClientFn; +const serverThenClientFnAbstracted = abstractedClientFn; +const clientThenServerFnAbstracted = abstractedClientFn; \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnStarImport.tsx b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnStarImport.tsx new file mode 100644 index 0000000000..ffb2ffe79c --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnStarImport.tsx @@ -0,0 +1,16 @@ +import * as TanStackStart from '@tanstack/start'; +const noImpl = () => {}; +const serverOnlyFn = () => {}; +const clientOnlyFn = () => 'client'; +const serverThenClientFn = () => 'client'; +const clientThenServerFn = () => 'client'; +function abstractedServerFn() { + return 'server'; +} +const serverOnlyFnAbstracted = () => {}; +function abstractedClientFn() { + return 'client'; +} +const clientOnlyFnAbstracted = abstractedClientFn; +const serverThenClientFnAbstracted = abstractedClientFn; +const clientThenServerFnAbstracted = abstractedClientFn; \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructured.tsx b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructured.tsx new file mode 100644 index 0000000000..0e0134e347 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructured.tsx @@ -0,0 +1,16 @@ +import { createIsomorphicFn } from '@tanstack/start'; +const noImpl = () => {}; +const serverOnlyFn = () => 'server'; +const clientOnlyFn = () => {}; +const serverThenClientFn = () => 'server'; +const clientThenServerFn = () => 'server'; +function abstractedServerFn() { + return 'server'; +} +const serverOnlyFnAbstracted = abstractedServerFn; +function abstractedClientFn() { + return 'client'; +} +const clientOnlyFnAbstracted = () => {}; +const serverThenClientFnAbstracted = abstractedServerFn; +const clientThenServerFnAbstracted = abstractedServerFn; \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructuredRename.tsx b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructuredRename.tsx new file mode 100644 index 0000000000..3ba9d0598f --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructuredRename.tsx @@ -0,0 +1,16 @@ +import { createIsomorphicFn as isomorphicFn } from '@tanstack/start'; +const noImpl = () => {}; +const serverOnlyFn = () => 'server'; +const clientOnlyFn = () => {}; +const serverThenClientFn = () => 'server'; +const clientThenServerFn = () => 'server'; +function abstractedServerFn() { + return 'server'; +} +const serverOnlyFnAbstracted = abstractedServerFn; +function abstractedClientFn() { + return 'client'; +} +const clientOnlyFnAbstracted = () => {}; +const serverThenClientFnAbstracted = abstractedServerFn; +const clientThenServerFnAbstracted = abstractedServerFn; \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnStarImport.tsx b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnStarImport.tsx new file mode 100644 index 0000000000..c77c6ac839 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnStarImport.tsx @@ -0,0 +1,16 @@ +import * as TanStackStart from '@tanstack/start'; +const noImpl = () => {}; +const serverOnlyFn = () => 'server'; +const clientOnlyFn = () => {}; +const serverThenClientFn = () => 'server'; +const clientThenServerFn = () => 'server'; +function abstractedServerFn() { + return 'server'; +} +const serverOnlyFnAbstracted = abstractedServerFn; +function abstractedClientFn() { + return 'client'; +} +const clientOnlyFnAbstracted = () => {}; +const serverThenClientFnAbstracted = abstractedServerFn; +const clientThenServerFnAbstracted = abstractedServerFn; \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnDestructured.tsx b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnDestructured.tsx new file mode 100644 index 0000000000..e6c5c35943 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnDestructured.tsx @@ -0,0 +1,35 @@ +import { createIsomorphicFn } from '@tanstack/start' + +const noImpl = createIsomorphicFn() + +const serverOnlyFn = createIsomorphicFn().server(() => 'server') + +const clientOnlyFn = createIsomorphicFn().client(() => 'client') + +const serverThenClientFn = createIsomorphicFn() + .server(() => 'server') + .client(() => 'client') + +const clientThenServerFn = createIsomorphicFn() + .client(() => 'client') + .server(() => 'server') + +function abstractedServerFn() { + return 'server' +} + +const serverOnlyFnAbstracted = createIsomorphicFn().server(abstractedServerFn) + +function abstractedClientFn() { + return 'client' +} + +const clientOnlyFnAbstracted = createIsomorphicFn().client(abstractedClientFn) + +const serverThenClientFnAbstracted = createIsomorphicFn() + .server(abstractedServerFn) + .client(abstractedClientFn) + +const clientThenServerFnAbstracted = createIsomorphicFn() + .client(abstractedClientFn) + .server(abstractedServerFn) diff --git a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnDestructuredRename.tsx b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnDestructuredRename.tsx new file mode 100644 index 0000000000..9c31330b1b --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnDestructuredRename.tsx @@ -0,0 +1,35 @@ +import { createIsomorphicFn as isomorphicFn } from '@tanstack/start' + +const noImpl = isomorphicFn() + +const serverOnlyFn = isomorphicFn().server(() => 'server') + +const clientOnlyFn = isomorphicFn().client(() => 'client') + +const serverThenClientFn = isomorphicFn() + .server(() => 'server') + .client(() => 'client') + +const clientThenServerFn = isomorphicFn() + .client(() => 'client') + .server(() => 'server') + +function abstractedServerFn() { + return 'server' +} + +const serverOnlyFnAbstracted = isomorphicFn().server(abstractedServerFn) + +function abstractedClientFn() { + return 'client' +} + +const clientOnlyFnAbstracted = isomorphicFn().client(abstractedClientFn) + +const serverThenClientFnAbstracted = isomorphicFn() + .server(abstractedServerFn) + .client(abstractedClientFn) + +const clientThenServerFnAbstracted = isomorphicFn() + .client(abstractedClientFn) + .server(abstractedServerFn) diff --git a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnStarImport.tsx b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnStarImport.tsx new file mode 100644 index 0000000000..4c1bed5741 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnStarImport.tsx @@ -0,0 +1,37 @@ +import * as TanStackStart from '@tanstack/start' + +const noImpl = TanStackStart.createIsomorphicFn() + +const serverOnlyFn = TanStackStart.createIsomorphicFn().server(() => 'server') + +const clientOnlyFn = TanStackStart.createIsomorphicFn().client(() => 'client') + +const serverThenClientFn = TanStackStart.createIsomorphicFn() + .server(() => 'server') + .client(() => 'client') + +const clientThenServerFn = TanStackStart.createIsomorphicFn() + .client(() => 'client') + .server(() => 'server') + +function abstractedServerFn() { + return 'server' +} + +const serverOnlyFnAbstracted = + TanStackStart.createIsomorphicFn().server(abstractedServerFn) + +function abstractedClientFn() { + return 'client' +} + +const clientOnlyFnAbstracted = + TanStackStart.createIsomorphicFn().client(abstractedClientFn) + +const serverThenClientFnAbstracted = TanStackStart.createIsomorphicFn() + .server(abstractedServerFn) + .client(abstractedClientFn) + +const clientThenServerFnAbstracted = TanStackStart.createIsomorphicFn() + .client(abstractedClientFn) + .server(abstractedServerFn) diff --git a/packages/server-functions-vite-plugin/tests/createMiddleware/createMiddleware.test.ts b/packages/server-functions-vite-plugin/tests/createMiddleware/createMiddleware.test.ts new file mode 100644 index 0000000000..ae4ee2c221 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createMiddleware/createMiddleware.test.ts @@ -0,0 +1,36 @@ +import { readFile, readdir } from 'node:fs/promises' +import path from 'node:path' +import { describe, expect, test } from 'vitest' + +import { compileStartOutput } from '../../src/compilers' + +async function getFilenames() { + return await readdir(path.resolve(import.meta.dirname, './test-files')) +} + +describe('createMiddleware compiles correctly', async () => { + const filenames = await getFilenames() + + describe.each(filenames)('should handle "%s"', async (filename) => { + const file = await readFile( + path.resolve(import.meta.dirname, `./test-files/${filename}`), + ) + const code = file.toString() + + test.each(['client', 'server'] as const)( + `should compile for ${filename} %s`, + async (env) => { + const compiledResult = compileStartOutput({ + env, + code, + root: './test-files', + filename, + }) + + await expect(compiledResult.code).toMatchFileSnapshot( + `./snapshots/${env}/${filename}`, + ) + }, + ) + }) +}) diff --git a/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareDestructured.tsx b/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareDestructured.tsx new file mode 100644 index 0000000000..f0ed6717e4 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareDestructured.tsx @@ -0,0 +1,24 @@ +import { createMiddleware } from '@tanstack/start'; +import { z } from 'zod'; +export const withUseServer = createMiddleware({ + id: 'test' +}); +export const withoutUseServer = createMiddleware({ + id: 'test' +}); +export const withVariable = createMiddleware({ + id: 'test' +}); +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 = createMiddleware({ + id: 'test' +}); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareDestructuredRename.tsx b/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareDestructuredRename.tsx new file mode 100644 index 0000000000..c13c45a86c --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareDestructuredRename.tsx @@ -0,0 +1,24 @@ +import { createMiddleware as middlewareFn } from '@tanstack/start'; +import { z } from 'zod'; +export const withUseServer = middlewareFn({ + id: 'test' +}); +export const withoutUseServer = middlewareFn({ + id: 'test' +}); +export const withVariable = middlewareFn({ + id: 'test' +}); +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 = middlewareFn({ + id: 'test' +}); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareStarImport.tsx b/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareStarImport.tsx new file mode 100644 index 0000000000..0af363017d --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareStarImport.tsx @@ -0,0 +1,24 @@ +import * as TanStackStart from '@tanstack/start'; +import { z } from 'zod'; +export const withUseServer = TanStackStart.createMiddleware({ + id: 'test' +}); +export const withoutUseServer = TanStackStart.createMiddleware({ + id: 'test' +}); +export const withVariable = TanStackStart.createMiddleware({ + id: 'test' +}); +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.createMiddleware({ + id: 'test' +}); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareValidator.tsx b/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareValidator.tsx new file mode 100644 index 0000000000..e13946f2d8 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareValidator.tsx @@ -0,0 +1,5 @@ +import { createMiddleware } from '@tanstack/start'; +import { z } from 'zod'; +export const withUseServer = createMiddleware({ + id: 'test' +}); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareDestructured.tsx b/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareDestructured.tsx new file mode 100644 index 0000000000..a29e1ecb40 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareDestructured.tsx @@ -0,0 +1,36 @@ +import { createMiddleware } from '@tanstack/start'; +import { z } from 'zod'; +export const withUseServer = createMiddleware({ + id: 'test' +}).server(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 = createMiddleware({ + id: 'test' +}).server(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 = createMiddleware({ + id: 'test' +}).server(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 = createMiddleware({ + id: 'test' +}).server(zodValidator(z.number(), input => { + return { + 'you gave': input + }; +})); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareDestructuredRename.tsx b/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareDestructuredRename.tsx new file mode 100644 index 0000000000..d529f89fe1 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareDestructuredRename.tsx @@ -0,0 +1,36 @@ +import { createMiddleware as middlewareFn } from '@tanstack/start'; +import { z } from 'zod'; +export const withUseServer = middlewareFn({ + id: 'test' +}).server(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 = middlewareFn({ + id: 'test' +}).server(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 = middlewareFn({ + id: 'test' +}).server(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 = middlewareFn({ + id: 'test' +}).server(zodValidator(z.number(), input => { + return { + 'you gave': input + }; +})); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareStarImport.tsx b/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareStarImport.tsx new file mode 100644 index 0000000000..53baf6fa9c --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareStarImport.tsx @@ -0,0 +1,38 @@ +import * as TanStackStart from '@tanstack/start'; +import { z } from 'zod'; +export const withUseServer = TanStackStart.createMiddleware({ + id: 'test' +}).server(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.createMiddleware({ + id: 'test' +}).server(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.createMiddleware({ + id: 'test' +}).server(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.createMiddleware({ + id: 'test' +}).server(zodValidator(z.number(), input => { + return { + 'you gave': input + }; +})); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareValidator.tsx b/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareValidator.tsx new file mode 100644 index 0000000000..1e41c79866 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareValidator.tsx @@ -0,0 +1,7 @@ +import { createMiddleware } from '@tanstack/start'; +import { z } from 'zod'; +export const withUseServer = createMiddleware({ + id: 'test' +}).validator(z.number()).server(({ + input +}) => input + 1); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareDestructured.tsx b/packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareDestructured.tsx new file mode 100644 index 0000000000..c5ea2aa5b6 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareDestructured.tsx @@ -0,0 +1,51 @@ +import { createMiddleware } from '@tanstack/start' +import { z } from 'zod' + +export const withUseServer = createMiddleware({ + id: 'test', +}).server(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 = createMiddleware({ + id: 'test', +}).server(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 = createMiddleware({ + id: 'test', +}).server(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 = createMiddleware({ + id: 'test', +}).server( + zodValidator(z.number(), (input) => { + return { 'you gave': input } + }), +) diff --git a/packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareDestructuredRename.tsx b/packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareDestructuredRename.tsx new file mode 100644 index 0000000000..2e456131fe --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareDestructuredRename.tsx @@ -0,0 +1,51 @@ +import { createMiddleware as middlewareFn } from '@tanstack/start' +import { z } from 'zod' + +export const withUseServer = middlewareFn({ + id: 'test', +}).server(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 = middlewareFn({ + id: 'test', +}).server(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 = middlewareFn({ + id: 'test', +}).server(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 = middlewareFn({ + id: 'test', +}).server( + zodValidator(z.number(), (input) => { + return { 'you gave': input } + }), +) diff --git a/packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareStarImport.tsx b/packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareStarImport.tsx new file mode 100644 index 0000000000..c1df8e12b3 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareStarImport.tsx @@ -0,0 +1,52 @@ +import * as TanStackStart from '@tanstack/start' +import { z } from 'zod' + +export const withUseServer = TanStackStart.createMiddleware({ + id: 'test', +}).server(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.createMiddleware({ + id: 'test', +}).server(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.createMiddleware({ + id: 'test', +}).server(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.createMiddleware({ + id: 'test', +}).server( + zodValidator(z.number(), (input) => { + return { 'you gave': input } + }), +) diff --git a/packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareValidator.tsx b/packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareValidator.tsx new file mode 100644 index 0000000000..7e0d6ba449 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareValidator.tsx @@ -0,0 +1,8 @@ +import { createMiddleware } from '@tanstack/start' +import { z } from 'zod' + +export const withUseServer = createMiddleware({ + id: 'test', +}) + .validator(z.number()) + .server(({ input }) => input + 1) diff --git a/packages/server-functions-vite-plugin/tests/createServerFn/createServerFn.test.ts b/packages/server-functions-vite-plugin/tests/createServerFn/createServerFn.test.ts new file mode 100644 index 0000000000..19b86a0c01 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createServerFn/createServerFn.test.ts @@ -0,0 +1,62 @@ +import { readFile, readdir } from 'node:fs/promises' +import path from 'node:path' +import { describe, expect, test } from 'vitest' + +import { compileStartOutput } from '../../src/compilers' + +async function getFilenames() { + return await readdir(path.resolve(import.meta.dirname, './test-files')) +} + +describe('createServerFn compiles correctly', async () => { + const filenames = await getFilenames() + + describe.each(filenames)('should handle "%s"', async (filename) => { + const file = await readFile( + path.resolve(import.meta.dirname, `./test-files/${filename}`), + ) + const code = file.toString() + + test.each(['client', 'server'] as const)( + `should compile for ${filename} %s`, + async (env) => { + const compiledResult = compileStartOutput({ + env, + code, + root: './test-files', + filename, + }) + + await expect(compiledResult.code).toMatchFileSnapshot( + `./snapshots/${env}/${filename}`, + ) + }, + ) + }) + + test('should error if created without a handler', () => { + expect(() => { + compileStartOutput({ + env: 'client', + code: ` + import { createServerFn } from '@tanstack/start' + createServerFn()`, + root: './test-files', + filename: 'no-fn.ts', + }) + }).toThrowError() + }) + + test('should be assigned to a variable', () => { + expect(() => { + compileStartOutput({ + env: 'client', + code: ` + import { createServerFn } from '@tanstack/start' + createServerFn().handler(async () => {})`, + root: './test-files', + filename: 'no-fn.ts', + }) + }).toThrowError() + }) +}) diff --git a/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnDestructured.tsx b/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnDestructured.tsx new file mode 100644 index 0000000000..be1b58b186 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnDestructured.tsx @@ -0,0 +1,61 @@ +import { createServerFn } from '@tanstack/start'; +import { z } from 'zod'; +export const withUseServer = createServerFn({ + method: 'GET' +}).handler(opts => { + "use server"; + + return withUseServer.__executeServer(opts); +}); +export const withArrowFunction = createServerFn({ + method: 'GET' +}).handler(opts => { + "use server"; + + return withArrowFunction.__executeServer(opts); +}); +export const withArrowFunctionAndFunction = createServerFn({ + method: 'GET' +}).handler(opts => { + "use server"; + + return withArrowFunctionAndFunction.__executeServer(opts); +}); +export const withoutUseServer = createServerFn({ + method: 'GET' +}).handler(opts => { + "use server"; + + return withoutUseServer.__executeServer(opts); +}); +export const withVariable = createServerFn({ + method: 'GET' +}).handler(opts => { + "use server"; + + return withVariable.__executeServer(opts); +}); +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 => { + "use server"; + + return withZodValidator.__executeServer(opts); +}); +export const withValidatorFn = createServerFn({ + method: 'GET' +}).handler(opts => { + "use server"; + + return withValidatorFn.__executeServer(opts); +}); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnDestructuredRename.tsx b/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnDestructuredRename.tsx new file mode 100644 index 0000000000..f51f0ade21 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnDestructuredRename.tsx @@ -0,0 +1,40 @@ +import { createServerFn as serverFn } from '@tanstack/start'; +import { z } from 'zod'; +export const withUseServer = serverFn({ + method: 'GET' +}).handler(opts => { + "use server"; + + return withUseServer.__executeServer(opts); +}); +export const withoutUseServer = serverFn({ + method: 'GET' +}).handler(opts => { + "use server"; + + return withoutUseServer.__executeServer(opts); +}); +export const withVariable = serverFn({ + method: 'GET' +}).handler(opts => { + "use server"; + + return withVariable.__executeServer(opts); +}); +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 => { + "use server"; + + return withZodValidator.__executeServer(opts); +}); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnStarImport.tsx b/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnStarImport.tsx new file mode 100644 index 0000000000..750a52532a --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnStarImport.tsx @@ -0,0 +1,40 @@ +import * as TanStackStart from '@tanstack/start'; +import { z } from 'zod'; +export const withUseServer = TanStackStart.createServerFn({ + method: 'GET' +}).handler(opts => { + "use server"; + + return withUseServer.__executeServer(opts); +}); +export const withoutUseServer = TanStackStart.createServerFn({ + method: 'GET' +}).handler(opts => { + "use server"; + + return withoutUseServer.__executeServer(opts); +}); +export const withVariable = TanStackStart.createServerFn({ + method: 'GET' +}).handler(opts => { + "use server"; + + return withVariable.__executeServer(opts); +}); +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 => { + "use server"; + + return withZodValidator.__executeServer(opts); +}); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnValidator.tsx b/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnValidator.tsx new file mode 100644 index 0000000000..4e40b54a24 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnValidator.tsx @@ -0,0 +1,9 @@ +import { createServerFn } from '@tanstack/start'; +import { z } from 'zod'; +export const withUseServer = createServerFn({ + method: 'GET' +}).handler(opts => { + "use server"; + + return withUseServer.__executeServer(opts); +}); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnDestructured.tsx b/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnDestructured.tsx new file mode 100644 index 0000000000..0176442a89 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnDestructured.tsx @@ -0,0 +1,77 @@ +import { createServerFn } from '@tanstack/start'; +import { z } from 'zod'; +export const withUseServer = createServerFn({ + method: 'GET' +}).handler(opts => { + "use server"; + + return withUseServer.__executeServer(opts); +}, 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 => { + "use server"; + + return withArrowFunction.__executeServer(opts); +}, async () => null); +export const withArrowFunctionAndFunction = createServerFn({ + method: 'GET' +}).handler(opts => { + "use server"; + + return withArrowFunctionAndFunction.__executeServer(opts); +}, async () => test()); +export const withoutUseServer = createServerFn({ + method: 'GET' +}).handler(opts => { + "use server"; + + return withoutUseServer.__executeServer(opts); +}, 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 => { + "use server"; + + return withVariable.__executeServer(opts); +}, 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 => { + "use server"; + + return withZodValidator.__executeServer(opts); +}, zodValidator(z.number(), input => { + return { + 'you gave': input + }; +})); +export const withValidatorFn = createServerFn({ + method: 'GET' +}).validator(z.number()).handler(opts => { + "use server"; + + return withValidatorFn.__executeServer(opts); +}, async ({ + input +}) => { + return null; +}); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnDestructuredRename.tsx b/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnDestructuredRename.tsx new file mode 100644 index 0000000000..b391de555f --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnDestructuredRename.tsx @@ -0,0 +1,52 @@ +import { createServerFn as serverFn } from '@tanstack/start'; +import { z } from 'zod'; +export const withUseServer = serverFn({ + method: 'GET' +}).handler(opts => { + "use server"; + + return withUseServer.__executeServer(opts); +}, 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 => { + "use server"; + + return withoutUseServer.__executeServer(opts); +}, 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 => { + "use server"; + + return withVariable.__executeServer(opts); +}, 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 => { + "use server"; + + return withZodValidator.__executeServer(opts); +}, zodValidator(z.number(), input => { + return { + 'you gave': input + }; +})); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnStarImport.tsx b/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnStarImport.tsx new file mode 100644 index 0000000000..66088cb181 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnStarImport.tsx @@ -0,0 +1,52 @@ +import * as TanStackStart from '@tanstack/start'; +import { z } from 'zod'; +export const withUseServer = TanStackStart.createServerFn({ + method: 'GET' +}).handler(opts => { + "use server"; + + return withUseServer.__executeServer(opts); +}, 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 = TanStackStart.createServerFn({ + method: 'GET' +}).handler(opts => { + "use server"; + + return withoutUseServer.__executeServer(opts); +}, 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 => { + "use server"; + + return withVariable.__executeServer(opts); +}, 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 => { + "use server"; + + return withZodValidator.__executeServer(opts); +}, zodValidator(z.number(), input => { + return { + 'you gave': input + }; +})); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnValidator.tsx b/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnValidator.tsx new file mode 100644 index 0000000000..96cd71b9fb --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnValidator.tsx @@ -0,0 +1,11 @@ +import { createServerFn } from '@tanstack/start'; +import { z } from 'zod'; +export const withUseServer = createServerFn({ + method: 'GET' +}).validator(z.number()).handler(opts => { + "use server"; + + return withUseServer.__executeServer(opts); +}, ({ + input +}) => input + 1); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnDestructured.tsx b/packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnDestructured.tsx new file mode 100644 index 0000000000..780c1e3370 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnDestructured.tsx @@ -0,0 +1,67 @@ +import { createServerFn } from '@tanstack/start' +import { z } from 'zod' + +export const withUseServer = createServerFn({ + method: 'GET', +}).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)) +}) + +export const withArrowFunction = createServerFn({ + method: 'GET', +}).handler(async () => null) + +export const withArrowFunctionAndFunction = createServerFn({ + method: 'GET', +}).handler(async () => test()) + +export const withoutUseServer = createServerFn({ + method: 'GET', +}).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)) +}) + +export const withVariable = createServerFn({ + method: 'GET', +}).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)) + } +} + +export const withZodValidator = createServerFn({ + method: 'GET', +}).handler( + zodValidator(z.number(), (input) => { + return { 'you gave': input } + }), +) + +export const withValidatorFn = createServerFn({ + method: 'GET', +}) + .validator(z.number()) + .handler(async ({ input }) => { + return null + }) diff --git a/packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnDestructuredRename.tsx b/packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnDestructuredRename.tsx new file mode 100644 index 0000000000..aba9859be3 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnDestructuredRename.tsx @@ -0,0 +1,51 @@ +import { createServerFn as serverFn } from '@tanstack/start' +import { z } from 'zod' + +export const withUseServer = serverFn({ + method: 'GET', +}).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)) +}) + +export const withoutUseServer = serverFn({ + method: 'GET', +}).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)) +}) + +export const withVariable = serverFn({ method: 'GET' }).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)) + } +} + +export const withZodValidator = serverFn({ + method: 'GET', +}).handler( + zodValidator(z.number(), (input) => { + return { 'you gave': input } + }), +) diff --git a/packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnStarImport.tsx b/packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnStarImport.tsx new file mode 100644 index 0000000000..6d5ba022bb --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnStarImport.tsx @@ -0,0 +1,52 @@ +import * as TanStackStart from '@tanstack/start' +import { z } from 'zod' + +export const withUseServer = TanStackStart.createServerFn({ + method: 'GET', +}).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)) +}) + +export const withoutUseServer = TanStackStart.createServerFn({ + method: 'GET', +}).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)) +}) + +export const withVariable = TanStackStart.createServerFn({ + method: 'GET', +}).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)) + } +} + +export const withZodValidator = TanStackStart.createServerFn({ + method: 'GET', +}).handler( + zodValidator(z.number(), (input) => { + return { 'you gave': input } + }), +) diff --git a/packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnValidator.tsx b/packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnValidator.tsx new file mode 100644 index 0000000000..b30bcdc731 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnValidator.tsx @@ -0,0 +1,8 @@ +import { createServerFn } from '@tanstack/start' +import { z } from 'zod' + +export const withUseServer = createServerFn({ + method: 'GET', +}) + .validator(z.number()) + .handler(({ input }) => input + 1) diff --git a/packages/server-functions-vite-plugin/tests/envOnly/envOnly.test.ts b/packages/server-functions-vite-plugin/tests/envOnly/envOnly.test.ts new file mode 100644 index 0000000000..67336be35b --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/envOnly/envOnly.test.ts @@ -0,0 +1,58 @@ +import { readFile, readdir } from 'node:fs/promises' +import path from 'node:path' +import { describe, expect, test } from 'vitest' + +import { compileStartOutput } from '../../src/compilers' + +async function getFilenames() { + return await readdir(path.resolve(import.meta.dirname, './test-files')) +} + +describe('envOnly functions compile correctly', async () => { + const filenames = await getFilenames() + + describe.each(filenames)('should handle "%s"', async (filename) => { + const file = await readFile( + path.resolve(import.meta.dirname, `./test-files/${filename}`), + ) + const code = file.toString() + + test.each(['client', 'server'] as const)( + `should compile for ${filename} %s`, + async (env) => { + const compiledResult = compileStartOutput({ + env, + code, + root: './test-files', + filename, + }) + + await expect(compiledResult.code).toMatchFileSnapshot( + `./snapshots/${env}/${filename}`, + ) + }, + ) + }) + test('should error if implementation not provided', () => { + expect(() => { + compileStartOutput({ + env: 'client', + code: ` + import { clientOnly } from '@tanstack/start' + const fn = clientOnly()`, + root: './test-files', + filename: 'no-fn.ts', + }) + }).toThrowError() + expect(() => { + compileStartOutput({ + env: 'server', + code: ` + import { serverOnly } from '@tanstack/start' + const fn = serverOnly()`, + root: './test-files', + filename: 'no-fn.ts', + }) + }).toThrowError() + }) +}) diff --git a/packages/server-functions-vite-plugin/tests/envOnly/snapshots/client/envOnlyDestructured.tsx b/packages/server-functions-vite-plugin/tests/envOnly/snapshots/client/envOnlyDestructured.tsx new file mode 100644 index 0000000000..3dc714f696 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/envOnly/snapshots/client/envOnlyDestructured.tsx @@ -0,0 +1,5 @@ +import { serverOnly, clientOnly } from '@tanstack/start'; +const serverFunc = () => { + throw new Error("serverOnly() functions can only be called on the server!"); +}; +const clientFunc = () => 'client'; \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/envOnly/snapshots/client/envOnlyDestructuredRename.tsx b/packages/server-functions-vite-plugin/tests/envOnly/snapshots/client/envOnlyDestructuredRename.tsx new file mode 100644 index 0000000000..3dd661c865 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/envOnly/snapshots/client/envOnlyDestructuredRename.tsx @@ -0,0 +1,5 @@ +import { serverOnly as serverFn, clientOnly as clientFn } from '@tanstack/start'; +const serverFunc = () => { + throw new Error("serverOnly() functions can only be called on the server!"); +}; +const clientFunc = () => 'client'; \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/envOnly/snapshots/client/envOnlyStarImport.tsx b/packages/server-functions-vite-plugin/tests/envOnly/snapshots/client/envOnlyStarImport.tsx new file mode 100644 index 0000000000..61f0677953 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/envOnly/snapshots/client/envOnlyStarImport.tsx @@ -0,0 +1,5 @@ +import * as TanstackStart from '@tanstack/start'; +const serverFunc = () => { + throw new Error("serverOnly() functions can only be called on the server!"); +}; +const clientFunc = () => 'client'; \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/envOnly/snapshots/server/envOnlyDestructured.tsx b/packages/server-functions-vite-plugin/tests/envOnly/snapshots/server/envOnlyDestructured.tsx new file mode 100644 index 0000000000..24f736eac2 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/envOnly/snapshots/server/envOnlyDestructured.tsx @@ -0,0 +1,5 @@ +import { serverOnly, clientOnly } from '@tanstack/start'; +const serverFunc = () => 'server'; +const clientFunc = () => { + throw new Error("clientOnly() functions can only be called on the client!"); +}; \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/envOnly/snapshots/server/envOnlyDestructuredRename.tsx b/packages/server-functions-vite-plugin/tests/envOnly/snapshots/server/envOnlyDestructuredRename.tsx new file mode 100644 index 0000000000..790e1328d7 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/envOnly/snapshots/server/envOnlyDestructuredRename.tsx @@ -0,0 +1,5 @@ +import { serverOnly as serverFn, clientOnly as clientFn } from '@tanstack/start'; +const serverFunc = () => 'server'; +const clientFunc = () => { + throw new Error("clientOnly() functions can only be called on the client!"); +}; \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/envOnly/snapshots/server/envOnlyStarImport.tsx b/packages/server-functions-vite-plugin/tests/envOnly/snapshots/server/envOnlyStarImport.tsx new file mode 100644 index 0000000000..51e320d4a2 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/envOnly/snapshots/server/envOnlyStarImport.tsx @@ -0,0 +1,5 @@ +import * as TanstackStart from '@tanstack/start'; +const serverFunc = () => 'server'; +const clientFunc = () => { + throw new Error("clientOnly() functions can only be called on the client!"); +}; \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/envOnly/test-files/envOnlyDestructured.tsx b/packages/server-functions-vite-plugin/tests/envOnly/test-files/envOnlyDestructured.tsx new file mode 100644 index 0000000000..1389624d34 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/envOnly/test-files/envOnlyDestructured.tsx @@ -0,0 +1,5 @@ +import { serverOnly, clientOnly } from '@tanstack/start' + +const serverFunc = serverOnly(() => 'server') + +const clientFunc = clientOnly(() => 'client') diff --git a/packages/server-functions-vite-plugin/tests/envOnly/test-files/envOnlyDestructuredRename.tsx b/packages/server-functions-vite-plugin/tests/envOnly/test-files/envOnlyDestructuredRename.tsx new file mode 100644 index 0000000000..ac34f06134 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/envOnly/test-files/envOnlyDestructuredRename.tsx @@ -0,0 +1,5 @@ +import { serverOnly as serverFn, clientOnly as clientFn } from '@tanstack/start' + +const serverFunc = serverFn(() => 'server') + +const clientFunc = clientFn(() => 'client') diff --git a/packages/server-functions-vite-plugin/tests/envOnly/test-files/envOnlyStarImport.tsx b/packages/server-functions-vite-plugin/tests/envOnly/test-files/envOnlyStarImport.tsx new file mode 100644 index 0000000000..cd262b54a0 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/envOnly/test-files/envOnlyStarImport.tsx @@ -0,0 +1,5 @@ +import * as TanstackStart from '@tanstack/start' + +const serverFunc = TanstackStart.serverOnly(() => 'server') + +const clientFunc = TanstackStart.clientOnly(() => 'client') diff --git a/packages/server-functions-vite-plugin/tsconfig.json b/packages/server-functions-vite-plugin/tsconfig.json new file mode 100644 index 0000000000..37d21ef6ca --- /dev/null +++ b/packages/server-functions-vite-plugin/tsconfig.json @@ -0,0 +1,8 @@ +{ + "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-vite-plugin/vite.config.ts b/packages/server-functions-vite-plugin/vite.config.ts new file mode 100644 index 0000000000..5389f0f739 --- /dev/null +++ b/packages/server-functions-vite-plugin/vite.config.ts @@ -0,0 +1,20 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import { tanstackViteConfig } from '@tanstack/config/vite' +import packageJson from './package.json' + +const config = defineConfig({ + test: { + 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/start/package.json b/packages/start/package.json index afde4bd213..549da390f1 100644 --- a/packages/start/package.json +++ b/packages/start/package.json @@ -36,9 +36,10 @@ "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js", "test:types:ts57": "tsc", "test:build": "publint --strict && attw --ignore-rules no-resolution --pack .", - "build": "vite build && pnpm build:config && pnpm build:router-manifest", + "build": "pnpm build:config && pnpm build:router-manifest && pnpm build:vite", "build:config": "tsc --project tsconfigs/config.tsconfig.json", - "build:router-manifest": "tsc --project tsconfigs/router-manifest.tsconfig.json" + "build:router-manifest": "tsc --project tsconfigs/router-manifest.tsconfig.json", + "build:vite": "vite build" }, "type": "module", "exports": { @@ -110,10 +111,10 @@ "default": "./dist/esm/server-runtime/index.js" } }, - "./react-server-runtime": { + "./ssr-runtime": { "import": { - "types": "./dist/esm/react-server-runtime/index.d.ts", - "default": "./dist/esm/react-server-runtime/index.js" + "types": "./dist/esm/ssr-runtime/index.d.ts", + "default": "./dist/esm/ssr-runtime/index.js" } }, "./server-handler": { diff --git a/packages/start/src/client-runtime/index.tsx b/packages/start/src/client-runtime/index.tsx index 9e4f2b67f6..2c02f6b9c7 100644 --- a/packages/start/src/client-runtime/index.tsx +++ b/packages/start/src/client-runtime/index.tsx @@ -1,21 +1,19 @@ import { fetcher } from './fetcher' import { getBaseUrl } from './getBaseUrl' -import type { ServerFn } from '../client/createServerFn' +import type { CreateRpcFn } from '@tanstack/start/server-functions-plugin' -export function createServerReference( - _fn: ServerFn, - id: string, - name: string, -) { - // let base = getBaseUrl(import.meta.env.SERVER_BASE_URL, id, name) - const base = getBaseUrl(window.location.origin, id, name) +export const createClientRpc: CreateRpcFn = ( + filename: string, + functionId: string, +) => { + const base = getBaseUrl(window.location.origin, filename, functionId) - const proxyFn = (...args: Array) => fetcher(base, args, fetch) + const fn = (...args: Array) => fetcher(base, args, fetch) - return Object.assign(proxyFn, { + return Object.assign(fn, { url: base, - filename: id, - functionId: name, + filename, + functionId, }) } diff --git a/packages/start/src/config/index.ts b/packages/start/src/config/index.ts index f371997c8e..8e0d5dbdc2 100644 --- a/packages/start/src/config/index.ts +++ b/packages/start/src/config/index.ts @@ -14,10 +14,7 @@ import { createApp } from 'vinxi' import { config } from 'vinxi/plugins/config' // // @ts-expect-error // import { serverComponents } from '@vinxi/server-components/plugin' -// @ts-expect-error -import { serverFunctions } from '@vinxi/server-functions/plugin' -// @ts-expect-error -import { serverTransform } from '@vinxi/server-functions/server' +import { createServerFunctionsPlugin } from '@tanstack/start/server-functions-plugin' import { tanstackStartVinxiFileRouter } from './vinxi-file-router.js' import { checkDeploymentPresetInput, @@ -123,6 +120,8 @@ export function defineConfig( const apiEntryExists = existsSync(apiEntry) + const serverFunctionsPlugin = createServerFunctionsPlugin() + let vinxiApp = createApp({ server: { ...serverOptions, @@ -170,8 +169,10 @@ export function defineConfig( }), ...(viteConfig.plugins || []), ...(clientViteConfig.plugins || []), - serverFunctions.client({ - runtime: '@tanstack/start/client-runtime', + serverFunctionsPlugin.client({ + runtimeCode: `import { createClientRpc } from '@tanstack/start/client-runtime'`, + replacer: (opts) => + `createClientRpc('${opts.filename}', '${opts.functionId}')`, }), viteReact(opts.react), // TODO: RSCS - enable this @@ -211,8 +212,10 @@ export function defineConfig( }), ...(getUserViteConfig(opts.vite).plugins || []), ...(getUserViteConfig(opts.routers?.ssr?.vite).plugins || []), - serverTransform({ - runtime: '@tanstack/start/server-runtime', + serverFunctionsPlugin.server({ + runtimeCode: `import { createServerRpc } from '@tanstack/start/ssr-runtime'`, + replacer: (opts) => + `createSsrRpc('${opts.filename}', '${opts.functionId}')`, }), config('start-ssr', { ssr: { @@ -254,12 +257,14 @@ export function defineConfig( ...injectDefineEnv('TSS_API_BASE', apiBase), }, }), - serverFunctions.server({ - runtime: '@tanstack/start/react-server-runtime', + serverFunctionsPlugin.server({ + runtimeCode: `import { createServerRpc } from '@tanstack/start/server-runtime'`, + replacer: (opts) => + `createServerRpc('${opts.filename}', '${opts.functionId}')`, // TODO: RSCS - remove this - resolve: { - conditions: [], - }, + // resolve: { + // conditions: [], + // }, }), // TODO: RSCs - add this // serverComponents.serverActions({ diff --git a/packages/start/src/react-server-runtime/index.tsx b/packages/start/src/react-server-runtime/index.tsx deleted file mode 100644 index d7fbea3fe7..0000000000 --- a/packages/start/src/react-server-runtime/index.tsx +++ /dev/null @@ -1,14 +0,0 @@ -// import { getBaseUrl } from '../client-runtime/getBaseUrl' - -export function createServerReference( - fn: any, - id: string, - name: string, -) { - // const functionUrl = getBaseUrl('http://localhost:3000', id, name) - const functionUrl = 'https://localhost:3000' - - return Object.assign(fn, { - url: functionUrl, - }) -} diff --git a/packages/start/src/server-runtime/index.tsx b/packages/start/src/server-runtime/index.tsx index eda1e96e91..5d1a68cb87 100644 --- a/packages/start/src/server-runtime/index.tsx +++ b/packages/start/src/server-runtime/index.tsx @@ -1,159 +1,17 @@ -import { Readable } from 'node:stream' -import { getEvent, getRequestHeaders } from 'vinxi/http' -import invariant from 'tiny-invariant' -import { fetcher } from '../client-runtime/fetcher' -import { getBaseUrl } from '../client-runtime/getBaseUrl' -import { handleServerRequest } from '../server-handler/index' -/** - * - * @returns {import('node:http').IncomingMessage} - */ -export function createIncomingMessage( - url: string, - method: string, - headers: HeadersInit, -): Readable { - const readable = new Readable({ objectMode: true }) as any - readable._read = () => {} - - readable.url = url - readable.method = method - readable.headers = headers - readable.connection = {} - readable.getHeaders = () => { - return headers - } - return readable -} - -// function createAsyncStream(options?: WritableOptions) { -// let firstActivity = false -// let resolveActivity: () => void -// let finishActivity: () => void - -// const initialPromise = new Promise((resolve) => { -// resolveActivity = resolve -// }) - -// const finishPromise = new Promise((resolve) => { -// finishActivity = resolve -// }) - -// const readable = new Readable({ -// objectMode: true, -// }) - -// readable._read = () => {} - -// const writable = new Writable({ -// ...options, -// write(chunk, encoding, callback) { -// if (!firstActivity) { -// firstActivity = true -// resolveActivity() -// } -// readable.push(chunk, encoding) -// callback() -// }, -// }) as any - -// const headers = new Headers() - -// writable.setHeader = (key: string, value: string) => { -// headers.set(key, value) -// } - -// writable.on('finish', () => { -// readable.push(null) -// readable.destroy() -// finishActivity() -// }) - -// return { -// readable, -// writable, -// headers, -// initialPromise, -// finishPromise, -// } as const -// } - -const fakeHost = 'http://localhost:3000' - -export function createServerReference(_fn: any, id: string, name: string) { - const functionUrl = getBaseUrl(fakeHost, id, name) - - const proxyFn = (...args: Array) => { - invariant( - args.length === 1, - 'Server functions can only accept a single argument', - ) - - return fetcher(functionUrl, args, async (request) => { - const event = getEvent() - - const ogRequestHeaders = getRequestHeaders(event) - - Object.entries(ogRequestHeaders).forEach(([key, value]) => { - if (!request.headers.has(key)) { - request.headers.append(key, value!) - } - }) - - // // For RSC, we need to proxy this request back to the server under - // // the /_server path and let the RSC/Server-fn router handle it - // if (RSC) { - - // const incomingMessage = createIncomingMessage( - // new URL(request.url).pathname + new URL(request.url).search, - // request.method, - // Object.fromEntries(request.headers.entries()), - // ) - - // const asyncStream = createAsyncStream() - - // const result = await handleHTTPEvent( - // new H3Event(incomingMessage as any, asyncStream.writable), - // ) - - // console.info('awaiting initial promise', result, asyncStream) - - // await asyncStream.initialPromise - - // // Only augment the headers of the underlying document request - // // if the response headers have not been sent yet - // if (!(event as any).__tsrHeadersSent) { - // const ogResponseHeaders = getResponseHeaders(event) - - // asyncStream.headers.forEach((value, key) => { - // if (!Object.hasOwn(ogResponseHeaders, key)) { - // ogResponseHeaders[key] = value - // } - // }) - - // setResponseHeaders(event, ogResponseHeaders as any) - // } - - // console.info(asyncStream.readable) - - // // if (asyncStream.headers.get('content-type') === 'application/json') { - // // await asyncStream.finishPromise - // // } - - // return new Response(Readable.toWeb(asyncStream.readable) as any, { - // headers: asyncStream.headers, - // }) - // } - - // For now, we're not doing RSC, so we just handle the request - // in the current non-worker scope - return handleServerRequest(request, event) - }) - } - - return Object.assign(proxyFn, { - url: functionUrl.replace(fakeHost, ''), - filename: id, - functionId: name, +// import { getBaseUrl } from '../client-runtime/getBaseUrl' +import type { CreateRpcFn } from '@tanstack/start/server-functions-plugin' + +export const createServerRpc: CreateRpcFn = ( + fn: any, + filename: string, + functionId: string, +) => { + // const functionUrl = getBaseUrl('http://localhost:3000', id, name) + const functionUrl = 'https://localhost:3000' + + return Object.assign(fn, { + url: functionUrl, + filename, + functionId, }) } diff --git a/packages/start/src/ssr-runtime/index.tsx b/packages/start/src/ssr-runtime/index.tsx new file mode 100644 index 0000000000..f2c3582da6 --- /dev/null +++ b/packages/start/src/ssr-runtime/index.tsx @@ -0,0 +1,161 @@ +import { Readable } from 'node:stream' +import { getEvent, getRequestHeaders } from 'vinxi/http' +import invariant from 'tiny-invariant' +import { fetcher } from '../client-runtime/fetcher' +import { getBaseUrl } from '../client-runtime/getBaseUrl' +import { handleServerRequest } from '../server-handler/index' +import type { CreateRpcFn } from '@tanstack/start/server-functions-plugin' + +export function createIncomingMessage( + url: string, + method: string, + headers: HeadersInit, +): Readable { + const readable = new Readable({ objectMode: true }) as any + readable._read = () => {} + + readable.url = url + readable.method = method + readable.headers = headers + readable.connection = {} + readable.getHeaders = () => { + return headers + } + return readable +} + +// function createAsyncStream(options?: WritableOptions) { +// let firstActivity = false +// let resolveActivity: () => void +// let finishActivity: () => void + +// const initialPromise = new Promise((resolve) => { +// resolveActivity = resolve +// }) + +// const finishPromise = new Promise((resolve) => { +// finishActivity = resolve +// }) + +// const readable = new Readable({ +// objectMode: true, +// }) + +// readable._read = () => {} + +// const writable = new Writable({ +// ...options, +// write(chunk, encoding, callback) { +// if (!firstActivity) { +// firstActivity = true +// resolveActivity() +// } +// readable.push(chunk, encoding) +// callback() +// }, +// }) as any + +// const headers = new Headers() + +// writable.setHeader = (key: string, value: string) => { +// headers.set(key, value) +// } + +// writable.on('finish', () => { +// readable.push(null) +// readable.destroy() +// finishActivity() +// }) + +// return { +// readable, +// writable, +// headers, +// initialPromise, +// finishPromise, +// } as const +// } + +const fakeHost = 'http://localhost:3000' + +export const createSsrRpc: CreateRpcFn = ( + _fn: any, + filename: string, + functionId: string, +) => { + const functionUrl = getBaseUrl(fakeHost, filename, functionId) + + const proxyFn = (...args: Array) => { + invariant( + args.length === 1, + 'Server functions can only accept a single argument', + ) + + return fetcher(functionUrl, args, async (request) => { + const event = getEvent() + + const ogRequestHeaders = getRequestHeaders(event) + + Object.entries(ogRequestHeaders).forEach(([key, value]) => { + if (!request.headers.has(key)) { + request.headers.append(key, value!) + } + }) + + // // For RSC, we need to proxy this request back to the server under + // // the /_server path and let the RSC/Server-fn router handle it + // if (RSC) { + + // const incomingMessage = createIncomingMessage( + // new URL(request.url).pathname + new URL(request.url).search, + // request.method, + // Object.fromEntries(request.headers.entries()), + // ) + + // const asyncStream = createAsyncStream() + + // const result = await handleHTTPEvent( + // new H3Event(incomingMessage as any, asyncStream.writable), + // ) + + // console.info('awaiting initial promise', result, asyncStream) + + // await asyncStream.initialPromise + + // // Only augment the headers of the underlying document request + // // if the response headers have not been sent yet + // if (!(event as any).__tsrHeadersSent) { + // const ogResponseHeaders = getResponseHeaders(event) + + // asyncStream.headers.forEach((value, key) => { + // if (!Object.hasOwn(ogResponseHeaders, key)) { + // ogResponseHeaders[key] = value + // } + // }) + + // setResponseHeaders(event, ogResponseHeaders as any) + // } + + // console.info(asyncStream.readable) + + // // if (asyncStream.headers.get('content-type') === 'application/json') { + // // await asyncStream.finishPromise + // // } + + // return new Response(Readable.toWeb(asyncStream.readable) as any, { + // headers: asyncStream.headers, + // }) + // } + + // For now, we're not doing RSC, so we just handle the request + // in the current non-worker scope + return handleServerRequest(request, event) + }) + } + + return Object.assign(proxyFn, { + url: functionUrl.replace(fakeHost, ''), + filename, + functionId, + }) +} diff --git a/packages/start/tsconfig.json b/packages/start/tsconfig.json index fe3fb37c9e..51dda9abf2 100644 --- a/packages/start/tsconfig.json +++ b/packages/start/tsconfig.json @@ -4,5 +4,5 @@ "jsx": "react-jsx", "module": "esnext" }, - "include": ["src", "vite.config.ts", "src/client/DehydrateRouter.tsx"] + "include": ["src", "vite.config.ts"] } diff --git a/packages/start/tsconfigs/config.tsconfig.json b/packages/start/tsconfigs/config.tsconfig.json index 948b455bbc..58fc33fb0a 100644 --- a/packages/start/tsconfigs/config.tsconfig.json +++ b/packages/start/tsconfigs/config.tsconfig.json @@ -2,6 +2,7 @@ "extends": "../../../tsconfig.json", "include": ["../src/config/index.ts"], "compilerOptions": { + "rootDir": "../src/config", "outDir": "../dist/esm/config", "target": "esnext", "noEmit": false diff --git a/packages/start/tsconfigs/router-manifest.tsconfig.json b/packages/start/tsconfigs/router-manifest.tsconfig.json index 4849ec7e6a..cc108d2494 100644 --- a/packages/start/tsconfigs/router-manifest.tsconfig.json +++ b/packages/start/tsconfigs/router-manifest.tsconfig.json @@ -2,6 +2,7 @@ "extends": "../../../tsconfig.json", "include": ["../src/router-manifest/index.ts"], "compilerOptions": { + "rootDir": "../src/router-manifest", "outDir": "../dist/esm/router-manifest", "target": "esnext", "noEmit": false diff --git a/packages/start/tsconfigs/server-functions-plugin.tsconfig.json b/packages/start/tsconfigs/server-functions-plugin.tsconfig.json new file mode 100644 index 0000000000..440bc758da --- /dev/null +++ b/packages/start/tsconfigs/server-functions-plugin.tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.json", + "include": ["../src/server-functions-plugin/index.ts"], + "compilerOptions": { + "rootDir": "../src/server-functions-plugin", + "outDir": "../dist/esm/server-functions-plugin", + "target": "esnext", + "noEmit": false + } +} diff --git a/packages/start/vite.config.ts b/packages/start/vite.config.ts index 82a14ae09b..7f3b092680 100644 --- a/packages/start/vite.config.ts +++ b/packages/start/vite.config.ts @@ -20,10 +20,10 @@ export default mergeConfig( entry: [ './src/client/index.tsx', './src/server/index.tsx', - './src/client-runtime/index.tsx', './src/api/index.ts', + './src/client-runtime/index.tsx', './src/server-runtime/index.tsx', - './src/react-server-runtime/index.tsx', + './src/ssr-runtime/index.tsx', './src/server-handler/index.tsx', ], srcDir: './src', From 83e7a78b236088488a6fc400d99b5b6095b76839 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Thu, 12 Dec 2024 00:44:43 -0700 Subject: [PATCH 02/38] checkpoint --- .prettierignore | 3 +- .../server-functions-vite-plugin/package.json | 4 +- .../server-functions-vite-plugin/src/ast.ts | 44 +- .../src/compilers.ts | 704 ++---------------- .../server-functions-vite-plugin/src/index.ts | 33 +- packages/start/package.json | 1 + packages/start/src/client-runtime/index.tsx | 17 +- packages/start/src/config/index.ts | 13 +- packages/start/src/server-runtime/index.tsx | 14 +- packages/start/src/ssr-runtime/index.tsx | 14 +- pnpm-lock.yaml | 54 ++ 11 files changed, 194 insertions(+), 707 deletions(-) diff --git a/.prettierignore b/.prettierignore index 210d68c1b1..a234ee7bd1 100644 --- a/.prettierignore +++ b/.prettierignore @@ -13,4 +13,5 @@ pnpm-lock.yaml node_modules **/test-results -**/tests/generator/file-modification/routes/(test)/* \ No newline at end of file +**/tests/generator/file-modification/routes/(test)/* +/.nx/workspace-data \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/package.json b/packages/server-functions-vite-plugin/package.json index 41d0915a16..2dbf643a41 100644 --- a/packages/server-functions-vite-plugin/package.json +++ b/packages/server-functions-vite-plugin/package.json @@ -1,5 +1,5 @@ { - "name": "@tanstack/start-vite-plugin", + "name": "@tanstack/server-functions-plugin", "version": "1.87.3", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", @@ -7,7 +7,7 @@ "repository": { "type": "git", "url": "https://github.com/TanStack/router.git", - "directory": "packages/start-vite-plugin" + "directory": "packages/server-functions-plugin" }, "homepage": "https://tanstack.com/start", "funding": { diff --git a/packages/server-functions-vite-plugin/src/ast.ts b/packages/server-functions-vite-plugin/src/ast.ts index 6062993cfd..c353b97abc 100644 --- a/packages/server-functions-vite-plugin/src/ast.ts +++ b/packages/server-functions-vite-plugin/src/ast.ts @@ -1,44 +1,20 @@ -import * as babel from '@babel/core' -import '@babel/parser' -// @ts-expect-error -import _babelPluginJsx from '@babel/plugin-syntax-jsx' -// @ts-expect-error -import _babelPluginTypeScript from '@babel/plugin-syntax-typescript' - -let babelPluginJsx = _babelPluginJsx -let babelPluginTypeScript = _babelPluginTypeScript - -if (babelPluginJsx.default) { - babelPluginJsx = babelPluginJsx.default -} - -if (babelPluginTypeScript.default) { - babelPluginTypeScript = babelPluginTypeScript.default -} +import { parse } from '@babel/parser' +import type { ParseResult } from '@babel/parser' export type ParseAstOptions = { code: string filename: string root: string - env: 'server' | 'client' } -export function parseAst(opts: ParseAstOptions) { - const babelPlugins: Array = [ - babelPluginJsx, - [ - babelPluginTypeScript, - { - isTSX: true, - }, - ], - ] - - return babel.parse(opts.code, { - plugins: babelPlugins, - root: opts.root, - filename: opts.filename, - sourceMaps: true, +export function parseAst(opts: ParseAstOptions): ParseResult { + return parse(opts.code, { + plugins: ['jsx', 'typescript'], sourceType: 'module', + ...{ + root: opts.root, + filename: opts.filename, + sourceMaps: true, + }, }) } diff --git a/packages/server-functions-vite-plugin/src/compilers.ts b/packages/server-functions-vite-plugin/src/compilers.ts index a5d8c49b11..1103bba03b 100644 --- a/packages/server-functions-vite-plugin/src/compilers.ts +++ b/packages/server-functions-vite-plugin/src/compilers.ts @@ -1,28 +1,13 @@ import * as babel from '@babel/core' -import * as t from '@babel/types' -import _generate from '@babel/generator' +import generate from '@babel/generator' import { codeFrameColumns } from '@babel/code-frame' import { deadCodeElimination } from 'babel-dead-code-elimination' import { parseAst } from './ast' import type { ParseAstOptions } from './ast' -// Babel is a CJS package and uses `default` as named binding (`exports.default =`). -// https://github.com/babel/babel/issues/15269. -let generate = (_generate as any)['default'] as typeof _generate - -// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -if (!generate) { - generate = _generate -} - export function compileEliminateDeadCode(opts: ParseAstOptions) { const ast = parseAst(opts) - if (!ast) { - throw new Error( - `Failed to compile ast for compileEliminateDeadCode() for the file: ${opts.filename}`, - ) - } deadCodeElimination(ast) return generate(ast, { @@ -32,663 +17,112 @@ export function compileEliminateDeadCode(opts: ParseAstOptions) { const debug = process.env.TSR_VITE_DEBUG === 'true' -// build these once and reuse them -const handleServerOnlyCallExpression = - buildEnvOnlyCallExpressionHandler('server') -const handleClientOnlyCallExpression = - buildEnvOnlyCallExpressionHandler('client') - -type IdentifierConfig = { - name: string - type: 'ImportSpecifier' | 'ImportNamespaceSpecifier' - namespaceId: string - handleCallExpression: ( - path: babel.NodePath, - opts: ParseAstOptions, - ) => void - paths: Array -} - -export function compileStartOutput(opts: ParseAstOptions) { +export function compileServerFnClient(opts: ParseAstOptions) { const ast = parseAst(opts) - if (!ast) { - throw new Error( - `Failed to compile ast for compileStartOutput() for the file: ${opts.filename}`, - ) - } + const serverFnPaths: Array = [] babel.traverse(ast, { Program: { enter(programPath) { - const identifiers: { - createServerFn: IdentifierConfig - createMiddleware: IdentifierConfig - serverOnly: IdentifierConfig - clientOnly: IdentifierConfig - createIsomorphicFn: IdentifierConfig - } = { - createServerFn: { - name: 'createServerFn', - type: 'ImportSpecifier', - namespaceId: '', - handleCallExpression: handleCreateServerFnCallExpression, - paths: [], - }, - createMiddleware: { - name: 'createMiddleware', - type: 'ImportSpecifier', - namespaceId: '', - handleCallExpression: handleCreateMiddlewareCallExpression, - paths: [], - }, - serverOnly: { - name: 'serverOnly', - type: 'ImportSpecifier', - namespaceId: '', - handleCallExpression: handleServerOnlyCallExpression, - paths: [], - }, - clientOnly: { - name: 'clientOnly', - type: 'ImportSpecifier', - namespaceId: '', - handleCallExpression: handleClientOnlyCallExpression, - paths: [], - }, - createIsomorphicFn: { - name: 'createIsomorphicFn', - type: 'ImportSpecifier', - namespaceId: '', - handleCallExpression: handleCreateIsomorphicFnCallExpression, - paths: [], - }, - } - - const identifierKeys = Object.keys(identifiers) as Array< - keyof typeof identifiers - > - programPath.traverse({ - ImportDeclaration: (path) => { - if (path.node.source.value !== '@tanstack/start') { - return + enter(path) { + // Check for 'use server' directive in function declarations (named functions) + if (path.isFunctionDeclaration()) { + const directives = path.node.body.directives + for (const directive of directives) { + if (directive.value.value === 'use server') { + serverFnPaths.push(path) + } + } } - // handle a destructured imports being renamed like "import { createServerFn as myCreateServerFn } from '@tanstack/start';" - path.node.specifiers.forEach((specifier) => { - identifierKeys.forEach((identifierKey) => { - const identifier = identifiers[identifierKey] + // Check for 'use server' directive in function expressions (anonymous functions) + if (path.isFunctionExpression()) { + const directives = path.node.body.directives + for (const directive of directives) { + if (directive.value.value === 'use server') { + serverFnPaths.push(path) + } + } + } - if ( - specifier.type === 'ImportSpecifier' && - specifier.imported.type === 'Identifier' - ) { - if (specifier.imported.name === identifierKey) { - identifier.name = specifier.local.name - identifier.type = 'ImportSpecifier' + // Check for 'use server' directive in arrow functions + if (path.isArrowFunctionExpression()) { + if (babel.types.isBlockStatement(path.node.body)) { + const directives = path.node.body.directives + for (const directive of directives) { + if (directive.value.value === 'use server') { + serverFnPaths.push(path) } } + } + } - // handle namespace imports like "import * as TanStackStart from '@tanstack/start';" - if (specifier.type === 'ImportNamespaceSpecifier') { - identifier.type = 'ImportNamespaceSpecifier' - identifier.namespaceId = specifier.local.name - identifier.name = `${identifier.namespaceId}.${identifierKey}` + // Check for 'use server' directive in class methods + if (path.isClassMethod()) { + const directives = path.node.body.directives + for (const directive of directives) { + if (directive.value.value === 'use server') { + serverFnPaths.push(path) } - }) - }) - }, - CallExpression: (path) => { - identifierKeys.forEach((identifierKey) => { - // Check to see if the call expression is a call to the - // identifiers[identifierKey].name - if ( - t.isIdentifier(path.node.callee) && - path.node.callee.name === identifiers[identifierKey].name - ) { - // The identifier could be a call to the original function - // in the source code. If this is case, we need to ignore it. - // Check the scope to see if the identifier is a function declaration. - // if it is, then we can ignore it. + } + } - if ( - path.scope.getBinding(identifiers[identifierKey].name)?.path - .node.type === 'FunctionDeclaration' - ) { - return + // Check for 'use server' directive in object methods + if (path.isObjectMethod()) { + const directives = path.node.body.directives + for (const directive of directives) { + if (directive.value.value === 'use server') { + serverFnPaths.push(path) } - - return identifiers[identifierKey].paths.push(path) } + } - if (t.isMemberExpression(path.node.callee)) { - if ( - t.isIdentifier(path.node.callee.object) && - t.isIdentifier(path.node.callee.property) - ) { - const callname = [ - path.node.callee.object.name, - path.node.callee.property.name, - ].join('.') - - if (callname === identifiers[identifierKey].name) { - identifiers[identifierKey].paths.push(path) + // Check for 'use server' directive in variable declarations with function expressions + if ( + path.isVariableDeclarator() && + (babel.types.isFunctionExpression(path.node.init) || + babel.types.isArrowFunctionExpression(path.node.init)) + ) { + const init = path.node.init + if (babel.types.isBlockStatement(init.body)) { + const directives = init.body.directives + for (const directive of directives) { + if (directive.value.value === 'use server') { + serverFnPaths.push(path.get('init') as babel.NodePath) } } } + } - return - }) + return serverFnPaths }, }) - - identifierKeys.forEach((identifierKey) => { - identifiers[identifierKey].paths.forEach((path) => { - identifiers[identifierKey].handleCallExpression( - path as babel.NodePath, - opts, - ) - }) - }) }, }, }) - return generate(ast, { + const compiledCode = generate(ast, { sourceMaps: true, minified: process.env.NODE_ENV === 'production', }) -} - -function handleCreateServerFnCallExpression( - path: babel.NodePath, - opts: ParseAstOptions, -) { - // The function is the 'fn' property of the object passed to createServerFn - - // const firstArg = path.node.arguments[0] - // if (t.isObjectExpression(firstArg)) { - // // Was called with some options - // } - // Traverse the member expression and find the call expressions for - // the validator, handler, and middleware methods. Check to make sure they - // are children of the createServerFn call expression. + console.log(serverFnPaths) - const calledOptions = path.node.arguments[0] - ? (path.get('arguments.0') as babel.NodePath) - : null - - const shouldValidateClient = !!calledOptions?.node.properties.find((prop) => { - return ( - t.isObjectProperty(prop) && - t.isIdentifier(prop.key) && - prop.key.name === 'validateClient' && - t.isBooleanLiteral(prop.value) && - prop.value.value === true - ) - }) - - const callExpressionPaths = { - middleware: null as babel.NodePath | null, - validator: null as babel.NodePath | null, - handler: null as babel.NodePath | null, - } - - const validMethods = Object.keys(callExpressionPaths) - - const rootCallExpression = getRootCallExpression(path) - - if (debug) - console.info( - 'Handling createServerFn call expression:', - rootCallExpression.toString(), - ) - - // Check if the call is assigned to a variable - if (!rootCallExpression.parentPath.isVariableDeclarator()) { - throw new Error('createServerFn must be assigned to a variable!') - } - - // Get the identifier name of the variable - const variableDeclarator = rootCallExpression.parentPath.node - const existingVariableName = (variableDeclarator.id as t.Identifier).name - - rootCallExpression.traverse({ - MemberExpression(memberExpressionPath) { - if (t.isIdentifier(memberExpressionPath.node.property)) { - const name = memberExpressionPath.node.property - .name as keyof typeof callExpressionPaths - - if ( - validMethods.includes(name) && - memberExpressionPath.parentPath.isCallExpression() - ) { - callExpressionPaths[name] = memberExpressionPath.parentPath - } - } - }, - }) - - if (callExpressionPaths.validator) { - const innerInputExpression = callExpressionPaths.validator.node.arguments[0] - - if (!innerInputExpression) { - throw new Error( - 'createServerFn().validator() must be called with a validator!', - ) - } - - // If we're on the client, and we're not validating the client, remove the validator call expression - if ( - opts.env === 'client' && - !shouldValidateClient && - t.isMemberExpression(callExpressionPaths.validator.node.callee) - ) { - callExpressionPaths.validator.replaceWith( - callExpressionPaths.validator.node.callee.object, - ) - } + return { + compiledCode, + serverFns: serverFnPaths, } - - // 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 = callExpressionPaths.handler?.get( - 'arguments.0', - ) as babel.NodePath - - if (!callExpressionPaths.handler || !handlerFnPath.node) { - throw codeFrameError( - opts.code, - path.node.callee.loc!, - `createServerFn must be called with a "handler" property!`, - ) - } - - 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) - // }) - - removeUseServerDirective(handlerFnPath) - - handlerFnPath.replaceWith( - t.arrowFunctionExpression( - [t.identifier('opts')], - 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.directive(t.directiveLiteral('use server'))], - ), - ), - ) - - if (opts.env === 'server') { - callExpressionPaths.handler.node.arguments.push(handlerFn) - } -} - -function removeUseServerDirective(path: babel.NodePath) { - path.traverse({ - Directive(path) { - if (path.node.value.value === 'use server') { - path.remove() - } - }, - }) } -function handleCreateMiddlewareCallExpression( - path: babel.NodePath, - opts: ParseAstOptions, -) { - // const firstArg = path.node.arguments[0] - - // if (!t.isObjectExpression(firstArg)) { - // throw new Error( - // 'createMiddleware must be called with an object of options!', - // ) - // } - - // const idProperty = firstArg.properties.find((prop) => { - // return ( - // t.isObjectProperty(prop) && - // t.isIdentifier(prop.key) && - // prop.key.name === 'id' - // ) - // }) - - // if ( - // !idProperty || - // !t.isObjectProperty(idProperty) || - // !t.isStringLiteral(idProperty.value) - // ) { - // throw new Error( - // 'createMiddleware must be called with an "id" property!', - // ) - // } - - const rootCallExpression = getRootCallExpression(path) - - if (debug) - console.info( - 'Handling createMiddleware call expression:', - rootCallExpression.toString(), - ) - - // Check if the call is assigned to a variable - // if (!rootCallExpression.parentPath.isVariableDeclarator()) { - // TODO: move this logic out to eslint or something like - // the router generator code that can do autofixes on save. - - // // If not assigned to a variable, wrap the call in a variable declaration - // const variableDeclaration = t.variableDeclaration('const', [ - // t.variableDeclarator(t.identifier(middlewareName), path.node), - // ]) - - // // The parent could be an expression statement, if it is, we need to replace - // // it with the variable declaration - // if (path.parentPath.isExpressionStatement()) { - // path.parentPath.replaceWith(variableDeclaration) - // } else { - // // If the parent is not an expression statement, then it is a statement - // // that is not an expression, like a variable declaration or a return statement. - // // In this case, we need to insert the variable declaration before the statement - // path.parentPath.insertBefore(variableDeclaration) - // } - - // // Now we need to export it. Just add an export statement - // // to the program body - // path.findParent((parentPath) => { - // if (parentPath.isProgram()) { - // parentPath.node.body.push( - // t.exportNamedDeclaration(null, [ - // t.exportSpecifier( - // t.identifier(middlewareName), - // t.identifier(middlewareName), - // ), - // ]), - // ) - // } - // return false - // }) - - // throw new Error( - // 'createMiddleware must be assigned to a variable and exported!', - // ) - // } - - // const variableDeclarator = rootCallExpression.parentPath.node - // const existingVariableName = (variableDeclarator.id as t.Identifier).name - - // const program = rootCallExpression.findParent((parentPath) => { - // return parentPath.isProgram() - // }) as babel.NodePath - - // let isExported = false as boolean - - // program.traverse({ - // ExportNamedDeclaration: (path) => { - // if ( - // path.isExportNamedDeclaration() && - // path.node.declaration && - // t.isVariableDeclaration(path.node.declaration) && - // path.node.declaration.declarations.some((decl) => { - // return ( - // t.isVariableDeclarator(decl) && - // t.isIdentifier(decl.id) && - // decl.id.name === existingVariableName - // ) - // }) - // ) { - // isExported = true - // } - // }, - // }) - - // If not exported, export it - // if (!isExported) { - // TODO: move this logic out to eslint or something like - // the router generator code that can do autofixes on save. - - // path.parentPath.parentPath.insertAfter( - // t.exportNamedDeclaration(null, [ - // t.exportSpecifier( - // t.identifier(existingVariableName), - // t.identifier(existingVariableName), - // ), - // ]), - // ) - - // throw new Error( - // 'createMiddleware must be exported as a named export!', - // ) - // } - - // The function is the 'fn' property of the object passed to createMiddleware - - // const firstArg = path.node.arguments[0] - // if (t.isObjectExpression(firstArg)) { - // // Was called with some options - // } - - // Traverse the member expression and find the call expressions for - // the validator, handler, and middleware methods. Check to make sure they - // are children of the createMiddleware call expression. - - const callExpressionPaths = { - middleware: null as babel.NodePath | null, - validator: null as babel.NodePath | null, - client: null as babel.NodePath | null, - server: null as babel.NodePath | null, - } - - const validMethods = Object.keys(callExpressionPaths) - - rootCallExpression.traverse({ - MemberExpression(memberExpressionPath) { - if (t.isIdentifier(memberExpressionPath.node.property)) { - const name = memberExpressionPath.node.property - .name as keyof typeof callExpressionPaths - - if ( - validMethods.includes(name) && - memberExpressionPath.parentPath.isCallExpression() - ) { - callExpressionPaths[name] = memberExpressionPath.parentPath - } - } - }, - }) - - if (callExpressionPaths.validator) { - const innerInputExpression = callExpressionPaths.validator.node.arguments[0] - - if (!innerInputExpression) { - throw new Error( - 'createMiddleware().validator() must be called with a validator!', - ) - } - - // If we're on the client, remove the validator call expression - if (opts.env === 'client') { - if (t.isMemberExpression(callExpressionPaths.validator.node.callee)) { - callExpressionPaths.validator.replaceWith( - callExpressionPaths.validator.node.callee.object, - ) - } - } - } - - const useFnPath = callExpressionPaths.server?.get( - 'arguments.0', - ) as babel.NodePath - - if (!callExpressionPaths.server || !useFnPath.node) { - throw new Error('createMiddleware must be called with a "use" property!') - } - - // If we're on the client, remove the use call expression - - if (opts.env === 'client') { - if (t.isMemberExpression(callExpressionPaths.server.node.callee)) { - callExpressionPaths.server.replaceWith( - callExpressionPaths.server.node.callee.object, - ) - } - } -} - -function buildEnvOnlyCallExpressionHandler(env: 'client' | 'server') { - return function envOnlyCallExpressionHandler( - path: babel.NodePath, - opts: ParseAstOptions, - ) { - if (debug) - console.info(`Handling ${env}Only call expression:`, path.toString()) - - if (opts.env === env) { - // extract the inner function from the call expression - const innerInputExpression = path.node.arguments[0] - - if (!t.isExpression(innerInputExpression)) { - throw new Error( - `${env}Only() functions must be called with a function!`, - ) - } - - path.replaceWith(innerInputExpression) - return - } - - // If we're on the wrong environment, replace the call expression - // with a function that always throws an error. - path.replaceWith( - t.arrowFunctionExpression( - [], - t.blockStatement([ - t.throwStatement( - t.newExpression(t.identifier('Error'), [ - t.stringLiteral( - `${env}Only() functions can only be called on the ${env}!`, - ), - ]), - ), - ]), - ), - ) - } -} - -function handleCreateIsomorphicFnCallExpression( - path: babel.NodePath, - opts: ParseAstOptions, -) { - const rootCallExpression = getRootCallExpression(path) - - if (debug) - console.info( - 'Handling createIsomorphicFn call expression:', - rootCallExpression.toString(), - ) - - const callExpressionPaths = { - client: null as babel.NodePath | null, - server: null as babel.NodePath | null, - } - - const validMethods = Object.keys(callExpressionPaths) - - rootCallExpression.traverse({ - MemberExpression(memberExpressionPath) { - if (t.isIdentifier(memberExpressionPath.node.property)) { - const name = memberExpressionPath.node.property - .name as keyof typeof callExpressionPaths +export function compileServerFnServer(opts: ParseAstOptions) { + const ast = parseAst(opts) - if ( - validMethods.includes(name) && - memberExpressionPath.parentPath.isCallExpression() - ) { - callExpressionPaths[name] = memberExpressionPath.parentPath - } - } - }, + return generate(ast, { + sourceMaps: true, + minified: process.env.NODE_ENV === 'production', }) - - if ( - validMethods.every( - (method) => - !callExpressionPaths[method as keyof typeof callExpressionPaths], - ) - ) { - const variableId = rootCallExpression.parentPath.isVariableDeclarator() - ? rootCallExpression.parentPath.node.id - : null - console.warn( - 'createIsomorphicFn called without a client or server implementation!', - 'This will result in a no-op function.', - 'Variable name:', - t.isIdentifier(variableId) ? variableId.name : 'unknown', - ) - } - - const envCallExpression = callExpressionPaths[opts.env] - - if (!envCallExpression) { - // if we don't have an implementation for this environment, default to a no-op - rootCallExpression.replaceWith( - t.arrowFunctionExpression([], t.blockStatement([])), - ) - return - } - - const innerInputExpression = envCallExpression.node.arguments[0] - - if (!t.isExpression(innerInputExpression)) { - throw new Error( - `createIsomorphicFn().${opts.env}(func) must be called with a function!`, - ) - } - - rootCallExpression.replaceWith(innerInputExpression) -} - -function getRootCallExpression(path: babel.NodePath) { - // Find the highest callExpression parent - let rootCallExpression: babel.NodePath = path - - // Traverse up the chain of CallExpressions - while (rootCallExpression.parentPath.isMemberExpression()) { - const parent = rootCallExpression.parentPath - if (parent.parentPath.isCallExpression()) { - rootCallExpression = parent.parentPath - } - } - - return rootCallExpression } function codeFrameError( diff --git a/packages/server-functions-vite-plugin/src/index.ts b/packages/server-functions-vite-plugin/src/index.ts index 6067851fd3..709c86a4f4 100644 --- a/packages/server-functions-vite-plugin/src/index.ts +++ b/packages/server-functions-vite-plugin/src/index.ts @@ -1,14 +1,26 @@ import { fileURLToPath, pathToFileURL } from 'node:url' +import { compileServerFnClient } from './compilers' import type { Plugin } from 'vite' const debug = Boolean(process.env.TSR_VITE_DEBUG) -export type ServerFunctionsViteOptions = {} +export type ServerFunctionsViteOptions = { + runtimeCode: string + replacer: (opts: { filename: string; functionId: string }) => string +} const useServerRx = /"use server"|'use server'/ -export function TanStackStartViteServerFn(): Array { +export type CreateRpcFn = (opts: { + fn: (...args: Array) => any + filename: string + functionId: string +}) => (...args: Array) => any + +export function TanStackServerFnPluginClient( + opts: ServerFunctionsViteOptions, +): Array { // opts: ServerFunctionsViteOptions, let ROOT: string = process.cwd() @@ -28,7 +40,15 @@ export function TanStackStartViteServerFn(): Array { return null } - const { compiledCode, serverFns } = compileServerFnServer({ + if (debug) console.info('') + if (debug) console.info('Compiled createServerFn Input') + if (debug) console.info('') + if (debug) console.info(code) + if (debug) console.info('') + if (debug) console.info('') + if (debug) console.info('') + + const { compiledCode, serverFns } = compileServerFnClient({ code, root: ROOT, filename: id, @@ -45,6 +65,13 @@ export function TanStackStartViteServerFn(): Array { return compiledCode }, }, + ] +} + +export function TanStackServerFnPluginServer( + opts: ServerFunctionsViteOptions, +): Array { + return [ { name: 'tanstack-start-server-fn-server-vite-plugin', transform(code, id) { diff --git a/packages/start/package.json b/packages/start/package.json index 549da390f1..1f3f1fe2fa 100644 --- a/packages/start/package.json +++ b/packages/start/package.json @@ -139,6 +139,7 @@ "@tanstack/router-generator": "workspace:^", "@tanstack/router-plugin": "workspace:^", "@tanstack/start-vite-plugin": "workspace:^", + "@tanstack/server-functions-plugin": "workspace:^", "@vinxi/react": "0.2.5", "@vinxi/react-server-dom": "^0.0.3", "@vinxi/server-components": "0.5.0", diff --git a/packages/start/src/client-runtime/index.tsx b/packages/start/src/client-runtime/index.tsx index 2c02f6b9c7..9321438a70 100644 --- a/packages/start/src/client-runtime/index.tsx +++ b/packages/start/src/client-runtime/index.tsx @@ -1,19 +1,20 @@ import { fetcher } from './fetcher' import { getBaseUrl } from './getBaseUrl' -import type { CreateRpcFn } from '@tanstack/start/server-functions-plugin' +import type { CreateRpcFn } from '@tanstack/server-functions-plugin' -export const createClientRpc: CreateRpcFn = ( - filename: string, - functionId: string, -) => { - const base = getBaseUrl(window.location.origin, filename, functionId) +export const createClientRpc: CreateRpcFn = (opts) => { + const base = getBaseUrl( + window.location.origin, + opts.filename, + opts.functionId, + ) const fn = (...args: Array) => fetcher(base, args, fetch) return Object.assign(fn, { url: base, - filename, - functionId, + filename: opts.filename, + functionId: opts.functionId, }) } diff --git a/packages/start/src/config/index.ts b/packages/start/src/config/index.ts index 8e0d5dbdc2..093c4f875e 100644 --- a/packages/start/src/config/index.ts +++ b/packages/start/src/config/index.ts @@ -14,7 +14,10 @@ import { createApp } from 'vinxi' import { config } from 'vinxi/plugins/config' // // @ts-expect-error // import { serverComponents } from '@vinxi/server-components/plugin' -import { createServerFunctionsPlugin } from '@tanstack/start/server-functions-plugin' +import { + TanStackServerFnPluginClient, + TanStackServerFnPluginServer, +} from '@tanstack/server-functions-plugin' import { tanstackStartVinxiFileRouter } from './vinxi-file-router.js' import { checkDeploymentPresetInput, @@ -120,8 +123,6 @@ export function defineConfig( const apiEntryExists = existsSync(apiEntry) - const serverFunctionsPlugin = createServerFunctionsPlugin() - let vinxiApp = createApp({ server: { ...serverOptions, @@ -169,7 +170,7 @@ export function defineConfig( }), ...(viteConfig.plugins || []), ...(clientViteConfig.plugins || []), - serverFunctionsPlugin.client({ + TanStackServerFnPluginClient({ runtimeCode: `import { createClientRpc } from '@tanstack/start/client-runtime'`, replacer: (opts) => `createClientRpc('${opts.filename}', '${opts.functionId}')`, @@ -212,7 +213,7 @@ export function defineConfig( }), ...(getUserViteConfig(opts.vite).plugins || []), ...(getUserViteConfig(opts.routers?.ssr?.vite).plugins || []), - serverFunctionsPlugin.server({ + TanStackServerFnPluginServer({ runtimeCode: `import { createServerRpc } from '@tanstack/start/ssr-runtime'`, replacer: (opts) => `createSsrRpc('${opts.filename}', '${opts.functionId}')`, @@ -257,7 +258,7 @@ export function defineConfig( ...injectDefineEnv('TSS_API_BASE', apiBase), }, }), - serverFunctionsPlugin.server({ + TanStackServerFnPluginServer({ runtimeCode: `import { createServerRpc } from '@tanstack/start/server-runtime'`, replacer: (opts) => `createServerRpc('${opts.filename}', '${opts.functionId}')`, diff --git a/packages/start/src/server-runtime/index.tsx b/packages/start/src/server-runtime/index.tsx index 5d1a68cb87..6c68ed7866 100644 --- a/packages/start/src/server-runtime/index.tsx +++ b/packages/start/src/server-runtime/index.tsx @@ -1,17 +1,13 @@ // import { getBaseUrl } from '../client-runtime/getBaseUrl' -import type { CreateRpcFn } from '@tanstack/start/server-functions-plugin' +import type { CreateRpcFn } from '@tanstack/server-functions-plugin' -export const createServerRpc: CreateRpcFn = ( - fn: any, - filename: string, - functionId: string, -) => { +export const createServerRpc: CreateRpcFn = (opts) => { // const functionUrl = getBaseUrl('http://localhost:3000', id, name) const functionUrl = 'https://localhost:3000' - return Object.assign(fn, { + return Object.assign(opts.fn, { url: functionUrl, - filename, - functionId, + filename: opts.filename, + functionId: opts.functionId, }) } diff --git a/packages/start/src/ssr-runtime/index.tsx b/packages/start/src/ssr-runtime/index.tsx index f2c3582da6..f4dadc87f7 100644 --- a/packages/start/src/ssr-runtime/index.tsx +++ b/packages/start/src/ssr-runtime/index.tsx @@ -4,7 +4,7 @@ import invariant from 'tiny-invariant' import { fetcher } from '../client-runtime/fetcher' import { getBaseUrl } from '../client-runtime/getBaseUrl' import { handleServerRequest } from '../server-handler/index' -import type { CreateRpcFn } from '@tanstack/start/server-functions-plugin' +import type { CreateRpcFn } from '@tanstack/server-functions-plugin' export function createIncomingMessage( url: string, @@ -78,12 +78,8 @@ export function createIncomingMessage( const fakeHost = 'http://localhost:3000' -export const createSsrRpc: CreateRpcFn = ( - _fn: any, - filename: string, - functionId: string, -) => { - const functionUrl = getBaseUrl(fakeHost, filename, functionId) +export const createSsrRpc: CreateRpcFn = (opts) => { + const functionUrl = getBaseUrl(fakeHost, opts.filename, opts.functionId) const proxyFn = (...args: Array) => { invariant( @@ -155,7 +151,7 @@ export const createSsrRpc: CreateRpcFn = ( return Object.assign(proxyFn, { url: functionUrl.replace(fakeHost, ''), - filename, - functionId, + filename: opts.filename, + functionId: opts.functionId, }) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index af6cc1cdad..727e356e75 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3501,6 +3501,57 @@ importers: specifier: workspace:* version: link:../router-plugin + packages/server-functions-vite-plugin: + dependencies: + '@babel/code-frame': + specifier: 7.26.2 + version: 7.26.2 + '@babel/core': + specifier: ^7.26.0 + version: 7.26.0 + '@babel/generator': + specifier: ^7.26.3 + version: 7.26.3 + '@babel/parser': + specifier: ^7.26.3 + version: 7.26.3 + '@babel/plugin-syntax-jsx': + specifier: ^7.25.9 + version: 7.25.9(@babel/core@7.26.0) + '@babel/plugin-syntax-typescript': + specifier: ^7.25.9 + version: 7.25.9(@babel/core@7.26.0) + '@babel/template': + specifier: ^7.25.9 + version: 7.25.9 + '@babel/traverse': + specifier: ^7.26.4 + version: 7.26.4 + '@babel/types': + specifier: ^7.26.3 + version: 7.26.3 + '@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__generator': + specifier: ^7.6.8 + version: 7.6.8 + '@types/babel__template': + specifier: ^7.4.4 + version: 7.4.4 + '@types/babel__traverse': + specifier: ^7.20.6 + version: 7.20.6 + babel-dead-code-elimination: + specifier: ^1.0.6 + version: 1.0.6 + tiny-invariant: + specifier: ^1.3.3 + version: 1.3.3 + packages/start: dependencies: '@tanstack/react-cross-context': @@ -3515,6 +3566,9 @@ importers: '@tanstack/router-plugin': specifier: workspace:* version: link:../router-plugin + '@tanstack/server-functions-plugin': + specifier: workspace:^ + version: link:../server-functions-vite-plugin '@tanstack/start-vite-plugin': specifier: workspace:* version: link:../start-vite-plugin From 9ac11e8bcf28927134c740933972b20bce494f48 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Thu, 12 Dec 2024 10:16:36 -0700 Subject: [PATCH 03/38] cleanup --- package.json | 4 +--- packages/start/package.json | 6 +++--- .../tsconfigs/server-functions-plugin.tsconfig.json | 10 ---------- pnpm-lock.yaml | 2 -- 4 files changed, 4 insertions(+), 18 deletions(-) delete mode 100644 packages/start/tsconfigs/server-functions-plugin.tsconfig.json diff --git a/package.json b/package.json index 1c40996f6d..4007d6ba70 100644 --- a/package.json +++ b/package.json @@ -81,9 +81,7 @@ "@tanstack/arktype-adapter": "workspace:*", "@tanstack/start": "workspace:*", "@tanstack/start-vite-plugin": "workspace:*", - "@tanstack/eslint-plugin-router": "workspace:*", - "temp-react": "0.0.0-experimental-035a41c4e-20230704", - "temp-react-dom": "0.0.0-experimental-035a41c4e-20230704" + "@tanstack/eslint-plugin-router": "workspace:*" } } } diff --git a/packages/start/package.json b/packages/start/package.json index 1f3f1fe2fa..70f1fcc9e8 100644 --- a/packages/start/package.json +++ b/packages/start/package.json @@ -36,10 +36,10 @@ "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js", "test:types:ts57": "tsc", "test:build": "publint --strict && attw --ignore-rules no-resolution --pack .", - "build": "pnpm build:config && pnpm build:router-manifest && pnpm build:vite", + "build": "pnpm build:vite && pnpm build:config && pnpm build:router-manifest", + "build:vite": "vite build", "build:config": "tsc --project tsconfigs/config.tsconfig.json", - "build:router-manifest": "tsc --project tsconfigs/router-manifest.tsconfig.json", - "build:vite": "vite build" + "build:router-manifest": "tsc --project tsconfigs/router-manifest.tsconfig.json" }, "type": "module", "exports": { diff --git a/packages/start/tsconfigs/server-functions-plugin.tsconfig.json b/packages/start/tsconfigs/server-functions-plugin.tsconfig.json deleted file mode 100644 index 440bc758da..0000000000 --- a/packages/start/tsconfigs/server-functions-plugin.tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../../tsconfig.json", - "include": ["../src/server-functions-plugin/index.ts"], - "compilerOptions": { - "rootDir": "../src/server-functions-plugin", - "outDir": "../dist/esm/server-functions-plugin", - "target": "esnext", - "noEmit": false - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 727e356e75..e97a5cd541 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,8 +23,6 @@ overrides: '@tanstack/start': workspace:* '@tanstack/start-vite-plugin': workspace:* '@tanstack/eslint-plugin-router': workspace:* - temp-react: 0.0.0-experimental-035a41c4e-20230704 - temp-react-dom: 0.0.0-experimental-035a41c4e-20230704 importers: From 35823231c30ed64fad0da457e124bfa8af5cd894 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Thu, 12 Dec 2024 12:11:14 -0700 Subject: [PATCH 04/38] checkpoint --- .../react/start-basic/app/routes/deferred.tsx | 4 +- .../react/start-basic/app/utils/posts.tsx | 2 +- .../start-supabase-basic/app/utils/posts.ts | 2 +- .../src/compilers.ts | 223 ++++++++++++++---- .../server-functions-vite-plugin/src/index.ts | 2 + 5 files changed, 186 insertions(+), 47 deletions(-) diff --git a/examples/react/start-basic/app/routes/deferred.tsx b/examples/react/start-basic/app/routes/deferred.tsx index 7cc7f8b9b5..606ab69b80 100644 --- a/examples/react/start-basic/app/routes/deferred.tsx +++ b/examples/react/start-basic/app/routes/deferred.tsx @@ -3,13 +3,13 @@ import { createServerFn } from '@tanstack/start' import { Suspense, useState } from 'react' const personServerFn = createServerFn({ method: 'GET' }) - .validator((d) => d as string) + .validator((d: string) => d) .handler(({ data: name }) => { return { name, randomNumber: Math.floor(Math.random() * 100) } }) const slowServerFn = createServerFn({ method: 'GET' }) - .validator((d) => d as string) + .validator((d: string) => d) .handler(async ({ data: name }) => { await new Promise((r) => setTimeout(r, 1000)) return { name, randomNumber: Math.floor(Math.random() * 100) } diff --git a/examples/react/start-basic/app/utils/posts.tsx b/examples/react/start-basic/app/utils/posts.tsx index 196498f8c9..99e43f5197 100644 --- a/examples/react/start-basic/app/utils/posts.tsx +++ b/examples/react/start-basic/app/utils/posts.tsx @@ -11,7 +11,7 @@ export type PostType = { export const fetchPost = createServerFn({ method: 'GET' }) .middleware([logMiddleware]) - .validator((d) => d as string) + .validator((d: string) => d) .handler(async ({ data }) => { console.info(`Fetching post with id ${data}...`) const post = await axios diff --git a/examples/react/start-supabase-basic/app/utils/posts.ts b/examples/react/start-supabase-basic/app/utils/posts.ts index 3c8b8d2434..6eb213af1f 100644 --- a/examples/react/start-supabase-basic/app/utils/posts.ts +++ b/examples/react/start-supabase-basic/app/utils/posts.ts @@ -9,7 +9,7 @@ export type PostType = { } export const fetchPost = createServerFn({ method: 'GET' }) - .validator((d) => d as string) + .validator((d: string) => d) .handler(async ({ data: postId }) => { console.info(`Fetching post with id ${postId}...`) const post = await axios diff --git a/packages/server-functions-vite-plugin/src/compilers.ts b/packages/server-functions-vite-plugin/src/compilers.ts index 1103bba03b..b08101463d 100644 --- a/packages/server-functions-vite-plugin/src/compilers.ts +++ b/packages/server-functions-vite-plugin/src/compilers.ts @@ -1,11 +1,18 @@ +import path from 'node:path' import * as babel from '@babel/core' -import generate from '@babel/generator' -import { codeFrameColumns } from '@babel/code-frame' +import _generate from '@babel/generator' import { deadCodeElimination } from 'babel-dead-code-elimination' +import { isIdentifier, isVariableDeclarator } from '@babel/types' import { parseAst } from './ast' import type { ParseAstOptions } from './ast' +let generate = _generate + +if ('default' in generate) { + generate = generate.default as typeof generate +} + export function compileEliminateDeadCode(opts: ParseAstOptions) { const ast = parseAst(opts) deadCodeElimination(ast) @@ -15,71 +22,203 @@ export function compileEliminateDeadCode(opts: ParseAstOptions) { }) } -const debug = process.env.TSR_VITE_DEBUG === 'true' +// const debug = process.env.TSR_VITE_DEBUG === 'true' -export function compileServerFnClient(opts: ParseAstOptions) { - const ast = parseAst(opts) +function findNearestVariableName(path: babel.NodePath): 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('_') + } + } - const serverFnPaths: Array = [] + // 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) + ) { + if (babel.types.isIdentifier(currentPath.node.key)) { + return currentPath.node.key.name + } + if (babel.types.isStringLiteral(currentPath.node.key)) { + return currentPath.node.key.value + } + } + + return null + })() + + if (name) { + nameParts.unshift(name) + } + + currentPath = currentPath.parentPath + } + + return nameParts.length > 0 ? nameParts.join('_') : 'anonymous' +} + +function makeFileLocationUrlSafe(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 +} + +export function compileServerFnClient( + opts: ParseAstOptions & { + runtimeCode: string + replacer: (opts: { filename: string; functionId: string }) => string + }, +) { + const ast = parseAst(opts) + const serverFnPathsByLabel: Record< + string, + { + nodePath: babel.NodePath + functionName: string + functionId: string + } + > = {} + const counts: Record = {} babel.traverse(ast, { Program: { enter(programPath) { + const recordServerFunction = (nodePath: babel.NodePath) => { + const fnName = findNearestVariableName(nodePath) + + const baseLabel = makeFileLocationUrlSafe( + `${opts.filename.replace( + path.extname(opts.filename), + '', + )}--${fnName}`.replace(opts.root, ''), + ) + + counts[baseLabel] = (counts[baseLabel] || 0) + 1 + + const label = + counts[baseLabel] > 1 + ? `${baseLabel}_${counts[baseLabel] - 1}` + : baseLabel + + serverFnPathsByLabel[label] = { + nodePath, + functionName: fnName || '', + functionId: label, + } + } + programPath.traverse({ enter(path) { - // Check for 'use server' directive in function declarations (named functions) + // Function declarations if (path.isFunctionDeclaration()) { const directives = path.node.body.directives for (const directive of directives) { if (directive.value.value === 'use server') { - serverFnPaths.push(path) + recordServerFunction(path) } } } - // Check for 'use server' directive in function expressions (anonymous functions) + // Function expressions if (path.isFunctionExpression()) { const directives = path.node.body.directives for (const directive of directives) { if (directive.value.value === 'use server') { - serverFnPaths.push(path) + recordServerFunction(path) } } } - // Check for 'use server' directive in arrow functions + // Arrow functions if (path.isArrowFunctionExpression()) { if (babel.types.isBlockStatement(path.node.body)) { const directives = path.node.body.directives for (const directive of directives) { if (directive.value.value === 'use server') { - serverFnPaths.push(path) + recordServerFunction(path) } } } } - // Check for 'use server' directive in class methods + // Class methods if (path.isClassMethod()) { const directives = path.node.body.directives for (const directive of directives) { if (directive.value.value === 'use server') { - serverFnPaths.push(path) + recordServerFunction(path) } } } - // Check for 'use server' directive in object methods + // Object methods if (path.isObjectMethod()) { const directives = path.node.body.directives for (const directive of directives) { if (directive.value.value === 'use server') { - serverFnPaths.push(path) + recordServerFunction(path) } } } - // Check for 'use server' directive in variable declarations with function expressions + // Variable declarations with function expressions if ( path.isVariableDeclarator() && (babel.types.isFunctionExpression(path.node.init) || @@ -90,13 +229,11 @@ export function compileServerFnClient(opts: ParseAstOptions) { const directives = init.body.directives for (const directive of directives) { if (directive.value.value === 'use server') { - serverFnPaths.push(path.get('init') as babel.NodePath) + recordServerFunction(path.get('init') as babel.NodePath) } } } } - - return serverFnPaths }, }) }, @@ -108,11 +245,11 @@ export function compileServerFnClient(opts: ParseAstOptions) { minified: process.env.NODE_ENV === 'production', }) - console.log(serverFnPaths) + console.log(serverFnPathsByLabel) return { compiledCode, - serverFns: serverFnPaths, + serverFns: serverFnPathsByLabel, } } @@ -125,25 +262,25 @@ export function compileServerFnServer(opts: ParseAstOptions) { }) } -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, - }, - ) +// 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) -} +// return new Error(frame) +// } diff --git a/packages/server-functions-vite-plugin/src/index.ts b/packages/server-functions-vite-plugin/src/index.ts index 709c86a4f4..d6fe2101a5 100644 --- a/packages/server-functions-vite-plugin/src/index.ts +++ b/packages/server-functions-vite-plugin/src/index.ts @@ -52,6 +52,8 @@ export function TanStackServerFnPluginClient( code, root: ROOT, filename: id, + runtimeCode: opts.runtimeCode, + replacer: opts.replacer, }) if (debug) console.info('') From fa7c03592aedfa9630d880f75f393ebb26b743fc Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Thu, 12 Dec 2024 14:48:08 -0700 Subject: [PATCH 05/38] client babel now works, runtime and rpc injection --- .../server-functions-vite-plugin/package.json | 12 +- .../src/compilers.ts | 68 ++- .../server-functions-vite-plugin/src/index.ts | 33 +- .../src/logger.ts | 59 +++ .../tests/compiler.test.ts | 478 ++++++++++++++++++ .../createIsomorphicFn.test.ts | 90 ---- .../client/createIsomorphicFnDestructured.tsx | 16 - .../createIsomorphicFnDestructuredRename.tsx | 16 - .../client/createIsomorphicFnStarImport.tsx | 16 - .../server/createIsomorphicFnDestructured.tsx | 16 - .../createIsomorphicFnDestructuredRename.tsx | 16 - .../server/createIsomorphicFnStarImport.tsx | 16 - .../createIsomorphicFnDestructured.tsx | 35 -- .../createIsomorphicFnDestructuredRename.tsx | 35 -- .../createIsomorphicFnStarImport.tsx | 37 -- .../createMiddleware/createMiddleware.test.ts | 36 -- .../client/createMiddlewareDestructured.tsx | 24 - .../createMiddlewareDestructuredRename.tsx | 24 - .../client/createMiddlewareStarImport.tsx | 24 - .../client/createMiddlewareValidator.tsx | 5 - .../server/createMiddlewareDestructured.tsx | 36 -- .../createMiddlewareDestructuredRename.tsx | 36 -- .../server/createMiddlewareStarImport.tsx | 38 -- .../server/createMiddlewareValidator.tsx | 7 - .../createMiddlewareDestructured.tsx | 51 -- .../createMiddlewareDestructuredRename.tsx | 51 -- .../test-files/createMiddlewareStarImport.tsx | 52 -- .../test-files/createMiddlewareValidator.tsx | 8 - .../createServerFn/createServerFn.test.ts | 62 --- .../client/createServerFnDestructured.tsx | 61 --- .../createServerFnDestructuredRename.tsx | 40 -- .../client/createServerFnStarImport.tsx | 40 -- .../client/createServerFnValidator.tsx | 9 - .../server/createServerFnDestructured.tsx | 77 --- .../createServerFnDestructuredRename.tsx | 52 -- .../server/createServerFnStarImport.tsx | 52 -- .../server/createServerFnValidator.tsx | 11 - .../test-files/createServerFnDestructured.tsx | 67 --- .../createServerFnDestructuredRename.tsx | 51 -- .../test-files/createServerFnStarImport.tsx | 52 -- .../test-files/createServerFnValidator.tsx | 8 - .../tests/envOnly/envOnly.test.ts | 58 --- .../snapshots/client/envOnlyDestructured.tsx | 5 - .../client/envOnlyDestructuredRename.tsx | 5 - .../snapshots/client/envOnlyStarImport.tsx | 5 - .../snapshots/server/envOnlyDestructured.tsx | 5 - .../server/envOnlyDestructuredRename.tsx | 5 - .../snapshots/server/envOnlyStarImport.tsx | 5 - .../test-files/envOnlyDestructured.tsx | 5 - .../test-files/envOnlyDestructuredRename.tsx | 5 - .../envOnly/test-files/envOnlyStarImport.tsx | 5 - packages/start/src/config/index.ts | 9 +- pnpm-lock.yaml | 20 + 53 files changed, 645 insertions(+), 1404 deletions(-) create mode 100644 packages/server-functions-vite-plugin/src/logger.ts create mode 100644 packages/server-functions-vite-plugin/tests/compiler.test.ts delete mode 100644 packages/server-functions-vite-plugin/tests/createIsomorphicFn/createIsomorphicFn.test.ts delete mode 100644 packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructured.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructuredRename.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnStarImport.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructured.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructuredRename.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnStarImport.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnDestructured.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnDestructuredRename.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnStarImport.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createMiddleware/createMiddleware.test.ts delete mode 100644 packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareDestructured.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareDestructuredRename.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareStarImport.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareValidator.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareDestructured.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareDestructuredRename.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareStarImport.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareValidator.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareDestructured.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareDestructuredRename.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareStarImport.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareValidator.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createServerFn/createServerFn.test.ts delete mode 100644 packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnDestructured.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnDestructuredRename.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnStarImport.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnValidator.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnDestructured.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnDestructuredRename.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnStarImport.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnValidator.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnDestructured.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnDestructuredRename.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnStarImport.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnValidator.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/envOnly/envOnly.test.ts delete mode 100644 packages/server-functions-vite-plugin/tests/envOnly/snapshots/client/envOnlyDestructured.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/envOnly/snapshots/client/envOnlyDestructuredRename.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/envOnly/snapshots/client/envOnlyStarImport.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/envOnly/snapshots/server/envOnlyDestructured.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/envOnly/snapshots/server/envOnlyDestructuredRename.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/envOnly/snapshots/server/envOnlyStarImport.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/envOnly/test-files/envOnlyDestructured.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/envOnly/test-files/envOnlyDestructuredRename.tsx delete mode 100644 packages/server-functions-vite-plugin/tests/envOnly/test-files/envOnlyStarImport.tsx diff --git a/packages/server-functions-vite-plugin/package.json b/packages/server-functions-vite-plugin/package.json index 2dbf643a41..2dd6e26e72 100644 --- a/packages/server-functions-vite-plugin/package.json +++ b/packages/server-functions-vite-plugin/package.json @@ -27,6 +27,7 @@ "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:ts52": "node ../../node_modules/typescript52/lib/tsc.js", @@ -64,8 +65,8 @@ "node": ">=12" }, "dependencies": { - "@babel/core": "^7.26.0", "@babel/code-frame": "7.26.2", + "@babel/core": "^7.26.0", "@babel/generator": "^7.26.3", "@babel/parser": "^7.26.3", "@babel/plugin-syntax-jsx": "^7.25.9", @@ -73,12 +74,15 @@ "@babel/template": "^7.25.9", "@babel/traverse": "^7.26.4", "@babel/types": "^7.26.3", + "@types/babel__code-frame": "^7.0.6", "@types/babel__core": "^7.20.5", "@types/babel__generator": "^7.6.8", - "@types/babel__code-frame": "^7.0.6", "@types/babel__template": "^7.4.4", "@types/babel__traverse": "^7.20.6", - "tiny-invariant": "^1.3.3", - "babel-dead-code-elimination": "^1.0.6" + "@types/diff": "^6.0.0", + "babel-dead-code-elimination": "^1.0.6", + "chalk": "^5.3.0", + "diff": "^7.0.0", + "tiny-invariant": "^1.3.3" } } diff --git a/packages/server-functions-vite-plugin/src/compilers.ts b/packages/server-functions-vite-plugin/src/compilers.ts index b08101463d..b9679e9c6e 100644 --- a/packages/server-functions-vite-plugin/src/compilers.ts +++ b/packages/server-functions-vite-plugin/src/compilers.ts @@ -122,12 +122,17 @@ function makeFileLocationUrlSafe(location: string): string { export function compileServerFnClient( opts: ParseAstOptions & { - runtimeCode: string + getRuntimeCode: (opts: { + serverFnPathsByFunctionId: Record< + string, + { nodePath: babel.NodePath; functionName: string; functionId: string } + > + }) => string replacer: (opts: { filename: string; functionId: string }) => string }, ) { const ast = parseAst(opts) - const serverFnPathsByLabel: Record< + const serverFnPathsByFunctionId: Record< string, { nodePath: babel.NodePath @@ -152,15 +157,15 @@ export function compileServerFnClient( counts[baseLabel] = (counts[baseLabel] || 0) + 1 - const label = + const functionId = counts[baseLabel] > 1 ? `${baseLabel}_${counts[baseLabel] - 1}` : baseLabel - serverFnPathsByLabel[label] = { + serverFnPathsByFunctionId[functionId] = { nodePath, functionName: fnName || '', - functionId: label, + functionId: functionId, } } @@ -236,6 +241,49 @@ export function compileServerFnClient( } }, }) + + if (Object.keys(serverFnPathsByFunctionId).length > 0) { + const runtimeImport = babel.template.statement( + opts.getRuntimeCode({ serverFnPathsByFunctionId }), + )() + ast.program.body.unshift(runtimeImport) + } + + // Replace server functions with client-side stubs + for (const [functionId, fnPath] of Object.entries( + serverFnPathsByFunctionId, + )) { + const replacementCode = opts.replacer({ + filename: opts.filename, + functionId: functionId, + }) + const replacement = babel.template.expression(replacementCode)() + + // For variable declarations, replace the init + if (fnPath.nodePath.isVariableDeclarator()) { + fnPath.nodePath.get('init').replaceWith(replacement) + } + // For class/object methods, replace the whole method + else if ( + fnPath.nodePath.isClassMethod() || + fnPath.nodePath.isObjectMethod() + ) { + fnPath.nodePath.replaceWith( + babel.types.classMethod( + fnPath.nodePath.node.kind, + fnPath.nodePath.node.key, + fnPath.nodePath.node.params, + babel.types.blockStatement([ + babel.types.returnStatement(replacement), + ]), + ), + ) + } + // For function expressions, replace the whole path + else { + fnPath.nodePath.replaceWith(replacement) + } + } }, }, }) @@ -245,21 +293,23 @@ export function compileServerFnClient( minified: process.env.NODE_ENV === 'production', }) - console.log(serverFnPathsByLabel) - return { compiledCode, - serverFns: serverFnPathsByLabel, + serverFns: serverFnPathsByFunctionId, } } export function compileServerFnServer(opts: ParseAstOptions) { const ast = parseAst(opts) - return generate(ast, { + const compiledCode = generate(ast, { sourceMaps: true, minified: process.env.NODE_ENV === 'production', }) + + return { + compiledCode, + } } // function codeFrameError( diff --git a/packages/server-functions-vite-plugin/src/index.ts b/packages/server-functions-vite-plugin/src/index.ts index d6fe2101a5..8ee6b4cda8 100644 --- a/packages/server-functions-vite-plugin/src/index.ts +++ b/packages/server-functions-vite-plugin/src/index.ts @@ -1,12 +1,22 @@ import { fileURLToPath, pathToFileURL } from 'node:url' import { compileServerFnClient } from './compilers' +import { logDiff } from './logger' import type { Plugin } from 'vite' -const debug = Boolean(process.env.TSR_VITE_DEBUG) +const debug = Boolean(process.env.TSR_VITE_DEBUG) || (true as boolean) export type ServerFunctionsViteOptions = { - runtimeCode: string + getRuntimeCode: (opts: { + serverFnPathsByFunctionId: Record< + string, + { + nodePath: babel.NodePath + functionName: string + functionId: string + } + > + }) => string replacer: (opts: { filename: string; functionId: string }) => string } @@ -40,29 +50,16 @@ export function TanStackServerFnPluginClient( return null } - if (debug) console.info('') - if (debug) console.info('Compiled createServerFn Input') - if (debug) console.info('') - if (debug) console.info(code) - if (debug) console.info('') - if (debug) console.info('') - if (debug) console.info('') - const { compiledCode, serverFns } = compileServerFnClient({ code, root: ROOT, filename: id, - runtimeCode: opts.runtimeCode, + getRuntimeCode: opts.getRuntimeCode, replacer: opts.replacer, }) - if (debug) console.info('') - if (debug) console.info('Compiled createServerFn Output') - if (debug) console.info('') - if (debug) console.info(compiledCode) - if (debug) console.info('') - if (debug) console.info('') - if (debug) console.info('') + if (debug) console.info('createServerFn Input/Output') + if (debug) logDiff(code, compiledCode.code.replace(/ctx/g, 'blah')) return compiledCode }, diff --git a/packages/server-functions-vite-plugin/src/logger.ts b/packages/server-functions-vite-plugin/src/logger.ts new file mode 100644 index 0000000000..e1a3c37eea --- /dev/null +++ b/packages/server-functions-vite-plugin/src/logger.ts @@ -0,0 +1,59 @@ +import chalk from 'chalk' +import { diffChars } from 'diff' + +export function logDiff(oldStr: string, newStr: string) { + const differences = diffChars(oldStr, newStr) + + let output = '' + let unchangedLines = '' + + function processUnchangedLines(lines: string): string { + const lineArray = lines.split('\n') + if (lineArray.length > 4) { + return [ + chalk.dim(lineArray[0]), + chalk.dim(lineArray[1]), + '', + chalk.dim.bold(`... (${lineArray.length - 4} lines) ...`), + '', + chalk.dim(lineArray[lineArray.length - 2]), + chalk.dim(lineArray[lineArray.length - 1]), + ].join('\n') + } + return chalk.dim(lines) + } + + differences.forEach((part, index) => { + const nextPart = differences[index + 1] + + if (part.added) { + if (unchangedLines) { + output += processUnchangedLines(unchangedLines) + unchangedLines = '' + } + output += chalk.green.bold(part.value) + if (nextPart?.removed) output += ' ' + } else if (part.removed) { + if (unchangedLines) { + output += processUnchangedLines(unchangedLines) + unchangedLines = '' + } + output += chalk.red.bold(part.value) + if (nextPart?.added) output += ' ' + } else { + unchangedLines += part.value + } + }) + + // Process any remaining unchanged lines at the end + if (unchangedLines) { + output += processUnchangedLines(unchangedLines) + } + + if (output) { + console.log('\nDiff:') + console.log(output) + } else { + console.log('No changes') + } +} diff --git a/packages/server-functions-vite-plugin/tests/compiler.test.ts b/packages/server-functions-vite-plugin/tests/compiler.test.ts new file mode 100644 index 0000000000..6c287b2f18 --- /dev/null +++ b/packages/server-functions-vite-plugin/tests/compiler.test.ts @@ -0,0 +1,478 @@ +import { describe, expect, test } from 'vitest' + +import { compileServerFnClient, compileServerFnServer } from '../src/compilers' + +const clientConfig = { + root: './test-files', + filename: 'test.ts', + getRuntimeCode: () => 'import { createClientRpc } from "my-rpc-lib"', + replacer: (opts: { filename: string; functionId: string }) => + `createClientRpc({ + filename: ${JSON.stringify(opts.filename)}, + functionId: ${JSON.stringify(opts.functionId)}, + })`, +} + +const serverConfig = { + root: './test-files', + filename: 'test.ts', +} + +describe('server function compilation', () => { + test('basic function declaration', async () => { + const code = ` + function useServer() { + 'use server' + return 'hello' + } + ` + + const client = compileServerFnClient({ ...clientConfig, code }) + await expect(client.compiledCode.code).toMatchInlineSnapshot(` + "import { createClientRpc } from "my-rpc-lib"; + createClientRpc({ + filename: "test.ts", + functionId: "test--useServer" + });" + `) + + const server = compileServerFnServer({ ...serverConfig, code }) + await expect(server.compiledCode.code).toMatchInlineSnapshot(` + "function useServer() { + 'use server'; + + return 'hello'; + }" + `) + }) + + test('arrow function', () => { + const code = ` + const fn = () => { + 'use server' + return 'hello' + } + ` + + const client = compileServerFnClient({ ...clientConfig, code }) + expect(client.compiledCode.code).toMatchInlineSnapshot(` + "import { createClientRpc } from "my-rpc-lib"; + const fn = createClientRpc({ + filename: "test.ts", + functionId: "test--fn_1" + });" + `) + + const server = compileServerFnServer({ ...serverConfig, code }) + expect(server.compiledCode.code).toMatchInlineSnapshot(` + "const fn = () => { + 'use server'; + + return 'hello'; + };" + `) + }) + + test('anonymous function', () => { + const code = ` + const anonymousFn = function () { + 'use server' + } + ` + + const client = compileServerFnClient({ ...clientConfig, code }) + expect(client.compiledCode.code).toMatchInlineSnapshot(` + "import { createClientRpc } from "my-rpc-lib"; + const anonymousFn = createClientRpc({ + filename: "test.ts", + functionId: "test--anonymousFn_1" + });" + `) + + const server = compileServerFnServer({ ...serverConfig, code }) + expect(server.compiledCode.code).toMatchInlineSnapshot(` + "const anonymousFn = function () { + 'use server'; + };" + `) + }) + + test('class methods', () => { + const code = ` + class TestClass { + method() { + 'use server' + return 'hello' + } + + static staticMethod() { + 'use server' + return 'hello' + } + } + ` + + const client = compileServerFnClient({ ...clientConfig, code }) + expect(client.compiledCode.code).toMatchInlineSnapshot(` + "import { createClientRpc } from "my-rpc-lib"; + class TestClass { + method() { + return createClientRpc({ + filename: "test.ts", + functionId: "test--method" + }); + } + staticMethod() { + return createClientRpc({ + filename: "test.ts", + functionId: "test--staticMethod" + }); + } + }" + `) + + const server = compileServerFnServer({ ...serverConfig, code }) + expect(server.compiledCode.code).toMatchInlineSnapshot(` + "class TestClass { + method() { + 'use server'; + + return 'hello'; + } + static staticMethod() { + 'use server'; + + return 'hello'; + } + }" + `) + }) + + test('object methods', () => { + const code = ` + const obj = { + method() { + 'use server' + return 'hello' + }, + } + ` + + const client = compileServerFnClient({ ...clientConfig, code }) + expect(client.compiledCode.code).toMatchInlineSnapshot(` + "import { createClientRpc } from "my-rpc-lib"; + const obj = { + method() { + return createClientRpc({ + filename: "test.ts", + functionId: "test--obj_method" + }); + } + };" + `) + + const server = compileServerFnServer({ ...serverConfig, code }) + expect(server.compiledCode.code).toMatchInlineSnapshot(` + "const obj = { + method() { + 'use server'; + + return 'hello'; + } + };" + `) + }) + + test('async functions', () => { + const code = ` + async function asyncServer() { + 'use server' + return 'hello' + } + + const asyncArrow = async () => { + 'use server' + return 'hello' + } + ` + + const client = compileServerFnClient({ ...clientConfig, code }) + expect(client.compiledCode.code).toMatchInlineSnapshot(` + "import { createClientRpc } from "my-rpc-lib"; + createClientRpc({ + filename: "test.ts", + functionId: "test--asyncServer" + }); + const asyncArrow = createClientRpc({ + filename: "test.ts", + functionId: "test--asyncArrow_1" + });" + `) + + const server = compileServerFnServer({ ...serverConfig, code }) + expect(server.compiledCode.code).toMatchInlineSnapshot(` + "async function asyncServer() { + 'use server'; + + return 'hello'; + } + const asyncArrow = async () => { + 'use server'; + + return 'hello'; + };" + `) + }) + + test('generator functions', () => { + const code = ` + function* generatorServer() { + 'use server' + yield 'hello' + } + + async function* asyncGeneratorServer() { + 'use server' + yield 'hello' + } + ` + + const client = compileServerFnClient({ ...clientConfig, code }) + expect(client.compiledCode.code).toMatchInlineSnapshot(` + "import { createClientRpc } from "my-rpc-lib"; + createClientRpc({ + filename: "test.ts", + functionId: "test--generatorServer" + }); + createClientRpc({ + filename: "test.ts", + functionId: "test--asyncGeneratorServer" + });" + `) + + const server = compileServerFnServer({ ...serverConfig, code }) + expect(server.compiledCode.code).toMatchInlineSnapshot(` + "function* generatorServer() { + 'use server'; + + yield 'hello'; + } + async function* asyncGeneratorServer() { + 'use server'; + + yield 'hello'; + }" + `) + }) + + test('nested functions', () => { + const code = ` + function outer() { + function inner() { + 'use server' + return 'hello' + } + return inner + } + ` + + const client = compileServerFnClient({ ...clientConfig, code }) + expect(client.compiledCode.code).toMatchInlineSnapshot(` + "import { createClientRpc } from "my-rpc-lib"; + function outer() { + createClientRpc({ + filename: "test.ts", + functionId: "test--outer_inner" + }); + return inner; + }" + `) + + const server = compileServerFnServer({ ...serverConfig, code }) + expect(server.compiledCode.code).toMatchInlineSnapshot(` + "function outer() { + function inner() { + 'use server'; + + return 'hello'; + } + return inner; + }" + `) + }) + + test('multiple directives', () => { + const code = ` + function multiDirective() { + 'use strict' + 'use server' + return 'hello' + } + ` + + const client = compileServerFnClient({ ...clientConfig, code }) + expect(client.compiledCode.code).toMatchInlineSnapshot(` + "import { createClientRpc } from "my-rpc-lib"; + createClientRpc({ + filename: "test.ts", + functionId: "test--multiDirective" + });" + `) + + const server = compileServerFnServer({ ...serverConfig, code }) + expect(server.compiledCode.code).toMatchInlineSnapshot(` + "function multiDirective() { + 'use strict'; + 'use server'; + + return 'hello'; + }" + `) + }) + + test('function with parameters', () => { + const code = ` + function withParams(a: string, b: number) { + 'use server' + return \`\${a} \${b}\` + } + ` + + const client = compileServerFnClient({ ...clientConfig, code }) + expect(client.compiledCode.code).toMatchInlineSnapshot(` + "import { createClientRpc } from "my-rpc-lib"; + createClientRpc({ + filename: "test.ts", + functionId: "test--withParams" + });" + `) + + const server = compileServerFnServer({ ...serverConfig, code }) + expect(server.compiledCode.code).toMatchInlineSnapshot(` + "function withParams(a: string, b: number) { + 'use server'; + + return \`\${a} \${b}\`; + }" + `) + }) + + test('IIFE', () => { + const code = ` + const iife = (function () { + 'use server' + return 'hello' + })() + ` + + const client = compileServerFnClient({ ...clientConfig, code }) + expect(client.compiledCode.code).toMatchInlineSnapshot(` + "import { createClientRpc } from "my-rpc-lib"; + const iife = createClientRpc({ + filename: "test.ts", + functionId: "test--iife" + })();" + `) + + const server = compileServerFnServer({ ...serverConfig, code }) + expect(server.compiledCode.code).toMatchInlineSnapshot(` + "const iife = function () { + 'use server'; + + return 'hello'; + }();" + `) + }) + + test('higher order functions', () => { + const code = ` + function higherOrder() { + return function () { + 'use server' + return 'hello' + } + } + ` + + const client = compileServerFnClient({ ...clientConfig, code }) + expect(client.compiledCode.code).toMatchInlineSnapshot(` + "import { createClientRpc } from "my-rpc-lib"; + function higherOrder() { + return createClientRpc({ + filename: "test.ts", + functionId: "test--higherOrder" + }); + }" + `) + + const server = compileServerFnServer({ ...serverConfig, code }) + expect(server.compiledCode.code).toMatchInlineSnapshot(` + "function higherOrder() { + return function () { + 'use server'; + + return 'hello'; + }; + }" + `) + }) + + test('functions that might have the same functionId', () => { + const code = ` + function main() { + function middle() { + const useServer = function () { + 'use server' + return 'hello' + } + return useServer + } + return middle + } + + main().middle(function useServer() { + 'use server' + return 'hello' + }) + ` + + const client = compileServerFnClient({ ...clientConfig, code }) + expect(client.compiledCode.code).toMatchInlineSnapshot(` + "import { createClientRpc } from "my-rpc-lib"; + function main() { + function middle() { + const useServer = createClientRpc({ + filename: "test.ts", + functionId: "test--main_middle_useServer_1" + }); + return useServer; + } + return middle; + } + main().middle(createClientRpc({ + filename: "test.ts", + functionId: "test--main_middle_useServer_2" + }));" + `) + + const server = compileServerFnServer({ ...serverConfig, code }) + expect(server.compiledCode.code).toMatchInlineSnapshot(` + "function main() { + function middle() { + const useServer = function () { + 'use server'; + + return 'hello'; + }; + return useServer; + } + return middle; + } + main().middle(function useServer() { + 'use server'; + + return 'hello'; + });" + `) + }) +}) diff --git a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/createIsomorphicFn.test.ts b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/createIsomorphicFn.test.ts deleted file mode 100644 index 28f29510a7..0000000000 --- a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/createIsomorphicFn.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { readFile, readdir } from 'node:fs/promises' -import path from 'node:path' -import { afterAll, describe, expect, test, vi } from 'vitest' - -import { compileStartOutput } from '../../src/compilers' - -async function getFilenames() { - return await readdir(path.resolve(import.meta.dirname, './test-files')) -} - -describe('createIsomorphicFn compiles correctly', async () => { - const noImplWarning = - 'createIsomorphicFn called without a client or server implementation!' - - const originalConsoleWarn = console.warn - const consoleSpy = vi.spyOn(console, 'warn').mockImplementation((...args) => { - // we want to avoid sending this warning to the console, we know about it - if (args[0] === noImplWarning) { - return - } - originalConsoleWarn(...args) - }) - - afterAll(() => { - consoleSpy.mockRestore() - }) - - const filenames = await getFilenames() - - describe.each(filenames)('should handle "%s"', async (filename) => { - const file = await readFile( - path.resolve(import.meta.dirname, `./test-files/${filename}`), - ) - const code = file.toString() - - test.each(['client', 'server'] as const)( - `should compile for ${filename} %s`, - async (env) => { - const compiledResult = compileStartOutput({ - env, - code, - root: './test-files', - filename, - }) - - await expect(compiledResult.code).toMatchFileSnapshot( - `./snapshots/${env}/${filename}`, - ) - }, - ) - }) - test('should error if implementation not provided', () => { - expect(() => { - compileStartOutput({ - env: 'client', - code: ` - import { createIsomorphicFn } from '@tanstack/start' - const clientOnly = createIsomorphicFn().client()`, - root: './test-files', - filename: 'no-fn.ts', - }) - }).toThrowError() - expect(() => { - compileStartOutput({ - env: 'server', - code: ` - import { createIsomorphicFn } from '@tanstack/start' - const serverOnly = createIsomorphicFn().server()`, - root: './test-files', - filename: 'no-fn.ts', - }) - }).toThrowError() - }) - test('should warn to console if no implementations provided', () => { - compileStartOutput({ - env: 'client', - code: ` - import { createIsomorphicFn } from '@tanstack/start' - const noImpl = createIsomorphicFn()`, - root: './test-files', - filename: 'no-fn.ts', - }) - expect(consoleSpy).toHaveBeenCalledWith( - noImplWarning, - 'This will result in a no-op function.', - 'Variable name:', - 'noImpl', - ) - }) -}) diff --git a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructured.tsx b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructured.tsx deleted file mode 100644 index 6fcb30691c..0000000000 --- a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructured.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { createIsomorphicFn } from '@tanstack/start'; -const noImpl = () => {}; -const serverOnlyFn = () => {}; -const clientOnlyFn = () => 'client'; -const serverThenClientFn = () => 'client'; -const clientThenServerFn = () => 'client'; -function abstractedServerFn() { - return 'server'; -} -const serverOnlyFnAbstracted = () => {}; -function abstractedClientFn() { - return 'client'; -} -const clientOnlyFnAbstracted = abstractedClientFn; -const serverThenClientFnAbstracted = abstractedClientFn; -const clientThenServerFnAbstracted = abstractedClientFn; \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructuredRename.tsx b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructuredRename.tsx deleted file mode 100644 index d273a49f40..0000000000 --- a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructuredRename.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { createIsomorphicFn as isomorphicFn } from '@tanstack/start'; -const noImpl = () => {}; -const serverOnlyFn = () => {}; -const clientOnlyFn = () => 'client'; -const serverThenClientFn = () => 'client'; -const clientThenServerFn = () => 'client'; -function abstractedServerFn() { - return 'server'; -} -const serverOnlyFnAbstracted = () => {}; -function abstractedClientFn() { - return 'client'; -} -const clientOnlyFnAbstracted = abstractedClientFn; -const serverThenClientFnAbstracted = abstractedClientFn; -const clientThenServerFnAbstracted = abstractedClientFn; \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnStarImport.tsx b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnStarImport.tsx deleted file mode 100644 index ffb2ffe79c..0000000000 --- a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnStarImport.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import * as TanStackStart from '@tanstack/start'; -const noImpl = () => {}; -const serverOnlyFn = () => {}; -const clientOnlyFn = () => 'client'; -const serverThenClientFn = () => 'client'; -const clientThenServerFn = () => 'client'; -function abstractedServerFn() { - return 'server'; -} -const serverOnlyFnAbstracted = () => {}; -function abstractedClientFn() { - return 'client'; -} -const clientOnlyFnAbstracted = abstractedClientFn; -const serverThenClientFnAbstracted = abstractedClientFn; -const clientThenServerFnAbstracted = abstractedClientFn; \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructured.tsx b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructured.tsx deleted file mode 100644 index 0e0134e347..0000000000 --- a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructured.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { createIsomorphicFn } from '@tanstack/start'; -const noImpl = () => {}; -const serverOnlyFn = () => 'server'; -const clientOnlyFn = () => {}; -const serverThenClientFn = () => 'server'; -const clientThenServerFn = () => 'server'; -function abstractedServerFn() { - return 'server'; -} -const serverOnlyFnAbstracted = abstractedServerFn; -function abstractedClientFn() { - return 'client'; -} -const clientOnlyFnAbstracted = () => {}; -const serverThenClientFnAbstracted = abstractedServerFn; -const clientThenServerFnAbstracted = abstractedServerFn; \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructuredRename.tsx b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructuredRename.tsx deleted file mode 100644 index 3ba9d0598f..0000000000 --- a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructuredRename.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { createIsomorphicFn as isomorphicFn } from '@tanstack/start'; -const noImpl = () => {}; -const serverOnlyFn = () => 'server'; -const clientOnlyFn = () => {}; -const serverThenClientFn = () => 'server'; -const clientThenServerFn = () => 'server'; -function abstractedServerFn() { - return 'server'; -} -const serverOnlyFnAbstracted = abstractedServerFn; -function abstractedClientFn() { - return 'client'; -} -const clientOnlyFnAbstracted = () => {}; -const serverThenClientFnAbstracted = abstractedServerFn; -const clientThenServerFnAbstracted = abstractedServerFn; \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnStarImport.tsx b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnStarImport.tsx deleted file mode 100644 index c77c6ac839..0000000000 --- a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnStarImport.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import * as TanStackStart from '@tanstack/start'; -const noImpl = () => {}; -const serverOnlyFn = () => 'server'; -const clientOnlyFn = () => {}; -const serverThenClientFn = () => 'server'; -const clientThenServerFn = () => 'server'; -function abstractedServerFn() { - return 'server'; -} -const serverOnlyFnAbstracted = abstractedServerFn; -function abstractedClientFn() { - return 'client'; -} -const clientOnlyFnAbstracted = () => {}; -const serverThenClientFnAbstracted = abstractedServerFn; -const clientThenServerFnAbstracted = abstractedServerFn; \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnDestructured.tsx b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnDestructured.tsx deleted file mode 100644 index e6c5c35943..0000000000 --- a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnDestructured.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { createIsomorphicFn } from '@tanstack/start' - -const noImpl = createIsomorphicFn() - -const serverOnlyFn = createIsomorphicFn().server(() => 'server') - -const clientOnlyFn = createIsomorphicFn().client(() => 'client') - -const serverThenClientFn = createIsomorphicFn() - .server(() => 'server') - .client(() => 'client') - -const clientThenServerFn = createIsomorphicFn() - .client(() => 'client') - .server(() => 'server') - -function abstractedServerFn() { - return 'server' -} - -const serverOnlyFnAbstracted = createIsomorphicFn().server(abstractedServerFn) - -function abstractedClientFn() { - return 'client' -} - -const clientOnlyFnAbstracted = createIsomorphicFn().client(abstractedClientFn) - -const serverThenClientFnAbstracted = createIsomorphicFn() - .server(abstractedServerFn) - .client(abstractedClientFn) - -const clientThenServerFnAbstracted = createIsomorphicFn() - .client(abstractedClientFn) - .server(abstractedServerFn) diff --git a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnDestructuredRename.tsx b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnDestructuredRename.tsx deleted file mode 100644 index 9c31330b1b..0000000000 --- a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnDestructuredRename.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { createIsomorphicFn as isomorphicFn } from '@tanstack/start' - -const noImpl = isomorphicFn() - -const serverOnlyFn = isomorphicFn().server(() => 'server') - -const clientOnlyFn = isomorphicFn().client(() => 'client') - -const serverThenClientFn = isomorphicFn() - .server(() => 'server') - .client(() => 'client') - -const clientThenServerFn = isomorphicFn() - .client(() => 'client') - .server(() => 'server') - -function abstractedServerFn() { - return 'server' -} - -const serverOnlyFnAbstracted = isomorphicFn().server(abstractedServerFn) - -function abstractedClientFn() { - return 'client' -} - -const clientOnlyFnAbstracted = isomorphicFn().client(abstractedClientFn) - -const serverThenClientFnAbstracted = isomorphicFn() - .server(abstractedServerFn) - .client(abstractedClientFn) - -const clientThenServerFnAbstracted = isomorphicFn() - .client(abstractedClientFn) - .server(abstractedServerFn) diff --git a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnStarImport.tsx b/packages/server-functions-vite-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnStarImport.tsx deleted file mode 100644 index 4c1bed5741..0000000000 --- a/packages/server-functions-vite-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnStarImport.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import * as TanStackStart from '@tanstack/start' - -const noImpl = TanStackStart.createIsomorphicFn() - -const serverOnlyFn = TanStackStart.createIsomorphicFn().server(() => 'server') - -const clientOnlyFn = TanStackStart.createIsomorphicFn().client(() => 'client') - -const serverThenClientFn = TanStackStart.createIsomorphicFn() - .server(() => 'server') - .client(() => 'client') - -const clientThenServerFn = TanStackStart.createIsomorphicFn() - .client(() => 'client') - .server(() => 'server') - -function abstractedServerFn() { - return 'server' -} - -const serverOnlyFnAbstracted = - TanStackStart.createIsomorphicFn().server(abstractedServerFn) - -function abstractedClientFn() { - return 'client' -} - -const clientOnlyFnAbstracted = - TanStackStart.createIsomorphicFn().client(abstractedClientFn) - -const serverThenClientFnAbstracted = TanStackStart.createIsomorphicFn() - .server(abstractedServerFn) - .client(abstractedClientFn) - -const clientThenServerFnAbstracted = TanStackStart.createIsomorphicFn() - .client(abstractedClientFn) - .server(abstractedServerFn) diff --git a/packages/server-functions-vite-plugin/tests/createMiddleware/createMiddleware.test.ts b/packages/server-functions-vite-plugin/tests/createMiddleware/createMiddleware.test.ts deleted file mode 100644 index ae4ee2c221..0000000000 --- a/packages/server-functions-vite-plugin/tests/createMiddleware/createMiddleware.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { readFile, readdir } from 'node:fs/promises' -import path from 'node:path' -import { describe, expect, test } from 'vitest' - -import { compileStartOutput } from '../../src/compilers' - -async function getFilenames() { - return await readdir(path.resolve(import.meta.dirname, './test-files')) -} - -describe('createMiddleware compiles correctly', async () => { - const filenames = await getFilenames() - - describe.each(filenames)('should handle "%s"', async (filename) => { - const file = await readFile( - path.resolve(import.meta.dirname, `./test-files/${filename}`), - ) - const code = file.toString() - - test.each(['client', 'server'] as const)( - `should compile for ${filename} %s`, - async (env) => { - const compiledResult = compileStartOutput({ - env, - code, - root: './test-files', - filename, - }) - - await expect(compiledResult.code).toMatchFileSnapshot( - `./snapshots/${env}/${filename}`, - ) - }, - ) - }) -}) diff --git a/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareDestructured.tsx b/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareDestructured.tsx deleted file mode 100644 index f0ed6717e4..0000000000 --- a/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareDestructured.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { createMiddleware } from '@tanstack/start'; -import { z } from 'zod'; -export const withUseServer = createMiddleware({ - id: 'test' -}); -export const withoutUseServer = createMiddleware({ - id: 'test' -}); -export const withVariable = createMiddleware({ - id: 'test' -}); -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 = createMiddleware({ - id: 'test' -}); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareDestructuredRename.tsx b/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareDestructuredRename.tsx deleted file mode 100644 index c13c45a86c..0000000000 --- a/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareDestructuredRename.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { createMiddleware as middlewareFn } from '@tanstack/start'; -import { z } from 'zod'; -export const withUseServer = middlewareFn({ - id: 'test' -}); -export const withoutUseServer = middlewareFn({ - id: 'test' -}); -export const withVariable = middlewareFn({ - id: 'test' -}); -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 = middlewareFn({ - id: 'test' -}); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareStarImport.tsx b/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareStarImport.tsx deleted file mode 100644 index 0af363017d..0000000000 --- a/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareStarImport.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import * as TanStackStart from '@tanstack/start'; -import { z } from 'zod'; -export const withUseServer = TanStackStart.createMiddleware({ - id: 'test' -}); -export const withoutUseServer = TanStackStart.createMiddleware({ - id: 'test' -}); -export const withVariable = TanStackStart.createMiddleware({ - id: 'test' -}); -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.createMiddleware({ - id: 'test' -}); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareValidator.tsx b/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareValidator.tsx deleted file mode 100644 index e13946f2d8..0000000000 --- a/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/client/createMiddlewareValidator.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { createMiddleware } from '@tanstack/start'; -import { z } from 'zod'; -export const withUseServer = createMiddleware({ - id: 'test' -}); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareDestructured.tsx b/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareDestructured.tsx deleted file mode 100644 index a29e1ecb40..0000000000 --- a/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareDestructured.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { createMiddleware } from '@tanstack/start'; -import { z } from 'zod'; -export const withUseServer = createMiddleware({ - id: 'test' -}).server(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 = createMiddleware({ - id: 'test' -}).server(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 = createMiddleware({ - id: 'test' -}).server(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 = createMiddleware({ - id: 'test' -}).server(zodValidator(z.number(), input => { - return { - 'you gave': input - }; -})); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareDestructuredRename.tsx b/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareDestructuredRename.tsx deleted file mode 100644 index d529f89fe1..0000000000 --- a/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareDestructuredRename.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { createMiddleware as middlewareFn } from '@tanstack/start'; -import { z } from 'zod'; -export const withUseServer = middlewareFn({ - id: 'test' -}).server(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 = middlewareFn({ - id: 'test' -}).server(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 = middlewareFn({ - id: 'test' -}).server(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 = middlewareFn({ - id: 'test' -}).server(zodValidator(z.number(), input => { - return { - 'you gave': input - }; -})); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareStarImport.tsx b/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareStarImport.tsx deleted file mode 100644 index 53baf6fa9c..0000000000 --- a/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareStarImport.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import * as TanStackStart from '@tanstack/start'; -import { z } from 'zod'; -export const withUseServer = TanStackStart.createMiddleware({ - id: 'test' -}).server(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.createMiddleware({ - id: 'test' -}).server(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.createMiddleware({ - id: 'test' -}).server(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.createMiddleware({ - id: 'test' -}).server(zodValidator(z.number(), input => { - return { - 'you gave': input - }; -})); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareValidator.tsx b/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareValidator.tsx deleted file mode 100644 index 1e41c79866..0000000000 --- a/packages/server-functions-vite-plugin/tests/createMiddleware/snapshots/server/createMiddlewareValidator.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { createMiddleware } from '@tanstack/start'; -import { z } from 'zod'; -export const withUseServer = createMiddleware({ - id: 'test' -}).validator(z.number()).server(({ - input -}) => input + 1); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareDestructured.tsx b/packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareDestructured.tsx deleted file mode 100644 index c5ea2aa5b6..0000000000 --- a/packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareDestructured.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { createMiddleware } from '@tanstack/start' -import { z } from 'zod' - -export const withUseServer = createMiddleware({ - id: 'test', -}).server(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 = createMiddleware({ - id: 'test', -}).server(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 = createMiddleware({ - id: 'test', -}).server(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 = createMiddleware({ - id: 'test', -}).server( - zodValidator(z.number(), (input) => { - return { 'you gave': input } - }), -) diff --git a/packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareDestructuredRename.tsx b/packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareDestructuredRename.tsx deleted file mode 100644 index 2e456131fe..0000000000 --- a/packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareDestructuredRename.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { createMiddleware as middlewareFn } from '@tanstack/start' -import { z } from 'zod' - -export const withUseServer = middlewareFn({ - id: 'test', -}).server(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 = middlewareFn({ - id: 'test', -}).server(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 = middlewareFn({ - id: 'test', -}).server(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 = middlewareFn({ - id: 'test', -}).server( - zodValidator(z.number(), (input) => { - return { 'you gave': input } - }), -) diff --git a/packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareStarImport.tsx b/packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareStarImport.tsx deleted file mode 100644 index c1df8e12b3..0000000000 --- a/packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareStarImport.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import * as TanStackStart from '@tanstack/start' -import { z } from 'zod' - -export const withUseServer = TanStackStart.createMiddleware({ - id: 'test', -}).server(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.createMiddleware({ - id: 'test', -}).server(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.createMiddleware({ - id: 'test', -}).server(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.createMiddleware({ - id: 'test', -}).server( - zodValidator(z.number(), (input) => { - return { 'you gave': input } - }), -) diff --git a/packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareValidator.tsx b/packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareValidator.tsx deleted file mode 100644 index 7e0d6ba449..0000000000 --- a/packages/server-functions-vite-plugin/tests/createMiddleware/test-files/createMiddlewareValidator.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { createMiddleware } from '@tanstack/start' -import { z } from 'zod' - -export const withUseServer = createMiddleware({ - id: 'test', -}) - .validator(z.number()) - .server(({ input }) => input + 1) diff --git a/packages/server-functions-vite-plugin/tests/createServerFn/createServerFn.test.ts b/packages/server-functions-vite-plugin/tests/createServerFn/createServerFn.test.ts deleted file mode 100644 index 19b86a0c01..0000000000 --- a/packages/server-functions-vite-plugin/tests/createServerFn/createServerFn.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { readFile, readdir } from 'node:fs/promises' -import path from 'node:path' -import { describe, expect, test } from 'vitest' - -import { compileStartOutput } from '../../src/compilers' - -async function getFilenames() { - return await readdir(path.resolve(import.meta.dirname, './test-files')) -} - -describe('createServerFn compiles correctly', async () => { - const filenames = await getFilenames() - - describe.each(filenames)('should handle "%s"', async (filename) => { - const file = await readFile( - path.resolve(import.meta.dirname, `./test-files/${filename}`), - ) - const code = file.toString() - - test.each(['client', 'server'] as const)( - `should compile for ${filename} %s`, - async (env) => { - const compiledResult = compileStartOutput({ - env, - code, - root: './test-files', - filename, - }) - - await expect(compiledResult.code).toMatchFileSnapshot( - `./snapshots/${env}/${filename}`, - ) - }, - ) - }) - - test('should error if created without a handler', () => { - expect(() => { - compileStartOutput({ - env: 'client', - code: ` - import { createServerFn } from '@tanstack/start' - createServerFn()`, - root: './test-files', - filename: 'no-fn.ts', - }) - }).toThrowError() - }) - - test('should be assigned to a variable', () => { - expect(() => { - compileStartOutput({ - env: 'client', - code: ` - import { createServerFn } from '@tanstack/start' - createServerFn().handler(async () => {})`, - root: './test-files', - filename: 'no-fn.ts', - }) - }).toThrowError() - }) -}) diff --git a/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnDestructured.tsx b/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnDestructured.tsx deleted file mode 100644 index be1b58b186..0000000000 --- a/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnDestructured.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { createServerFn } from '@tanstack/start'; -import { z } from 'zod'; -export const withUseServer = createServerFn({ - method: 'GET' -}).handler(opts => { - "use server"; - - return withUseServer.__executeServer(opts); -}); -export const withArrowFunction = createServerFn({ - method: 'GET' -}).handler(opts => { - "use server"; - - return withArrowFunction.__executeServer(opts); -}); -export const withArrowFunctionAndFunction = createServerFn({ - method: 'GET' -}).handler(opts => { - "use server"; - - return withArrowFunctionAndFunction.__executeServer(opts); -}); -export const withoutUseServer = createServerFn({ - method: 'GET' -}).handler(opts => { - "use server"; - - return withoutUseServer.__executeServer(opts); -}); -export const withVariable = createServerFn({ - method: 'GET' -}).handler(opts => { - "use server"; - - return withVariable.__executeServer(opts); -}); -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 => { - "use server"; - - return withZodValidator.__executeServer(opts); -}); -export const withValidatorFn = createServerFn({ - method: 'GET' -}).handler(opts => { - "use server"; - - return withValidatorFn.__executeServer(opts); -}); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnDestructuredRename.tsx b/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnDestructuredRename.tsx deleted file mode 100644 index f51f0ade21..0000000000 --- a/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnDestructuredRename.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { createServerFn as serverFn } from '@tanstack/start'; -import { z } from 'zod'; -export const withUseServer = serverFn({ - method: 'GET' -}).handler(opts => { - "use server"; - - return withUseServer.__executeServer(opts); -}); -export const withoutUseServer = serverFn({ - method: 'GET' -}).handler(opts => { - "use server"; - - return withoutUseServer.__executeServer(opts); -}); -export const withVariable = serverFn({ - method: 'GET' -}).handler(opts => { - "use server"; - - return withVariable.__executeServer(opts); -}); -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 => { - "use server"; - - return withZodValidator.__executeServer(opts); -}); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnStarImport.tsx b/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnStarImport.tsx deleted file mode 100644 index 750a52532a..0000000000 --- a/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnStarImport.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import * as TanStackStart from '@tanstack/start'; -import { z } from 'zod'; -export const withUseServer = TanStackStart.createServerFn({ - method: 'GET' -}).handler(opts => { - "use server"; - - return withUseServer.__executeServer(opts); -}); -export const withoutUseServer = TanStackStart.createServerFn({ - method: 'GET' -}).handler(opts => { - "use server"; - - return withoutUseServer.__executeServer(opts); -}); -export const withVariable = TanStackStart.createServerFn({ - method: 'GET' -}).handler(opts => { - "use server"; - - return withVariable.__executeServer(opts); -}); -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 => { - "use server"; - - return withZodValidator.__executeServer(opts); -}); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnValidator.tsx b/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnValidator.tsx deleted file mode 100644 index 4e40b54a24..0000000000 --- a/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/client/createServerFnValidator.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { createServerFn } from '@tanstack/start'; -import { z } from 'zod'; -export const withUseServer = createServerFn({ - method: 'GET' -}).handler(opts => { - "use server"; - - return withUseServer.__executeServer(opts); -}); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnDestructured.tsx b/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnDestructured.tsx deleted file mode 100644 index 0176442a89..0000000000 --- a/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnDestructured.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { createServerFn } from '@tanstack/start'; -import { z } from 'zod'; -export const withUseServer = createServerFn({ - method: 'GET' -}).handler(opts => { - "use server"; - - return withUseServer.__executeServer(opts); -}, 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 => { - "use server"; - - return withArrowFunction.__executeServer(opts); -}, async () => null); -export const withArrowFunctionAndFunction = createServerFn({ - method: 'GET' -}).handler(opts => { - "use server"; - - return withArrowFunctionAndFunction.__executeServer(opts); -}, async () => test()); -export const withoutUseServer = createServerFn({ - method: 'GET' -}).handler(opts => { - "use server"; - - return withoutUseServer.__executeServer(opts); -}, 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 => { - "use server"; - - return withVariable.__executeServer(opts); -}, 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 => { - "use server"; - - return withZodValidator.__executeServer(opts); -}, zodValidator(z.number(), input => { - return { - 'you gave': input - }; -})); -export const withValidatorFn = createServerFn({ - method: 'GET' -}).validator(z.number()).handler(opts => { - "use server"; - - return withValidatorFn.__executeServer(opts); -}, async ({ - input -}) => { - return null; -}); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnDestructuredRename.tsx b/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnDestructuredRename.tsx deleted file mode 100644 index b391de555f..0000000000 --- a/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnDestructuredRename.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { createServerFn as serverFn } from '@tanstack/start'; -import { z } from 'zod'; -export const withUseServer = serverFn({ - method: 'GET' -}).handler(opts => { - "use server"; - - return withUseServer.__executeServer(opts); -}, 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 => { - "use server"; - - return withoutUseServer.__executeServer(opts); -}, 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 => { - "use server"; - - return withVariable.__executeServer(opts); -}, 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 => { - "use server"; - - return withZodValidator.__executeServer(opts); -}, zodValidator(z.number(), input => { - return { - 'you gave': input - }; -})); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnStarImport.tsx b/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnStarImport.tsx deleted file mode 100644 index 66088cb181..0000000000 --- a/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnStarImport.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import * as TanStackStart from '@tanstack/start'; -import { z } from 'zod'; -export const withUseServer = TanStackStart.createServerFn({ - method: 'GET' -}).handler(opts => { - "use server"; - - return withUseServer.__executeServer(opts); -}, 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 = TanStackStart.createServerFn({ - method: 'GET' -}).handler(opts => { - "use server"; - - return withoutUseServer.__executeServer(opts); -}, 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 => { - "use server"; - - return withVariable.__executeServer(opts); -}, 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 => { - "use server"; - - return withZodValidator.__executeServer(opts); -}, zodValidator(z.number(), input => { - return { - 'you gave': input - }; -})); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnValidator.tsx b/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnValidator.tsx deleted file mode 100644 index 96cd71b9fb..0000000000 --- a/packages/server-functions-vite-plugin/tests/createServerFn/snapshots/server/createServerFnValidator.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createServerFn } from '@tanstack/start'; -import { z } from 'zod'; -export const withUseServer = createServerFn({ - method: 'GET' -}).validator(z.number()).handler(opts => { - "use server"; - - return withUseServer.__executeServer(opts); -}, ({ - input -}) => input + 1); \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnDestructured.tsx b/packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnDestructured.tsx deleted file mode 100644 index 780c1e3370..0000000000 --- a/packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnDestructured.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { createServerFn } from '@tanstack/start' -import { z } from 'zod' - -export const withUseServer = createServerFn({ - method: 'GET', -}).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)) -}) - -export const withArrowFunction = createServerFn({ - method: 'GET', -}).handler(async () => null) - -export const withArrowFunctionAndFunction = createServerFn({ - method: 'GET', -}).handler(async () => test()) - -export const withoutUseServer = createServerFn({ - method: 'GET', -}).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)) -}) - -export const withVariable = createServerFn({ - method: 'GET', -}).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)) - } -} - -export const withZodValidator = createServerFn({ - method: 'GET', -}).handler( - zodValidator(z.number(), (input) => { - return { 'you gave': input } - }), -) - -export const withValidatorFn = createServerFn({ - method: 'GET', -}) - .validator(z.number()) - .handler(async ({ input }) => { - return null - }) diff --git a/packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnDestructuredRename.tsx b/packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnDestructuredRename.tsx deleted file mode 100644 index aba9859be3..0000000000 --- a/packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnDestructuredRename.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { createServerFn as serverFn } from '@tanstack/start' -import { z } from 'zod' - -export const withUseServer = serverFn({ - method: 'GET', -}).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)) -}) - -export const withoutUseServer = serverFn({ - method: 'GET', -}).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)) -}) - -export const withVariable = serverFn({ method: 'GET' }).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)) - } -} - -export const withZodValidator = serverFn({ - method: 'GET', -}).handler( - zodValidator(z.number(), (input) => { - return { 'you gave': input } - }), -) diff --git a/packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnStarImport.tsx b/packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnStarImport.tsx deleted file mode 100644 index 6d5ba022bb..0000000000 --- a/packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnStarImport.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import * as TanStackStart from '@tanstack/start' -import { z } from 'zod' - -export const withUseServer = TanStackStart.createServerFn({ - method: 'GET', -}).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)) -}) - -export const withoutUseServer = TanStackStart.createServerFn({ - method: 'GET', -}).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)) -}) - -export const withVariable = TanStackStart.createServerFn({ - method: 'GET', -}).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)) - } -} - -export const withZodValidator = TanStackStart.createServerFn({ - method: 'GET', -}).handler( - zodValidator(z.number(), (input) => { - return { 'you gave': input } - }), -) diff --git a/packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnValidator.tsx b/packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnValidator.tsx deleted file mode 100644 index b30bcdc731..0000000000 --- a/packages/server-functions-vite-plugin/tests/createServerFn/test-files/createServerFnValidator.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { createServerFn } from '@tanstack/start' -import { z } from 'zod' - -export const withUseServer = createServerFn({ - method: 'GET', -}) - .validator(z.number()) - .handler(({ input }) => input + 1) diff --git a/packages/server-functions-vite-plugin/tests/envOnly/envOnly.test.ts b/packages/server-functions-vite-plugin/tests/envOnly/envOnly.test.ts deleted file mode 100644 index 67336be35b..0000000000 --- a/packages/server-functions-vite-plugin/tests/envOnly/envOnly.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { readFile, readdir } from 'node:fs/promises' -import path from 'node:path' -import { describe, expect, test } from 'vitest' - -import { compileStartOutput } from '../../src/compilers' - -async function getFilenames() { - return await readdir(path.resolve(import.meta.dirname, './test-files')) -} - -describe('envOnly functions compile correctly', async () => { - const filenames = await getFilenames() - - describe.each(filenames)('should handle "%s"', async (filename) => { - const file = await readFile( - path.resolve(import.meta.dirname, `./test-files/${filename}`), - ) - const code = file.toString() - - test.each(['client', 'server'] as const)( - `should compile for ${filename} %s`, - async (env) => { - const compiledResult = compileStartOutput({ - env, - code, - root: './test-files', - filename, - }) - - await expect(compiledResult.code).toMatchFileSnapshot( - `./snapshots/${env}/${filename}`, - ) - }, - ) - }) - test('should error if implementation not provided', () => { - expect(() => { - compileStartOutput({ - env: 'client', - code: ` - import { clientOnly } from '@tanstack/start' - const fn = clientOnly()`, - root: './test-files', - filename: 'no-fn.ts', - }) - }).toThrowError() - expect(() => { - compileStartOutput({ - env: 'server', - code: ` - import { serverOnly } from '@tanstack/start' - const fn = serverOnly()`, - root: './test-files', - filename: 'no-fn.ts', - }) - }).toThrowError() - }) -}) diff --git a/packages/server-functions-vite-plugin/tests/envOnly/snapshots/client/envOnlyDestructured.tsx b/packages/server-functions-vite-plugin/tests/envOnly/snapshots/client/envOnlyDestructured.tsx deleted file mode 100644 index 3dc714f696..0000000000 --- a/packages/server-functions-vite-plugin/tests/envOnly/snapshots/client/envOnlyDestructured.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { serverOnly, clientOnly } from '@tanstack/start'; -const serverFunc = () => { - throw new Error("serverOnly() functions can only be called on the server!"); -}; -const clientFunc = () => 'client'; \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/envOnly/snapshots/client/envOnlyDestructuredRename.tsx b/packages/server-functions-vite-plugin/tests/envOnly/snapshots/client/envOnlyDestructuredRename.tsx deleted file mode 100644 index 3dd661c865..0000000000 --- a/packages/server-functions-vite-plugin/tests/envOnly/snapshots/client/envOnlyDestructuredRename.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { serverOnly as serverFn, clientOnly as clientFn } from '@tanstack/start'; -const serverFunc = () => { - throw new Error("serverOnly() functions can only be called on the server!"); -}; -const clientFunc = () => 'client'; \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/envOnly/snapshots/client/envOnlyStarImport.tsx b/packages/server-functions-vite-plugin/tests/envOnly/snapshots/client/envOnlyStarImport.tsx deleted file mode 100644 index 61f0677953..0000000000 --- a/packages/server-functions-vite-plugin/tests/envOnly/snapshots/client/envOnlyStarImport.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import * as TanstackStart from '@tanstack/start'; -const serverFunc = () => { - throw new Error("serverOnly() functions can only be called on the server!"); -}; -const clientFunc = () => 'client'; \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/envOnly/snapshots/server/envOnlyDestructured.tsx b/packages/server-functions-vite-plugin/tests/envOnly/snapshots/server/envOnlyDestructured.tsx deleted file mode 100644 index 24f736eac2..0000000000 --- a/packages/server-functions-vite-plugin/tests/envOnly/snapshots/server/envOnlyDestructured.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { serverOnly, clientOnly } from '@tanstack/start'; -const serverFunc = () => 'server'; -const clientFunc = () => { - throw new Error("clientOnly() functions can only be called on the client!"); -}; \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/envOnly/snapshots/server/envOnlyDestructuredRename.tsx b/packages/server-functions-vite-plugin/tests/envOnly/snapshots/server/envOnlyDestructuredRename.tsx deleted file mode 100644 index 790e1328d7..0000000000 --- a/packages/server-functions-vite-plugin/tests/envOnly/snapshots/server/envOnlyDestructuredRename.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { serverOnly as serverFn, clientOnly as clientFn } from '@tanstack/start'; -const serverFunc = () => 'server'; -const clientFunc = () => { - throw new Error("clientOnly() functions can only be called on the client!"); -}; \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/envOnly/snapshots/server/envOnlyStarImport.tsx b/packages/server-functions-vite-plugin/tests/envOnly/snapshots/server/envOnlyStarImport.tsx deleted file mode 100644 index 51e320d4a2..0000000000 --- a/packages/server-functions-vite-plugin/tests/envOnly/snapshots/server/envOnlyStarImport.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import * as TanstackStart from '@tanstack/start'; -const serverFunc = () => 'server'; -const clientFunc = () => { - throw new Error("clientOnly() functions can only be called on the client!"); -}; \ No newline at end of file diff --git a/packages/server-functions-vite-plugin/tests/envOnly/test-files/envOnlyDestructured.tsx b/packages/server-functions-vite-plugin/tests/envOnly/test-files/envOnlyDestructured.tsx deleted file mode 100644 index 1389624d34..0000000000 --- a/packages/server-functions-vite-plugin/tests/envOnly/test-files/envOnlyDestructured.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { serverOnly, clientOnly } from '@tanstack/start' - -const serverFunc = serverOnly(() => 'server') - -const clientFunc = clientOnly(() => 'client') diff --git a/packages/server-functions-vite-plugin/tests/envOnly/test-files/envOnlyDestructuredRename.tsx b/packages/server-functions-vite-plugin/tests/envOnly/test-files/envOnlyDestructuredRename.tsx deleted file mode 100644 index ac34f06134..0000000000 --- a/packages/server-functions-vite-plugin/tests/envOnly/test-files/envOnlyDestructuredRename.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { serverOnly as serverFn, clientOnly as clientFn } from '@tanstack/start' - -const serverFunc = serverFn(() => 'server') - -const clientFunc = clientFn(() => 'client') diff --git a/packages/server-functions-vite-plugin/tests/envOnly/test-files/envOnlyStarImport.tsx b/packages/server-functions-vite-plugin/tests/envOnly/test-files/envOnlyStarImport.tsx deleted file mode 100644 index cd262b54a0..0000000000 --- a/packages/server-functions-vite-plugin/tests/envOnly/test-files/envOnlyStarImport.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import * as TanstackStart from '@tanstack/start' - -const serverFunc = TanstackStart.serverOnly(() => 'server') - -const clientFunc = TanstackStart.clientOnly(() => 'client') diff --git a/packages/start/src/config/index.ts b/packages/start/src/config/index.ts index 093c4f875e..f8c14fee4f 100644 --- a/packages/start/src/config/index.ts +++ b/packages/start/src/config/index.ts @@ -171,7 +171,8 @@ export function defineConfig( ...(viteConfig.plugins || []), ...(clientViteConfig.plugins || []), TanStackServerFnPluginClient({ - runtimeCode: `import { createClientRpc } from '@tanstack/start/client-runtime'`, + getRuntimeCode: (opts) => + `import { createClientRpc } from '@tanstack/start/client-runtime'`, replacer: (opts) => `createClientRpc('${opts.filename}', '${opts.functionId}')`, }), @@ -214,7 +215,8 @@ export function defineConfig( ...(getUserViteConfig(opts.vite).plugins || []), ...(getUserViteConfig(opts.routers?.ssr?.vite).plugins || []), TanStackServerFnPluginServer({ - runtimeCode: `import { createServerRpc } from '@tanstack/start/ssr-runtime'`, + getRuntimeCode: (opts) => + `import { createServerRpc } from '@tanstack/start/ssr-runtime'`, replacer: (opts) => `createSsrRpc('${opts.filename}', '${opts.functionId}')`, }), @@ -259,7 +261,8 @@ export function defineConfig( }, }), TanStackServerFnPluginServer({ - runtimeCode: `import { createServerRpc } from '@tanstack/start/server-runtime'`, + getRuntimeCode: (opts) => + `import { createServerRpc } from '@tanstack/start/server-runtime'`, replacer: (opts) => `createServerRpc('${opts.filename}', '${opts.functionId}')`, // TODO: RSCS - remove this diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e97a5cd541..2269a73f44 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3543,9 +3543,18 @@ importers: '@types/babel__traverse': specifier: ^7.20.6 version: 7.20.6 + '@types/diff': + specifier: ^6.0.0 + version: 6.0.0 babel-dead-code-elimination: specifier: ^1.0.6 version: 1.0.6 + chalk: + specifier: ^5.3.0 + version: 5.3.0 + diff: + specifier: ^7.0.0 + version: 7.0.0 tiny-invariant: specifier: ^1.3.3 version: 1.3.3 @@ -6018,6 +6027,9 @@ packages: '@types/cross-spawn@6.0.6': resolution: {integrity: sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==} + '@types/diff@6.0.0': + resolution: {integrity: sha512-dhVCYGv3ZSbzmQaBSagrv1WJ6rXCdkyTcDyoNu1MD8JohI7pR7k8wdZEm+mvdxRKXyHVwckFzWU1vJc+Z29MlA==} + '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -7290,6 +7302,10 @@ packages: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + diff@7.0.0: + resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==} + engines: {node: '>=0.3.1'} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -13009,6 +13025,8 @@ snapshots: dependencies: '@types/node': 22.10.1 + '@types/diff@6.0.0': {} + '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 @@ -14420,6 +14438,8 @@ snapshots: diff-sequences@29.6.3: {} + diff@7.0.0: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 From e0d8a313d23e4748d41cde3a7c3975f0fb46826e Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Thu, 12 Dec 2024 15:40:39 -0700 Subject: [PATCH 06/38] fix: dont touch anything outside of the function --- .../src/compilers.ts | 238 ++++++++++-------- .../tests/compiler.test.ts | 154 +++++++----- 2 files changed, 222 insertions(+), 170 deletions(-) diff --git a/packages/server-functions-vite-plugin/src/compilers.ts b/packages/server-functions-vite-plugin/src/compilers.ts index b9679e9c6e..5bad33d8ee 100644 --- a/packages/server-functions-vite-plugin/src/compilers.ts +++ b/packages/server-functions-vite-plugin/src/compilers.ts @@ -1,7 +1,6 @@ import path from 'node:path' import * as babel from '@babel/core' import _generate from '@babel/generator' -import { deadCodeElimination } from 'babel-dead-code-elimination' import { isIdentifier, isVariableDeclarator } from '@babel/types' import { parseAst } from './ast' @@ -13,15 +12,21 @@ if ('default' in generate) { generate = generate.default as typeof generate } -export function compileEliminateDeadCode(opts: ParseAstOptions) { - const ast = parseAst(opts) - deadCodeElimination(ast) - - return generate(ast, { - sourceMaps: true, - }) +interface ServerFunctionInfo { + nodePath: babel.NodePath + functionName: string + functionId: string } +// export function compileEliminateDeadCode(opts: ParseAstOptions) { +// const ast = parseAst(opts) +// deadCodeElimination(ast) + +// return generate(ast, { +// sourceMaps: true, +// }) +// } + // const debug = process.env.TSR_VITE_DEBUG === 'true' function findNearestVariableName(path: babel.NodePath): string { @@ -123,52 +128,133 @@ function makeFileLocationUrlSafe(location: string): string { export function compileServerFnClient( opts: ParseAstOptions & { getRuntimeCode: (opts: { - serverFnPathsByFunctionId: Record< - string, - { nodePath: babel.NodePath; functionName: string; functionId: string } - > + serverFnPathsByFunctionId: Record }) => string replacer: (opts: { filename: string; functionId: string }) => string }, ) { const ast = parseAst(opts) - const serverFnPathsByFunctionId: Record< - string, - { - nodePath: babel.NodePath - functionName: string - functionId: string + const serverFnPathsByFunctionId = findServerFunctions(ast, opts) + + // Add runtime code if there are server functions + if (Object.keys(serverFnPathsByFunctionId).length > 0) { + const runtimeImport = babel.template.statement( + opts.getRuntimeCode({ serverFnPathsByFunctionId }), + )() + ast.program.body.unshift(runtimeImport) + } + + // Replace server functions with client-side stubs + for (const [functionId, { nodePath }] of Object.entries( + serverFnPathsByFunctionId, + )) { + const replacementCode = opts.replacer({ + filename: opts.filename, + functionId: functionId, + }) + + // Check to see if we can do this: + if ( + babel.types.isArrowFunctionExpression(nodePath.node) || + babel.types.isFunctionExpression(nodePath.node) || + babel.types.isFunctionDeclaration(nodePath.node) || + babel.types.isObjectMethod(nodePath.node) || + babel.types.isClassMethod(nodePath.node) + ) { + nodePath.node.params = [ + babel.types.restElement(babel.types.identifier('args')), + ] } - > = {} + + // Get the function body statements + const bodyStatements: Array = [] + + // Remove 'use server' directive + if (babel.types.isBlockStatement(nodePath.node.body)) { + nodePath.node.body.directives = nodePath.node.body.directives.filter( + (directive) => directive.value.value !== 'use server', + ) + } + + const replacement = babel.template.expression( + `(${replacementCode})(...args)`, + )() + + if (babel.types.isArrowFunctionExpression(nodePath.node)) { + if (babel.types.isBlockStatement(nodePath.node.body)) { + nodePath.node.body.body = [babel.types.returnStatement(replacement)] + } else { + nodePath.node.body = babel.types.blockStatement([ + babel.types.returnStatement(replacement), + ]) + } + } else if ( + babel.types.isFunctionExpression(nodePath.node) || + babel.types.isFunctionDeclaration(nodePath.node) || + babel.types.isObjectMethod(nodePath.node) || + babel.types.isClassMethod(nodePath.node) + ) { + nodePath.node.body.body = [babel.types.returnStatement(replacement)] + } + } + + const compiledCode = generate(ast, { + sourceMaps: true, + minified: process.env.NODE_ENV === 'production', + }) + + return { + compiledCode, + serverFns: serverFnPathsByFunctionId, + } +} + +export function compileServerFnServer(opts: ParseAstOptions) { + const ast = parseAst(opts) + const serverFnPathsByFunctionId = findServerFunctions(ast, opts) + + const compiledCode = generate(ast, { + sourceMaps: true, + minified: process.env.NODE_ENV === 'production', + }) + + return { + compiledCode, + serverFns: serverFnPathsByFunctionId, + } +} + +function findServerFunctions(ast: babel.types.File, opts: ParseAstOptions) { + const serverFnPathsByFunctionId: Record = {} const counts: Record = {} + const recordServerFunction = (nodePath: babel.NodePath) => { + const fnName = findNearestVariableName(nodePath) + + const baseLabel = makeFileLocationUrlSafe( + `${opts.filename.replace( + path.extname(opts.filename), + '', + )}--${fnName}`.replace(opts.root, ''), + ) + + counts[baseLabel] = (counts[baseLabel] || 0) + 1 + + const functionId = + counts[baseLabel] > 1 + ? `${baseLabel}_${counts[baseLabel] - 1}` + : baseLabel + + serverFnPathsByFunctionId[functionId] = { + nodePath, + functionName: fnName || '', + functionId: functionId, + } + } + babel.traverse(ast, { Program: { enter(programPath) { - const recordServerFunction = (nodePath: babel.NodePath) => { - const fnName = findNearestVariableName(nodePath) - - const baseLabel = makeFileLocationUrlSafe( - `${opts.filename.replace( - path.extname(opts.filename), - '', - )}--${fnName}`.replace(opts.root, ''), - ) - - counts[baseLabel] = (counts[baseLabel] || 0) + 1 - - const functionId = - counts[baseLabel] > 1 - ? `${baseLabel}_${counts[baseLabel] - 1}` - : baseLabel - - serverFnPathsByFunctionId[functionId] = { - nodePath, - functionName: fnName || '', - functionId: functionId, - } - } - programPath.traverse({ enter(path) { // Function declarations @@ -241,75 +327,11 @@ export function compileServerFnClient( } }, }) - - if (Object.keys(serverFnPathsByFunctionId).length > 0) { - const runtimeImport = babel.template.statement( - opts.getRuntimeCode({ serverFnPathsByFunctionId }), - )() - ast.program.body.unshift(runtimeImport) - } - - // Replace server functions with client-side stubs - for (const [functionId, fnPath] of Object.entries( - serverFnPathsByFunctionId, - )) { - const replacementCode = opts.replacer({ - filename: opts.filename, - functionId: functionId, - }) - const replacement = babel.template.expression(replacementCode)() - - // For variable declarations, replace the init - if (fnPath.nodePath.isVariableDeclarator()) { - fnPath.nodePath.get('init').replaceWith(replacement) - } - // For class/object methods, replace the whole method - else if ( - fnPath.nodePath.isClassMethod() || - fnPath.nodePath.isObjectMethod() - ) { - fnPath.nodePath.replaceWith( - babel.types.classMethod( - fnPath.nodePath.node.kind, - fnPath.nodePath.node.key, - fnPath.nodePath.node.params, - babel.types.blockStatement([ - babel.types.returnStatement(replacement), - ]), - ), - ) - } - // For function expressions, replace the whole path - else { - fnPath.nodePath.replaceWith(replacement) - } - } }, }, }) - const compiledCode = generate(ast, { - sourceMaps: true, - minified: process.env.NODE_ENV === 'production', - }) - - return { - compiledCode, - serverFns: serverFnPathsByFunctionId, - } -} - -export function compileServerFnServer(opts: ParseAstOptions) { - const ast = parseAst(opts) - - const compiledCode = generate(ast, { - sourceMaps: true, - minified: process.env.NODE_ENV === 'production', - }) - - return { - compiledCode, - } + return serverFnPathsByFunctionId } // function codeFrameError( diff --git a/packages/server-functions-vite-plugin/tests/compiler.test.ts b/packages/server-functions-vite-plugin/tests/compiler.test.ts index 6c287b2f18..e34a8d7de7 100644 --- a/packages/server-functions-vite-plugin/tests/compiler.test.ts +++ b/packages/server-functions-vite-plugin/tests/compiler.test.ts @@ -30,10 +30,12 @@ describe('server function compilation', () => { const client = compileServerFnClient({ ...clientConfig, code }) await expect(client.compiledCode.code).toMatchInlineSnapshot(` "import { createClientRpc } from "my-rpc-lib"; - createClientRpc({ - filename: "test.ts", - functionId: "test--useServer" - });" + function useServer(...args) { + return createClientRpc({ + filename: "test.ts", + functionId: "test--useServer" + })(...args); + }" `) const server = compileServerFnServer({ ...serverConfig, code }) @@ -57,10 +59,12 @@ describe('server function compilation', () => { const client = compileServerFnClient({ ...clientConfig, code }) expect(client.compiledCode.code).toMatchInlineSnapshot(` "import { createClientRpc } from "my-rpc-lib"; - const fn = createClientRpc({ - filename: "test.ts", - functionId: "test--fn_1" - });" + const fn = (...args) => { + return createClientRpc({ + filename: "test.ts", + functionId: "test--fn_1" + })(...args); + };" `) const server = compileServerFnServer({ ...serverConfig, code }) @@ -83,10 +87,12 @@ describe('server function compilation', () => { const client = compileServerFnClient({ ...clientConfig, code }) expect(client.compiledCode.code).toMatchInlineSnapshot(` "import { createClientRpc } from "my-rpc-lib"; - const anonymousFn = createClientRpc({ - filename: "test.ts", - functionId: "test--anonymousFn_1" - });" + const anonymousFn = function (...args) { + return createClientRpc({ + filename: "test.ts", + functionId: "test--anonymousFn_1" + })(...args); + };" `) const server = compileServerFnServer({ ...serverConfig, code }) @@ -116,17 +122,17 @@ describe('server function compilation', () => { expect(client.compiledCode.code).toMatchInlineSnapshot(` "import { createClientRpc } from "my-rpc-lib"; class TestClass { - method() { + method(...args) { return createClientRpc({ filename: "test.ts", functionId: "test--method" - }); + })(...args); } - staticMethod() { + static staticMethod(...args) { return createClientRpc({ filename: "test.ts", functionId: "test--staticMethod" - }); + })(...args); } }" `) @@ -162,11 +168,11 @@ describe('server function compilation', () => { expect(client.compiledCode.code).toMatchInlineSnapshot(` "import { createClientRpc } from "my-rpc-lib"; const obj = { - method() { + method(...args) { return createClientRpc({ filename: "test.ts", functionId: "test--obj_method" - }); + })(...args); } };" `) @@ -199,14 +205,18 @@ describe('server function compilation', () => { const client = compileServerFnClient({ ...clientConfig, code }) expect(client.compiledCode.code).toMatchInlineSnapshot(` "import { createClientRpc } from "my-rpc-lib"; - createClientRpc({ - filename: "test.ts", - functionId: "test--asyncServer" - }); - const asyncArrow = createClientRpc({ - filename: "test.ts", - functionId: "test--asyncArrow_1" - });" + async function asyncServer(...args) { + return createClientRpc({ + filename: "test.ts", + functionId: "test--asyncServer" + })(...args); + } + const asyncArrow = async (...args) => { + return createClientRpc({ + filename: "test.ts", + functionId: "test--asyncArrow_1" + })(...args); + };" `) const server = compileServerFnServer({ ...serverConfig, code }) @@ -240,14 +250,18 @@ describe('server function compilation', () => { const client = compileServerFnClient({ ...clientConfig, code }) expect(client.compiledCode.code).toMatchInlineSnapshot(` "import { createClientRpc } from "my-rpc-lib"; - createClientRpc({ - filename: "test.ts", - functionId: "test--generatorServer" - }); - createClientRpc({ - filename: "test.ts", - functionId: "test--asyncGeneratorServer" - });" + function* generatorServer(...args) { + return createClientRpc({ + filename: "test.ts", + functionId: "test--generatorServer" + })(...args); + } + async function* asyncGeneratorServer(...args) { + return createClientRpc({ + filename: "test.ts", + functionId: "test--asyncGeneratorServer" + })(...args); + }" `) const server = compileServerFnServer({ ...serverConfig, code }) @@ -280,10 +294,12 @@ describe('server function compilation', () => { expect(client.compiledCode.code).toMatchInlineSnapshot(` "import { createClientRpc } from "my-rpc-lib"; function outer() { - createClientRpc({ - filename: "test.ts", - functionId: "test--outer_inner" - }); + function inner(...args) { + return createClientRpc({ + filename: "test.ts", + functionId: "test--outer_inner" + })(...args); + } return inner; }" `) @@ -313,10 +329,14 @@ describe('server function compilation', () => { const client = compileServerFnClient({ ...clientConfig, code }) expect(client.compiledCode.code).toMatchInlineSnapshot(` "import { createClientRpc } from "my-rpc-lib"; - createClientRpc({ - filename: "test.ts", - functionId: "test--multiDirective" - });" + function multiDirective(...args) { + 'use strict'; + + return createClientRpc({ + filename: "test.ts", + functionId: "test--multiDirective" + })(...args); + }" `) const server = compileServerFnServer({ ...serverConfig, code }) @@ -341,10 +361,12 @@ describe('server function compilation', () => { const client = compileServerFnClient({ ...clientConfig, code }) expect(client.compiledCode.code).toMatchInlineSnapshot(` "import { createClientRpc } from "my-rpc-lib"; - createClientRpc({ - filename: "test.ts", - functionId: "test--withParams" - });" + function withParams(...args) { + return createClientRpc({ + filename: "test.ts", + functionId: "test--withParams" + })(...args); + }" `) const server = compileServerFnServer({ ...serverConfig, code }) @@ -368,10 +390,12 @@ describe('server function compilation', () => { const client = compileServerFnClient({ ...clientConfig, code }) expect(client.compiledCode.code).toMatchInlineSnapshot(` "import { createClientRpc } from "my-rpc-lib"; - const iife = createClientRpc({ - filename: "test.ts", - functionId: "test--iife" - })();" + const iife = function (...args) { + return createClientRpc({ + filename: "test.ts", + functionId: "test--iife" + })(...args); + }();" `) const server = compileServerFnServer({ ...serverConfig, code }) @@ -398,10 +422,12 @@ describe('server function compilation', () => { expect(client.compiledCode.code).toMatchInlineSnapshot(` "import { createClientRpc } from "my-rpc-lib"; function higherOrder() { - return createClientRpc({ - filename: "test.ts", - functionId: "test--higherOrder" - }); + return function (...args) { + return createClientRpc({ + filename: "test.ts", + functionId: "test--higherOrder" + })(...args); + }; }" `) @@ -441,18 +467,22 @@ describe('server function compilation', () => { "import { createClientRpc } from "my-rpc-lib"; function main() { function middle() { - const useServer = createClientRpc({ - filename: "test.ts", - functionId: "test--main_middle_useServer_1" - }); + const useServer = function (...args) { + return createClientRpc({ + filename: "test.ts", + functionId: "test--main_middle_useServer_1" + })(...args); + }; return useServer; } return middle; } - main().middle(createClientRpc({ - filename: "test.ts", - functionId: "test--main_middle_useServer_2" - }));" + main().middle(function useServer(...args) { + return createClientRpc({ + filename: "test.ts", + functionId: "test--main_middle_useServer_2" + })(...args); + });" `) const server = compileServerFnServer({ ...serverConfig, code }) From 44dac10664c2c1140fd229c109ab1e84d9b97986 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Thu, 12 Dec 2024 16:15:54 -0700 Subject: [PATCH 07/38] server functions now import a tsr-serverfn-split file version --- .../src/core/code-splitter/compilers.ts | 2 + .../src/compilers.ts | 109 +++++++++++++++--- .../tests/compiler.test.ts | 99 ++++++---------- packages/start-vite-plugin/src/compilers.ts | 3 + 4 files changed, 132 insertions(+), 81 deletions(-) diff --git a/packages/router-plugin/src/core/code-splitter/compilers.ts b/packages/router-plugin/src/core/code-splitter/compilers.ts index 84f082e1cc..ba2485b7d8 100644 --- a/packages/router-plugin/src/core/code-splitter/compilers.ts +++ b/packages/router-plugin/src/core/code-splitter/compilers.ts @@ -270,6 +270,7 @@ export function compileCodeSplitReferenceRoute(opts: ParseAstOptions) { return generate(ast, { sourceMaps: true, sourceFileName: opts.filename, + minified: process.env.NODE_ENV === 'production', }) } @@ -543,6 +544,7 @@ export function compileCodeSplitVirtualRoute(opts: ParseAstOptions) { return generate(ast, { sourceMaps: true, sourceFileName: opts.filename, + minified: process.env.NODE_ENV === 'production', }) } diff --git a/packages/server-functions-vite-plugin/src/compilers.ts b/packages/server-functions-vite-plugin/src/compilers.ts index 5bad33d8ee..f08b6eb408 100644 --- a/packages/server-functions-vite-plugin/src/compilers.ts +++ b/packages/server-functions-vite-plugin/src/compilers.ts @@ -24,6 +24,8 @@ interface ServerFunctionInfo { // return generate(ast, { // sourceMaps: true, +// sourceFileName: opts.filename, +// minified: process.env.NODE_ENV === 'production', // }) // } @@ -118,6 +120,23 @@ function findNearestVariableName(path: babel.NodePath): string { return nameParts.length > 0 ? nameParts.join('_') : 'anonymous' } +function isFunctionDeclaration( + node: babel.types.Node, +): node is + | babel.types.FunctionDeclaration + | babel.types.FunctionExpression + | babel.types.ArrowFunctionExpression + | babel.types.ClassMethod + | babel.types.ObjectMethod { + return ( + babel.types.isFunctionDeclaration(node) || + babel.types.isFunctionExpression(node) || + babel.types.isArrowFunctionExpression(node) || + babel.types.isClassMethod(node) || + babel.types.isObjectMethod(node) + ) +} + function makeFileLocationUrlSafe(location: string): string { return location .replace(/[^a-zA-Z0-9-_]/g, '_') // Replace unsafe chars with underscore @@ -153,29 +172,30 @@ export function compileServerFnClient( functionId: functionId, }) - // Check to see if we can do this: - if ( - babel.types.isArrowFunctionExpression(nodePath.node) || - babel.types.isFunctionExpression(nodePath.node) || - babel.types.isFunctionDeclaration(nodePath.node) || - babel.types.isObjectMethod(nodePath.node) || - babel.types.isClassMethod(nodePath.node) - ) { - nodePath.node.params = [ - babel.types.restElement(babel.types.identifier('args')), - ] + // Check to see if the function is a valid function declaration + if (!isFunctionDeclaration(nodePath.node)) { + throw new Error( + `Server function is not a function declaration: ${nodePath.node}`, + ) } - // Get the function body statements - const bodyStatements: Array = [] + nodePath.node.params = [ + babel.types.restElement(babel.types.identifier('args')), + ] // Remove 'use server' directive - if (babel.types.isBlockStatement(nodePath.node.body)) { - nodePath.node.body.directives = nodePath.node.body.directives.filter( - (directive) => directive.value.value !== 'use server', + // Check if the node body is a block statement + if (!babel.types.isBlockStatement(nodePath.node.body)) { + throw new Error( + `Server function body is not a block statement: ${nodePath.node.body}`, ) } + const nodeBody = nodePath.node.body + nodeBody.directives = nodeBody.directives.filter( + (directive) => directive.value.value !== 'use server', + ) + const replacement = babel.template.expression( `(${replacementCode})(...args)`, )() @@ -200,6 +220,7 @@ export function compileServerFnClient( const compiledCode = generate(ast, { sourceMaps: true, + sourceFileName: opts.filename, minified: process.env.NODE_ENV === 'production', }) @@ -213,8 +234,64 @@ export function compileServerFnServer(opts: ParseAstOptions) { const ast = parseAst(opts) const serverFnPathsByFunctionId = findServerFunctions(ast, opts) + // Replace server function bodies with dynamic imports + Object.entries(serverFnPathsByFunctionId).forEach( + ([functionId, { nodePath }]) => { + // Check if the node is a function declaration + if (!isFunctionDeclaration(nodePath.node)) { + throw new Error( + `Server function is not a function declaration: ${nodePath.node}`, + ) + } + + nodePath.node.params = [ + babel.types.restElement(babel.types.identifier('args')), + ] + + // Check if the node body is a block statement + if (!babel.types.isBlockStatement(nodePath.node.body)) { + throw new Error( + `Server function body is not a block statement: ${nodePath.node.body}`, + ) + } + + const nodeBody = nodePath.node.body + nodeBody.directives = nodeBody.directives.filter( + (directive) => directive.value.value !== 'use server', + ) + + // Create the dynamic import expression + const importExpression = babel.template.expression(` + import(${JSON.stringify(`${opts.filename}?tsr-serverfn-split=${functionId}`)}) + .then(mod => mod.serverFn(...args)) + `)() + + if (babel.types.isArrowFunctionExpression(nodePath.node)) { + if (babel.types.isBlockStatement(nodePath.node.body)) { + nodePath.node.body.body = [ + babel.types.returnStatement(importExpression), + ] + } else { + nodePath.node.body = babel.types.blockStatement([ + babel.types.returnStatement(importExpression), + ]) + } + } else if ( + babel.types.isFunctionExpression(nodePath.node) || + babel.types.isFunctionDeclaration(nodePath.node) || + babel.types.isObjectMethod(nodePath.node) || + babel.types.isClassMethod(nodePath.node) + ) { + nodePath.node.body.body = [ + babel.types.returnStatement(importExpression), + ] + } + }, + ) + const compiledCode = generate(ast, { sourceMaps: true, + sourceFileName: opts.filename, minified: process.env.NODE_ENV === 'production', }) diff --git a/packages/server-functions-vite-plugin/tests/compiler.test.ts b/packages/server-functions-vite-plugin/tests/compiler.test.ts index e34a8d7de7..ee958f0084 100644 --- a/packages/server-functions-vite-plugin/tests/compiler.test.ts +++ b/packages/server-functions-vite-plugin/tests/compiler.test.ts @@ -40,10 +40,8 @@ describe('server function compilation', () => { const server = compileServerFnServer({ ...serverConfig, code }) await expect(server.compiledCode.code).toMatchInlineSnapshot(` - "function useServer() { - 'use server'; - - return 'hello'; + "function useServer(...args) { + return import("test.ts?tsr-serverfn-split=test--useServer").then(mod => mod.serverFn(...args)); }" `) }) @@ -69,10 +67,8 @@ describe('server function compilation', () => { const server = compileServerFnServer({ ...serverConfig, code }) expect(server.compiledCode.code).toMatchInlineSnapshot(` - "const fn = () => { - 'use server'; - - return 'hello'; + "const fn = (...args) => { + return import("test.ts?tsr-serverfn-split=test--fn_1").then(mod => mod.serverFn(...args)); };" `) }) @@ -97,8 +93,8 @@ describe('server function compilation', () => { const server = compileServerFnServer({ ...serverConfig, code }) expect(server.compiledCode.code).toMatchInlineSnapshot(` - "const anonymousFn = function () { - 'use server'; + "const anonymousFn = function (...args) { + return import("test.ts?tsr-serverfn-split=test--anonymousFn_1").then(mod => mod.serverFn(...args)); };" `) }) @@ -140,15 +136,11 @@ describe('server function compilation', () => { const server = compileServerFnServer({ ...serverConfig, code }) expect(server.compiledCode.code).toMatchInlineSnapshot(` "class TestClass { - method() { - 'use server'; - - return 'hello'; + method(...args) { + return import("test.ts?tsr-serverfn-split=test--method").then(mod => mod.serverFn(...args)); } - static staticMethod() { - 'use server'; - - return 'hello'; + static staticMethod(...args) { + return import("test.ts?tsr-serverfn-split=test--staticMethod").then(mod => mod.serverFn(...args)); } }" `) @@ -180,10 +172,8 @@ describe('server function compilation', () => { const server = compileServerFnServer({ ...serverConfig, code }) expect(server.compiledCode.code).toMatchInlineSnapshot(` "const obj = { - method() { - 'use server'; - - return 'hello'; + method(...args) { + return import("test.ts?tsr-serverfn-split=test--obj_method").then(mod => mod.serverFn(...args)); } };" `) @@ -221,15 +211,11 @@ describe('server function compilation', () => { const server = compileServerFnServer({ ...serverConfig, code }) expect(server.compiledCode.code).toMatchInlineSnapshot(` - "async function asyncServer() { - 'use server'; - - return 'hello'; + "async function asyncServer(...args) { + return import("test.ts?tsr-serverfn-split=test--asyncServer").then(mod => mod.serverFn(...args)); } - const asyncArrow = async () => { - 'use server'; - - return 'hello'; + const asyncArrow = async (...args) => { + return import("test.ts?tsr-serverfn-split=test--asyncArrow_1").then(mod => mod.serverFn(...args)); };" `) }) @@ -266,15 +252,11 @@ describe('server function compilation', () => { const server = compileServerFnServer({ ...serverConfig, code }) expect(server.compiledCode.code).toMatchInlineSnapshot(` - "function* generatorServer() { - 'use server'; - - yield 'hello'; + "function* generatorServer(...args) { + return import("test.ts?tsr-serverfn-split=test--generatorServer").then(mod => mod.serverFn(...args)); } - async function* asyncGeneratorServer() { - 'use server'; - - yield 'hello'; + async function* asyncGeneratorServer(...args) { + return import("test.ts?tsr-serverfn-split=test--asyncGeneratorServer").then(mod => mod.serverFn(...args)); }" `) }) @@ -307,10 +289,8 @@ describe('server function compilation', () => { const server = compileServerFnServer({ ...serverConfig, code }) expect(server.compiledCode.code).toMatchInlineSnapshot(` "function outer() { - function inner() { - 'use server'; - - return 'hello'; + function inner(...args) { + return import("test.ts?tsr-serverfn-split=test--outer_inner").then(mod => mod.serverFn(...args)); } return inner; }" @@ -341,11 +321,10 @@ describe('server function compilation', () => { const server = compileServerFnServer({ ...serverConfig, code }) expect(server.compiledCode.code).toMatchInlineSnapshot(` - "function multiDirective() { + "function multiDirective(...args) { 'use strict'; - 'use server'; - return 'hello'; + return import("test.ts?tsr-serverfn-split=test--multiDirective").then(mod => mod.serverFn(...args)); }" `) }) @@ -371,10 +350,8 @@ describe('server function compilation', () => { const server = compileServerFnServer({ ...serverConfig, code }) expect(server.compiledCode.code).toMatchInlineSnapshot(` - "function withParams(a: string, b: number) { - 'use server'; - - return \`\${a} \${b}\`; + "function withParams(...args) { + return import("test.ts?tsr-serverfn-split=test--withParams").then(mod => mod.serverFn(...args)); }" `) }) @@ -400,10 +377,8 @@ describe('server function compilation', () => { const server = compileServerFnServer({ ...serverConfig, code }) expect(server.compiledCode.code).toMatchInlineSnapshot(` - "const iife = function () { - 'use server'; - - return 'hello'; + "const iife = function (...args) { + return import("test.ts?tsr-serverfn-split=test--iife").then(mod => mod.serverFn(...args)); }();" `) }) @@ -434,10 +409,8 @@ describe('server function compilation', () => { const server = compileServerFnServer({ ...serverConfig, code }) expect(server.compiledCode.code).toMatchInlineSnapshot(` "function higherOrder() { - return function () { - 'use server'; - - return 'hello'; + return function (...args) { + return import("test.ts?tsr-serverfn-split=test--higherOrder").then(mod => mod.serverFn(...args)); }; }" `) @@ -489,19 +462,15 @@ describe('server function compilation', () => { expect(server.compiledCode.code).toMatchInlineSnapshot(` "function main() { function middle() { - const useServer = function () { - 'use server'; - - return 'hello'; + const useServer = function (...args) { + return import("test.ts?tsr-serverfn-split=test--main_middle_useServer_1").then(mod => mod.serverFn(...args)); }; return useServer; } return middle; } - main().middle(function useServer() { - 'use server'; - - return 'hello'; + main().middle(function useServer(...args) { + return import("test.ts?tsr-serverfn-split=test--main_middle_useServer_2").then(mod => mod.serverFn(...args)); });" `) }) diff --git a/packages/start-vite-plugin/src/compilers.ts b/packages/start-vite-plugin/src/compilers.ts index 365a38609d..41b54b8ac2 100644 --- a/packages/start-vite-plugin/src/compilers.ts +++ b/packages/start-vite-plugin/src/compilers.ts @@ -21,6 +21,8 @@ export function compileEliminateDeadCode(opts: ParseAstOptions) { deadCodeElimination(ast) return generate(ast, { sourceMaps: true, + sourceFileName: opts.filename, + minified: process.env.NODE_ENV === 'production', filename: opts.filename, }) } @@ -186,6 +188,7 @@ export function compileStartOutput(opts: ParseAstOptions) { return generate(ast, { sourceMaps: true, + sourceFileName: opts.filename, filename: opts.filename, minified: process.env.NODE_ENV === 'production', }) From b1335a3be2ec512f13608c3261b9dfb63efc6f7e Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Wed, 18 Dec 2024 09:34:31 -0700 Subject: [PATCH 08/38] checkpoint --- .../server-functions-vite-plugin/package.json | 1 + .../src/compilers.ts | 663 ++++++------ .../server-functions-vite-plugin/src/index.ts | 17 +- .../tests/compiler.test.ts | 980 +++++++++++------- pnpm-lock.yaml | 13 + 5 files changed, 1028 insertions(+), 646 deletions(-) diff --git a/packages/server-functions-vite-plugin/package.json b/packages/server-functions-vite-plugin/package.json index 2dd6e26e72..dd6f839ff9 100644 --- a/packages/server-functions-vite-plugin/package.json +++ b/packages/server-functions-vite-plugin/package.json @@ -82,6 +82,7 @@ "@types/diff": "^6.0.0", "babel-dead-code-elimination": "^1.0.6", "chalk": "^5.3.0", + "dedent": "^1.5.3", "diff": "^7.0.0", "tiny-invariant": "^1.3.3" } diff --git a/packages/server-functions-vite-plugin/src/compilers.ts b/packages/server-functions-vite-plugin/src/compilers.ts index f08b6eb408..70ecafcb73 100644 --- a/packages/server-functions-vite-plugin/src/compilers.ts +++ b/packages/server-functions-vite-plugin/src/compilers.ts @@ -3,6 +3,8 @@ import * as babel from '@babel/core' import _generate from '@babel/generator' import { isIdentifier, isVariableDeclarator } from '@babel/types' +import { codeFrameColumns } from '@babel/code-frame' +import { deadCodeElimination } from 'babel-dead-code-elimination' import { parseAst } from './ast' import type { ParseAstOptions } from './ast' @@ -12,25 +14,97 @@ if ('default' in generate) { generate = generate.default as typeof generate } -interface ServerFunctionInfo { - nodePath: babel.NodePath +export interface ServerFunctionInfo { + nodePath: SupportedFunctionPath functionName: string functionId: string + referenceName: string + splitFileId: string } -// export function compileEliminateDeadCode(opts: ParseAstOptions) { -// const ast = parseAst(opts) -// deadCodeElimination(ast) +export type SupportedFunctionPath = + | babel.NodePath + | babel.NodePath + | babel.NodePath -// return generate(ast, { -// sourceMaps: true, -// sourceFileName: opts.filename, -// minified: process.env.NODE_ENV === 'production', -// }) -// } +export type ReplacerFn = (opts: { + fn: string + splitImportFn: string + filename: string + functionId: string +}) => string // const debug = process.env.TSR_VITE_DEBUG === 'true' +export type CompileDirectivesOpts = ParseAstOptions & { + directive: string + getRuntimeCode?: (opts: { + serverFnPathsByFunctionId: Record + }) => string + replacer: ReplacerFn +} + +const tsrServerFnSplitParam = 'tsr-serverfn-split' + +export function compileDirectives(opts: CompileDirectivesOpts) { + const [_, searchParamsStr] = opts.filename.split('?') + const searchParams = new URLSearchParams(searchParamsStr) + const functionId = searchParams.get(tsrServerFnSplitParam) + + const ast = parseAst(opts) + const serverFnPathsByFunctionId = findServerFunctions(ast, { + ...opts, + splitFunctionId: functionId, + }) + + // Add runtime code if there are server functions + // Add runtime code if there are server functions + if ( + Object.keys(serverFnPathsByFunctionId).length > 0 && + opts.getRuntimeCode + ) { + const runtimeImport = babel.template.statement( + opts.getRuntimeCode({ serverFnPathsByFunctionId }), + )() + ast.program.body.unshift(runtimeImport) + } + + // If there is a functionId, we need to remove all exports + // then make sure that our function is exported under the + // "serverFn" name + if (functionId) { + const serverFn = serverFnPathsByFunctionId[functionId] + + if (!serverFn) { + throw new Error(`Server function ${functionId} not found`) + } + + console.log('functionId', functionId) + safeRemoveExports(ast) + + ast.program.body.push( + babel.types.exportDefaultDeclaration( + babel.types.identifier(serverFn.referenceName), + ), + ) + + // If we have a functionId, we're also going to DCE the file + // so that the functionId is not exported + deadCodeElimination(ast) + } + + const compiledResult = generate(ast, { + sourceMaps: true, + sourceFileName: opts.filename, + minified: process.env.NODE_ENV === 'production', + }) + + return { + compiledResult, + serverFns: serverFnPathsByFunctionId, + } +} + function findNearestVariableName(path: babel.NodePath): string { let currentPath: babel.NodePath | null = path const nameParts: Array = [] @@ -99,15 +173,12 @@ function findNearestVariableName(path: babel.NodePath): string { babel.types.isClassMethod(currentPath.node) || babel.types.isObjectMethod(currentPath.node) ) { - if (babel.types.isIdentifier(currentPath.node.key)) { - return currentPath.node.key.name - } - if (babel.types.isStringLiteral(currentPath.node.key)) { - return currentPath.node.key.value - } + throw new Error( + 'use server in ClassMethod or ObjectMethod not supported', + ) } - return null + return '' })() if (name) { @@ -120,23 +191,6 @@ function findNearestVariableName(path: babel.NodePath): string { return nameParts.length > 0 ? nameParts.join('_') : 'anonymous' } -function isFunctionDeclaration( - node: babel.types.Node, -): node is - | babel.types.FunctionDeclaration - | babel.types.FunctionExpression - | babel.types.ArrowFunctionExpression - | babel.types.ClassMethod - | babel.types.ObjectMethod { - return ( - babel.types.isFunctionDeclaration(node) || - babel.types.isFunctionExpression(node) || - babel.types.isArrowFunctionExpression(node) || - babel.types.isClassMethod(node) || - babel.types.isObjectMethod(node) - ) -} - function makeFileLocationUrlSafe(location: string): string { return location .replace(/[^a-zA-Z0-9-_]/g, '_') // Replace unsafe chars with underscore @@ -144,292 +198,317 @@ function makeFileLocationUrlSafe(location: string): string { .replace(/^_|_$/g, '') // Trim leading/trailing underscores } -export function compileServerFnClient( +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 findServerFunctions( + ast: babel.types.File, opts: ParseAstOptions & { - getRuntimeCode: (opts: { - serverFnPathsByFunctionId: Record - }) => string - replacer: (opts: { filename: string; functionId: string }) => string + replacer?: ReplacerFn + splitFunctionId?: string | null }, ) { - const ast = parseAst(opts) - const serverFnPathsByFunctionId = findServerFunctions(ast, opts) - - // Add runtime code if there are server functions - if (Object.keys(serverFnPathsByFunctionId).length > 0) { - const runtimeImport = babel.template.statement( - opts.getRuntimeCode({ serverFnPathsByFunctionId }), - )() - ast.program.body.unshift(runtimeImport) - } - - // Replace server functions with client-side stubs - for (const [functionId, { nodePath }] of Object.entries( - serverFnPathsByFunctionId, - )) { - const replacementCode = opts.replacer({ - filename: opts.filename, - functionId: functionId, - }) - - // Check to see if the function is a valid function declaration - if (!isFunctionDeclaration(nodePath.node)) { - throw new Error( - `Server function is not a function declaration: ${nodePath.node}`, - ) - } + const serverFnPathsByFunctionId: Record = {} + const functionIdCounts: Record = {} + const referenceCounts: Map = new Map() - nodePath.node.params = [ - babel.types.restElement(babel.types.identifier('args')), - ] + let programPath: babel.NodePath - // Remove 'use server' directive - // Check if the node body is a block statement - if (!babel.types.isBlockStatement(nodePath.node.body)) { - throw new Error( - `Server function body is not a block statement: ${nodePath.node.body}`, - ) - } + babel.traverse(ast, { + Program(path) { + programPath = path + }, + }) - const nodeBody = nodePath.node.body - nodeBody.directives = nodeBody.directives.filter( - (directive) => directive.value.value !== 'use server', - ) + // Find all server functions + babel.traverse(ast, { + DirectiveLiteral(nodePath) { + if (nodePath.node.value === 'use server') { + let directiveFn = nodePath.findParent((p) => p.isFunction()) as + | SupportedFunctionPath + | undefined + + if (!directiveFn) return + + // Handle class and object methods which are not supported + const isGenerator = + directiveFn.isFunction() && directiveFn.node.generator + + const isClassMethod = directiveFn.isClassMethod() + const isObjectMethod = directiveFn.isObjectMethod() + + if (isClassMethod || isObjectMethod || isGenerator) { + throw codeFrameError( + opts.code, + directiveFn.node.loc, + `use server in ${isClassMethod ? 'class' : isObjectMethod ? 'object method' : 'generator function'} not supported`, + ) + } - const replacement = babel.template.expression( - `(${replacementCode})(...args)`, - )() + // 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 (babel.types.isArrowFunctionExpression(nodePath.node)) { - if (babel.types.isBlockStatement(nodePath.node.body)) { - nodePath.node.body.body = [babel.types.returnStatement(replacement)] - } else { - nodePath.node.body = babel.types.blockStatement([ - babel.types.returnStatement(replacement), - ]) - } - } else if ( - babel.types.isFunctionExpression(nodePath.node) || - babel.types.isFunctionDeclaration(nodePath.node) || - babel.types.isObjectMethod(nodePath.node) || - babel.types.isClassMethod(nodePath.node) - ) { - nodePath.node.body.body = [babel.types.returnStatement(replacement)] - } - } + if (nearestBlock) { + throw codeFrameError( + opts.code, + nearestBlock.node.loc, + 'Server functions cannot be nested in other blocks or functions', + ) + } - const compiledCode = generate(ast, { - sourceMaps: true, - sourceFileName: opts.filename, - minified: process.env.NODE_ENV === 'production', - }) + // Handle supported function types + if ( + directiveFn.isFunctionDeclaration() || + directiveFn.isFunctionExpression() || + (directiveFn.isArrowFunctionExpression() && + babel.types.isBlockStatement(directiveFn.node.body)) + ) { + // Remove the 'use server' 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 !== 'use server', + ) + } - return { - compiledCode, - serverFns: serverFnPathsByFunctionId, - } -} + // Find the nearest variable name + const fnName = findNearestVariableName(directiveFn) + + // Make the functionId safe for use in a URL + const baseLabel = makeFileLocationUrlSafe( + `${opts.filename.replace( + path.extname(opts.filename), + '', + )}--${fnName}`.replace(opts.root, ''), + ) + + // Count the number of functions with the same baseLabel + functionIdCounts[baseLabel] = (functionIdCounts[baseLabel] || 0) + 1 + + // If there are multiple functions with the same baseLabel, + // append a unique identifier to the functionId + const functionId = + functionIdCounts[baseLabel] > 1 + ? `${baseLabel}_${functionIdCounts[baseLabel] - 1}` + : baseLabel + + // Move the function to program level while preserving its position + // in the program body + const programBody = programPath.node.body + + const topParent = + directiveFn.findParent((p) => !!p.parentPath?.isProgram()) || + directiveFn + + const topParentIndex = programBody.indexOf(topParent.node as any) + + // Determine the reference name for the function + let referenceName = makeIdentifierSafe(fnName) + + // If we find this referece in the scope, we need to make it unique + while (programPath.scope.hasBinding(referenceName)) { + referenceCounts.set( + referenceName, + (referenceCounts.get(referenceName) || 0) + 1, + ) + referenceName += `_${referenceCounts.get(referenceName) || 0}` + } -export function compileServerFnServer(opts: ParseAstOptions) { - const ast = parseAst(opts) - const serverFnPathsByFunctionId = findServerFunctions(ast, opts) + // if (referenceCounts.get(referenceName) === 0) { + + // referenceName += `_${(referenceCounts.get(referenceName) || 0) + 1}` + + // If the reference name came from the function declaration, + // // We need to update the function name to match the reference name + // if (babel.types.isFunctionDeclaration(directiveFn.node)) { + // console.log('updating function name', directiveFn.node.id!.name) + // directiveFn.node.id!.name = referenceName + // } + + // 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()) { + // Then place the function at the top level + programBody.splice( + topParentIndex, + 0, + babel.types.variableDeclaration('const', [ + babel.types.variableDeclarator( + babel.types.identifier(referenceName), + babel.types.toExpression(directiveFn.node as any), + ), + ]), + ) + + // If it's an exported named function, we need to swap it with an + // export const originalFunctionName = referenceName + if ( + babel.types.isExportNamedDeclaration( + 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(referenceName), + ), + ]), + ), + ) + + directiveFn.remove() + } else { + directiveFn.replaceWith(babel.types.identifier(referenceName)) + } - // Replace server function bodies with dynamic imports - Object.entries(serverFnPathsByFunctionId).forEach( - ([functionId, { nodePath }]) => { - // Check if the node is a function declaration - if (!isFunctionDeclaration(nodePath.node)) { - throw new Error( - `Server function is not a function declaration: ${nodePath.node}`, - ) - } + directiveFn = programPath.get( + `body.${topParentIndex}.declarations.0.init`, + ) as SupportedFunctionPath + } - nodePath.node.params = [ - babel.types.restElement(babel.types.identifier('args')), - ] + const [filename, searchParamsStr] = opts.filename.split('?') + + const searchParams = new URLSearchParams(searchParamsStr) + searchParams.set('tsr-serverfn-split', functionId) + const splitFileId = `${filename}?${searchParams.toString()}` + + // If a replacer is provided, replace the function with the replacer + if (functionId !== opts.splitFunctionId && opts.replacer) { + const replacer = opts.replacer({ + fn: '$$fn$$', + splitImportFn: '$$splitImportFn$$', + // splitFileId, + filename: filename!, + functionId: functionId, + }) + + const replacement = babel.template.expression(replacer, { + placeholderPattern: false, + placeholderWhitelist: new Set(['$$fn$$', '$$splitImportFn$$']), + })({ + ...(replacer.includes('$$fn$$') + ? { $$fn$$: babel.types.toExpression(directiveFn.node) } + : {}), + ...(replacer.includes('$$splitImportFn$$') + ? { + $$splitImportFn$$: `(...args) => import(${JSON.stringify(splitFileId)}).then(module => module.default(...args))`, + } + : {}), + }) - // Check if the node body is a block statement - if (!babel.types.isBlockStatement(nodePath.node.body)) { - throw new Error( - `Server function body is not a block statement: ${nodePath.node.body}`, - ) - } + directiveFn.replaceWith(replacement) + } - const nodeBody = nodePath.node.body - nodeBody.directives = nodeBody.directives.filter( - (directive) => directive.value.value !== 'use server', - ) - - // Create the dynamic import expression - const importExpression = babel.template.expression(` - import(${JSON.stringify(`${opts.filename}?tsr-serverfn-split=${functionId}`)}) - .then(mod => mod.serverFn(...args)) - `)() - - if (babel.types.isArrowFunctionExpression(nodePath.node)) { - if (babel.types.isBlockStatement(nodePath.node.body)) { - nodePath.node.body.body = [ - babel.types.returnStatement(importExpression), - ] - } else { - nodePath.node.body = babel.types.blockStatement([ - babel.types.returnStatement(importExpression), - ]) + // Finally register the server function to + // our map of server functions + serverFnPathsByFunctionId[functionId] = { + nodePath: directiveFn, + referenceName, + functionName: fnName || '', + functionId: functionId, + splitFileId, + } } - } else if ( - babel.types.isFunctionExpression(nodePath.node) || - babel.types.isFunctionDeclaration(nodePath.node) || - babel.types.isObjectMethod(nodePath.node) || - babel.types.isClassMethod(nodePath.node) - ) { - nodePath.node.body.body = [ - babel.types.returnStatement(importExpression), - ] } }, - ) - - const compiledCode = generate(ast, { - sourceMaps: true, - sourceFileName: opts.filename, - minified: process.env.NODE_ENV === 'production', }) - return { - compiledCode, - serverFns: serverFnPathsByFunctionId, - } + return serverFnPathsByFunctionId } -function findServerFunctions(ast: babel.types.File, opts: ParseAstOptions) { - const serverFnPathsByFunctionId: Record = {} - const counts: Record = {} - - const recordServerFunction = (nodePath: babel.NodePath) => { - const fnName = findNearestVariableName(nodePath) - - const baseLabel = makeFileLocationUrlSafe( - `${opts.filename.replace( - path.extname(opts.filename), - '', - )}--${fnName}`.replace(opts.root, ''), - ) +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`) + } - counts[baseLabel] = (counts[baseLabel] || 0) + 1 + const frame = codeFrameColumns( + code, + { + start: loc.start, + end: loc.end, + }, + { + highlightCode: true, + message, + }, + ) - const functionId = - counts[baseLabel] > 1 - ? `${baseLabel}_${counts[baseLabel] - 1}` - : baseLabel + return new Error(frame) +} - serverFnPathsByFunctionId[functionId] = { - nodePath, - functionName: fnName || '', - functionId: functionId, +const safeRemoveExports = (ast: babel.types.File) => { + const programBody = ast.program.body + + const removeExport = ( + path: + | babel.NodePath + | babel.NodePath, + ) => { + // If the value is a function declaration, class declaration, or variable declaration, + // That means it has a name and can remain in the file, just unexported. + if ( + babel.types.isFunctionDeclaration(path.node.declaration) || + babel.types.isClassDeclaration(path.node.declaration) || + babel.types.isVariableDeclaration(path.node.declaration) + ) { + // If the value is a function declaration, class declaration, or variable declaration, + // That means it has a name and can remain in the file, just unexported. + if ( + babel.types.isFunctionDeclaration(path.node.declaration) || + babel.types.isClassDeclaration(path.node.declaration) || + babel.types.isVariableDeclaration(path.node.declaration) + ) { + // Move the declaration to the top level at the same index + const insertIndex = programBody.findIndex( + (node) => node === path.node.declaration, + ) + programBody.splice(insertIndex, 0, path.node.declaration as any) + } } + + // Otherwise, remove the export declaration + path.remove() } + // Before we add our export, remove any other exports. + // Don't remove the thing they export, just the export declaration babel.traverse(ast, { - Program: { - enter(programPath) { - programPath.traverse({ - enter(path) { - // Function declarations - if (path.isFunctionDeclaration()) { - const directives = path.node.body.directives - for (const directive of directives) { - if (directive.value.value === 'use server') { - recordServerFunction(path) - } - } - } - - // Function expressions - if (path.isFunctionExpression()) { - const directives = path.node.body.directives - for (const directive of directives) { - if (directive.value.value === 'use server') { - recordServerFunction(path) - } - } - } - - // Arrow functions - if (path.isArrowFunctionExpression()) { - if (babel.types.isBlockStatement(path.node.body)) { - const directives = path.node.body.directives - for (const directive of directives) { - if (directive.value.value === 'use server') { - recordServerFunction(path) - } - } - } - } - - // Class methods - if (path.isClassMethod()) { - const directives = path.node.body.directives - for (const directive of directives) { - if (directive.value.value === 'use server') { - recordServerFunction(path) - } - } - } - - // Object methods - if (path.isObjectMethod()) { - const directives = path.node.body.directives - for (const directive of directives) { - if (directive.value.value === 'use server') { - recordServerFunction(path) - } - } - } - - // Variable declarations with function expressions - if ( - path.isVariableDeclarator() && - (babel.types.isFunctionExpression(path.node.init) || - babel.types.isArrowFunctionExpression(path.node.init)) - ) { - const init = path.node.init - if (babel.types.isBlockStatement(init.body)) { - const directives = init.body.directives - for (const directive of directives) { - if (directive.value.value === 'use server') { - recordServerFunction(path.get('init') as babel.NodePath) - } - } - } - } - }, - }) - }, + ExportDefaultDeclaration(path) { + removeExport(path) + }, + ExportNamedDeclaration(path) { + removeExport(path) }, }) - - return serverFnPathsByFunctionId } - -// 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/server-functions-vite-plugin/src/index.ts b/packages/server-functions-vite-plugin/src/index.ts index 8ee6b4cda8..12a710f138 100644 --- a/packages/server-functions-vite-plugin/src/index.ts +++ b/packages/server-functions-vite-plugin/src/index.ts @@ -1,7 +1,8 @@ import { fileURLToPath, pathToFileURL } from 'node:url' -import { compileServerFnClient } from './compilers' +import { compileDirectives } from './compilers' import { logDiff } from './logger' +import type { ReplacerFn } from './compilers' import type { Plugin } from 'vite' const debug = Boolean(process.env.TSR_VITE_DEBUG) || (true as boolean) @@ -17,7 +18,7 @@ export type ServerFunctionsViteOptions = { } > }) => string - replacer: (opts: { filename: string; functionId: string }) => string + replacer: ReplacerFn } const useServerRx = /"use server"|'use server'/ @@ -28,10 +29,9 @@ export type CreateRpcFn = (opts: { functionId: string }) => (...args: Array) => any -export function TanStackServerFnPluginClient( +export function TanStackServerFnPluginRuntime( opts: ServerFunctionsViteOptions, ): Array { - // opts: ServerFunctionsViteOptions, let ROOT: string = process.cwd() return [ @@ -50,7 +50,8 @@ export function TanStackServerFnPluginClient( return null } - const { compiledCode, serverFns } = compileServerFnClient({ + const { compiledResult, serverFns } = compileDirectives({ + directive: 'use server', code, root: ROOT, filename: id, @@ -59,15 +60,15 @@ export function TanStackServerFnPluginClient( }) if (debug) console.info('createServerFn Input/Output') - if (debug) logDiff(code, compiledCode.code.replace(/ctx/g, 'blah')) + if (debug) logDiff(code, compiledResult.code.replace(/ctx/g, 'blah')) - return compiledCode + return compiledResult }, }, ] } -export function TanStackServerFnPluginServer( +export function TanStackServerFnPluginSplitFn( opts: ServerFunctionsViteOptions, ): Array { return [ diff --git a/packages/server-functions-vite-plugin/tests/compiler.test.ts b/packages/server-functions-vite-plugin/tests/compiler.test.ts index ee958f0084..dc62daee66 100644 --- a/packages/server-functions-vite-plugin/tests/compiler.test.ts +++ b/packages/server-functions-vite-plugin/tests/compiler.test.ts @@ -1,105 +1,384 @@ import { describe, expect, test } from 'vitest' -import { compileServerFnClient, compileServerFnServer } from '../src/compilers' +import { compileDirectives } from '../src/compilers' +import type { CompileDirectivesOpts } from '../src/compilers' -const clientConfig = { +const clientConfig: Omit = { + directive: 'use server', root: './test-files', filename: 'test.ts', - getRuntimeCode: () => 'import { createClientRpc } from "my-rpc-lib"', - replacer: (opts: { filename: string; functionId: string }) => + getRuntimeCode: () => 'import { createClientRpc } from "my-rpc-lib-client"', + replacer: (opts) => `createClientRpc({ filename: ${JSON.stringify(opts.filename)}, functionId: ${JSON.stringify(opts.functionId)}, })`, } -const serverConfig = { +const serverConfig: Omit = { + directive: 'use server', root: './test-files', filename: 'test.ts', + getRuntimeCode: () => 'import { createServerRpc } from "my-rpc-lib-server"', + replacer: (opts) => + `createServerRpc({ + fn: ${opts.splitImportFn}, + filename: ${JSON.stringify(opts.filename)}, + functionId: ${JSON.stringify(opts.functionId)}, + })`, } describe('server function compilation', () => { - test('basic function declaration', async () => { - const code = ` - function useServer() { + const code = ` + const namedFunction = wrapper(function namedFunction() { 'use server' return 'hello' + }) + + const arrowFunction = wrapper(() => { + 'use server' + return 'hello' + }) + + const anonymousFunction = wrapper(function () { + 'use server' + return 'hello' + }) + + const multipleDirectives = function multipleDirectives() { + 'use server' + 'use strict' + return 'hello' } - ` - const client = compileServerFnClient({ ...clientConfig, code }) - await expect(client.compiledCode.code).toMatchInlineSnapshot(` - "import { createClientRpc } from "my-rpc-lib"; - function useServer(...args) { - return createClientRpc({ - filename: "test.ts", - functionId: "test--useServer" - })(...args); - }" - `) + const iife = (function () { + 'use server' + return 'hello' + })() - const server = compileServerFnServer({ ...serverConfig, code }) - await expect(server.compiledCode.code).toMatchInlineSnapshot(` - "function useServer(...args) { - return import("test.ts?tsr-serverfn-split=test--useServer").then(mod => mod.serverFn(...args)); - }" - `) - }) + export default function defaultExportFn() { + 'use server' + return 'hello' + } - test('arrow function', () => { - const code = ` - const fn = () => { + export function namedExportFn() { + 'use server' + return 'hello' + } + + export const exportedArrowFunction = wrapper(() => { + 'use server' + return 'hello' + }) + + export const namedExportConst = () => { 'use server' return 'hello' } + + const namedDefaultExport = 'namedDefaultExport' + export default namedDefaultExport + + export const usedButNotExported = 'usedButNotExported' + + const namedExport = 'namedExport' + + export { + namedExport + } + ` - const client = compileServerFnClient({ ...clientConfig, code }) - expect(client.compiledCode.code).toMatchInlineSnapshot(` - "import { createClientRpc } from "my-rpc-lib"; - const fn = (...args) => { - return createClientRpc({ - filename: "test.ts", - functionId: "test--fn_1" - })(...args); - };" + test('basic function declaration nested in other variable', () => { + const client = compileDirectives({ + ...clientConfig, + code, + }) + const ssr = compileDirectives({ ...serverConfig, code }) + const splitFiles = Object.entries(ssr.serverFns) + .map(([_fnId, serverFn]) => { + return `// ${serverFn.functionName}\n\n${ + compileDirectives({ + ...serverConfig, + code, + filename: serverFn.splitFileId, + }).compiledResult.code + }` + }) + .join('\n\n\n') + + expect(client.compiledResult.code).toMatchInlineSnapshot(` + "import { createClientRpc } from "my-rpc-lib-client"; + const namedFunction_wrapper_namedFunction = createClientRpc({ + filename: "test.ts", + functionId: "test--namedFunction_wrapper_namedFunction" + }); + const namedFunction = wrapper(namedFunction_wrapper_namedFunction); + const arrowFunction_wrapper = createClientRpc({ + filename: "test.ts", + functionId: "test--arrowFunction_wrapper" + }); + const arrowFunction = wrapper(arrowFunction_wrapper); + const anonymousFunction_wrapper = createClientRpc({ + filename: "test.ts", + functionId: "test--anonymousFunction_wrapper" + }); + const anonymousFunction = wrapper(anonymousFunction_wrapper); + const multipleDirectives_multipleDirectives = createClientRpc({ + filename: "test.ts", + functionId: "test--multipleDirectives_multipleDirectives" + }); + const multipleDirectives = multipleDirectives_multipleDirectives; + const iife_1 = createClientRpc({ + filename: "test.ts", + functionId: "test--iife" + }); + const iife = iife_1(); + const defaultExportFn_1 = createClientRpc({ + filename: "test.ts", + functionId: "test--defaultExportFn" + }); + export default defaultExportFn_1; + const namedExportFn_1 = createClientRpc({ + filename: "test.ts", + functionId: "test--namedExportFn" + }); + export const namedExportFn = namedExportFn_1; + const exportedArrowFunction_wrapper = createClientRpc({ + filename: "test.ts", + functionId: "test--exportedArrowFunction_wrapper" + }); + export const exportedArrowFunction = wrapper(exportedArrowFunction_wrapper); + const namedExportConst_1 = createClientRpc({ + filename: "test.ts", + functionId: "test--namedExportConst" + }); + export const namedExportConst = namedExportConst_1; + const namedDefaultExport = 'namedDefaultExport'; + export default namedDefaultExport; + export const usedButNotExported = 'usedButNotExported'; + const namedExport = 'namedExport'; + export { namedExport };" `) - const server = compileServerFnServer({ ...serverConfig, code }) - expect(server.compiledCode.code).toMatchInlineSnapshot(` - "const fn = (...args) => { - return import("test.ts?tsr-serverfn-split=test--fn_1").then(mod => mod.serverFn(...args)); - };" + expect(ssr.compiledResult.code).toMatchInlineSnapshot(` + "import { createServerRpc } from "my-rpc-lib-server"; + const namedFunction_wrapper_namedFunction = createServerRpc({ + fn: (...args) => import("test.ts?tsr-serverfn-split=test--namedFunction_wrapper_namedFunction").then(module => module.default(...args)), + filename: "test.ts", + functionId: "test--namedFunction_wrapper_namedFunction" + }); + const namedFunction = wrapper(namedFunction_wrapper_namedFunction); + const arrowFunction_wrapper = createServerRpc({ + fn: (...args) => import("test.ts?tsr-serverfn-split=test--arrowFunction_wrapper").then(module => module.default(...args)), + filename: "test.ts", + functionId: "test--arrowFunction_wrapper" + }); + const arrowFunction = wrapper(arrowFunction_wrapper); + const anonymousFunction_wrapper = createServerRpc({ + fn: (...args) => import("test.ts?tsr-serverfn-split=test--anonymousFunction_wrapper").then(module => module.default(...args)), + filename: "test.ts", + functionId: "test--anonymousFunction_wrapper" + }); + const anonymousFunction = wrapper(anonymousFunction_wrapper); + const multipleDirectives_multipleDirectives = createServerRpc({ + fn: (...args) => import("test.ts?tsr-serverfn-split=test--multipleDirectives_multipleDirectives").then(module => module.default(...args)), + filename: "test.ts", + functionId: "test--multipleDirectives_multipleDirectives" + }); + const multipleDirectives = multipleDirectives_multipleDirectives; + const iife_1 = createServerRpc({ + fn: (...args) => import("test.ts?tsr-serverfn-split=test--iife").then(module => module.default(...args)), + filename: "test.ts", + functionId: "test--iife" + }); + const iife = iife_1(); + const defaultExportFn_1 = createServerRpc({ + fn: (...args) => import("test.ts?tsr-serverfn-split=test--defaultExportFn").then(module => module.default(...args)), + filename: "test.ts", + functionId: "test--defaultExportFn" + }); + export default defaultExportFn_1; + const namedExportFn_1 = createServerRpc({ + fn: (...args) => import("test.ts?tsr-serverfn-split=test--namedExportFn").then(module => module.default(...args)), + filename: "test.ts", + functionId: "test--namedExportFn" + }); + export const namedExportFn = namedExportFn_1; + const exportedArrowFunction_wrapper = createServerRpc({ + fn: (...args) => import("test.ts?tsr-serverfn-split=test--exportedArrowFunction_wrapper").then(module => module.default(...args)), + filename: "test.ts", + functionId: "test--exportedArrowFunction_wrapper" + }); + export const exportedArrowFunction = wrapper(exportedArrowFunction_wrapper); + const namedExportConst_1 = createServerRpc({ + fn: (...args) => import("test.ts?tsr-serverfn-split=test--namedExportConst").then(module => module.default(...args)), + filename: "test.ts", + functionId: "test--namedExportConst" + }); + export const namedExportConst = namedExportConst_1; + const namedDefaultExport = 'namedDefaultExport'; + export default namedDefaultExport; + export const usedButNotExported = 'usedButNotExported'; + const namedExport = 'namedExport'; + export { namedExport };" `) + + expect(splitFiles).toMatchInlineSnapshot( + ` + "// namedFunction_wrapper_namedFunction + + const namedFunction_wrapper_namedFunction = function namedFunction() { + return 'hello'; + }; + export default namedFunction_wrapper_namedFunction; + + + // arrowFunction_wrapper + + const arrowFunction_wrapper = () => { + return 'hello'; + }; + export default arrowFunction_wrapper; + + + // anonymousFunction_wrapper + + const anonymousFunction_wrapper = function () { + return 'hello'; + }; + export default anonymousFunction_wrapper; + + + // multipleDirectives_multipleDirectives + + const multipleDirectives_multipleDirectives = function multipleDirectives() { + 'use strict'; + + return 'hello'; + }; + export default multipleDirectives_multipleDirectives; + + + // iife + + const iife_1 = function () { + return 'hello'; + }; + export default iife_1; + + + // defaultExportFn + + const defaultExportFn_1 = function defaultExportFn() { + return 'hello'; + }; + export default defaultExportFn_1; + + + // namedExportFn + + const namedExportFn_1 = function namedExportFn() { + return 'hello'; + }; + export default namedExportFn_1; + + + // exportedArrowFunction_wrapper + + const exportedArrowFunction_wrapper = () => { + return 'hello'; + }; + export default exportedArrowFunction_wrapper; + + + // namedExportConst + + const namedExportConst_1 = () => { + return 'hello'; + }; + export default namedExportConst_1;" + `, + ) }) - test('anonymous function', () => { + test('Does not support function declarations nested in other blocks', () => { const code = ` - const anonymousFn = function () { - 'use server' - } + outer(() => { + function useServer() { + 'use server' + return 'hello' + } + }) ` - const client = compileServerFnClient({ ...clientConfig, code }) - expect(client.compiledCode.code).toMatchInlineSnapshot(` - "import { createClientRpc } from "my-rpc-lib"; - const anonymousFn = function (...args) { - return createClientRpc({ - filename: "test.ts", - functionId: "test--anonymousFn_1" - })(...args); - };" - `) - - const server = compileServerFnServer({ ...serverConfig, code }) - expect(server.compiledCode.code).toMatchInlineSnapshot(` - "const anonymousFn = function (...args) { - return import("test.ts?tsr-serverfn-split=test--anonymousFn_1").then(mod => mod.serverFn(...args)); - };" - `) + expect(() => + compileDirectives({ ...clientConfig, code }), + ).toThrowErrorMatchingInlineSnapshot( + ` + [Error: 1 | + > 2 | outer(() => { + | ^^ + > 3 | function useServer() { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 4 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 5 | return 'hello' + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 6 | } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 7 | }) + | ^^^^^^^ Server functions cannot be nested in other blocks or functions + 8 | ] + `, + ) + expect(() => + compileDirectives({ ...serverConfig, code }), + ).toThrowErrorMatchingInlineSnapshot( + ` + [Error: 1 | + > 2 | outer(() => { + | ^^ + > 3 | function useServer() { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 4 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 5 | return 'hello' + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 6 | } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 7 | }) + | ^^^^^^^ Server functions cannot be nested in other blocks or functions + 8 | ] + `, + ) + expect(() => + compileDirectives({ + ...serverConfig, + code, + filename: serverConfig.filename + `?tsr-serverfn-split=temp`, + }), + ).toThrowErrorMatchingInlineSnapshot( + ` + [Error: 1 | + > 2 | outer(() => { + | ^^ + > 3 | function useServer() { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 4 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 5 | return 'hello' + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 6 | } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 7 | }) + | ^^^^^^^ Server functions cannot be nested in other blocks or functions + 8 | ] + `, + ) }) - test('class methods', () => { + test('does not support class methods', () => { const code = ` class TestClass { method() { @@ -114,39 +393,62 @@ describe('server function compilation', () => { } ` - const client = compileServerFnClient({ ...clientConfig, code }) - expect(client.compiledCode.code).toMatchInlineSnapshot(` - "import { createClientRpc } from "my-rpc-lib"; - class TestClass { - method(...args) { - return createClientRpc({ - filename: "test.ts", - functionId: "test--method" - })(...args); - } - static staticMethod(...args) { - return createClientRpc({ - filename: "test.ts", - functionId: "test--staticMethod" - })(...args); - } - }" + expect(() => compileDirectives({ ...clientConfig, code })) + .toThrowErrorMatchingInlineSnapshot(` + [Error: 1 | + 2 | class TestClass { + > 3 | method() { + | ^^^^^^^^^^^ + > 4 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 5 | return 'hello' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 6 | } + | ^^^^^^^^^ use server in class not supported + 7 | + 8 | static staticMethod() { + 9 | 'use server'] `) - - const server = compileServerFnServer({ ...serverConfig, code }) - expect(server.compiledCode.code).toMatchInlineSnapshot(` - "class TestClass { - method(...args) { - return import("test.ts?tsr-serverfn-split=test--method").then(mod => mod.serverFn(...args)); - } - static staticMethod(...args) { - return import("test.ts?tsr-serverfn-split=test--staticMethod").then(mod => mod.serverFn(...args)); - } - }" + expect(() => compileDirectives({ ...serverConfig, code })) + .toThrowErrorMatchingInlineSnapshot(` + [Error: 1 | + 2 | class TestClass { + > 3 | method() { + | ^^^^^^^^^^^ + > 4 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 5 | return 'hello' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 6 | } + | ^^^^^^^^^ use server in class not supported + 7 | + 8 | static staticMethod() { + 9 | 'use server'] + `) + expect(() => + compileDirectives({ + ...serverConfig, + code, + filename: serverConfig.filename + `?tsr-serverfn-split=temp`, + }), + ).toThrowErrorMatchingInlineSnapshot(` + [Error: 1 | + 2 | class TestClass { + > 3 | method() { + | ^^^^^^^^^^^ + > 4 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 5 | return 'hello' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 6 | } + | ^^^^^^^^^ use server in class not supported + 7 | + 8 | static staticMethod() { + 9 | 'use server'] `) }) - test('object methods', () => { + test('does not support object methods', () => { const code = ` const obj = { method() { @@ -156,71 +458,59 @@ describe('server function compilation', () => { } ` - const client = compileServerFnClient({ ...clientConfig, code }) - expect(client.compiledCode.code).toMatchInlineSnapshot(` - "import { createClientRpc } from "my-rpc-lib"; - const obj = { - method(...args) { - return createClientRpc({ - filename: "test.ts", - functionId: "test--obj_method" - })(...args); - } - };" + expect(() => compileDirectives({ ...clientConfig, code })) + .toThrowErrorMatchingInlineSnapshot(` + [Error: 1 | + 2 | const obj = { + > 3 | method() { + | ^^^^^^^^^^^ + > 4 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 5 | return 'hello' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 6 | }, + | ^^^^^^^^^ use server in object method not supported + 7 | } + 8 | ] `) - - const server = compileServerFnServer({ ...serverConfig, code }) - expect(server.compiledCode.code).toMatchInlineSnapshot(` - "const obj = { - method(...args) { - return import("test.ts?tsr-serverfn-split=test--obj_method").then(mod => mod.serverFn(...args)); - } - };" + expect(() => compileDirectives({ ...serverConfig, code })) + .toThrowErrorMatchingInlineSnapshot(` + [Error: 1 | + 2 | const obj = { + > 3 | method() { + | ^^^^^^^^^^^ + > 4 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 5 | return 'hello' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 6 | }, + | ^^^^^^^^^ use server in object method not supported + 7 | } + 8 | ] `) - }) - - test('async functions', () => { - const code = ` - async function asyncServer() { - 'use server' - return 'hello' - } - - const asyncArrow = async () => { - 'use server' - return 'hello' - } - ` - - const client = compileServerFnClient({ ...clientConfig, code }) - expect(client.compiledCode.code).toMatchInlineSnapshot(` - "import { createClientRpc } from "my-rpc-lib"; - async function asyncServer(...args) { - return createClientRpc({ - filename: "test.ts", - functionId: "test--asyncServer" - })(...args); - } - const asyncArrow = async (...args) => { - return createClientRpc({ - filename: "test.ts", - functionId: "test--asyncArrow_1" - })(...args); - };" - `) - - const server = compileServerFnServer({ ...serverConfig, code }) - expect(server.compiledCode.code).toMatchInlineSnapshot(` - "async function asyncServer(...args) { - return import("test.ts?tsr-serverfn-split=test--asyncServer").then(mod => mod.serverFn(...args)); - } - const asyncArrow = async (...args) => { - return import("test.ts?tsr-serverfn-split=test--asyncArrow_1").then(mod => mod.serverFn(...args)); - };" + expect(() => + compileDirectives({ + ...serverConfig, + code, + filename: serverConfig.filename + `?tsr-serverfn-split=temp`, + }), + ).toThrowErrorMatchingInlineSnapshot(` + [Error: 1 | + 2 | const obj = { + > 3 | method() { + | ^^^^^^^^^^^ + > 4 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 5 | return 'hello' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 6 | }, + | ^^^^^^^^^ use server in object method not supported + 7 | } + 8 | ] `) }) - test('generator functions', () => { + test('does not support generator functions', () => { const code = ` function* generatorServer() { 'use server' @@ -233,67 +523,55 @@ describe('server function compilation', () => { } ` - const client = compileServerFnClient({ ...clientConfig, code }) - expect(client.compiledCode.code).toMatchInlineSnapshot(` - "import { createClientRpc } from "my-rpc-lib"; - function* generatorServer(...args) { - return createClientRpc({ - filename: "test.ts", - functionId: "test--generatorServer" - })(...args); - } - async function* asyncGeneratorServer(...args) { - return createClientRpc({ - filename: "test.ts", - functionId: "test--asyncGeneratorServer" - })(...args); - }" + expect(() => compileDirectives({ ...clientConfig, code })) + .toThrowErrorMatchingInlineSnapshot(` + [Error: 1 | + > 2 | function* generatorServer() { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 3 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^ + > 4 | yield 'hello' + | ^^^^^^^^^^^^^^^^^^^^ + > 5 | } + | ^^^^^^^ use server in generator function not supported + 6 | + 7 | async function* asyncGeneratorServer() { + 8 | 'use server'] `) - - const server = compileServerFnServer({ ...serverConfig, code }) - expect(server.compiledCode.code).toMatchInlineSnapshot(` - "function* generatorServer(...args) { - return import("test.ts?tsr-serverfn-split=test--generatorServer").then(mod => mod.serverFn(...args)); - } - async function* asyncGeneratorServer(...args) { - return import("test.ts?tsr-serverfn-split=test--asyncGeneratorServer").then(mod => mod.serverFn(...args)); - }" + expect(() => compileDirectives({ ...serverConfig, code })) + .toThrowErrorMatchingInlineSnapshot(` + [Error: 1 | + > 2 | function* generatorServer() { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 3 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^ + > 4 | yield 'hello' + | ^^^^^^^^^^^^^^^^^^^^ + > 5 | } + | ^^^^^^^ use server in generator function not supported + 6 | + 7 | async function* asyncGeneratorServer() { + 8 | 'use server'] `) - }) - - test('nested functions', () => { - const code = ` - function outer() { - function inner() { - 'use server' - return 'hello' - } - return inner - } - ` - - const client = compileServerFnClient({ ...clientConfig, code }) - expect(client.compiledCode.code).toMatchInlineSnapshot(` - "import { createClientRpc } from "my-rpc-lib"; - function outer() { - function inner(...args) { - return createClientRpc({ - filename: "test.ts", - functionId: "test--outer_inner" - })(...args); - } - return inner; - }" - `) - - const server = compileServerFnServer({ ...serverConfig, code }) - expect(server.compiledCode.code).toMatchInlineSnapshot(` - "function outer() { - function inner(...args) { - return import("test.ts?tsr-serverfn-split=test--outer_inner").then(mod => mod.serverFn(...args)); - } - return inner; - }" + expect(() => + compileDirectives({ + ...serverConfig, + code, + filename: serverConfig.filename + `?tsr-serverfn-split=temp`, + }), + ).toThrowErrorMatchingInlineSnapshot(` + [Error: 1 | + > 2 | function* generatorServer() { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 3 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^ + > 4 | yield 'hello' + | ^^^^^^^^^^^^^^^^^^^^ + > 5 | } + | ^^^^^^^ use server in generator function not supported + 6 | + 7 | async function* asyncGeneratorServer() { + 8 | 'use server'] `) }) @@ -306,53 +584,39 @@ describe('server function compilation', () => { } ` - const client = compileServerFnClient({ ...clientConfig, code }) - expect(client.compiledCode.code).toMatchInlineSnapshot(` - "import { createClientRpc } from "my-rpc-lib"; - function multiDirective(...args) { - 'use strict'; - - return createClientRpc({ - filename: "test.ts", - functionId: "test--multiDirective" - })(...args); - }" - `) - - const server = compileServerFnServer({ ...serverConfig, code }) - expect(server.compiledCode.code).toMatchInlineSnapshot(` - "function multiDirective(...args) { - 'use strict'; + const client = compileDirectives({ ...clientConfig, code }) + const ssr = compileDirectives({ ...serverConfig, code }) + const splitFiles = Object.entries(ssr.serverFns) + .map(([_fnId, serverFn]) => { + return `// ${serverFn.functionName}\n\n${ + compileDirectives({ + ...serverConfig, + code, + filename: serverFn.splitFileId, + }).compiledResult.code + }` + }) + .join('\n\n\n') - return import("test.ts?tsr-serverfn-split=test--multiDirective").then(mod => mod.serverFn(...args)); - }" + expect(client.compiledResult.code).toMatchInlineSnapshot(` + "import { createClientRpc } from "my-rpc-lib-client"; + createClientRpc({ + filename: "test.ts", + functionId: "test--multiDirective" + });" `) - }) - - test('function with parameters', () => { - const code = ` - function withParams(a: string, b: number) { - 'use server' - return \`\${a} \${b}\` - } - ` - - const client = compileServerFnClient({ ...clientConfig, code }) - expect(client.compiledCode.code).toMatchInlineSnapshot(` - "import { createClientRpc } from "my-rpc-lib"; - function withParams(...args) { - return createClientRpc({ - filename: "test.ts", - functionId: "test--withParams" - })(...args); - }" + expect(ssr.compiledResult.code).toMatchInlineSnapshot(` + "import { createServerRpc } from "my-rpc-lib-server"; + createServerRpc({ + fn: (...args) => import("test.ts?tsr-serverfn-split=test--multiDirective").then(module => module.default(...args)), + filename: "test.ts", + functionId: "test--multiDirective" + });" `) + expect(splitFiles).toMatchInlineSnapshot(` + "// multiDirective - const server = compileServerFnServer({ ...serverConfig, code }) - expect(server.compiledCode.code).toMatchInlineSnapshot(` - "function withParams(...args) { - return import("test.ts?tsr-serverfn-split=test--withParams").then(mod => mod.serverFn(...args)); - }" + export default multiDirective_1;" `) }) @@ -364,114 +628,138 @@ describe('server function compilation', () => { })() ` - const client = compileServerFnClient({ ...clientConfig, code }) - expect(client.compiledCode.code).toMatchInlineSnapshot(` - "import { createClientRpc } from "my-rpc-lib"; - const iife = function (...args) { - return createClientRpc({ - filename: "test.ts", - functionId: "test--iife" - })(...args); - }();" + const client = compileDirectives({ ...clientConfig, code }) + const ssr = compileDirectives({ ...serverConfig, code }) + const splitFiles = Object.entries(ssr.serverFns) + .map(([_fnId, serverFn]) => { + return `// ${serverFn.functionName}\n\n${ + compileDirectives({ + ...serverConfig, + code, + filename: serverFn.splitFileId, + }).compiledResult.code + }` + }) + .join('\n\n\n') + + expect(client.compiledResult.code).toMatchInlineSnapshot(` + "import { createClientRpc } from "my-rpc-lib-client"; + const iife_1 = createClientRpc({ + filename: "test.ts", + functionId: "test--iife" + }); + const iife = iife_1();" `) - const server = compileServerFnServer({ ...serverConfig, code }) - expect(server.compiledCode.code).toMatchInlineSnapshot(` - "const iife = function (...args) { - return import("test.ts?tsr-serverfn-split=test--iife").then(mod => mod.serverFn(...args)); - }();" + expect(ssr.compiledResult.code).toMatchInlineSnapshot(` + "import { createServerRpc } from "my-rpc-lib-server"; + const iife_1 = createServerRpc({ + fn: (...args) => import("test.ts?tsr-serverfn-split=test--iife").then(module => module.default(...args)), + filename: "test.ts", + functionId: "test--iife" + }); + const iife = iife_1();" `) - }) - - test('higher order functions', () => { - const code = ` - function higherOrder() { - return function () { - 'use server' - return 'hello' - } - } - ` - const client = compileServerFnClient({ ...clientConfig, code }) - expect(client.compiledCode.code).toMatchInlineSnapshot(` - "import { createClientRpc } from "my-rpc-lib"; - function higherOrder() { - return function (...args) { - return createClientRpc({ - filename: "test.ts", - functionId: "test--higherOrder" - })(...args); - }; - }" - `) + expect(splitFiles).toMatchInlineSnapshot(` + "// iife - const server = compileServerFnServer({ ...serverConfig, code }) - expect(server.compiledCode.code).toMatchInlineSnapshot(` - "function higherOrder() { - return function (...args) { - return import("test.ts?tsr-serverfn-split=test--higherOrder").then(mod => mod.serverFn(...args)); - }; - }" + const iife_1 = function () { + return 'hello'; + }; + export default iife_1;" `) }) test('functions that might have the same functionId', () => { const code = ` - function main() { - function middle() { - const useServer = function () { - 'use server' - return 'hello' - } - return useServer - } - return middle - } + outer(function useServer() { + 'use server' + return 'hello' + }) - main().middle(function useServer() { + outer(function useServer() { 'use server' return 'hello' }) ` - const client = compileServerFnClient({ ...clientConfig, code }) - expect(client.compiledCode.code).toMatchInlineSnapshot(` - "import { createClientRpc } from "my-rpc-lib"; - function main() { - function middle() { - const useServer = function (...args) { - return createClientRpc({ - filename: "test.ts", - functionId: "test--main_middle_useServer_1" - })(...args); - }; - return useServer; - } - return middle; - } - main().middle(function useServer(...args) { - return createClientRpc({ - filename: "test.ts", - functionId: "test--main_middle_useServer_2" - })(...args); - });" + const client = compileDirectives({ ...clientConfig, code }) + const ssr = compileDirectives({ ...serverConfig, code }) + console.log('serverFns', ssr.serverFns) + const splitFiles = Object.entries(ssr.serverFns) + .map(([_fnId, serverFn]) => { + return `// ${serverFn.functionName}\n\n${ + compileDirectives({ + ...serverConfig, + code, + filename: serverFn.splitFileId, + }).compiledResult.code + }` + }) + .join('\n\n\n') + + expect(client.compiledResult.code).toMatchInlineSnapshot(` + "import { createClientRpc } from "my-rpc-lib-client"; + const outer_useServer_2 = createClientRpc({ + filename: "test.ts", + functionId: "test--outer_useServer" + }); + outer(outer_useServer_2); + const outer_useServer_3 = createClientRpc({ + filename: "test.ts", + functionId: "test--outer_useServer_1" + }); + outer(outer_useServer_3);" `) - const server = compileServerFnServer({ ...serverConfig, code }) - expect(server.compiledCode.code).toMatchInlineSnapshot(` - "function main() { - function middle() { - const useServer = function (...args) { - return import("test.ts?tsr-serverfn-split=test--main_middle_useServer_1").then(mod => mod.serverFn(...args)); - }; - return useServer; - } - return middle; - } - main().middle(function useServer(...args) { - return import("test.ts?tsr-serverfn-split=test--main_middle_useServer_2").then(mod => mod.serverFn(...args)); - });" + expect(ssr.compiledResult.code).toMatchInlineSnapshot(` + "import { createServerRpc } from "my-rpc-lib-server"; + const outer_useServer_2 = createServerRpc({ + fn: (...args) => import("test.ts?tsr-serverfn-split=test--outer_useServer").then(module => module.default(...args)), + filename: "test.ts", + functionId: "test--outer_useServer" + }); + outer(outer_useServer_2); + const outer_useServer_3 = createServerRpc({ + fn: (...args) => import("test.ts?tsr-serverfn-split=test--outer_useServer_1").then(module => module.default(...args)), + filename: "test.ts", + functionId: "test--outer_useServer_1" + }); + outer(outer_useServer_3);" + `) + + expect(splitFiles).toMatchInlineSnapshot(` + "// outer_useServer + + import { createServerRpc } from "my-rpc-lib-server"; + const outer_useServer_2 = function useServer() { + return 'hello'; + }; + outer(outer_useServer_2); + const outer_useServer_3 = createServerRpc({ + fn: (...args) => import("test.ts?tsr-serverfn-split=test--outer_useServer_1").then(module => module.default(...args)), + filename: "test.ts", + functionId: "test--outer_useServer_1" + }); + outer(outer_useServer_3); + export default outer_useServer_2; + + + // outer_useServer + + import { createServerRpc } from "my-rpc-lib-server"; + const outer_useServer_2 = createServerRpc({ + fn: (...args) => import("test.ts?tsr-serverfn-split=test--outer_useServer").then(module => module.default(...args)), + filename: "test.ts", + functionId: "test--outer_useServer" + }); + outer(outer_useServer_2); + const outer_useServer_3 = function useServer() { + return 'hello'; + }; + outer(outer_useServer_3); + export default outer_useServer_3;" `) }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2269a73f44..f7c941e7cf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3552,6 +3552,9 @@ importers: chalk: specifier: ^5.3.0 version: 5.3.0 + dedent: + specifier: ^1.5.3 + version: 1.5.3 diff: specifier: ^7.0.0 version: 7.0.0 @@ -7209,6 +7212,14 @@ packages: decimal.js@10.4.3: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + dedent@1.5.3: + resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -14379,6 +14390,8 @@ snapshots: decimal.js@10.4.3: {} + dedent@1.5.3: {} + deep-eql@5.0.2: {} deep-is@0.1.4: {} From 9e2d9cc4a7d44c5738197234b5a985d4d18b38ec Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Thu, 19 Dec 2024 17:16:59 -0700 Subject: [PATCH 09/38] checkpoint --- .../server-functions-vite-plugin/package.json | 4 +- .../src/compilers.ts | 462 ++++++++++-------- .../server-functions-vite-plugin/src/index.ts | 185 ++++--- .../tests/compiler.test.ts | 413 ++++++++++------ packages/start/package.json | 2 +- packages/start/src/client-runtime/index.tsx | 2 +- packages/start/src/config/index.ts | 6 +- packages/start/src/server-runtime/index.tsx | 2 +- .../start/src/server/defaultStreamHandler.tsx | 4 +- packages/start/src/ssr-runtime/index.tsx | 2 +- pnpm-lock.yaml | 8 +- 11 files changed, 644 insertions(+), 446 deletions(-) diff --git a/packages/server-functions-vite-plugin/package.json b/packages/server-functions-vite-plugin/package.json index dd6f839ff9..831a76d956 100644 --- a/packages/server-functions-vite-plugin/package.json +++ b/packages/server-functions-vite-plugin/package.json @@ -1,5 +1,5 @@ { - "name": "@tanstack/server-functions-plugin", + "name": "@tanstack/directive-functions-plugin", "version": "1.87.3", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", @@ -7,7 +7,7 @@ "repository": { "type": "git", "url": "https://github.com/TanStack/router.git", - "directory": "packages/server-functions-plugin" + "directory": "packages/directive-functions-plugin" }, "homepage": "https://tanstack.com/start", "funding": { diff --git a/packages/server-functions-vite-plugin/src/compilers.ts b/packages/server-functions-vite-plugin/src/compilers.ts index 70ecafcb73..120f30c0fb 100644 --- a/packages/server-functions-vite-plugin/src/compilers.ts +++ b/packages/server-functions-vite-plugin/src/compilers.ts @@ -14,7 +14,7 @@ if ('default' in generate) { generate = generate.default as typeof generate } -export interface ServerFunctionInfo { +export interface DirectiveFn { nodePath: SupportedFunctionPath functionName: string functionId: string @@ -38,61 +38,56 @@ export type ReplacerFn = (opts: { export type CompileDirectivesOpts = ParseAstOptions & { directive: string + directiveLabel: string getRuntimeCode?: (opts: { - serverFnPathsByFunctionId: Record + directiveFnsById: Record }) => string replacer: ReplacerFn } -const tsrServerFnSplitParam = 'tsr-serverfn-split' - export function compileDirectives(opts: CompileDirectivesOpts) { const [_, searchParamsStr] = opts.filename.split('?') const searchParams = new URLSearchParams(searchParamsStr) - const functionId = searchParams.get(tsrServerFnSplitParam) + const directiveSplitParam = `tsr-directive-${opts.directive.replace(/[^a-zA-Z0-9]/g, '-')}-split` + const functionId = searchParams.get(directiveSplitParam) const ast = parseAst(opts) - const serverFnPathsByFunctionId = findServerFunctions(ast, { + const directiveFnsById = findDirectives(ast, { ...opts, splitFunctionId: functionId, + directiveSplitParam, }) - // Add runtime code if there are server functions - // Add runtime code if there are server functions - if ( - Object.keys(serverFnPathsByFunctionId).length > 0 && - opts.getRuntimeCode - ) { + // Add runtime code if there are directives + // Add runtime code if there are directives + if (Object.keys(directiveFnsById).length > 0 && opts.getRuntimeCode) { const runtimeImport = babel.template.statement( - opts.getRuntimeCode({ serverFnPathsByFunctionId }), + opts.getRuntimeCode({ directiveFnsById }), )() ast.program.body.unshift(runtimeImport) } // If there is a functionId, we need to remove all exports // then make sure that our function is exported under the - // "serverFn" name + // directive name if (functionId) { - const serverFn = serverFnPathsByFunctionId[functionId] + const directiveFn = directiveFnsById[functionId] - if (!serverFn) { - throw new Error(`Server function ${functionId} not found`) + if (!directiveFn) { + throw new Error(`${opts.directiveLabel} ${functionId} not found`) } - console.log('functionId', functionId) safeRemoveExports(ast) ast.program.body.push( babel.types.exportDefaultDeclaration( - babel.types.identifier(serverFn.referenceName), + babel.types.identifier(directiveFn.referenceName), ), ) - - // If we have a functionId, we're also going to DCE the file - // so that the functionId is not exported - deadCodeElimination(ast) } + deadCodeElimination(ast) + const compiledResult = generate(ast, { sourceMaps: true, sourceFileName: opts.filename, @@ -101,11 +96,14 @@ export function compileDirectives(opts: CompileDirectivesOpts) { return { compiledResult, - serverFns: serverFnPathsByFunctionId, + directiveFnsById, } } -function findNearestVariableName(path: babel.NodePath): string { +function findNearestVariableName( + path: babel.NodePath, + directiveLabel: string, +): string { let currentPath: babel.NodePath | null = path const nameParts: Array = [] @@ -174,7 +172,7 @@ function findNearestVariableName(path: babel.NodePath): string { babel.types.isObjectMethod(currentPath.node) ) { throw new Error( - 'use server in ClassMethod or ObjectMethod not supported', + `${directiveLabel} in ClassMethod or ObjectMethod not supported`, ) } @@ -207,16 +205,18 @@ function makeIdentifierSafe(identifier: string): string { .replace(/^_|_$/g, '') // Trim leading/trailing underscores } -export function findServerFunctions( +export function findDirectives( ast: babel.types.File, opts: ParseAstOptions & { + directive: string + directiveLabel: string replacer?: ReplacerFn splitFunctionId?: string | null + directiveSplitParam: string }, ) { - const serverFnPathsByFunctionId: Record = {} + const directiveFnsById: Record = {} const functionIdCounts: Record = {} - const referenceCounts: Map = new Map() let programPath: babel.NodePath @@ -226,215 +226,247 @@ export function findServerFunctions( }, }) - // Find all server functions - babel.traverse(ast, { - DirectiveLiteral(nodePath) { - if (nodePath.node.value === 'use server') { - let directiveFn = nodePath.findParent((p) => p.isFunction()) as - | SupportedFunctionPath - | undefined - - if (!directiveFn) return - - // Handle class and object methods which are not supported - const isGenerator = - directiveFn.isFunction() && directiveFn.node.generator - - const isClassMethod = directiveFn.isClassMethod() - const isObjectMethod = directiveFn.isObjectMethod() - - if (isClassMethod || isObjectMethod || isGenerator) { - throw codeFrameError( - opts.code, - directiveFn.node.loc, - `use server in ${isClassMethod ? 'class' : isObjectMethod ? 'object method' : 'generator function'} not supported`, - ) - } + // Does the file have the directive in the program body? + const hasFileDirective = ast.program.directives.some( + (directive) => directive.value.value === opts.directive, + ) - // 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 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) + } + }, + ExportNamedDeclaration(path) { + if (babel.types.isFunctionDeclaration(path.node.declaration)) { + compileDirective(path.get('declaration') as SupportedFunctionPath) + } + }, + }) + } else { + // Find all directives + babel.traverse(ast, { + DirectiveLiteral(nodePath) { + if (nodePath.node.value === opts.directive) { + const directiveFn = nodePath.findParent((p) => p.isFunction()) as + | SupportedFunctionPath + | undefined + + if (!directiveFn) return + + // Handle class and object methods which are not supported + const isGenerator = + directiveFn.isFunction() && directiveFn.node.generator + + const isClassMethod = directiveFn.isClassMethod() + const isObjectMethod = directiveFn.isObjectMethod() + + if (isClassMethod || isObjectMethod || isGenerator) { + throw codeFrameError( + opts.code, + directiveFn.node.loc, + `"${opts.directive}" in ${isClassMethod ? 'class' : isObjectMethod ? 'object method' : 'generator function'} not supported`, + ) + } - if (nearestBlock) { - throw codeFrameError( - opts.code, - nearestBlock.node.loc, - 'Server functions cannot be nested in other blocks or functions', + // 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(), ) - } - // Handle supported function types - if ( - directiveFn.isFunctionDeclaration() || - directiveFn.isFunctionExpression() || - (directiveFn.isArrowFunctionExpression() && - babel.types.isBlockStatement(directiveFn.node.body)) - ) { - // Remove the 'use server' directive from the function body + if (nearestBlock) { + throw codeFrameError( + opts.code, + nearestBlock.node.loc, + `${opts.directiveLabel}s cannot be nested in other blocks or functions`, + ) + } + if ( - babel.types.isFunction(directiveFn.node) && - babel.types.isBlockStatement(directiveFn.node.body) + !directiveFn.isFunctionDeclaration() && + !directiveFn.isFunctionExpression() && + !( + directiveFn.isArrowFunctionExpression() && + babel.types.isBlockStatement(directiveFn.node.body) + ) ) { - directiveFn.node.body.directives = - directiveFn.node.body.directives.filter( - (directive) => directive.value.value !== 'use server', - ) + throw codeFrameError( + opts.code, + directiveFn.node.loc, + `${opts.directiveLabel}s must be function declarations or function expressions`, + ) } - // Find the nearest variable name - const fnName = findNearestVariableName(directiveFn) + compileDirective(directiveFn) + } + }, + }) + } + + return directiveFnsById - // Make the functionId safe for use in a URL - const baseLabel = makeFileLocationUrlSafe( - `${opts.filename.replace( - path.extname(opts.filename), - '', - )}--${fnName}`.replace(opts.root, ''), - ) + function compileDirective(directiveFn: SupportedFunctionPath) { + // 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, + ) + } - // Count the number of functions with the same baseLabel - functionIdCounts[baseLabel] = (functionIdCounts[baseLabel] || 0) + 1 + // Find the nearest variable name + const fnName = findNearestVariableName(directiveFn, opts.directiveLabel) - // If there are multiple functions with the same baseLabel, - // append a unique identifier to the functionId - const functionId = - functionIdCounts[baseLabel] > 1 - ? `${baseLabel}_${functionIdCounts[baseLabel] - 1}` - : baseLabel + // Make the functionId safe for use in a URL + const baseLabel = makeFileLocationUrlSafe( + `${opts.filename.replace( + path.extname(opts.filename), + '', + )}--${fnName}`.replace(opts.root, ''), + ) - // Move the function to program level while preserving its position - // in the program body - const programBody = programPath.node.body + // Count the number of functions with the same baseLabel + functionIdCounts[baseLabel] = (functionIdCounts[baseLabel] || 0) + 1 - const topParent = - directiveFn.findParent((p) => !!p.parentPath?.isProgram()) || - directiveFn + // If there are multiple functions with the same baseLabel, + // append a unique identifier to the functionId + const functionId = + functionIdCounts[baseLabel] > 1 + ? `${baseLabel}_${functionIdCounts[baseLabel] - 1}` + : baseLabel - const topParentIndex = programBody.indexOf(topParent.node as any) + // Move the function to program level while preserving its position + // in the program body + const programBody = programPath.node.body - // Determine the reference name for the function - let referenceName = makeIdentifierSafe(fnName) + const topParent = + directiveFn.findParent((p) => !!p.parentPath?.isProgram()) || directiveFn - // If we find this referece in the scope, we need to make it unique - while (programPath.scope.hasBinding(referenceName)) { - referenceCounts.set( - referenceName, - (referenceCounts.get(referenceName) || 0) + 1, - ) - referenceName += `_${referenceCounts.get(referenceName) || 0}` - } + const topParentIndex = programBody.indexOf(topParent.node as any) - // if (referenceCounts.get(referenceName) === 0) { - - // referenceName += `_${(referenceCounts.get(referenceName) || 0) + 1}` - - // If the reference name came from the function declaration, - // // We need to update the function name to match the reference name - // if (babel.types.isFunctionDeclaration(directiveFn.node)) { - // console.log('updating function name', directiveFn.node.id!.name) - // directiveFn.node.id!.name = referenceName - // } - - // 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()) { - // Then place the function at the top level - programBody.splice( - topParentIndex, - 0, - babel.types.variableDeclaration('const', [ - babel.types.variableDeclarator( - babel.types.identifier(referenceName), - babel.types.toExpression(directiveFn.node as any), - ), - ]), - ) + // Determine the reference name for the function + let referenceName = makeIdentifierSafe(fnName) - // If it's an exported named function, we need to swap it with an - // export const originalFunctionName = referenceName - if ( - babel.types.isExportNamedDeclaration( - 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(referenceName), - ), - ]), - ), - ) - - directiveFn.remove() - } else { - directiveFn.replaceWith(babel.types.identifier(referenceName)) - } + // Crawl the scope to refresh all the bindings + programPath.scope.crawl() - directiveFn = programPath.get( - `body.${topParentIndex}.declarations.0.init`, - ) as SupportedFunctionPath - } + // If we find this referece in the scope, we need to make it unique + while (programPath.scope.hasBinding(referenceName)) { + const [realReferenceName, count] = referenceName.split(/_(\d+)$/) + referenceName = realReferenceName + `_${Number(count || '0') + 1}` + } - const [filename, searchParamsStr] = opts.filename.split('?') - - const searchParams = new URLSearchParams(searchParamsStr) - searchParams.set('tsr-serverfn-split', functionId) - const splitFileId = `${filename}?${searchParams.toString()}` - - // If a replacer is provided, replace the function with the replacer - if (functionId !== opts.splitFunctionId && opts.replacer) { - const replacer = opts.replacer({ - fn: '$$fn$$', - splitImportFn: '$$splitImportFn$$', - // splitFileId, - filename: filename!, - functionId: functionId, - }) - - const replacement = babel.template.expression(replacer, { - placeholderPattern: false, - placeholderWhitelist: new Set(['$$fn$$', '$$splitImportFn$$']), - })({ - ...(replacer.includes('$$fn$$') - ? { $$fn$$: babel.types.toExpression(directiveFn.node) } - : {}), - ...(replacer.includes('$$splitImportFn$$') - ? { - $$splitImportFn$$: `(...args) => import(${JSON.stringify(splitFileId)}).then(module => module.default(...args))`, - } - : {}), - }) - - directiveFn.replaceWith(replacement) - } + // if (referenceCounts.get(referenceName) === 0) { + + // referenceName += `_${(referenceCounts.get(referenceName) || 0) + 1}` + + // If the reference name came from the function declaration, + // // We need to update the function name to match the reference name + // if (babel.types.isFunctionDeclaration(directiveFn.node)) { + // console.log('updating function name', directiveFn.node.id!.name) + // directiveFn.node.id!.name = referenceName + // } + + // 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()) { + // Then place the function at the top level + programBody.splice( + topParentIndex, + 0, + babel.types.variableDeclaration('const', [ + babel.types.variableDeclarator( + babel.types.identifier(referenceName), + babel.types.toExpression(directiveFn.node as any), + ), + ]), + ) + + // If it's an exported named function, we need to swap it with an + // export const originalFunctionName = referenceName + if ( + babel.types.isExportNamedDeclaration(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(referenceName), + ), + ]), + ), + ) - // Finally register the server function to - // our map of server functions - serverFnPathsByFunctionId[functionId] = { - nodePath: directiveFn, - referenceName, - functionName: fnName || '', - functionId: functionId, - splitFileId, - } - } + directiveFn.remove() + } else { + directiveFn.replaceWith(babel.types.identifier(referenceName)) } - }, - }) - return serverFnPathsByFunctionId + directiveFn = programPath.get( + `body.${topParentIndex}.declarations.0.init`, + ) as SupportedFunctionPath + } + + const [filename, searchParamsStr] = opts.filename.split('?') + + const searchParams = new URLSearchParams(searchParamsStr) + searchParams.set(opts.directiveSplitParam, functionId) + const splitFileId = `${filename}?${searchParams.toString()}` + + // If a replacer is provided, replace the function with the replacer + if (functionId !== opts.splitFunctionId && opts.replacer) { + const replacer = opts.replacer({ + fn: '$$fn$$', + splitImportFn: '$$splitImportFn$$', + // splitFileId, + filename: filename!, + functionId: functionId, + }) + + const replacement = babel.template.expression(replacer, { + placeholderPattern: false, + placeholderWhitelist: new Set(['$$fn$$', '$$splitImportFn$$']), + })({ + ...(replacer.includes('$$fn$$') + ? { $$fn$$: babel.types.toExpression(directiveFn.node) } + : {}), + ...(replacer.includes('$$splitImportFn$$') + ? { + $$splitImportFn$$: `(...args) => import(${JSON.stringify(splitFileId)}).then(module => module.default(...args))`, + } + : {}), + }) + + directiveFn.replaceWith(replacement) + } + + // Finally register the directive to + // our map of directives + directiveFnsById[functionId] = { + nodePath: directiveFn, + referenceName, + functionName: fnName || '', + functionId: functionId, + splitFileId, + } + } } function codeFrameError( diff --git a/packages/server-functions-vite-plugin/src/index.ts b/packages/server-functions-vite-plugin/src/index.ts index 12a710f138..8a96c502e9 100644 --- a/packages/server-functions-vite-plugin/src/index.ts +++ b/packages/server-functions-vite-plugin/src/index.ts @@ -2,81 +2,148 @@ import { fileURLToPath, pathToFileURL } from 'node:url' import { compileDirectives } from './compilers' import { logDiff } from './logger' -import type { ReplacerFn } from './compilers' +import type { CompileDirectivesOpts, DirectiveFn } from './compilers' import type { Plugin } from 'vite' const debug = Boolean(process.env.TSR_VITE_DEBUG) || (true as boolean) -export type ServerFunctionsViteOptions = { - getRuntimeCode: (opts: { - serverFnPathsByFunctionId: Record< - string, - { - nodePath: babel.NodePath - functionName: string - functionId: string - } - > - }) => string - replacer: ReplacerFn -} +export type ServerFunctionsViteOptions = Pick< + CompileDirectivesOpts, + 'directive' | 'directiveLabel' | 'getRuntimeCode' | 'replacer' +> -const useServerRx = /"use server"|'use server'/ +export type CreateClientRpcFn = (functionId: string) => any +export type CreateSsrRpcFn = (functionId: string) => any +export type CreateServerRpcFn = ( + functionId: string, + splitImportFn: string, +) => any -export type CreateRpcFn = (opts: { - fn: (...args: Array) => any - filename: string - functionId: string -}) => (...args: Array) => any +const createDirectiveRx = (directive: string) => + new RegExp(`"${directive}"|'${directive}'`, 'g') -export function TanStackServerFnPluginRuntime( +export function createTanStackServerFnPlugin( opts: ServerFunctionsViteOptions, -): Array { - let ROOT: string = process.cwd() +): { + client: Array + ssr: Array + server: Array +} { + let directiveFnsById: Record = {} - return [ - { - name: 'tanstack-start-server-fn-client-vite-plugin', - enforce: 'pre', - configResolved: (config) => { - ROOT = config.root - }, - transform(code, id) { - const url = pathToFileURL(id) - url.searchParams.delete('v') - id = fileURLToPath(url).replace(/\\/g, '/') + return { + client: [ + // The client plugin is used to compile the client directives + // and save them so we can create a manifest + TanStackDirectiveFunctionsPlugin({ + directive: 'use server', + directiveLabel: 'Server Function', + getRuntimeCode: () => + `import { createClientRpc } from '@tanstack/start/client-runtime'`, + replacer: (opts) => + // On the client, all we need is the function ID + `createClientRpc(${JSON.stringify(opts.functionId)})`, + onDirectiveFnsById: (d) => { + // When the client directives are compiled, save them so we + // can create a manifest + directiveFnsById = d + }, + }), + // Now that we have the directiveFnsById, we need to create a new + // virtual module that can be used to import that manifest + { + name: 'tanstack-start-server-fn-vite-plugin-manifest', + resolveId: (id) => { + if (id === 'virtual:tanstack-start-server-fn-manifest') { + return JSON.stringify( + Object.fromEntries( + Object.entries(directiveFnsById).map(([id, fn]) => [ + id, + { + functionName: fn.functionName, + referenceName: fn.referenceName, + splitFileId: fn.splitFileId, + }, + ]), + ), + ) + } - if (!useServerRx.test(code)) { return null - } + }, + }, + ], + ssr: [ + // The SSR plugin is used to compile the server directives + TanStackDirectiveFunctionsPlugin({ + directive: 'use server', + directiveLabel: 'Server Function', + getRuntimeCode: () => + `import { createSsrRpc } from '@tanstack/start/ssr-runtime'`, + replacer: (opts) => + // During SSR, we just need the function ID since our server environment + // is split into a worker. Similar to the client, we'll use the ID + // to call into the worker using a local http event. + `createSsrRpc(${JSON.stringify(opts.functionId)})`, + }), + ], + server: [ + // On the server, we need to compile the server functions + // so they can be called by other server functions. + // This is also where we split the server function into a separate file + // so we can load them on demand in the worker. + TanStackDirectiveFunctionsPlugin({ + directive: 'use server', + directiveLabel: 'Server Function', + getRuntimeCode: () => + `import { createServerRpc } from '@tanstack/start/ssr-runtime'`, + replacer: (opts) => + // By using the provided splitImportFn, we can both trigger vite + // to create a new chunk/entry for the server function and also + // replace other function references to it with the import statement + `createServerRpc(${JSON.stringify(opts.functionId)}, ${JSON.stringify(opts.splitImportFn)})`, + }), + ], + } +} - const { compiledResult, serverFns } = compileDirectives({ - directive: 'use server', - code, - root: ROOT, - filename: id, - getRuntimeCode: opts.getRuntimeCode, - replacer: opts.replacer, - }) +export function TanStackDirectiveFunctionsPlugin( + opts: ServerFunctionsViteOptions & { + onDirectiveFnsById?: (directiveFnsById: Record) => void + }, +): Plugin { + let ROOT: string = process.cwd() - if (debug) console.info('createServerFn Input/Output') - if (debug) logDiff(code, compiledResult.code.replace(/ctx/g, 'blah')) + const directiveRx = createDirectiveRx(opts.directive) - return compiledResult - }, + return { + name: 'tanstack-start-directive-vite-plugin', + enforce: 'pre', + configResolved: (config) => { + ROOT = config.root }, - ] -} + transform(code, id) { + const url = pathToFileURL(id) + url.searchParams.delete('v') + id = fileURLToPath(url).replace(/\\/g, '/') -export function TanStackServerFnPluginSplitFn( - opts: ServerFunctionsViteOptions, -): Array { - return [ - { - name: 'tanstack-start-server-fn-server-vite-plugin', - transform(code, id) { + if (!directiveRx.test(code)) { return null - }, + } + + const { compiledResult, directiveFnsById } = compileDirectives({ + ...opts, + code, + root: ROOT, + filename: id, + }) + + opts.onDirectiveFnsById?.(directiveFnsById) + + if (debug) console.info('Directive Input/Output') + if (debug) logDiff(code, compiledResult.code.replace(/ctx/g, 'blah')) + + return compiledResult }, - ] + } } diff --git a/packages/server-functions-vite-plugin/tests/compiler.test.ts b/packages/server-functions-vite-plugin/tests/compiler.test.ts index dc62daee66..3fc16a059c 100644 --- a/packages/server-functions-vite-plugin/tests/compiler.test.ts +++ b/packages/server-functions-vite-plugin/tests/compiler.test.ts @@ -5,6 +5,7 @@ import type { CompileDirectivesOpts } from '../src/compilers' const clientConfig: Omit = { directive: 'use server', + directiveLabel: 'Server function', root: './test-files', filename: 'test.ts', getRuntimeCode: () => 'import { createClientRpc } from "my-rpc-lib-client"', @@ -17,6 +18,7 @@ const clientConfig: Omit = { const serverConfig: Omit = { directive: 'use server', + directiveLabel: 'Server function', root: './test-files', filename: 'test.ts', getRuntimeCode: () => 'import { createServerRpc } from "my-rpc-lib-server"', @@ -30,28 +32,28 @@ const serverConfig: Omit = { describe('server function compilation', () => { const code = ` - const namedFunction = wrapper(function namedFunction() { + export const namedFunction = wrapper(function namedFunction() { 'use server' return 'hello' }) - const arrowFunction = wrapper(() => { + export const arrowFunction = wrapper(() => { 'use server' return 'hello' }) - const anonymousFunction = wrapper(function () { + export const anonymousFunction = wrapper(function () { 'use server' return 'hello' }) - const multipleDirectives = function multipleDirectives() { + export const multipleDirectives = function multipleDirectives() { 'use server' 'use strict' return 'hello' } - const iife = (function () { + export const iife = (function () { 'use server' return 'hello' })() @@ -73,13 +75,21 @@ describe('server function compilation', () => { export const namedExportConst = () => { 'use server' + return usedFn() + } + + function usedFn() { + return 'hello' + } + + function unusedFn() { return 'hello' } const namedDefaultExport = 'namedDefaultExport' export default namedDefaultExport - export const usedButNotExported = 'usedButNotExported' + const usedButNotExported = 'usedButNotExported' const namedExport = 'namedExport' @@ -95,13 +105,13 @@ describe('server function compilation', () => { code, }) const ssr = compileDirectives({ ...serverConfig, code }) - const splitFiles = Object.entries(ssr.serverFns) - .map(([_fnId, serverFn]) => { - return `// ${serverFn.functionName}\n\n${ + const splitFiles = Object.entries(ssr.directives) + .map(([_fnId, directiveFn]) => { + return `// ${directiveFn.functionName}\n\n${ compileDirectives({ ...serverConfig, code, - filename: serverFn.splitFileId, + filename: directiveFn.splitFileId, }).compiledResult.code }` }) @@ -113,27 +123,27 @@ describe('server function compilation', () => { filename: "test.ts", functionId: "test--namedFunction_wrapper_namedFunction" }); - const namedFunction = wrapper(namedFunction_wrapper_namedFunction); + export const namedFunction = wrapper(namedFunction_wrapper_namedFunction); const arrowFunction_wrapper = createClientRpc({ filename: "test.ts", functionId: "test--arrowFunction_wrapper" }); - const arrowFunction = wrapper(arrowFunction_wrapper); + export const arrowFunction = wrapper(arrowFunction_wrapper); const anonymousFunction_wrapper = createClientRpc({ filename: "test.ts", functionId: "test--anonymousFunction_wrapper" }); - const anonymousFunction = wrapper(anonymousFunction_wrapper); + export const anonymousFunction = wrapper(anonymousFunction_wrapper); const multipleDirectives_multipleDirectives = createClientRpc({ filename: "test.ts", functionId: "test--multipleDirectives_multipleDirectives" }); - const multipleDirectives = multipleDirectives_multipleDirectives; + export const multipleDirectives = multipleDirectives_multipleDirectives; const iife_1 = createClientRpc({ filename: "test.ts", functionId: "test--iife" }); - const iife = iife_1(); + export const iife = iife_1(); const defaultExportFn_1 = createClientRpc({ filename: "test.ts", functionId: "test--defaultExportFn" @@ -156,7 +166,6 @@ describe('server function compilation', () => { export const namedExportConst = namedExportConst_1; const namedDefaultExport = 'namedDefaultExport'; export default namedDefaultExport; - export const usedButNotExported = 'usedButNotExported'; const namedExport = 'namedExport'; export { namedExport };" `) @@ -164,62 +173,61 @@ describe('server function compilation', () => { expect(ssr.compiledResult.code).toMatchInlineSnapshot(` "import { createServerRpc } from "my-rpc-lib-server"; const namedFunction_wrapper_namedFunction = createServerRpc({ - fn: (...args) => import("test.ts?tsr-serverfn-split=test--namedFunction_wrapper_namedFunction").then(module => module.default(...args)), + fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--namedFunction_wrapper_namedFunction").then(module => module.default(...args)), filename: "test.ts", functionId: "test--namedFunction_wrapper_namedFunction" }); - const namedFunction = wrapper(namedFunction_wrapper_namedFunction); + export const namedFunction = wrapper(namedFunction_wrapper_namedFunction); const arrowFunction_wrapper = createServerRpc({ - fn: (...args) => import("test.ts?tsr-serverfn-split=test--arrowFunction_wrapper").then(module => module.default(...args)), + fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--arrowFunction_wrapper").then(module => module.default(...args)), filename: "test.ts", functionId: "test--arrowFunction_wrapper" }); - const arrowFunction = wrapper(arrowFunction_wrapper); + export const arrowFunction = wrapper(arrowFunction_wrapper); const anonymousFunction_wrapper = createServerRpc({ - fn: (...args) => import("test.ts?tsr-serverfn-split=test--anonymousFunction_wrapper").then(module => module.default(...args)), + fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--anonymousFunction_wrapper").then(module => module.default(...args)), filename: "test.ts", functionId: "test--anonymousFunction_wrapper" }); - const anonymousFunction = wrapper(anonymousFunction_wrapper); + export const anonymousFunction = wrapper(anonymousFunction_wrapper); const multipleDirectives_multipleDirectives = createServerRpc({ - fn: (...args) => import("test.ts?tsr-serverfn-split=test--multipleDirectives_multipleDirectives").then(module => module.default(...args)), + fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--multipleDirectives_multipleDirectives").then(module => module.default(...args)), filename: "test.ts", functionId: "test--multipleDirectives_multipleDirectives" }); - const multipleDirectives = multipleDirectives_multipleDirectives; + export const multipleDirectives = multipleDirectives_multipleDirectives; const iife_1 = createServerRpc({ - fn: (...args) => import("test.ts?tsr-serverfn-split=test--iife").then(module => module.default(...args)), + fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--iife").then(module => module.default(...args)), filename: "test.ts", functionId: "test--iife" }); - const iife = iife_1(); + export const iife = iife_1(); const defaultExportFn_1 = createServerRpc({ - fn: (...args) => import("test.ts?tsr-serverfn-split=test--defaultExportFn").then(module => module.default(...args)), + fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--defaultExportFn").then(module => module.default(...args)), filename: "test.ts", functionId: "test--defaultExportFn" }); export default defaultExportFn_1; const namedExportFn_1 = createServerRpc({ - fn: (...args) => import("test.ts?tsr-serverfn-split=test--namedExportFn").then(module => module.default(...args)), + fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--namedExportFn").then(module => module.default(...args)), filename: "test.ts", functionId: "test--namedExportFn" }); export const namedExportFn = namedExportFn_1; const exportedArrowFunction_wrapper = createServerRpc({ - fn: (...args) => import("test.ts?tsr-serverfn-split=test--exportedArrowFunction_wrapper").then(module => module.default(...args)), + fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--exportedArrowFunction_wrapper").then(module => module.default(...args)), filename: "test.ts", functionId: "test--exportedArrowFunction_wrapper" }); export const exportedArrowFunction = wrapper(exportedArrowFunction_wrapper); const namedExportConst_1 = createServerRpc({ - fn: (...args) => import("test.ts?tsr-serverfn-split=test--namedExportConst").then(module => module.default(...args)), + fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--namedExportConst").then(module => module.default(...args)), filename: "test.ts", functionId: "test--namedExportConst" }); export const namedExportConst = namedExportConst_1; const namedDefaultExport = 'namedDefaultExport'; export default namedDefaultExport; - export const usedButNotExported = 'usedButNotExported'; const namedExport = 'namedExport'; export { namedExport };" `) @@ -295,8 +303,11 @@ describe('server function compilation', () => { // namedExportConst const namedExportConst_1 = () => { - return 'hello'; + return usedFn(); }; + function usedFn() { + return 'hello'; + } export default namedExportConst_1;" `, ) @@ -395,36 +406,36 @@ describe('server function compilation', () => { expect(() => compileDirectives({ ...clientConfig, code })) .toThrowErrorMatchingInlineSnapshot(` - [Error: 1 | - 2 | class TestClass { - > 3 | method() { - | ^^^^^^^^^^^ - > 4 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 5 | return 'hello' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 6 | } - | ^^^^^^^^^ use server in class not supported - 7 | - 8 | static staticMethod() { - 9 | 'use server'] - `) + [Error: 1 | + 2 | class TestClass { + > 3 | method() { + | ^^^^^^^^^^^ + > 4 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 5 | return 'hello' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 6 | } + | ^^^^^^^^^ "use server" in class not supported + 7 | + 8 | static staticMethod() { + 9 | 'use server'] + `) expect(() => compileDirectives({ ...serverConfig, code })) .toThrowErrorMatchingInlineSnapshot(` - [Error: 1 | - 2 | class TestClass { - > 3 | method() { - | ^^^^^^^^^^^ - > 4 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 5 | return 'hello' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 6 | } - | ^^^^^^^^^ use server in class not supported - 7 | - 8 | static staticMethod() { - 9 | 'use server'] - `) + [Error: 1 | + 2 | class TestClass { + > 3 | method() { + | ^^^^^^^^^^^ + > 4 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 5 | return 'hello' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 6 | } + | ^^^^^^^^^ "use server" in class not supported + 7 | + 8 | static staticMethod() { + 9 | 'use server'] + `) expect(() => compileDirectives({ ...serverConfig, @@ -441,7 +452,7 @@ describe('server function compilation', () => { > 5 | return 'hello' | ^^^^^^^^^^^^^^^^^^^^^^ > 6 | } - | ^^^^^^^^^ use server in class not supported + | ^^^^^^^^^ "use server" in class not supported 7 | 8 | static staticMethod() { 9 | 'use server'] @@ -460,34 +471,34 @@ describe('server function compilation', () => { expect(() => compileDirectives({ ...clientConfig, code })) .toThrowErrorMatchingInlineSnapshot(` - [Error: 1 | - 2 | const obj = { - > 3 | method() { - | ^^^^^^^^^^^ - > 4 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 5 | return 'hello' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 6 | }, - | ^^^^^^^^^ use server in object method not supported - 7 | } - 8 | ] - `) + [Error: 1 | + 2 | const obj = { + > 3 | method() { + | ^^^^^^^^^^^ + > 4 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 5 | return 'hello' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 6 | }, + | ^^^^^^^^^ "use server" in object method not supported + 7 | } + 8 | ] + `) expect(() => compileDirectives({ ...serverConfig, code })) .toThrowErrorMatchingInlineSnapshot(` - [Error: 1 | - 2 | const obj = { - > 3 | method() { - | ^^^^^^^^^^^ - > 4 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 5 | return 'hello' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 6 | }, - | ^^^^^^^^^ use server in object method not supported - 7 | } - 8 | ] - `) + [Error: 1 | + 2 | const obj = { + > 3 | method() { + | ^^^^^^^^^^^ + > 4 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 5 | return 'hello' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 6 | }, + | ^^^^^^^^^ "use server" in object method not supported + 7 | } + 8 | ] + `) expect(() => compileDirectives({ ...serverConfig, @@ -504,7 +515,7 @@ describe('server function compilation', () => { > 5 | return 'hello' | ^^^^^^^^^^^^^^^^^^^^^^ > 6 | }, - | ^^^^^^^^^ use server in object method not supported + | ^^^^^^^^^ "use server" in object method not supported 7 | } 8 | ] `) @@ -525,34 +536,34 @@ describe('server function compilation', () => { expect(() => compileDirectives({ ...clientConfig, code })) .toThrowErrorMatchingInlineSnapshot(` - [Error: 1 | - > 2 | function* generatorServer() { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 3 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^ - > 4 | yield 'hello' - | ^^^^^^^^^^^^^^^^^^^^ - > 5 | } - | ^^^^^^^ use server in generator function not supported - 6 | - 7 | async function* asyncGeneratorServer() { - 8 | 'use server'] - `) + [Error: 1 | + > 2 | function* generatorServer() { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 3 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^ + > 4 | yield 'hello' + | ^^^^^^^^^^^^^^^^^^^^ + > 5 | } + | ^^^^^^^ "use server" in generator function not supported + 6 | + 7 | async function* asyncGeneratorServer() { + 8 | 'use server'] + `) expect(() => compileDirectives({ ...serverConfig, code })) .toThrowErrorMatchingInlineSnapshot(` - [Error: 1 | - > 2 | function* generatorServer() { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 3 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^ - > 4 | yield 'hello' - | ^^^^^^^^^^^^^^^^^^^^ - > 5 | } - | ^^^^^^^ use server in generator function not supported - 6 | - 7 | async function* asyncGeneratorServer() { - 8 | 'use server'] - `) + [Error: 1 | + > 2 | function* generatorServer() { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 3 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^ + > 4 | yield 'hello' + | ^^^^^^^^^^^^^^^^^^^^ + > 5 | } + | ^^^^^^^ "use server" in generator function not supported + 6 | + 7 | async function* asyncGeneratorServer() { + 8 | 'use server'] + `) expect(() => compileDirectives({ ...serverConfig, @@ -568,7 +579,7 @@ describe('server function compilation', () => { > 4 | yield 'hello' | ^^^^^^^^^^^^^^^^^^^^ > 5 | } - | ^^^^^^^ use server in generator function not supported + | ^^^^^^^ "use server" in generator function not supported 6 | 7 | async function* asyncGeneratorServer() { 8 | 'use server'] @@ -586,13 +597,13 @@ describe('server function compilation', () => { const client = compileDirectives({ ...clientConfig, code }) const ssr = compileDirectives({ ...serverConfig, code }) - const splitFiles = Object.entries(ssr.serverFns) - .map(([_fnId, serverFn]) => { - return `// ${serverFn.functionName}\n\n${ + const splitFiles = Object.entries(ssr.directives) + .map(([_fnId, directiveFn]) => { + return `// ${directiveFn.functionName}\n\n${ compileDirectives({ ...serverConfig, code, - filename: serverFn.splitFileId, + filename: directiveFn.splitFileId, }).compiledResult.code }` }) @@ -608,7 +619,7 @@ describe('server function compilation', () => { expect(ssr.compiledResult.code).toMatchInlineSnapshot(` "import { createServerRpc } from "my-rpc-lib-server"; createServerRpc({ - fn: (...args) => import("test.ts?tsr-serverfn-split=test--multiDirective").then(module => module.default(...args)), + fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--multiDirective").then(module => module.default(...args)), filename: "test.ts", functionId: "test--multiDirective" });" @@ -622,7 +633,7 @@ describe('server function compilation', () => { test('IIFE', () => { const code = ` - const iife = (function () { + export const iife = (function () { 'use server' return 'hello' })() @@ -630,13 +641,13 @@ describe('server function compilation', () => { const client = compileDirectives({ ...clientConfig, code }) const ssr = compileDirectives({ ...serverConfig, code }) - const splitFiles = Object.entries(ssr.serverFns) - .map(([_fnId, serverFn]) => { - return `// ${serverFn.functionName}\n\n${ + const splitFiles = Object.entries(ssr.directives) + .map(([_fnId, directiveFn]) => { + return `// ${directiveFn.functionName}\n\n${ compileDirectives({ ...serverConfig, code, - filename: serverFn.splitFileId, + filename: directiveFn.splitFileId, }).compiledResult.code }` }) @@ -648,17 +659,17 @@ describe('server function compilation', () => { filename: "test.ts", functionId: "test--iife" }); - const iife = iife_1();" + export const iife = iife_1();" `) expect(ssr.compiledResult.code).toMatchInlineSnapshot(` "import { createServerRpc } from "my-rpc-lib-server"; const iife_1 = createServerRpc({ - fn: (...args) => import("test.ts?tsr-serverfn-split=test--iife").then(module => module.default(...args)), + fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--iife").then(module => module.default(...args)), filename: "test.ts", functionId: "test--iife" }); - const iife = iife_1();" + export const iife = iife_1();" `) expect(splitFiles).toMatchInlineSnapshot(` @@ -686,14 +697,14 @@ describe('server function compilation', () => { const client = compileDirectives({ ...clientConfig, code }) const ssr = compileDirectives({ ...serverConfig, code }) - console.log('serverFns', ssr.serverFns) - const splitFiles = Object.entries(ssr.serverFns) - .map(([_fnId, serverFn]) => { - return `// ${serverFn.functionName}\n\n${ + + const splitFiles = Object.entries(ssr.directives) + .map(([_fnId, directiveFn]) => { + return `// ${directiveFn.functionName}\n\n${ compileDirectives({ ...serverConfig, code, - filename: serverFn.splitFileId, + filename: directiveFn.splitFileId, }).compiledResult.code }` }) @@ -701,65 +712,157 @@ describe('server function compilation', () => { expect(client.compiledResult.code).toMatchInlineSnapshot(` "import { createClientRpc } from "my-rpc-lib-client"; - const outer_useServer_2 = createClientRpc({ + const outer_useServer = createClientRpc({ filename: "test.ts", functionId: "test--outer_useServer" }); - outer(outer_useServer_2); - const outer_useServer_3 = createClientRpc({ + outer(outer_useServer); + const outer_useServer_1 = createClientRpc({ filename: "test.ts", functionId: "test--outer_useServer_1" }); - outer(outer_useServer_3);" + outer(outer_useServer_1);" `) expect(ssr.compiledResult.code).toMatchInlineSnapshot(` "import { createServerRpc } from "my-rpc-lib-server"; - const outer_useServer_2 = createServerRpc({ - fn: (...args) => import("test.ts?tsr-serverfn-split=test--outer_useServer").then(module => module.default(...args)), + const outer_useServer = createServerRpc({ + fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--outer_useServer").then(module => module.default(...args)), filename: "test.ts", functionId: "test--outer_useServer" }); - outer(outer_useServer_2); - const outer_useServer_3 = createServerRpc({ - fn: (...args) => import("test.ts?tsr-serverfn-split=test--outer_useServer_1").then(module => module.default(...args)), + outer(outer_useServer); + const outer_useServer_1 = createServerRpc({ + fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--outer_useServer_1").then(module => module.default(...args)), filename: "test.ts", functionId: "test--outer_useServer_1" }); - outer(outer_useServer_3);" + outer(outer_useServer_1);" `) expect(splitFiles).toMatchInlineSnapshot(` "// outer_useServer import { createServerRpc } from "my-rpc-lib-server"; - const outer_useServer_2 = function useServer() { + const outer_useServer = function useServer() { return 'hello'; }; - outer(outer_useServer_2); - const outer_useServer_3 = createServerRpc({ - fn: (...args) => import("test.ts?tsr-serverfn-split=test--outer_useServer_1").then(module => module.default(...args)), + outer(outer_useServer); + const outer_useServer_1 = createServerRpc({ + fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--outer_useServer_1").then(module => module.default(...args)), filename: "test.ts", functionId: "test--outer_useServer_1" }); - outer(outer_useServer_3); - export default outer_useServer_2; + outer(outer_useServer_1); + export default outer_useServer; // outer_useServer import { createServerRpc } from "my-rpc-lib-server"; - const outer_useServer_2 = createServerRpc({ - fn: (...args) => import("test.ts?tsr-serverfn-split=test--outer_useServer").then(module => module.default(...args)), + const outer_useServer = createServerRpc({ + fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--outer_useServer").then(module => module.default(...args)), filename: "test.ts", functionId: "test--outer_useServer" }); - outer(outer_useServer_2); - const outer_useServer_3 = function useServer() { + outer(outer_useServer); + const outer_useServer_1 = function useServer() { + return 'hello'; + }; + outer(outer_useServer_1); + export default 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({ ...serverConfig, code }) + const splitFiles = Object.entries(ssr.directives) + .map(([_fnId, directive]) => { + return `// ${directive.functionName}\n\n${ + compileDirectives({ + ...serverConfig, + code, + filename: directive.splitFileId, + }).compiledResult.code + }` + }) + .join('\n\n\n') + + expect(client.compiledResult.code).toMatchInlineSnapshot(` + "'use server'; + + import { createClientRpc } from "my-rpc-lib-client"; + const useServer_1 = createClientRpc({ + filename: "test.ts", + functionId: "test--useServer" + }); + export const useServer = useServer_1; + const defaultExport_1 = createClientRpc({ + filename: "test.ts", + functionId: "test--defaultExport" + }); + export default defaultExport_1;" + `) + expect(ssr.compiledResult.code).toMatchInlineSnapshot(` + "'use server'; + + import { createServerRpc } from "my-rpc-lib-server"; + const useServer_1 = createServerRpc({ + fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--useServer").then(module => module.default(...args)), + filename: "test.ts", + functionId: "test--useServer" + }); + export const useServer = useServer_1; + const defaultExport_1 = createServerRpc({ + fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--defaultExport").then(module => module.default(...args)), + filename: "test.ts", + functionId: "test--defaultExport" + }); + export default defaultExport_1;" + `) + expect(splitFiles).toMatchInlineSnapshot(` + "// useServer + + 'use server'; + + const useServer_1 = function useServer() { + return usedInUseServer(); + }; + function usedInUseServer() { + return 'hello'; + } + export default useServer_1; + + + // defaultExport + + 'use server'; + + const defaultExport_1 = function defaultExport() { return 'hello'; }; - outer(outer_useServer_3); - export default outer_useServer_3;" + export default defaultExport_1;" `) }) }) diff --git a/packages/start/package.json b/packages/start/package.json index 70f1fcc9e8..c03e27a0ca 100644 --- a/packages/start/package.json +++ b/packages/start/package.json @@ -139,7 +139,7 @@ "@tanstack/router-generator": "workspace:^", "@tanstack/router-plugin": "workspace:^", "@tanstack/start-vite-plugin": "workspace:^", - "@tanstack/server-functions-plugin": "workspace:^", + "@tanstack/directive-functions-plugin": "workspace:^", "@vinxi/react": "0.2.5", "@vinxi/react-server-dom": "^0.0.3", "@vinxi/server-components": "0.5.0", diff --git a/packages/start/src/client-runtime/index.tsx b/packages/start/src/client-runtime/index.tsx index 9321438a70..59b4434d4d 100644 --- a/packages/start/src/client-runtime/index.tsx +++ b/packages/start/src/client-runtime/index.tsx @@ -1,6 +1,6 @@ import { fetcher } from './fetcher' import { getBaseUrl } from './getBaseUrl' -import type { CreateRpcFn } from '@tanstack/server-functions-plugin' +import type { CreateRpcFn } from '@tanstack/directive-functions-plugin' export const createClientRpc: CreateRpcFn = (opts) => { const base = getBaseUrl( diff --git a/packages/start/src/config/index.ts b/packages/start/src/config/index.ts index f8c14fee4f..c6e9c80d0d 100644 --- a/packages/start/src/config/index.ts +++ b/packages/start/src/config/index.ts @@ -14,10 +14,6 @@ import { createApp } from 'vinxi' import { config } from 'vinxi/plugins/config' // // @ts-expect-error // import { serverComponents } from '@vinxi/server-components/plugin' -import { - TanStackServerFnPluginClient, - TanStackServerFnPluginServer, -} from '@tanstack/server-functions-plugin' import { tanstackStartVinxiFileRouter } from './vinxi-file-router.js' import { checkDeploymentPresetInput, @@ -170,7 +166,7 @@ export function defineConfig( }), ...(viteConfig.plugins || []), ...(clientViteConfig.plugins || []), - TanStackServerFnPluginClient({ + Directive({ getRuntimeCode: (opts) => `import { createClientRpc } from '@tanstack/start/client-runtime'`, replacer: (opts) => diff --git a/packages/start/src/server-runtime/index.tsx b/packages/start/src/server-runtime/index.tsx index 6c68ed7866..1b222d6ec9 100644 --- a/packages/start/src/server-runtime/index.tsx +++ b/packages/start/src/server-runtime/index.tsx @@ -1,5 +1,5 @@ // import { getBaseUrl } from '../client-runtime/getBaseUrl' -import type { CreateRpcFn } from '@tanstack/server-functions-plugin' +import type { CreateRpcFn } from '@tanstack/directive-functions-plugin' export const createServerRpc: CreateRpcFn = (opts) => { // const functionUrl = getBaseUrl('http://localhost:3000', id, name) diff --git a/packages/start/src/server/defaultStreamHandler.tsx b/packages/start/src/server/defaultStreamHandler.tsx index af86e4fc85..3a8707017a 100644 --- a/packages/start/src/server/defaultStreamHandler.tsx +++ b/packages/start/src/server/defaultStreamHandler.tsx @@ -63,12 +63,12 @@ export const defaultStreamHandler: HandlerCallback = async ({ }, }), onError: (error, info) => { - console.log('Error in renderToPipeableStream:', error, info) + console.error('Error in renderToPipeableStream:', error, info) }, }, ) } catch (e) { - console.log('Error in renderToPipeableStream:', e) + console.error('Error in renderToPipeableStream:', e) } const transforms = [transformStreamWithRouter(router)] diff --git a/packages/start/src/ssr-runtime/index.tsx b/packages/start/src/ssr-runtime/index.tsx index f4dadc87f7..4a148fdda2 100644 --- a/packages/start/src/ssr-runtime/index.tsx +++ b/packages/start/src/ssr-runtime/index.tsx @@ -4,7 +4,7 @@ import invariant from 'tiny-invariant' import { fetcher } from '../client-runtime/fetcher' import { getBaseUrl } from '../client-runtime/getBaseUrl' import { handleServerRequest } from '../server-handler/index' -import type { CreateRpcFn } from '@tanstack/server-functions-plugin' +import type { CreateRpcFn } from '@tanstack/directive-functions-plugin' export function createIncomingMessage( url: string, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f7c941e7cf..b4adb9bbf2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3548,7 +3548,7 @@ importers: version: 6.0.0 babel-dead-code-elimination: specifier: ^1.0.6 - version: 1.0.6 + version: 1.0.8 chalk: specifier: ^5.3.0 version: 5.3.0 @@ -3564,6 +3564,9 @@ importers: packages/start: dependencies: + '@tanstack/directive-functions-plugin': + specifier: workspace:^ + version: link:../server-functions-vite-plugin '@tanstack/react-cross-context': specifier: workspace:* version: link:../react-cross-context @@ -3576,9 +3579,6 @@ importers: '@tanstack/router-plugin': specifier: workspace:* version: link:../router-plugin - '@tanstack/server-functions-plugin': - specifier: workspace:^ - version: link:../server-functions-vite-plugin '@tanstack/start-vite-plugin': specifier: workspace:* version: link:../start-vite-plugin From ce2ab1082de979e94e483c2e5544a5dcb88edf63 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Thu, 19 Dec 2024 17:27:28 -0700 Subject: [PATCH 10/38] fis tests --- .../tests/compiler.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/server-functions-vite-plugin/tests/compiler.test.ts b/packages/server-functions-vite-plugin/tests/compiler.test.ts index 3fc16a059c..4aeb219618 100644 --- a/packages/server-functions-vite-plugin/tests/compiler.test.ts +++ b/packages/server-functions-vite-plugin/tests/compiler.test.ts @@ -105,7 +105,7 @@ describe('server function compilation', () => { code, }) const ssr = compileDirectives({ ...serverConfig, code }) - const splitFiles = Object.entries(ssr.directives) + const splitFiles = Object.entries(ssr.directiveFnsById) .map(([_fnId, directiveFn]) => { return `// ${directiveFn.functionName}\n\n${ compileDirectives({ @@ -586,7 +586,7 @@ describe('server function compilation', () => { `) }) - test('multiple directives', () => { + test('multiple directiveFnsById', () => { const code = ` function multiDirective() { 'use strict' @@ -597,7 +597,7 @@ describe('server function compilation', () => { const client = compileDirectives({ ...clientConfig, code }) const ssr = compileDirectives({ ...serverConfig, code }) - const splitFiles = Object.entries(ssr.directives) + const splitFiles = Object.entries(ssr.directiveFnsById) .map(([_fnId, directiveFn]) => { return `// ${directiveFn.functionName}\n\n${ compileDirectives({ @@ -641,7 +641,7 @@ describe('server function compilation', () => { const client = compileDirectives({ ...clientConfig, code }) const ssr = compileDirectives({ ...serverConfig, code }) - const splitFiles = Object.entries(ssr.directives) + const splitFiles = Object.entries(ssr.directiveFnsById) .map(([_fnId, directiveFn]) => { return `// ${directiveFn.functionName}\n\n${ compileDirectives({ @@ -698,7 +698,7 @@ describe('server function compilation', () => { const client = compileDirectives({ ...clientConfig, code }) const ssr = compileDirectives({ ...serverConfig, code }) - const splitFiles = Object.entries(ssr.directives) + const splitFiles = Object.entries(ssr.directiveFnsById) .map(([_fnId, directiveFn]) => { return `// ${directiveFn.functionName}\n\n${ compileDirectives({ @@ -797,7 +797,7 @@ describe('server function compilation', () => { const client = compileDirectives({ ...clientConfig, code }) const ssr = compileDirectives({ ...serverConfig, code }) - const splitFiles = Object.entries(ssr.directives) + const splitFiles = Object.entries(ssr.directiveFnsById) .map(([_fnId, directive]) => { return `// ${directive.functionName}\n\n${ compileDirectives({ From bc985743c33beecb6536d98987039c328cd436f2 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Fri, 20 Dec 2024 17:22:30 -0700 Subject: [PATCH 11/38] It's working!!! --- .../src/compilers.ts | 105 +++-- .../server-functions-vite-plugin/src/index.ts | 153 +++++-- .../src/logger.ts | 4 +- .../tests/compiler.test.ts | 431 +++++++++++++----- packages/start-vite-plugin/src/ast.ts | 2 +- packages/start-vite-plugin/src/compilers.ts | 174 ++----- packages/start-vite-plugin/src/index.ts | 4 +- .../createServerFn/createServerFn.test.ts | 44 ++ .../client/createServerFnDestructured.tsx | 5 - .../createServerFnDestructuredRename.tsx | 5 - .../client/createServerFnStarImport.tsx | 5 - .../server/createServerFnStarImport.tsx | 2 + packages/start/src/client-runtime/fetcher.tsx | 8 +- .../start/src/client-runtime/getBaseUrl.tsx | 4 +- packages/start/src/client-runtime/index.tsx | 13 +- packages/start/src/client/createServerFn.ts | 2 +- packages/start/src/config/index.ts | 38 +- packages/start/src/server-handler/index.tsx | 47 +- packages/start/src/server-runtime/index.tsx | 21 +- packages/start/src/ssr-runtime/index.tsx | 13 +- packages/start/vite.config.ts | 2 +- 21 files changed, 675 insertions(+), 407 deletions(-) diff --git a/packages/server-functions-vite-plugin/src/compilers.ts b/packages/server-functions-vite-plugin/src/compilers.ts index 120f30c0fb..eb16b459bc 100644 --- a/packages/server-functions-vite-plugin/src/compilers.ts +++ b/packages/server-functions-vite-plugin/src/compilers.ts @@ -19,7 +19,9 @@ export interface DirectiveFn { functionName: string functionId: string referenceName: string - splitFileId: string + splitFilename: string + filename: string + chunkName: string } export type SupportedFunctionPath = @@ -32,6 +34,7 @@ export type ReplacerFn = (opts: { splitImportFn: string filename: string functionId: string + isSplitFn: boolean }) => string // const debug = process.env.TSR_VITE_DEBUG === 'true' @@ -43,38 +46,53 @@ export type CompileDirectivesOpts = ParseAstOptions & { directiveFnsById: Record }) => string replacer: ReplacerFn + devSplitImporter: string } export function compileDirectives(opts: CompileDirectivesOpts) { const [_, searchParamsStr] = opts.filename.split('?') const searchParams = new URLSearchParams(searchParamsStr) const directiveSplitParam = `tsr-directive-${opts.directive.replace(/[^a-zA-Z0-9]/g, '-')}-split` - const functionId = searchParams.get(directiveSplitParam) + const functionName = searchParams.get(directiveSplitParam) const ast = parseAst(opts) const directiveFnsById = findDirectives(ast, { ...opts, - splitFunctionId: functionId, + splitFunctionName: functionName, directiveSplitParam, }) + const directiveFnsByFunctionName = Object.fromEntries( + Object.entries(directiveFnsById).map(([id, fn]) => [fn.functionName, fn]), + ) + // Add runtime code if there are directives // Add runtime code if there are directives - if (Object.keys(directiveFnsById).length > 0 && opts.getRuntimeCode) { - const runtimeImport = babel.template.statement( - opts.getRuntimeCode({ directiveFnsById }), - )() - ast.program.body.unshift(runtimeImport) + if (Object.keys(directiveFnsById).length > 0) { + // Add a vite import to the top of the file + ast.program.body.unshift( + babel.types.importDeclaration( + [babel.types.importDefaultSpecifier(babel.types.identifier('vite'))], + babel.types.stringLiteral('vite'), + ), + ) + + if (opts.getRuntimeCode) { + const runtimeImport = babel.template.statement( + opts.getRuntimeCode({ directiveFnsById }), + )() + ast.program.body.unshift(runtimeImport) + } } - // If there is a functionId, we need to remove all exports + // If there is a functionName, we need to remove all exports // then make sure that our function is exported under the // directive name - if (functionId) { - const directiveFn = directiveFnsById[functionId] + if (functionName) { + const directiveFn = directiveFnsByFunctionName[functionName] if (!directiveFn) { - throw new Error(`${opts.directiveLabel} ${functionId} not found`) + throw new Error(`${opts.directiveLabel} ${functionName} not found`) } safeRemoveExports(ast) @@ -211,12 +229,13 @@ export function findDirectives( directive: string directiveLabel: string replacer?: ReplacerFn - splitFunctionId?: string | null + splitFunctionName?: string | null directiveSplitParam: string + devSplitImporter: string }, ) { const directiveFnsById: Record = {} - const functionIdCounts: Record = {} + const functionNameCounts: Record = {} let programPath: babel.NodePath @@ -324,25 +343,18 @@ export function findDirectives( } // Find the nearest variable name - const fnName = findNearestVariableName(directiveFn, opts.directiveLabel) - - // Make the functionId safe for use in a URL - const baseLabel = makeFileLocationUrlSafe( - `${opts.filename.replace( - path.extname(opts.filename), - '', - )}--${fnName}`.replace(opts.root, ''), - ) + let functionName = findNearestVariableName(directiveFn, opts.directiveLabel) // Count the number of functions with the same baseLabel - functionIdCounts[baseLabel] = (functionIdCounts[baseLabel] || 0) + 1 + functionNameCounts[functionName] = + (functionNameCounts[functionName] || 0) + 1 - // If there are multiple functions with the same baseLabel, + // If there are multiple functions with the same fnName, // append a unique identifier to the functionId - const functionId = - functionIdCounts[baseLabel] > 1 - ? `${baseLabel}_${functionIdCounts[baseLabel] - 1}` - : baseLabel + functionName = + functionNameCounts[functionName]! > 1 + ? `${functionName}_${functionNameCounts[functionName]! - 1}` + : functionName // Move the function to program level while preserving its position // in the program body @@ -354,7 +366,7 @@ export function findDirectives( const topParentIndex = programBody.indexOf(topParent.node as any) // Determine the reference name for the function - let referenceName = makeIdentifierSafe(fnName) + let referenceName = makeIdentifierSafe(functionName) // Crawl the scope to refresh all the bindings programPath.scope.crawl() @@ -424,20 +436,27 @@ export function findDirectives( ) as SupportedFunctionPath } - const [filename, searchParamsStr] = opts.filename.split('?') + const functionId = makeFileLocationUrlSafe( + `${opts.filename.replace( + path.extname(opts.filename), + '', + )}--${functionName}`.replace(opts.root, ''), + ) + const [filename, searchParamsStr] = opts.filename.split('?') const searchParams = new URLSearchParams(searchParamsStr) - searchParams.set(opts.directiveSplitParam, functionId) - const splitFileId = `${filename}?${searchParams.toString()}` + searchParams.set(opts.directiveSplitParam, functionName) + const splitFilename = `${filename}?${searchParams.toString()}` // If a replacer is provided, replace the function with the replacer - if (functionId !== opts.splitFunctionId && opts.replacer) { + if (opts.replacer) { const replacer = opts.replacer({ fn: '$$fn$$', splitImportFn: '$$splitImportFn$$', - // splitFileId, + // splitFilename, filename: filename!, functionId: functionId, + isSplitFn: functionName === opts.splitFunctionName, }) const replacement = babel.template.expression(replacer, { @@ -449,7 +468,10 @@ export function findDirectives( : {}), ...(replacer.includes('$$splitImportFn$$') ? { - $$splitImportFn$$: `(...args) => import(${JSON.stringify(splitFileId)}).then(module => module.default(...args))`, + $$splitImportFn$$: + process.env.NODE_ENV === 'production' + ? `(...args) => import(${JSON.stringify(splitFilename)}).then(module => module.default(...args))` + : `(...args) => ${opts.devSplitImporter}(${JSON.stringify(splitFilename)}).then(module => module.default(...args))`, } : {}), }) @@ -462,9 +484,11 @@ export function findDirectives( directiveFnsById[functionId] = { nodePath: directiveFn, referenceName, - functionName: fnName || '', + functionName: functionName || '', functionId: functionId, - splitFileId, + splitFilename, + filename: opts.filename, + chunkName: fileNameToChunkName(opts.root, splitFilename), } } } @@ -544,3 +568,8 @@ const safeRemoveExports = (ast: babel.types.File) => { }, }) } + +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/server-functions-vite-plugin/src/index.ts b/packages/server-functions-vite-plugin/src/index.ts index 8a96c502e9..e6f1f6cd69 100644 --- a/packages/server-functions-vite-plugin/src/index.ts +++ b/packages/server-functions-vite-plugin/src/index.ts @@ -1,11 +1,13 @@ import { fileURLToPath, pathToFileURL } from 'node:url' +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import path from 'node:path' import { compileDirectives } from './compilers' import { logDiff } from './logger' import type { CompileDirectivesOpts, DirectiveFn } from './compilers' import type { Plugin } from 'vite' -const debug = Boolean(process.env.TSR_VITE_DEBUG) || (true as boolean) +const debug = Boolean(process.env.TSR_VITE_DEBUG) export type ServerFunctionsViteOptions = Pick< CompileDirectivesOpts, @@ -22,14 +24,52 @@ export type CreateServerRpcFn = ( const createDirectiveRx = (directive: string) => new RegExp(`"${directive}"|'${directive}'`, 'g') -export function createTanStackServerFnPlugin( - opts: ServerFunctionsViteOptions, -): { +export function createTanStackServerFnPlugin(_opts?: {}): { client: Array ssr: Array server: Array } { - let directiveFnsById: Record = {} + const ROOT = process.cwd() + const manifestFilename = + 'node_modules/.tanstack-start/server-functions-manifest.json' + const directiveFnsById: Record = {} + + const directiveFnsByIdToManifest = ( + directiveFnsById: Record, + ) => + Object.fromEntries( + Object.entries(directiveFnsById).map(([id, fn]) => [ + id, + { + functionName: fn.functionName, + referenceName: fn.referenceName, + splitFilename: fn.splitFilename, + filename: fn.filename, + chunkName: fn.chunkName, + }, + ]), + ) + + const readManifestPlugin = (): Plugin => ({ + name: 'tanstack-start-server-fn-vite-plugin-manifest', + enforce: 'pre', + resolveId: (id) => { + if (id === 'tsr:server-fn-manifest') { + return id + } + + return null + }, + load(id) { + if (id === 'tsr:server-fn-manifest') { + return `export default ${JSON.stringify( + directiveFnsByIdToManifest(directiveFnsById), + )}` + } + + return null + }, + }) return { client: [ @@ -44,32 +84,26 @@ export function createTanStackServerFnPlugin( // On the client, all we need is the function ID `createClientRpc(${JSON.stringify(opts.functionId)})`, onDirectiveFnsById: (d) => { - // When the client directives are compiled, save them so we + // When directives are compiled, save them so we // can create a manifest - directiveFnsById = d + Object.assign(directiveFnsById, d) }, }), // Now that we have the directiveFnsById, we need to create a new // virtual module that can be used to import that manifest + readManifestPlugin(), { - name: 'tanstack-start-server-fn-vite-plugin-manifest', - resolveId: (id) => { - if (id === 'virtual:tanstack-start-server-fn-manifest') { - return JSON.stringify( - Object.fromEntries( - Object.entries(directiveFnsById).map(([id, fn]) => [ - id, - { - functionName: fn.functionName, - referenceName: fn.referenceName, - splitFileId: fn.splitFileId, - }, - ]), - ), - ) - } - - return null + name: 'tanstack-start-server-fn-vite-plugin-build-client', + generateBundle() { + // We also need to create a production manifest so we can + // access it the server build, which does not run in the + // same vite build environment + console.log('Generating production manifest') + mkdirSync(path.dirname(manifestFilename), { recursive: true }) + writeFileSync( + path.join(ROOT, manifestFilename), + JSON.stringify(directiveFnsByIdToManifest(directiveFnsById)), + ) }, }, ], @@ -85,9 +119,16 @@ export function createTanStackServerFnPlugin( // is split into a worker. Similar to the client, we'll use the ID // to call into the worker using a local http event. `createSsrRpc(${JSON.stringify(opts.functionId)})`, + onDirectiveFnsById: (d) => { + // When directives are compiled, save them so we + // can create a manifest + Object.assign(directiveFnsById, d) + }, }), + readManifestPlugin(), ], server: [ + readManifestPlugin(), // On the server, we need to compile the server functions // so they can be called by other server functions. // This is also where we split the server function into a separate file @@ -96,13 +137,66 @@ export function createTanStackServerFnPlugin( directive: 'use server', directiveLabel: 'Server Function', getRuntimeCode: () => - `import { createServerRpc } from '@tanstack/start/ssr-runtime'`, + `import { createServerRpc } from '@tanstack/start/server-runtime'`, replacer: (opts) => // By using the provided splitImportFn, we can both trigger vite // to create a new chunk/entry for the server function and also // replace other function references to it with the import statement - `createServerRpc(${JSON.stringify(opts.functionId)}, ${JSON.stringify(opts.splitImportFn)})`, + `createServerRpc(${JSON.stringify(opts.functionId)}, ${opts.isSplitFn ? opts.fn : opts.splitImportFn})`, + onDirectiveFnsById: (d) => { + // When directives are compiled, save them so we + // can create a manifest + Object.assign(directiveFnsById, d) + }, }), + (() => { + let serverFunctionsManifest: Record + return { + name: 'tanstack-start-server-fn-vite-plugin-build-', + enforce: 'post', + apply: 'build', + config(config) { + // We use a file to store the manifest so we can access it between + // vite environments + serverFunctionsManifest = JSON.parse( + readFileSync(path.join(ROOT, manifestFilename), 'utf-8'), + ) + + return { + build: { + rollupOptions: { + output: { + chunkFileNames: '[name].mjs', + entryFileNames: '[name].mjs', + }, + treeshake: true, + }, + }, + } + }, + + configResolved(config) { + const serverFnEntries = Object.fromEntries( + Object.entries(serverFunctionsManifest).map(([id, fn]) => { + return [fn.chunkName, fn.splitFilename] + }), + ) + + config.build.rollupOptions.input = { + entry: Array.isArray(config.build.rollupOptions.input) + ? config.build.rollupOptions.input[0] + : typeof config.build.rollupOptions.input === 'object' && + 'entry' in config.build.rollupOptions.input + ? config.build.rollupOptions.input.entry + : ((() => { + throw new Error('Invalid input') + }) as any), + ...serverFnEntries, + } + console.log(config.build.rollupOptions.input) + }, + } + })(), ], } } @@ -136,12 +230,15 @@ export function TanStackDirectiveFunctionsPlugin( code, root: ROOT, filename: id, + // globalThis.app currently refers to Vinxi's app instance. In the future, it can just be the + // vite dev server instance we get from Nitro. + devSplitImporter: `(globalThis.app.getRouter('server').internals.devServer.ssrLoadModule)`, }) opts.onDirectiveFnsById?.(directiveFnsById) if (debug) console.info('Directive Input/Output') - if (debug) logDiff(code, compiledResult.code.replace(/ctx/g, 'blah')) + if (debug) logDiff(code, compiledResult.code) return compiledResult }, diff --git a/packages/server-functions-vite-plugin/src/logger.ts b/packages/server-functions-vite-plugin/src/logger.ts index e1a3c37eea..fd777080c3 100644 --- a/packages/server-functions-vite-plugin/src/logger.ts +++ b/packages/server-functions-vite-plugin/src/logger.ts @@ -1,8 +1,8 @@ import chalk from 'chalk' -import { diffChars } from 'diff' +import { diffWords } from 'diff' export function logDiff(oldStr: string, newStr: string) { - const differences = diffChars(oldStr, newStr) + const differences = diffWords(oldStr, newStr) let output = '' let unchangedLines = '' diff --git a/packages/server-functions-vite-plugin/tests/compiler.test.ts b/packages/server-functions-vite-plugin/tests/compiler.test.ts index 4aeb219618..7092e1e348 100644 --- a/packages/server-functions-vite-plugin/tests/compiler.test.ts +++ b/packages/server-functions-vite-plugin/tests/compiler.test.ts @@ -14,6 +14,21 @@ const clientConfig: Omit = { filename: ${JSON.stringify(opts.filename)}, functionId: ${JSON.stringify(opts.functionId)}, })`, + devSplitImporter: `devImport`, +} + +const ssrConfig: Omit = { + directive: 'use server', + directiveLabel: 'Server function', + root: './test-files', + filename: 'test.ts', + getRuntimeCode: () => 'import { createSsrRpc } from "my-rpc-lib-server"', + replacer: (opts) => + `createSsrRpc({ + filename: ${JSON.stringify(opts.filename)}, + functionId: ${JSON.stringify(opts.functionId)}, + })`, + devSplitImporter: `devImport`, } const serverConfig: Omit = { @@ -23,11 +38,18 @@ const serverConfig: Omit = { filename: 'test.ts', getRuntimeCode: () => 'import { createServerRpc } from "my-rpc-lib-server"', 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({ - fn: ${opts.splitImportFn}, + fn: ${opts.isSplitFn ? opts.fn : opts.splitImportFn}, filename: ${JSON.stringify(opts.filename)}, functionId: ${JSON.stringify(opts.functionId)}, })`, + devSplitImporter: `devImport`, } describe('server function compilation', () => { @@ -104,14 +126,14 @@ describe('server function compilation', () => { ...clientConfig, code, }) - const ssr = compileDirectives({ ...serverConfig, code }) + const ssr = compileDirectives({ ...ssrConfig, code }) const splitFiles = Object.entries(ssr.directiveFnsById) .map(([_fnId, directiveFn]) => { - return `// ${directiveFn.functionName}\n\n${ + return `// ${directiveFn.functionId}\n\n${ compileDirectives({ ...serverConfig, code, - filename: directiveFn.splitFileId, + filename: directiveFn.splitFilename, }).compiledResult.code }` }) @@ -171,57 +193,48 @@ describe('server function compilation', () => { `) expect(ssr.compiledResult.code).toMatchInlineSnapshot(` - "import { createServerRpc } from "my-rpc-lib-server"; - const namedFunction_wrapper_namedFunction = createServerRpc({ - fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--namedFunction_wrapper_namedFunction").then(module => module.default(...args)), + "import { createSsrRpc } from "my-rpc-lib-server"; + const namedFunction_wrapper_namedFunction = createSsrRpc({ filename: "test.ts", functionId: "test--namedFunction_wrapper_namedFunction" }); export const namedFunction = wrapper(namedFunction_wrapper_namedFunction); - const arrowFunction_wrapper = createServerRpc({ - fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--arrowFunction_wrapper").then(module => module.default(...args)), + const arrowFunction_wrapper = createSsrRpc({ filename: "test.ts", functionId: "test--arrowFunction_wrapper" }); export const arrowFunction = wrapper(arrowFunction_wrapper); - const anonymousFunction_wrapper = createServerRpc({ - fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--anonymousFunction_wrapper").then(module => module.default(...args)), + const anonymousFunction_wrapper = createSsrRpc({ filename: "test.ts", functionId: "test--anonymousFunction_wrapper" }); export const anonymousFunction = wrapper(anonymousFunction_wrapper); - const multipleDirectives_multipleDirectives = createServerRpc({ - fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--multipleDirectives_multipleDirectives").then(module => module.default(...args)), + const multipleDirectives_multipleDirectives = createSsrRpc({ filename: "test.ts", functionId: "test--multipleDirectives_multipleDirectives" }); export const multipleDirectives = multipleDirectives_multipleDirectives; - const iife_1 = createServerRpc({ - fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--iife").then(module => module.default(...args)), + const iife_1 = createSsrRpc({ filename: "test.ts", functionId: "test--iife" }); export const iife = iife_1(); - const defaultExportFn_1 = createServerRpc({ - fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--defaultExportFn").then(module => module.default(...args)), + const defaultExportFn_1 = createSsrRpc({ filename: "test.ts", functionId: "test--defaultExportFn" }); export default defaultExportFn_1; - const namedExportFn_1 = createServerRpc({ - fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--namedExportFn").then(module => module.default(...args)), + const namedExportFn_1 = createSsrRpc({ filename: "test.ts", functionId: "test--namedExportFn" }); export const namedExportFn = namedExportFn_1; - const exportedArrowFunction_wrapper = createServerRpc({ - fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--exportedArrowFunction_wrapper").then(module => module.default(...args)), + const exportedArrowFunction_wrapper = createSsrRpc({ filename: "test.ts", functionId: "test--exportedArrowFunction_wrapper" }); export const exportedArrowFunction = wrapper(exportedArrowFunction_wrapper); - const namedExportConst_1 = createServerRpc({ - fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--namedExportConst").then(module => module.default(...args)), + const namedExportConst_1 = createSsrRpc({ filename: "test.ts", functionId: "test--namedExportConst" }); @@ -234,77 +247,122 @@ describe('server function compilation', () => { expect(splitFiles).toMatchInlineSnapshot( ` - "// namedFunction_wrapper_namedFunction + "// test--namedFunction_wrapper_namedFunction - const namedFunction_wrapper_namedFunction = function namedFunction() { - return 'hello'; - }; + import { createServerRpc } from "my-rpc-lib-server"; + const namedFunction_wrapper_namedFunction = createServerRpc({ + fn: function namedFunction() { + return 'hello'; + }, + filename: "test.ts", + functionId: "test--namedFunction_wrapper_namedFunction" + }); export default namedFunction_wrapper_namedFunction; - // arrowFunction_wrapper + // test--arrowFunction_wrapper - const arrowFunction_wrapper = () => { - return 'hello'; - }; + import { createServerRpc } from "my-rpc-lib-server"; + const arrowFunction_wrapper = createServerRpc({ + fn: () => { + return 'hello'; + }, + filename: "test.ts", + functionId: "test--arrowFunction_wrapper" + }); export default arrowFunction_wrapper; - // anonymousFunction_wrapper + // test--anonymousFunction_wrapper - const anonymousFunction_wrapper = function () { - return 'hello'; - }; + import { createServerRpc } from "my-rpc-lib-server"; + const anonymousFunction_wrapper = createServerRpc({ + fn: function () { + return 'hello'; + }, + filename: "test.ts", + functionId: "test--anonymousFunction_wrapper" + }); export default anonymousFunction_wrapper; - // multipleDirectives_multipleDirectives + // test--multipleDirectives_multipleDirectives - const multipleDirectives_multipleDirectives = function multipleDirectives() { - 'use strict'; + import { createServerRpc } from "my-rpc-lib-server"; + const multipleDirectives_multipleDirectives = createServerRpc({ + fn: function multipleDirectives() { + 'use strict'; - return 'hello'; - }; + return 'hello'; + }, + filename: "test.ts", + functionId: "test--multipleDirectives_multipleDirectives" + }); export default multipleDirectives_multipleDirectives; - // iife + // test--iife - const iife_1 = function () { - return 'hello'; - }; + import { createServerRpc } from "my-rpc-lib-server"; + const iife_1 = createServerRpc({ + fn: function () { + return 'hello'; + }, + filename: "test.ts", + functionId: "test--iife" + }); export default iife_1; - // defaultExportFn + // test--defaultExportFn - const defaultExportFn_1 = function defaultExportFn() { - return 'hello'; - }; + import { createServerRpc } from "my-rpc-lib-server"; + const defaultExportFn_1 = createServerRpc({ + fn: function defaultExportFn() { + return 'hello'; + }, + filename: "test.ts", + functionId: "test--defaultExportFn" + }); export default defaultExportFn_1; - // namedExportFn + // test--namedExportFn - const namedExportFn_1 = function namedExportFn() { - return 'hello'; - }; + import { createServerRpc } from "my-rpc-lib-server"; + const namedExportFn_1 = createServerRpc({ + fn: function namedExportFn() { + return 'hello'; + }, + filename: "test.ts", + functionId: "test--namedExportFn" + }); export default namedExportFn_1; - // exportedArrowFunction_wrapper + // test--exportedArrowFunction_wrapper - const exportedArrowFunction_wrapper = () => { - return 'hello'; - }; + import { createServerRpc } from "my-rpc-lib-server"; + const exportedArrowFunction_wrapper = createServerRpc({ + fn: () => { + return 'hello'; + }, + filename: "test.ts", + functionId: "test--exportedArrowFunction_wrapper" + }); export default exportedArrowFunction_wrapper; - // namedExportConst + // test--namedExportConst - const namedExportConst_1 = () => { - return usedFn(); - }; + import { createServerRpc } from "my-rpc-lib-server"; + const namedExportConst_1 = createServerRpc({ + fn: () => { + return usedFn(); + }, + filename: "test.ts", + functionId: "test--namedExportConst" + }); function usedFn() { return 'hello'; } @@ -596,14 +654,14 @@ describe('server function compilation', () => { ` const client = compileDirectives({ ...clientConfig, code }) - const ssr = compileDirectives({ ...serverConfig, code }) + const ssr = compileDirectives({ ...ssrConfig, code }) const splitFiles = Object.entries(ssr.directiveFnsById) .map(([_fnId, directiveFn]) => { - return `// ${directiveFn.functionName}\n\n${ + return `// ${directiveFn.functionId}\n\n${ compileDirectives({ ...serverConfig, code, - filename: directiveFn.splitFileId, + filename: directiveFn.splitFilename, }).compiledResult.code }` }) @@ -617,16 +675,25 @@ describe('server function compilation', () => { });" `) expect(ssr.compiledResult.code).toMatchInlineSnapshot(` - "import { createServerRpc } from "my-rpc-lib-server"; - createServerRpc({ - fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--multiDirective").then(module => module.default(...args)), + "import { createSsrRpc } from "my-rpc-lib-server"; + createSsrRpc({ filename: "test.ts", functionId: "test--multiDirective" });" `) expect(splitFiles).toMatchInlineSnapshot(` - "// multiDirective + "// test--multiDirective + + import { createServerRpc } from "my-rpc-lib-server"; + createServerRpc({ + fn: function multiDirective() { + 'use strict'; + return 'hello'; + }, + filename: "test.ts", + functionId: "test--multiDirective" + }); export default multiDirective_1;" `) }) @@ -640,14 +707,14 @@ describe('server function compilation', () => { ` const client = compileDirectives({ ...clientConfig, code }) - const ssr = compileDirectives({ ...serverConfig, code }) + const ssr = compileDirectives({ ...ssrConfig, code }) const splitFiles = Object.entries(ssr.directiveFnsById) .map(([_fnId, directiveFn]) => { - return `// ${directiveFn.functionName}\n\n${ + return `// ${directiveFn.functionId}\n\n${ compileDirectives({ ...serverConfig, code, - filename: directiveFn.splitFileId, + filename: directiveFn.splitFilename, }).compiledResult.code }` }) @@ -663,9 +730,8 @@ describe('server function compilation', () => { `) expect(ssr.compiledResult.code).toMatchInlineSnapshot(` - "import { createServerRpc } from "my-rpc-lib-server"; - const iife_1 = createServerRpc({ - fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--iife").then(module => module.default(...args)), + "import { createSsrRpc } from "my-rpc-lib-server"; + const iife_1 = createSsrRpc({ filename: "test.ts", functionId: "test--iife" }); @@ -673,11 +739,16 @@ describe('server function compilation', () => { `) expect(splitFiles).toMatchInlineSnapshot(` - "// iife + "// test--iife - const iife_1 = function () { - return 'hello'; - }; + import { createServerRpc } from "my-rpc-lib-server"; + const iife_1 = createServerRpc({ + fn: function () { + return 'hello'; + }, + filename: "test.ts", + functionId: "test--iife" + }); export default iife_1;" `) }) @@ -696,15 +767,15 @@ describe('server function compilation', () => { ` const client = compileDirectives({ ...clientConfig, code }) - const ssr = compileDirectives({ ...serverConfig, code }) + const ssr = compileDirectives({ ...ssrConfig, code }) const splitFiles = Object.entries(ssr.directiveFnsById) .map(([_fnId, directiveFn]) => { - return `// ${directiveFn.functionName}\n\n${ + return `// ${directiveFn.functionId}\n\n${ compileDirectives({ ...serverConfig, code, - filename: directiveFn.splitFileId, + filename: directiveFn.splitFilename, }).compiledResult.code }` }) @@ -725,15 +796,13 @@ describe('server function compilation', () => { `) expect(ssr.compiledResult.code).toMatchInlineSnapshot(` - "import { createServerRpc } from "my-rpc-lib-server"; - const outer_useServer = createServerRpc({ - fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--outer_useServer").then(module => module.default(...args)), + "import { createSsrRpc } from "my-rpc-lib-server"; + const outer_useServer = createSsrRpc({ filename: "test.ts", functionId: "test--outer_useServer" }); outer(outer_useServer); - const outer_useServer_1 = createServerRpc({ - fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--outer_useServer_1").then(module => module.default(...args)), + const outer_useServer_1 = createSsrRpc({ filename: "test.ts", functionId: "test--outer_useServer_1" }); @@ -741,15 +810,19 @@ describe('server function compilation', () => { `) expect(splitFiles).toMatchInlineSnapshot(` - "// outer_useServer + "// test--outer_useServer import { createServerRpc } from "my-rpc-lib-server"; - const outer_useServer = function useServer() { - return 'hello'; - }; + const outer_useServer = createServerRpc({ + fn: function useServer() { + return 'hello'; + }, + filename: "test.ts", + functionId: "test--outer_useServer" + }); outer(outer_useServer); const outer_useServer_1 = createServerRpc({ - fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--outer_useServer_1").then(module => module.default(...args)), + fn: (...args) => devImport("test.ts?tsr-directive-use-server-split=test--outer_useServer_1").then(module => module.default(...args)), filename: "test.ts", functionId: "test--outer_useServer_1" }); @@ -757,18 +830,22 @@ describe('server function compilation', () => { export default outer_useServer; - // outer_useServer + // test--outer_useServer_1 import { createServerRpc } from "my-rpc-lib-server"; const outer_useServer = createServerRpc({ - fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--outer_useServer").then(module => module.default(...args)), + fn: (...args) => devImport("test.ts?tsr-directive-use-server-split=test--outer_useServer").then(module => module.default(...args)), filename: "test.ts", functionId: "test--outer_useServer" }); outer(outer_useServer); - const outer_useServer_1 = function useServer() { - return 'hello'; - }; + const outer_useServer_1 = createServerRpc({ + fn: function useServer() { + return 'hello'; + }, + filename: "test.ts", + functionId: "test--outer_useServer_1" + }); outer(outer_useServer_1); export default outer_useServer_1;" `) @@ -796,14 +873,14 @@ describe('server function compilation', () => { ` const client = compileDirectives({ ...clientConfig, code }) - const ssr = compileDirectives({ ...serverConfig, code }) + const ssr = compileDirectives({ ...ssrConfig, code }) const splitFiles = Object.entries(ssr.directiveFnsById) .map(([_fnId, directive]) => { return `// ${directive.functionName}\n\n${ compileDirectives({ ...serverConfig, code, - filename: directive.splitFileId, + filename: directive.splitFilename, }).compiledResult.code }` }) @@ -827,15 +904,13 @@ describe('server function compilation', () => { expect(ssr.compiledResult.code).toMatchInlineSnapshot(` "'use server'; - import { createServerRpc } from "my-rpc-lib-server"; - const useServer_1 = createServerRpc({ - fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--useServer").then(module => module.default(...args)), + import { createSsrRpc } from "my-rpc-lib-server"; + const useServer_1 = createSsrRpc({ filename: "test.ts", functionId: "test--useServer" }); export const useServer = useServer_1; - const defaultExport_1 = createServerRpc({ - fn: (...args) => import("test.ts?tsr-directive-use-server-split=test--defaultExport").then(module => module.default(...args)), + const defaultExport_1 = createSsrRpc({ filename: "test.ts", functionId: "test--defaultExport" }); @@ -846,9 +921,14 @@ describe('server function compilation', () => { 'use server'; - const useServer_1 = function useServer() { - return usedInUseServer(); - }; + import { createServerRpc } from "my-rpc-lib-server"; + const useServer_1 = createServerRpc({ + fn: function useServer() { + return usedInUseServer(); + }, + filename: "test.ts", + functionId: "test--useServer" + }); function usedInUseServer() { return 'hello'; } @@ -859,10 +939,145 @@ describe('server function compilation', () => { 'use server'; - const defaultExport_1 = function defaultExport() { - return 'hello'; - }; + import { createServerRpc } from "my-rpc-lib-server"; + const defaultExport_1 = createServerRpc({ + fn: function defaultExport() { + return 'hello'; + }, + filename: "test.ts", + functionId: "test--defaultExport" + }); export default defaultExport_1;" `) }) + + test('createServerFn with identifier', () => { + // The following code is the client output of the tanstack-start-vite-plugin + // that compiles `createServerFn` calls to automatically add the `use server` + // directive in the right places. + const clientOrSsrCode = `import { createServerFn } from '@tanstack/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-vite-plugin + // that compiles `createServerFn` calls to automatically add the `use server` + // directive in the right places. + const serverCode = `import { createServerFn } from '@tanstack/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 splitFiles = Object.entries(ssr.directiveFnsById) + .map(([_fnId, directiveFn]) => { + return `// ${directiveFn.functionId}\n\n${ + compileDirectives({ + ...serverConfig, + code: serverCode, + filename: directiveFn.splitFilename, + }).compiledResult.code + }` + }) + .join('\n\n\n') + + expect(client.compiledResult.code).toMatchInlineSnapshot(` + "import { createClientRpc } from "my-rpc-lib-client"; + import { createServerFn } from '@tanstack/start'; + const myServerFn_createServerFn_handler = createClientRpc({ + filename: "test.ts", + functionId: "test--myServerFn_createServerFn_handler" + }); + export const myServerFn = createServerFn().handler(myServerFn_createServerFn_handler); + const myServerFn2_createServerFn_handler = createClientRpc({ + filename: "test.ts", + functionId: "test--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/start'; + const myServerFn_createServerFn_handler = createSsrRpc({ + filename: "test.ts", + functionId: "test--myServerFn_createServerFn_handler" + }); + export const myServerFn = createServerFn().handler(myServerFn_createServerFn_handler); + const myServerFn2_createServerFn_handler = createSsrRpc({ + filename: "test.ts", + functionId: "test--myServerFn2_createServerFn_handler" + }); + export const myServerFn2 = createServerFn().handler(myServerFn2_createServerFn_handler);" + `) + expect(splitFiles).toMatchInlineSnapshot(` + "// test--myServerFn_createServerFn_handler + + import { createServerRpc } from "my-rpc-lib-server"; + import { createServerFn } from '@tanstack/start'; + const myFunc = () => { + return 'hello from the server'; + }; + const myServerFn_createServerFn_handler = createServerRpc({ + fn: opts => { + return myServerFn.__executeServer(opts); + }, + filename: "test.ts", + functionId: "test--myServerFn_createServerFn_handler" + }); + const myServerFn = createServerFn().handler(myServerFn_createServerFn_handler, myFunc); + export default myServerFn_createServerFn_handler; + + + // test--myServerFn2_createServerFn_handler + + import { createServerRpc } from "my-rpc-lib-server"; + import { createServerFn } from '@tanstack/start'; + const myFunc = () => { + return 'hello from the server'; + }; + const myServerFn_createServerFn_handler = createServerRpc({ + fn: (...args) => devImport("test.ts?tsr-directive-use-server-split=test--myServerFn_createServerFn_handler").then(module => module.default(...args)), + filename: "test.ts", + functionId: "test--myServerFn_createServerFn_handler" + }); + const myFunc2 = () => { + return myServerFn({ + data: 'hello 2 from the server' + }); + }; + const myServerFn2_createServerFn_handler = createServerRpc({ + fn: opts => { + return myServerFn2.__executeServer(opts); + }, + filename: "test.ts", + functionId: "test--myServerFn2_createServerFn_handler" + }); + const myServerFn = createServerFn().handler(myServerFn_createServerFn_handler, myFunc); + const myServerFn2 = createServerFn().handler(myServerFn2_createServerFn_handler, myFunc2); + export default myServerFn2_createServerFn_handler;" + `) + }) }) diff --git a/packages/start-vite-plugin/src/ast.ts b/packages/start-vite-plugin/src/ast.ts index 4f1cb2f04e..99333fa6c4 100644 --- a/packages/start-vite-plugin/src/ast.ts +++ b/packages/start-vite-plugin/src/ast.ts @@ -4,7 +4,7 @@ export type ParseAstOptions = { code: string filename: string root: string - env: 'server' | 'client' + env: 'server' | 'client' | 'ssr' } export function parseAst(opts: ParseAstOptions) { diff --git a/packages/start-vite-plugin/src/compilers.ts b/packages/start-vite-plugin/src/compilers.ts index 41b54b8ac2..9024436413 100644 --- a/packages/start-vite-plugin/src/compilers.ts +++ b/packages/start-vite-plugin/src/compilers.ts @@ -318,7 +318,22 @@ function handleCreateServerFnCallExpression( // })(optsOut) // }) - removeUseServerDirective(handlerFnPath) + // 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' || opts.env === 'ssr') { + // 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( @@ -344,46 +359,10 @@ function handleCreateServerFnCallExpression( } } -function removeUseServerDirective(path: babel.NodePath) { - path.traverse({ - Directive(path) { - if (path.node.value.value === 'use server') { - path.remove() - } - }, - }) -} - function handleCreateMiddlewareCallExpression( path: babel.NodePath, opts: ParseAstOptions, ) { - // const firstArg = path.node.arguments[0] - - // if (!t.isObjectExpression(firstArg)) { - // throw new Error( - // 'createMiddleware must be called with an object of options!', - // ) - // } - - // const idProperty = firstArg.properties.find((prop) => { - // return ( - // t.isObjectProperty(prop) && - // t.isIdentifier(prop.key) && - // prop.key.name === 'id' - // ) - // }) - - // if ( - // !idProperty || - // !t.isObjectProperty(idProperty) || - // !t.isStringLiteral(idProperty.value) - // ) { - // throw new Error( - // 'createMiddleware must be called with an "id" property!', - // ) - // } - const rootCallExpression = getRootCallExpression(path) if (debug) @@ -392,106 +371,6 @@ function handleCreateMiddlewareCallExpression( rootCallExpression.toString(), ) - // Check if the call is assigned to a variable - // if (!rootCallExpression.parentPath.isVariableDeclarator()) { - // TODO: move this logic out to eslint or something like - // the router generator code that can do autofixes on save. - - // // If not assigned to a variable, wrap the call in a variable declaration - // const variableDeclaration = t.variableDeclaration('const', [ - // t.variableDeclarator(t.identifier(middlewareName), path.node), - // ]) - - // // The parent could be an expression statement, if it is, we need to replace - // // it with the variable declaration - // if (path.parentPath.isExpressionStatement()) { - // path.parentPath.replaceWith(variableDeclaration) - // } else { - // // If the parent is not an expression statement, then it is a statement - // // that is not an expression, like a variable declaration or a return statement. - // // In this case, we need to insert the variable declaration before the statement - // path.parentPath.insertBefore(variableDeclaration) - // } - - // // Now we need to export it. Just add an export statement - // // to the program body - // path.findParent((parentPath) => { - // if (parentPath.isProgram()) { - // parentPath.node.body.push( - // t.exportNamedDeclaration(null, [ - // t.exportSpecifier( - // t.identifier(middlewareName), - // t.identifier(middlewareName), - // ), - // ]), - // ) - // } - // return false - // }) - - // throw new Error( - // 'createMiddleware must be assigned to a variable and exported!', - // ) - // } - - // const variableDeclarator = rootCallExpression.parentPath.node - // const existingVariableName = (variableDeclarator.id as t.Identifier).name - - // const program = rootCallExpression.findParent((parentPath) => { - // return parentPath.isProgram() - // }) as babel.NodePath - - // let isExported = false as boolean - - // program.traverse({ - // ExportNamedDeclaration: (path) => { - // if ( - // path.isExportNamedDeclaration() && - // path.node.declaration && - // t.isVariableDeclaration(path.node.declaration) && - // path.node.declaration.declarations.some((decl) => { - // return ( - // t.isVariableDeclarator(decl) && - // t.isIdentifier(decl.id) && - // decl.id.name === existingVariableName - // ) - // }) - // ) { - // isExported = true - // } - // }, - // }) - - // If not exported, export it - // if (!isExported) { - // TODO: move this logic out to eslint or something like - // the router generator code that can do autofixes on save. - - // path.parentPath.parentPath.insertAfter( - // t.exportNamedDeclaration(null, [ - // t.exportSpecifier( - // t.identifier(existingVariableName), - // t.identifier(existingVariableName), - // ), - // ]), - // ) - - // throw new Error( - // 'createMiddleware must be exported as a named export!', - // ) - // } - - // The function is the 'fn' property of the object passed to createMiddleware - - // const firstArg = path.node.arguments[0] - // if (t.isObjectExpression(firstArg)) { - // // Was called with some options - // } - - // Traverse the member expression and find the call expressions for - // the validator, handler, and middleware methods. Check to make sure they - // are children of the createMiddleware call expression. - const callExpressionPaths = { middleware: null as babel.NodePath | null, validator: null as babel.NodePath | null, @@ -526,8 +405,8 @@ function handleCreateMiddlewareCallExpression( ) } - // If we're on the client, remove the validator call expression - if (opts.env === 'client') { + // If we're on the client or ssr, remove the validator call expression + if (opts.env === 'client' || opts.env === 'ssr') { if (t.isMemberExpression(callExpressionPaths.validator.node.callee)) { callExpressionPaths.validator.replaceWith( callExpressionPaths.validator.node.callee.object, @@ -544,9 +423,9 @@ function handleCreateMiddlewareCallExpression( throw new Error('createMiddleware must be called with a "use" property!') } - // If we're on the client, remove the use call expression + // If we're on the client, remove the server call expression - if (opts.env === 'client') { + if (opts.env === 'client' || opts.env === 'ssr') { if (t.isMemberExpression(callExpressionPaths.server.node.callee)) { callExpressionPaths.server.replaceWith( callExpressionPaths.server.node.callee.object, @@ -563,7 +442,12 @@ function buildEnvOnlyCallExpressionHandler(env: 'client' | 'server') { if (debug) console.info(`Handling ${env}Only call expression:`, path.toString()) - if (opts.env === env) { + const isEnvMatch = + env === 'client' + ? opts.env === 'client' + : opts.env === 'server' || opts.env === 'ssr' + + if (isEnvMatch) { // extract the inner function from the call expression const innerInputExpression = path.node.arguments[0] @@ -648,7 +532,9 @@ function handleCreateIsomorphicFnCallExpression( ) } - const envCallExpression = callExpressionPaths[opts.env] + const resolvedEnv = opts.env === 'ssr' ? 'client' : opts.env + + const envCallExpression = callExpressionPaths[resolvedEnv] if (!envCallExpression) { // if we don't have an implementation for this environment, default to a no-op @@ -662,7 +548,7 @@ function handleCreateIsomorphicFnCallExpression( if (!t.isExpression(innerInputExpression)) { throw new Error( - `createIsomorphicFn().${opts.env}(func) must be called with a function!`, + `createIsomorphicFn().${resolvedEnv}(func) must be called with a function!`, ) } diff --git a/packages/start-vite-plugin/src/index.ts b/packages/start-vite-plugin/src/index.ts index bd40710f2a..1245d7d082 100644 --- a/packages/start-vite-plugin/src/index.ts +++ b/packages/start-vite-plugin/src/index.ts @@ -7,7 +7,7 @@ import type { Plugin } from 'vite' const debug = Boolean(process.env.TSR_VITE_DEBUG) export type TanStackStartViteOptions = { - env: 'server' | 'client' + env: 'server' | 'ssr' | 'client' } const transformFuncs = [ @@ -22,7 +22,7 @@ const eitherFuncRegex = new RegExp( `(function ${transformFuncs.join('|function ')})`, ) -export function TanStackStartViteServerFn( +export function TanStackStartVitePlugin( opts: TanStackStartViteOptions, ): Plugin { let ROOT: string = process.cwd() diff --git a/packages/start-vite-plugin/tests/createServerFn/createServerFn.test.ts b/packages/start-vite-plugin/tests/createServerFn/createServerFn.test.ts index 19b86a0c01..d46d1babb7 100644 --- a/packages/start-vite-plugin/tests/createServerFn/createServerFn.test.ts +++ b/packages/start-vite-plugin/tests/createServerFn/createServerFn.test.ts @@ -59,4 +59,48 @@ describe('createServerFn compiles correctly', async () => { }) }).toThrowError() }) + + test('should work with identifiers of functions', () => { + const code = ` + import { createServerFn } from '@tanstack/start' + const myFunc = () => { + return 'hello from the server' + } + const myServerFn = createServerFn().handler(myFunc)` + + const compiledResultClient = compileStartOutput({ + root: '/test', + filename: 'test.ts', + code, + env: 'client', + }) + + const compiledResultServer = compileStartOutput({ + root: '/test', + filename: 'test.ts', + code, + env: 'server', + }) + + expect(compiledResultClient.code).toMatchInlineSnapshot(` + "import { createServerFn } from '@tanstack/start'; + const myServerFn = createServerFn().handler(opts => { + "use server"; + + return myServerFn.__executeServer(opts); + });" + `) + + expect(compiledResultServer.code).toMatchInlineSnapshot(` + "import { createServerFn } from '@tanstack/start'; + const myFunc = () => { + return 'hello from the server'; + }; + const myServerFn = createServerFn().handler(opts => { + "use server"; + + return myServerFn.__executeServer(opts); + }, myFunc);" + `) + }) }) diff --git a/packages/start-vite-plugin/tests/createServerFn/snapshots/client/createServerFnDestructured.tsx b/packages/start-vite-plugin/tests/createServerFn/snapshots/client/createServerFnDestructured.tsx index be1b58b186..752850e42e 100644 --- a/packages/start-vite-plugin/tests/createServerFn/snapshots/client/createServerFnDestructured.tsx +++ b/packages/start-vite-plugin/tests/createServerFn/snapshots/client/createServerFnDestructured.tsx @@ -35,11 +35,6 @@ export const withVariable = createServerFn({ return withVariable.__executeServer(opts); }); -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)); diff --git a/packages/start-vite-plugin/tests/createServerFn/snapshots/client/createServerFnDestructuredRename.tsx b/packages/start-vite-plugin/tests/createServerFn/snapshots/client/createServerFnDestructuredRename.tsx index f51f0ade21..55710f4321 100644 --- a/packages/start-vite-plugin/tests/createServerFn/snapshots/client/createServerFnDestructuredRename.tsx +++ b/packages/start-vite-plugin/tests/createServerFn/snapshots/client/createServerFnDestructuredRename.tsx @@ -21,11 +21,6 @@ export const withVariable = serverFn({ return withVariable.__executeServer(opts); }); -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)); diff --git a/packages/start-vite-plugin/tests/createServerFn/snapshots/client/createServerFnStarImport.tsx b/packages/start-vite-plugin/tests/createServerFn/snapshots/client/createServerFnStarImport.tsx index 750a52532a..b33435a9fe 100644 --- a/packages/start-vite-plugin/tests/createServerFn/snapshots/client/createServerFnStarImport.tsx +++ b/packages/start-vite-plugin/tests/createServerFn/snapshots/client/createServerFnStarImport.tsx @@ -21,11 +21,6 @@ export const withVariable = TanStackStart.createServerFn({ return withVariable.__executeServer(opts); }); -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)); diff --git a/packages/start-vite-plugin/tests/createServerFn/snapshots/server/createServerFnStarImport.tsx b/packages/start-vite-plugin/tests/createServerFn/snapshots/server/createServerFnStarImport.tsx index 66088cb181..3f79037fe7 100644 --- a/packages/start-vite-plugin/tests/createServerFn/snapshots/server/createServerFnStarImport.tsx +++ b/packages/start-vite-plugin/tests/createServerFn/snapshots/server/createServerFnStarImport.tsx @@ -7,6 +7,8 @@ export const withUseServer = TanStackStart.createServerFn({ return withUseServer.__executeServer(opts); }, 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)); diff --git a/packages/start/src/client-runtime/fetcher.tsx b/packages/start/src/client-runtime/fetcher.tsx index 70eea4b9ae..5cb729514e 100644 --- a/packages/start/src/client-runtime/fetcher.tsx +++ b/packages/start/src/client-runtime/fetcher.tsx @@ -43,7 +43,13 @@ export async function fetcher( }), }) - if (encodedPayload) base += `&${encodedPayload}` + if (encodedPayload) { + if (base.includes('?')) { + base += `&${encodedPayload}` + } else { + base += `?${encodedPayload}` + } + } } // Create the request diff --git a/packages/start/src/client-runtime/getBaseUrl.tsx b/packages/start/src/client-runtime/getBaseUrl.tsx index dfcf7947ed..99cc564263 100644 --- a/packages/start/src/client-runtime/getBaseUrl.tsx +++ b/packages/start/src/client-runtime/getBaseUrl.tsx @@ -9,6 +9,6 @@ function sanitizeBase(base: string | undefined) { return base.replace(/^\/|\/$/g, '') } -export function getBaseUrl(base: string | undefined, id: string, name: string) { - return `${base}/${sanitizeBase(process.env.TSS_SERVER_BASE)}/?_serverFnId=${encodeURI(id)}&_serverFnName=${encodeURI(name)}` +export function getBaseUrl(base: string | undefined, functionId: string) { + return `${base}/${sanitizeBase(process.env.TSS_SERVER_BASE)}/${encodeURI(functionId)}` } diff --git a/packages/start/src/client-runtime/index.tsx b/packages/start/src/client-runtime/index.tsx index 59b4434d4d..680bde58be 100644 --- a/packages/start/src/client-runtime/index.tsx +++ b/packages/start/src/client-runtime/index.tsx @@ -1,20 +1,15 @@ import { fetcher } from './fetcher' import { getBaseUrl } from './getBaseUrl' -import type { CreateRpcFn } from '@tanstack/directive-functions-plugin' +import type { CreateClientRpcFn } from '@tanstack/directive-functions-plugin' -export const createClientRpc: CreateRpcFn = (opts) => { - const base = getBaseUrl( - window.location.origin, - opts.filename, - opts.functionId, - ) +export const createClientRpc: CreateClientRpcFn = (functionId) => { + const base = getBaseUrl(window.location.origin, functionId) const fn = (...args: Array) => fetcher(base, args, fetch) return Object.assign(fn, { url: base, - filename: opts.filename, - functionId: opts.functionId, + functionId, }) } diff --git a/packages/start/src/client/createServerFn.ts b/packages/start/src/client/createServerFn.ts index 6408e783cd..de490aec65 100644 --- a/packages/start/src/client/createServerFn.ts +++ b/packages/start/src/client/createServerFn.ts @@ -221,7 +221,7 @@ export function createServerFn< invariant( extractedFn.url, - `createServerFn must be called with a function that is marked with the 'use server' pragma. Are you using the @tanstack/start-vite-plugin ?`, + `createServerFn must be called with a function that has a 'url' property. Are you using the @tanstack/start-vite-plugin properly?`, ) const resolvedMiddleware = [ diff --git a/packages/start/src/config/index.ts b/packages/start/src/config/index.ts index c6e9c80d0d..beb35e419e 100644 --- a/packages/start/src/config/index.ts +++ b/packages/start/src/config/index.ts @@ -7,13 +7,14 @@ import { resolve } from 'import-meta-resolve' import { TanStackRouterVite } from '@tanstack/router-plugin/vite' import { TanStackStartViteDeadCodeElimination, - TanStackStartViteServerFn, + TanStackStartVitePlugin, } from '@tanstack/start-vite-plugin' import { getConfig } from '@tanstack/router-generator' import { createApp } from 'vinxi' import { config } from 'vinxi/plugins/config' // // @ts-expect-error // import { serverComponents } from '@vinxi/server-components/plugin' +import { createTanStackServerFnPlugin } from '@tanstack/directive-functions-plugin' import { tanstackStartVinxiFileRouter } from './vinxi-file-router.js' import { checkDeploymentPresetInput, @@ -119,6 +120,8 @@ export function defineConfig( const apiEntryExists = existsSync(apiEntry) + const TanStackServerFnsPlugin = createTanStackServerFnPlugin() + let vinxiApp = createApp({ server: { ...serverOptions, @@ -166,12 +169,7 @@ export function defineConfig( }), ...(viteConfig.plugins || []), ...(clientViteConfig.plugins || []), - Directive({ - getRuntimeCode: (opts) => - `import { createClientRpc } from '@tanstack/start/client-runtime'`, - replacer: (opts) => - `createClientRpc('${opts.filename}', '${opts.functionId}')`, - }), + TanStackServerFnsPlugin.client, viteReact(opts.react), // TODO: RSCS - enable this // serverComponents.client(), @@ -210,12 +208,7 @@ export function defineConfig( }), ...(getUserViteConfig(opts.vite).plugins || []), ...(getUserViteConfig(opts.routers?.ssr?.vite).plugins || []), - TanStackServerFnPluginServer({ - getRuntimeCode: (opts) => - `import { createServerRpc } from '@tanstack/start/ssr-runtime'`, - replacer: (opts) => - `createSsrRpc('${opts.filename}', '${opts.functionId}')`, - }), + TanStackServerFnsPlugin.ssr, config('start-ssr', { ssr: { external: ['@vinxi/react-server-dom/client'], @@ -256,16 +249,11 @@ export function defineConfig( ...injectDefineEnv('TSS_API_BASE', apiBase), }, }), - TanStackServerFnPluginServer({ - getRuntimeCode: (opts) => - `import { createServerRpc } from '@tanstack/start/server-runtime'`, - replacer: (opts) => - `createServerRpc('${opts.filename}', '${opts.functionId}')`, - // TODO: RSCS - remove this - // resolve: { - // conditions: [], - // }, - }), + TanStackServerFnsPlugin.server, + // TODO: RSCS - remove this + // resolve: { + // conditions: [], + // }, // TODO: RSCs - add this // serverComponents.serverActions({ // resolve: { @@ -401,8 +389,8 @@ function withStartPlugins(opts: TanStackStartOutputConfig, router: RouterType) { ...tsrConfig.experimental, }, }), - TanStackStartViteServerFn({ - env: router === 'client' ? 'client' : 'server', + TanStackStartVitePlugin({ + env: router === 'server' ? 'server' : 'client', }), ], [ diff --git a/packages/start/src/server-handler/index.tsx b/packages/start/src/server-handler/index.tsx index 51c9881985..54151ba682 100644 --- a/packages/start/src/server-handler/index.tsx +++ b/packages/start/src/server-handler/index.tsx @@ -1,4 +1,6 @@ /// +import { join } from 'node:path' +import { pathToFileURL } from 'node:url' import { defaultTransformer, isNotFound, @@ -11,7 +13,8 @@ import { getResponseStatus, toWebRequest, } from 'vinxi/http' -import { getManifest } from 'vinxi/manifest' +// @ts-expect-error +import serverFnManifest from 'tsr:server-fn-manifest' import type { H3Event } from 'vinxi/server' export default eventHandler(handleServerAction) @@ -23,28 +26,44 @@ async function handleServerAction(event: H3Event) { export async function handleServerRequest(request: Request, _event?: H3Event) { const method = request.method const url = new URL(request.url, 'http://localhost:3000') + // extract the serverFnId from the url as host/_server/:serverFnId + // Define a regex to match the path and extract the :thing part + const regex = /\/_server\/([^/?#]+)/ + + // Execute the regex + const match = url.pathname.match(regex) + const serverFnId = match ? match[1] : null const search = Object.fromEntries(url.searchParams.entries()) as { - _serverFnId?: string - _serverFnName?: string payload?: any } - const serverFnId = search._serverFnId - const serverFnName = search._serverFnName + invariant(typeof serverFnId === 'string', 'Invalid server action') - if (!serverFnId || !serverFnName) { - throw new Error('Invalid request') - } + const serverFnInfo = serverFnManifest[serverFnId] - invariant(typeof serverFnId === 'string', 'Invalid server action') + invariant(serverFnInfo, 'Server function not found') if (process.env.NODE_ENV === 'development') - console.info(`ServerFn Request: ${serverFnId} - ${serverFnName}`) - if (process.env.NODE_ENV === 'development') console.info() + console.info(`\nServerFn Request: ${serverFnId}`) + + let action: Function + if (process.env.NODE_ENV === 'development') { + action = (await (globalThis as any).app + .getRouter('server') + .internals.devServer.ssrLoadModule(serverFnInfo.splitFilename) + .then((d: any) => d.default)) as Function + } else { + const router = (globalThis as any).app.getRouter('server') + const filePath = join( + router.outDir, + router.base, + serverFnInfo.chunkName + '.mjs', + ) + const url = pathToFileURL(filePath).toString() + action = (await import(url).then((d) => d.default)) as Function + } - const action = (await getManifest('server').chunks[serverFnId]?.import())?.[ - serverFnName - ] as Function + invariant(action, 'Server function not found') const response = await (async () => { try { diff --git a/packages/start/src/server-runtime/index.tsx b/packages/start/src/server-runtime/index.tsx index 1b222d6ec9..4434abec24 100644 --- a/packages/start/src/server-runtime/index.tsx +++ b/packages/start/src/server-runtime/index.tsx @@ -1,13 +1,16 @@ -// import { getBaseUrl } from '../client-runtime/getBaseUrl' -import type { CreateRpcFn } from '@tanstack/directive-functions-plugin' +import { getBaseUrl } from '../client-runtime/getBaseUrl' +import type { CreateServerRpcFn } from '@tanstack/directive-functions-plugin' -export const createServerRpc: CreateRpcFn = (opts) => { - // const functionUrl = getBaseUrl('http://localhost:3000', id, name) - const functionUrl = 'https://localhost:3000' +const fakeHost = 'http://localhost:3000' - return Object.assign(opts.fn, { - url: functionUrl, - filename: opts.filename, - functionId: opts.functionId, +export const createServerRpc: CreateServerRpcFn = ( + functionId, + splitImportFn, +) => { + const functionUrl = getBaseUrl(fakeHost, functionId) + + return Object.assign(splitImportFn, { + url: functionUrl.replace(fakeHost, ''), + functionId, }) } diff --git a/packages/start/src/ssr-runtime/index.tsx b/packages/start/src/ssr-runtime/index.tsx index 4a148fdda2..88688d9446 100644 --- a/packages/start/src/ssr-runtime/index.tsx +++ b/packages/start/src/ssr-runtime/index.tsx @@ -4,7 +4,7 @@ import invariant from 'tiny-invariant' import { fetcher } from '../client-runtime/fetcher' import { getBaseUrl } from '../client-runtime/getBaseUrl' import { handleServerRequest } from '../server-handler/index' -import type { CreateRpcFn } from '@tanstack/directive-functions-plugin' +import type { CreateSsrRpcFn } from '@tanstack/directive-functions-plugin' export function createIncomingMessage( url: string, @@ -78,10 +78,10 @@ export function createIncomingMessage( const fakeHost = 'http://localhost:3000' -export const createSsrRpc: CreateRpcFn = (opts) => { - const functionUrl = getBaseUrl(fakeHost, opts.filename, opts.functionId) +export const createSsrRpc: CreateSsrRpcFn = (functionId) => { + const functionUrl = getBaseUrl(fakeHost, functionId) - const proxyFn = (...args: Array) => { + const ssrFn = (...args: Array) => { invariant( args.length === 1, 'Server functions can only accept a single argument', @@ -149,9 +149,8 @@ export const createSsrRpc: CreateRpcFn = (opts) => { }) } - return Object.assign(proxyFn, { + return Object.assign(ssrFn, { url: functionUrl.replace(fakeHost, ''), - filename: opts.filename, - functionId: opts.functionId, + functionId, }) } diff --git a/packages/start/vite.config.ts b/packages/start/vite.config.ts index 7f3b092680..196e0fc33a 100644 --- a/packages/start/vite.config.ts +++ b/packages/start/vite.config.ts @@ -16,7 +16,7 @@ const config = defineConfig({ export default mergeConfig( config, tanstackViteConfig({ - externalDeps: ['@tanstack/start/router-manifest'], + externalDeps: ['@tanstack/start/router-manifest', 'tsr:server-fn-manifest'], entry: [ './src/client/index.tsx', './src/server/index.tsx', From 97af6f7366dd9096bd4a3fa72bc09f01699e4862 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Sun, 22 Dec 2024 22:22:35 -0700 Subject: [PATCH 12/38] move serverFnFetcher to start client --- .../server-functions-vite-plugin/src/index.ts | 3 +-- packages/start/src/client-runtime/index.tsx | 6 +++--- packages/start/src/client/index.tsx | 1 + .../fetcher.tsx => client/serverFnFetcher.tsx} | 16 ++++++++-------- packages/start/src/ssr-runtime/index.tsx | 4 ++-- 5 files changed, 15 insertions(+), 15 deletions(-) rename packages/start/src/{client-runtime/fetcher.tsx => client/serverFnFetcher.tsx} (92%) diff --git a/packages/server-functions-vite-plugin/src/index.ts b/packages/server-functions-vite-plugin/src/index.ts index e6f1f6cd69..7f42dd8db7 100644 --- a/packages/server-functions-vite-plugin/src/index.ts +++ b/packages/server-functions-vite-plugin/src/index.ts @@ -91,7 +91,6 @@ export function createTanStackServerFnPlugin(_opts?: {}): { }), // Now that we have the directiveFnsById, we need to create a new // virtual module that can be used to import that manifest - readManifestPlugin(), { name: 'tanstack-start-server-fn-vite-plugin-build-client', generateBundle() { @@ -108,6 +107,7 @@ export function createTanStackServerFnPlugin(_opts?: {}): { }, ], ssr: [ + readManifestPlugin(), // The SSR plugin is used to compile the server directives TanStackDirectiveFunctionsPlugin({ directive: 'use server', @@ -125,7 +125,6 @@ export function createTanStackServerFnPlugin(_opts?: {}): { Object.assign(directiveFnsById, d) }, }), - readManifestPlugin(), ], server: [ readManifestPlugin(), diff --git a/packages/start/src/client-runtime/index.tsx b/packages/start/src/client-runtime/index.tsx index 680bde58be..c4bf7b8536 100644 --- a/packages/start/src/client-runtime/index.tsx +++ b/packages/start/src/client-runtime/index.tsx @@ -1,11 +1,11 @@ -import { fetcher } from './fetcher' +import { serverFnFetcher } from '../client' import { getBaseUrl } from './getBaseUrl' import type { CreateClientRpcFn } from '@tanstack/directive-functions-plugin' export const createClientRpc: CreateClientRpcFn = (functionId) => { const base = getBaseUrl(window.location.origin, functionId) - const fn = (...args: Array) => fetcher(base, args, fetch) + const fn = (...args: Array) => serverFnFetcher(base, args, fetch) return Object.assign(fn, { url: base, @@ -13,4 +13,4 @@ export const createClientRpc: CreateClientRpcFn = (functionId) => { }) } -export { fetcher } +export { serverFnFetcher } diff --git a/packages/start/src/client/index.tsx b/packages/start/src/client/index.tsx index 08f8b6f2df..12b6942ca5 100644 --- a/packages/start/src/client/index.tsx +++ b/packages/start/src/client/index.tsx @@ -50,3 +50,4 @@ export { StartClient } from './StartClient' export { mergeHeaders } from './headers' export { renderRsc } from './renderRSC' export { useServerFn } from './useServerFn' +export { serverFnFetcher } from './serverFnFetcher' diff --git a/packages/start/src/client-runtime/fetcher.tsx b/packages/start/src/client/serverFnFetcher.tsx similarity index 92% rename from packages/start/src/client-runtime/fetcher.tsx rename to packages/start/src/client/serverFnFetcher.tsx index 5cb729514e..7aac71c7f1 100644 --- a/packages/start/src/client-runtime/fetcher.tsx +++ b/packages/start/src/client/serverFnFetcher.tsx @@ -5,10 +5,10 @@ import { isPlainObject, isRedirect, } from '@tanstack/react-router' -import type { MiddlewareOptions } from '../client/createServerFn' +import type { MiddlewareOptions } from './createServerFn' -export async function fetcher( - base: string, +export async function serverFnFetcher( + url: string, args: Array, handler: (request: Request) => Promise, ) { @@ -44,16 +44,16 @@ export async function fetcher( }) if (encodedPayload) { - if (base.includes('?')) { - base += `&${encodedPayload}` + if (url.includes('?')) { + url += `&${encodedPayload}` } else { - base += `?${encodedPayload}` + url += `?${encodedPayload}` } } } // Create the request - const request = new Request(base, { + const request = new Request(url, { method: first.method, headers, ...getFetcherRequestOptions(first), @@ -83,7 +83,7 @@ export async function fetcher( // If not a custom fetcher, just proxy the arguments // through as a POST request - const request = new Request(base, { + const request = new Request(url, { method: 'POST', headers: { Accept: 'application/json', diff --git a/packages/start/src/ssr-runtime/index.tsx b/packages/start/src/ssr-runtime/index.tsx index 88688d9446..5547b756ee 100644 --- a/packages/start/src/ssr-runtime/index.tsx +++ b/packages/start/src/ssr-runtime/index.tsx @@ -1,7 +1,7 @@ import { Readable } from 'node:stream' import { getEvent, getRequestHeaders } from 'vinxi/http' import invariant from 'tiny-invariant' -import { fetcher } from '../client-runtime/fetcher' +import { serverFnFetcher } from '../client' import { getBaseUrl } from '../client-runtime/getBaseUrl' import { handleServerRequest } from '../server-handler/index' import type { CreateSsrRpcFn } from '@tanstack/directive-functions-plugin' @@ -87,7 +87,7 @@ export const createSsrRpc: CreateSsrRpcFn = (functionId) => { 'Server functions can only accept a single argument', ) - return fetcher(functionUrl, args, async (request) => { + return serverFnFetcher(functionUrl, args, async (request) => { const event = getEvent() const ogRequestHeaders = getRequestHeaders(event) From 08c32e907ba3381b0dd7282c946bd61284daafd8 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Mon, 23 Dec 2024 11:57:43 -0700 Subject: [PATCH 13/38] fix: no extra context, throw errors from middleware --- packages/start/src/client/createServerFn.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/start/src/client/createServerFn.ts b/packages/start/src/client/createServerFn.ts index de490aec65..651c7033b4 100644 --- a/packages/start/src/client/createServerFn.ts +++ b/packages/start/src/client/createServerFn.ts @@ -239,8 +239,11 @@ export function createServerFn< method: resolvedOptions.method, data: opts?.data as any, headers: opts?.headers, - context: Object.assign({}, extractedFn), - }).then((d) => d.result) + context: {}, + }).then((d) => { + if (d.error) throw d.error + return d.result + }) }, { // This copies over the URL, function ID and filename From c73937e0c9e907996fc2b770c3f01242f57bbb8e Mon Sep 17 00:00:00 2001 From: Youssef Benlemlih Date: Wed, 18 Dec 2024 22:19:11 +0100 Subject: [PATCH 14/38] docs: add custom link example for mantine (#3033) --- docs/framework/react/guide/custom-link.md | 27 +++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/framework/react/guide/custom-link.md b/docs/framework/react/guide/custom-link.md index 3edd86396d..5db96a6277 100644 --- a/docs/framework/react/guide/custom-link.md +++ b/docs/framework/react/guide/custom-link.md @@ -159,3 +159,30 @@ export const CustomLink: LinkComponent = (props) => { return } ``` + +### Mantine example + +```tsx +import * as React from "react"; +import { createLink, LinkComponent } from "@tanstack/react-router"; +import { Anchor, AnchorProps } from "@mantine/core"; + +interface MantineAnchorProps extends Omit { + // Add any additional props you want to pass to the anchor +} + +const MantineLinkComponent = React.forwardRef< + HTMLAnchorElement, + MantineAnchorProps +>((props, ref) => { + return ; +}); + +const CreatedLinkComponent = createLink(MantineLinkComponent); + +export const CustomLink: LinkComponent = ( + props +) => { + return ; +}; +``` \ No newline at end of file From a3f6821316d7b1de51751ee018f7fdfdf0cbb949 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 18 Dec 2024 21:20:21 +0000 Subject: [PATCH 15/38] ci: apply automated fixes --- docs/framework/react/guide/custom-link.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/framework/react/guide/custom-link.md b/docs/framework/react/guide/custom-link.md index 5db96a6277..28c9f06038 100644 --- a/docs/framework/react/guide/custom-link.md +++ b/docs/framework/react/guide/custom-link.md @@ -163,11 +163,11 @@ export const CustomLink: LinkComponent = (props) => { ### Mantine example ```tsx -import * as React from "react"; -import { createLink, LinkComponent } from "@tanstack/react-router"; -import { Anchor, AnchorProps } from "@mantine/core"; +import * as React from 'react' +import { createLink, LinkComponent } from '@tanstack/react-router' +import { Anchor, AnchorProps } from '@mantine/core' -interface MantineAnchorProps extends Omit { +interface MantineAnchorProps extends Omit { // Add any additional props you want to pass to the anchor } @@ -175,14 +175,14 @@ const MantineLinkComponent = React.forwardRef< HTMLAnchorElement, MantineAnchorProps >((props, ref) => { - return ; -}); + return +}) -const CreatedLinkComponent = createLink(MantineLinkComponent); +const CreatedLinkComponent = createLink(MantineLinkComponent) export const CustomLink: LinkComponent = ( - props + props, ) => { - return ; -}; -``` \ No newline at end of file + return +} +``` From 707c1882d21ddad5f45de43403eed38fb5e1ddd1 Mon Sep 17 00:00:00 2001 From: Manuel Schiller Date: Thu, 19 Dec 2024 03:31:35 +0100 Subject: [PATCH 16/38] fix(react-router): make sure full matches are passed into route functions (#3039) --- packages/react-router/src/router.ts | 54 ++++++++++++++++++----------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts index 33185f2499..f51c88f322 100644 --- a/packages/react-router/src/router.ts +++ b/packages/react-router/src/router.ts @@ -1224,6 +1224,16 @@ export class Router< const matches: Array = [] + const getParentContext = (parentMatch?: AnyRouteMatch) => { + const parentMatchId = parentMatch?.id + + const parentContext = !parentMatchId + ? ((this.options.context as any) ?? {}) + : (parentMatch.context ?? this.options.context ?? {}) + + return parentContext + } + matchedRoutes.forEach((route, index) => { // Take each matched route and resolve + validate its search params // This has to happen serially because each route's search params @@ -1361,17 +1371,6 @@ export class Router< } } - const headFnContent = route.options.head?.({ - matches, - match, - params: match.params, - loaderData: match.loaderData ?? undefined, - }) - - match.links = headFnContent?.links - match.scripts = headFnContent?.scripts - match.meta = headFnContent?.meta - // If it's already a success, update the headers // These may get updated again if the match is refreshed // due to being stale @@ -1389,11 +1388,7 @@ export class Router< // update the searchError if there is one match.searchError = searchError - const parentMatchId = parentMatch?.id - - const parentContext = !parentMatchId - ? ((this.options.context as any) ?? {}) - : (parentMatch.context ?? this.options.context ?? {}) + const parentContext = getParentContext(parentMatch) match.context = { ...parentContext, @@ -1401,13 +1396,23 @@ export class Router< ...match.__beforeLoadContext, } + matches.push(match) + }) + + matches.forEach((match, index) => { + const route = this.looseRoutesById[match.routeId]! + const existingMatch = this.getMatch(match.id) + // only execute `context` if we are not just building a location if (!existingMatch && opts?._buildLocation !== true) { + const parentMatch = matches[index - 1] + const parentContext = getParentContext(parentMatch) + // Update the match's context const contextFnContext: RouteContextOptions = { - deps: loaderDeps, + deps: match.loaderDeps, params: match.params, - context: match.context, + context: parentContext, location: next, navigate: (opts: any) => this.navigate({ ...opts, _fromLocation: next }), @@ -1428,10 +1433,19 @@ export class Router< } } - matches.push(match) + const headFnContent = route.options.head?.({ + matches, + match, + params: match.params, + loaderData: match.loaderData ?? undefined, + }) + + match.links = headFnContent?.links + match.scripts = headFnContent?.scripts + match.meta = headFnContent?.meta }) - return matches as any + return matches } getMatchedRoutes = (next: ParsedLocation, dest?: BuildNextOptions) => { From c48ab4f722bccc830c7c5dd7d53316f5632a907c Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Thu, 19 Dec 2024 02:32:54 +0000 Subject: [PATCH 17/38] release: v1.91.3 --- examples/react/authenticated-routes/package.json | 4 ++-- .../react/basic-default-search-params/package.json | 4 ++-- .../react/basic-file-based-codesplitting/package.json | 4 ++-- examples/react/basic-file-based/package.json | 4 ++-- .../react/basic-react-query-file-based/package.json | 4 ++-- examples/react/basic-react-query/package.json | 4 ++-- examples/react/basic-ssr-file-based/package.json | 6 +++--- .../react/basic-ssr-streaming-file-based/package.json | 6 +++--- examples/react/basic-virtual-file-based/package.json | 4 ++-- .../react/basic-virtual-inside-file-based/package.json | 4 ++-- examples/react/basic/package.json | 4 ++-- examples/react/deferred-data/package.json | 4 ++-- examples/react/kitchen-sink-file-based/package.json | 4 ++-- .../kitchen-sink-react-query-file-based/package.json | 4 ++-- examples/react/kitchen-sink-react-query/package.json | 4 ++-- examples/react/kitchen-sink/package.json | 4 ++-- examples/react/large-file-based/package.json | 4 ++-- examples/react/location-masking/package.json | 4 ++-- examples/react/navigation-blocking/package.json | 4 ++-- .../react/quickstart-esbuild-file-based/package.json | 4 ++-- examples/react/quickstart-file-based/package.json | 4 ++-- .../react/quickstart-rspack-file-based/package.json | 4 ++-- .../react/quickstart-webpack-file-based/package.json | 4 ++-- examples/react/quickstart/package.json | 4 ++-- .../react/router-monorepo-react-query/package.json | 4 ++-- .../packages/app/package.json | 2 +- .../packages/router/package.json | 2 +- examples/react/router-monorepo-simple/package.json | 4 ++-- .../router-monorepo-simple/packages/app/package.json | 2 +- .../packages/router/package.json | 2 +- examples/react/scroll-restoration/package.json | 4 ++-- examples/react/search-validator-adapters/package.json | 10 +++++----- examples/react/start-basic-auth/package.json | 6 +++--- examples/react/start-basic-react-query/package.json | 8 ++++---- examples/react/start-basic-rsc/package.json | 6 +++--- examples/react/start-basic/package.json | 6 +++--- examples/react/start-clerk-basic/package.json | 6 +++--- examples/react/start-convex-trellaux/package.json | 8 ++++---- examples/react/start-counter/package.json | 4 ++-- examples/react/start-large/package.json | 6 +++--- examples/react/start-supabase-basic/package.json | 6 +++--- examples/react/start-trellaux/package.json | 8 ++++---- examples/react/with-framer-motion/package.json | 4 ++-- examples/react/with-trpc-react-query/package.json | 4 ++-- examples/react/with-trpc/package.json | 4 ++-- packages/arktype-adapter/package.json | 2 +- packages/create-router/package.json | 2 +- packages/react-router-with-query/package.json | 2 +- packages/react-router/package.json | 2 +- packages/router-devtools/package.json | 2 +- packages/start/package.json | 2 +- packages/valibot-adapter/package.json | 2 +- packages/zod-adapter/package.json | 2 +- 53 files changed, 111 insertions(+), 111 deletions(-) diff --git a/examples/react/authenticated-routes/package.json b/examples/react/authenticated-routes/package.json index 7a33a75c30..1edb5629fa 100644 --- a/examples/react/authenticated-routes/package.json +++ b/examples/react/authenticated-routes/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "@tanstack/router-plugin": "^1.91.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/basic-default-search-params/package.json b/examples/react/basic-default-search-params/package.json index fd791fe4fe..603825423e 100644 --- a/examples/react/basic-default-search-params/package.json +++ b/examples/react/basic-default-search-params/package.json @@ -10,8 +10,8 @@ }, "dependencies": { "@tanstack/react-query": "^5.62.3", - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "redaxios": "^0.5.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/basic-file-based-codesplitting/package.json b/examples/react/basic-file-based-codesplitting/package.json index 99e2232c31..2a2c43cfc2 100644 --- a/examples/react/basic-file-based-codesplitting/package.json +++ b/examples/react/basic-file-based-codesplitting/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "@tanstack/router-plugin": "^1.91.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/basic-file-based/package.json b/examples/react/basic-file-based/package.json index df904f8b7b..a74a8194f8 100644 --- a/examples/react/basic-file-based/package.json +++ b/examples/react/basic-file-based/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "@tanstack/router-plugin": "^1.91.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/basic-react-query-file-based/package.json b/examples/react/basic-react-query-file-based/package.json index 7451763686..a1d75f0e89 100644 --- a/examples/react/basic-react-query-file-based/package.json +++ b/examples/react/basic-react-query-file-based/package.json @@ -11,8 +11,8 @@ "dependencies": { "@tanstack/react-query": "^5.62.3", "@tanstack/react-query-devtools": "^5.62.3", - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "@tanstack/router-plugin": "^1.91.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/basic-react-query/package.json b/examples/react/basic-react-query/package.json index 26313247e7..817956d94b 100644 --- a/examples/react/basic-react-query/package.json +++ b/examples/react/basic-react-query/package.json @@ -11,8 +11,8 @@ "dependencies": { "@tanstack/react-query": "^5.62.3", "@tanstack/react-query-devtools": "^5.62.3", - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "react": "^18.2.0", "react-dom": "^18.2.0", "redaxios": "^0.5.1" diff --git a/examples/react/basic-ssr-file-based/package.json b/examples/react/basic-ssr-file-based/package.json index 9802bdf98c..01a4f30898 100644 --- a/examples/react/basic-ssr-file-based/package.json +++ b/examples/react/basic-ssr-file-based/package.json @@ -11,10 +11,10 @@ "debug": "node --inspect-brk server" }, "dependencies": { - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "@tanstack/router-plugin": "^1.91.1", - "@tanstack/start": "^1.91.2", + "@tanstack/start": "^1.91.3", "get-port": "^7.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/basic-ssr-streaming-file-based/package.json b/examples/react/basic-ssr-streaming-file-based/package.json index bc4e0db977..1259dae6b1 100644 --- a/examples/react/basic-ssr-streaming-file-based/package.json +++ b/examples/react/basic-ssr-streaming-file-based/package.json @@ -11,10 +11,10 @@ "debug": "node --inspect-brk server" }, "dependencies": { - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "@tanstack/router-plugin": "^1.91.1", - "@tanstack/start": "^1.91.2", + "@tanstack/start": "^1.91.3", "get-port": "^7.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/basic-virtual-file-based/package.json b/examples/react/basic-virtual-file-based/package.json index 5ea4ae18f9..469479cae7 100644 --- a/examples/react/basic-virtual-file-based/package.json +++ b/examples/react/basic-virtual-file-based/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "@tanstack/router-plugin": "^1.91.1", "@tanstack/virtual-file-routes": "^1.87.6", "react": "^18.2.0", diff --git a/examples/react/basic-virtual-inside-file-based/package.json b/examples/react/basic-virtual-inside-file-based/package.json index dc090057ac..d0902c695d 100644 --- a/examples/react/basic-virtual-inside-file-based/package.json +++ b/examples/react/basic-virtual-inside-file-based/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "@tanstack/router-plugin": "^1.91.1", "@tanstack/virtual-file-routes": "^1.87.6", "react": "^18.2.0", diff --git a/examples/react/basic/package.json b/examples/react/basic/package.json index 59d7306eb3..a3570ae5a3 100644 --- a/examples/react/basic/package.json +++ b/examples/react/basic/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "react": "^18.2.0", "react-dom": "^18.2.0", "redaxios": "^0.5.1" diff --git a/examples/react/deferred-data/package.json b/examples/react/deferred-data/package.json index 5b857166fc..bae3fa1ffd 100644 --- a/examples/react/deferred-data/package.json +++ b/examples/react/deferred-data/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "redaxios": "^0.5.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/kitchen-sink-file-based/package.json b/examples/react/kitchen-sink-file-based/package.json index 68658c1272..25f5312334 100644 --- a/examples/react/kitchen-sink-file-based/package.json +++ b/examples/react/kitchen-sink-file-based/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "@tanstack/router-plugin": "^1.91.1", "immer": "^10.1.1", "react": "^18.2.0", diff --git a/examples/react/kitchen-sink-react-query-file-based/package.json b/examples/react/kitchen-sink-react-query-file-based/package.json index c34a454d63..4f20caaebd 100644 --- a/examples/react/kitchen-sink-react-query-file-based/package.json +++ b/examples/react/kitchen-sink-react-query-file-based/package.json @@ -11,8 +11,8 @@ "dependencies": { "@tanstack/react-query": "^5.62.3", "@tanstack/react-query-devtools": "^5.62.3", - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "@tanstack/router-plugin": "^1.91.1", "immer": "^10.1.1", "react": "^18.2.0", diff --git a/examples/react/kitchen-sink-react-query/package.json b/examples/react/kitchen-sink-react-query/package.json index 5befe2c954..963c9177a1 100644 --- a/examples/react/kitchen-sink-react-query/package.json +++ b/examples/react/kitchen-sink-react-query/package.json @@ -11,8 +11,8 @@ "dependencies": { "@tanstack/react-query": "^5.62.3", "@tanstack/react-query-devtools": "^5.62.3", - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "redaxios": "^0.5.1", "immer": "^10.1.1", "react": "^18.2.0", diff --git a/examples/react/kitchen-sink/package.json b/examples/react/kitchen-sink/package.json index efa5310b5b..40dc7ff942 100644 --- a/examples/react/kitchen-sink/package.json +++ b/examples/react/kitchen-sink/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "redaxios": "^0.5.1", "immer": "^10.1.1", "react": "^18.2.0", diff --git a/examples/react/large-file-based/package.json b/examples/react/large-file-based/package.json index 29c86107d3..5ec9fbae23 100644 --- a/examples/react/large-file-based/package.json +++ b/examples/react/large-file-based/package.json @@ -12,8 +12,8 @@ }, "dependencies": { "@tanstack/react-query": "^5.62.3", - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "@tanstack/router-plugin": "^1.91.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/location-masking/package.json b/examples/react/location-masking/package.json index 00ebc584e2..153bbec3f0 100644 --- a/examples/react/location-masking/package.json +++ b/examples/react/location-masking/package.json @@ -11,8 +11,8 @@ "dependencies": { "@radix-ui/react-dialog": "^1.1.2", "@tanstack/react-query": "^5.62.3", - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "redaxios": "^0.5.1", "react": "^18.2.0", "react-dom": "^18.2.0" diff --git a/examples/react/navigation-blocking/package.json b/examples/react/navigation-blocking/package.json index 45d31f64ea..85d0269d89 100644 --- a/examples/react/navigation-blocking/package.json +++ b/examples/react/navigation-blocking/package.json @@ -10,8 +10,8 @@ }, "dependencies": { "@tanstack/react-query": "^5.62.3", - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "redaxios": "^0.5.1", "react": "^18.2.0", "react-dom": "^18.2.0" diff --git a/examples/react/quickstart-esbuild-file-based/package.json b/examples/react/quickstart-esbuild-file-based/package.json index 9db4a3855c..9a75ed9e7d 100644 --- a/examples/react/quickstart-esbuild-file-based/package.json +++ b/examples/react/quickstart-esbuild-file-based/package.json @@ -9,8 +9,8 @@ "start": "dev" }, "dependencies": { - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "@tanstack/router-plugin": "^1.91.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/quickstart-file-based/package.json b/examples/react/quickstart-file-based/package.json index eb306b7e02..8a1cd02d0c 100644 --- a/examples/react/quickstart-file-based/package.json +++ b/examples/react/quickstart-file-based/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "@tanstack/router-plugin": "^1.91.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/quickstart-rspack-file-based/package.json b/examples/react/quickstart-rspack-file-based/package.json index 8197a09385..b365b23269 100644 --- a/examples/react/quickstart-rspack-file-based/package.json +++ b/examples/react/quickstart-rspack-file-based/package.json @@ -8,8 +8,8 @@ "preview": "rsbuild preview" }, "dependencies": { - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/examples/react/quickstart-webpack-file-based/package.json b/examples/react/quickstart-webpack-file-based/package.json index 5a222ae4fd..ef05207b76 100644 --- a/examples/react/quickstart-webpack-file-based/package.json +++ b/examples/react/quickstart-webpack-file-based/package.json @@ -7,8 +7,8 @@ "build": "webpack build && tsc --noEmit" }, "dependencies": { - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/examples/react/quickstart/package.json b/examples/react/quickstart/package.json index 49f0dd8889..948dc6e4f8 100644 --- a/examples/react/quickstart/package.json +++ b/examples/react/quickstart/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/examples/react/router-monorepo-react-query/package.json b/examples/react/router-monorepo-react-query/package.json index 8142304d58..3cddaf44b9 100644 --- a/examples/react/router-monorepo-react-query/package.json +++ b/examples/react/router-monorepo-react-query/package.json @@ -12,8 +12,8 @@ "dependencies": { "@tanstack/react-query": "^5.62.3", "@tanstack/react-query-devtools": "^5.62.3", - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "@tanstack/router-plugin": "^1.91.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/router-monorepo-react-query/packages/app/package.json b/examples/react/router-monorepo-react-query/packages/app/package.json index a16316347a..5fa21820c1 100644 --- a/examples/react/router-monorepo-react-query/packages/app/package.json +++ b/examples/react/router-monorepo-react-query/packages/app/package.json @@ -20,7 +20,7 @@ "@types/react-dom": "^18.2.18", "@vitejs/plugin-react": "^4.3.4", "typescript": "^5.7.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/router-devtools": "^1.91.3", "vite": "^6.0.3", "vite-plugin-dts": "^4.3.0" }, diff --git a/examples/react/router-monorepo-react-query/packages/router/package.json b/examples/react/router-monorepo-react-query/packages/router/package.json index 3e3e4bf450..99426797a6 100644 --- a/examples/react/router-monorepo-react-query/packages/router/package.json +++ b/examples/react/router-monorepo-react-query/packages/router/package.json @@ -10,7 +10,7 @@ "dependencies": { "@tanstack/history": "^1.90.0", "@tanstack/react-query": "^5.62.3", - "@tanstack/react-router": "^1.91.2", + "@tanstack/react-router": "^1.91.3", "@tanstack/router-plugin": "^1.91.1", "@router-mono-react-query/post-query": "workspace:*", "redaxios": "^0.5.1", diff --git a/examples/react/router-monorepo-simple/package.json b/examples/react/router-monorepo-simple/package.json index 88b0429dca..945416e9c6 100644 --- a/examples/react/router-monorepo-simple/package.json +++ b/examples/react/router-monorepo-simple/package.json @@ -8,8 +8,8 @@ "dev": "pnpm router build && pnpm post-feature build && pnpm app dev" }, "dependencies": { - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "@tanstack/router-plugin": "^1.91.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/router-monorepo-simple/packages/app/package.json b/examples/react/router-monorepo-simple/packages/app/package.json index 6010293138..e36a6963a7 100644 --- a/examples/react/router-monorepo-simple/packages/app/package.json +++ b/examples/react/router-monorepo-simple/packages/app/package.json @@ -19,7 +19,7 @@ "@types/react-dom": "^18.2.18", "@vitejs/plugin-react": "^4.3.4", "typescript": "^5.7.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/router-devtools": "^1.91.3", "vite": "^6.0.3", "vite-plugin-dts": "^4.3.0" }, diff --git a/examples/react/router-monorepo-simple/packages/router/package.json b/examples/react/router-monorepo-simple/packages/router/package.json index db316ae8f7..6a36379c7c 100644 --- a/examples/react/router-monorepo-simple/packages/router/package.json +++ b/examples/react/router-monorepo-simple/packages/router/package.json @@ -9,7 +9,7 @@ "types": "./dist/index.d.ts", "dependencies": { "@tanstack/history": "^1.90.0", - "@tanstack/react-router": "^1.91.2", + "@tanstack/react-router": "^1.91.3", "@tanstack/router-plugin": "^1.91.1", "redaxios": "^0.5.1", "zod": "^3.23.8", diff --git a/examples/react/scroll-restoration/package.json b/examples/react/scroll-restoration/package.json index fdd9436580..3c162e97f2 100644 --- a/examples/react/scroll-restoration/package.json +++ b/examples/react/scroll-restoration/package.json @@ -9,9 +9,9 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.91.2", + "@tanstack/react-router": "^1.91.3", "@tanstack/react-virtual": "^3.11.1", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/router-devtools": "^1.91.3", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/examples/react/search-validator-adapters/package.json b/examples/react/search-validator-adapters/package.json index f470654132..e2c911e4b1 100644 --- a/examples/react/search-validator-adapters/package.json +++ b/examples/react/search-validator-adapters/package.json @@ -11,12 +11,12 @@ }, "dependencies": { "@tanstack/react-query": "^5.62.3", - "@tanstack/react-router": "^1.91.2", - "@tanstack/arktype-adapter": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/arktype-adapter": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "@tanstack/router-plugin": "^1.91.1", - "@tanstack/valibot-adapter": "^1.91.2", - "@tanstack/zod-adapter": "^1.91.2", + "@tanstack/valibot-adapter": "^1.91.3", + "@tanstack/zod-adapter": "^1.91.3", "arktype": "2.0.0-rc.26", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/start-basic-auth/package.json b/examples/react/start-basic-auth/package.json index 19a94f9093..23c71bf022 100644 --- a/examples/react/start-basic-auth/package.json +++ b/examples/react/start-basic-auth/package.json @@ -11,9 +11,9 @@ }, "dependencies": { "@prisma/client": "5.22.0", - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", - "@tanstack/start": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", + "@tanstack/start": "^1.91.3", "prisma": "^5.22.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/examples/react/start-basic-react-query/package.json b/examples/react/start-basic-react-query/package.json index 6771472d25..4e96d66d9b 100644 --- a/examples/react/start-basic-react-query/package.json +++ b/examples/react/start-basic-react-query/package.json @@ -11,10 +11,10 @@ "dependencies": { "@tanstack/react-query": "^5.62.3", "@tanstack/react-query-devtools": "^5.62.3", - "@tanstack/react-router": "^1.91.2", - "@tanstack/react-router-with-query": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", - "@tanstack/start": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/react-router-with-query": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", + "@tanstack/start": "^1.91.3", "react": "^18.3.1", "react-dom": "^18.3.1", "redaxios": "^0.5.1", diff --git a/examples/react/start-basic-rsc/package.json b/examples/react/start-basic-rsc/package.json index b532d5812f..66a417119a 100644 --- a/examples/react/start-basic-rsc/package.json +++ b/examples/react/start-basic-rsc/package.json @@ -10,9 +10,9 @@ }, "dependencies": { "@babel/plugin-syntax-typescript": "^7.25.9", - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", - "@tanstack/start": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", + "@tanstack/start": "^1.91.3", "redaxios": "^0.5.1", "tailwind-merge": "^2.5.5", "vinxi": "0.5.1" diff --git a/examples/react/start-basic/package.json b/examples/react/start-basic/package.json index 32588b68da..3bef128dff 100644 --- a/examples/react/start-basic/package.json +++ b/examples/react/start-basic/package.json @@ -9,9 +9,9 @@ "start": "vinxi start" }, "dependencies": { - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", - "@tanstack/start": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", + "@tanstack/start": "^1.91.3", "react": "^18.3.1", "react-dom": "^18.3.1", "redaxios": "^0.5.1", diff --git a/examples/react/start-clerk-basic/package.json b/examples/react/start-clerk-basic/package.json index e0cda0c0df..060e5a816a 100644 --- a/examples/react/start-clerk-basic/package.json +++ b/examples/react/start-clerk-basic/package.json @@ -10,9 +10,9 @@ }, "dependencies": { "@clerk/tanstack-start": "0.6.5", - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", - "@tanstack/start": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", + "@tanstack/start": "^1.91.3", "react": "^18.3.1", "react-dom": "^18.3.1", "redaxios": "^0.5.1", diff --git a/examples/react/start-convex-trellaux/package.json b/examples/react/start-convex-trellaux/package.json index db99980188..e6a54c02b2 100644 --- a/examples/react/start-convex-trellaux/package.json +++ b/examples/react/start-convex-trellaux/package.json @@ -13,10 +13,10 @@ "dependencies": { "@tanstack/react-query": "^5.62.3", "@tanstack/react-query-devtools": "^5.62.3", - "@tanstack/react-router": "^1.91.2", - "@tanstack/react-router-with-query": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", - "@tanstack/start": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/react-router-with-query": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", + "@tanstack/start": "^1.91.3", "@convex-dev/react-query": "0.0.0-alpha.8", "concurrently": "^8.2.2", "convex": "^1.17.3", diff --git a/examples/react/start-counter/package.json b/examples/react/start-counter/package.json index f40595ef44..628d3c0c5d 100644 --- a/examples/react/start-counter/package.json +++ b/examples/react/start-counter/package.json @@ -9,8 +9,8 @@ "start": "vinxi start" }, "dependencies": { - "@tanstack/react-router": "^1.91.2", - "@tanstack/start": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/start": "^1.91.3", "react": "^18.3.1", "react-dom": "^18.3.1", "vinxi": "0.5.1" diff --git a/examples/react/start-large/package.json b/examples/react/start-large/package.json index ad7d18bfe4..e41bca4756 100644 --- a/examples/react/start-large/package.json +++ b/examples/react/start-large/package.json @@ -12,9 +12,9 @@ }, "dependencies": { "@tanstack/react-query": "^5.62.3", - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", - "@tanstack/start": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", + "@tanstack/start": "^1.91.3", "react": "^18.3.1", "react-dom": "^18.3.1", "redaxios": "^0.5.1", diff --git a/examples/react/start-supabase-basic/package.json b/examples/react/start-supabase-basic/package.json index cc911f25b9..f93e6b3564 100644 --- a/examples/react/start-supabase-basic/package.json +++ b/examples/react/start-supabase-basic/package.json @@ -15,9 +15,9 @@ "dependencies": { "@supabase/ssr": "^0.5.2", "@supabase/supabase-js": "^2.47.3", - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", - "@tanstack/start": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", + "@tanstack/start": "^1.91.3", "react": "^18.3.1", "react-dom": "^18.3.1", "vinxi": "0.5.1" diff --git a/examples/react/start-trellaux/package.json b/examples/react/start-trellaux/package.json index f9e35cac06..fca0779f77 100644 --- a/examples/react/start-trellaux/package.json +++ b/examples/react/start-trellaux/package.json @@ -11,10 +11,10 @@ "dependencies": { "@tanstack/react-query": "^5.62.3", "@tanstack/react-query-devtools": "^5.62.3", - "@tanstack/react-router": "^1.91.2", - "@tanstack/react-router-with-query": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", - "@tanstack/start": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/react-router-with-query": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", + "@tanstack/start": "^1.91.3", "ky": "^1.7.2", "msw": "^2.6.8", "react": "^18.3.1", diff --git a/examples/react/with-framer-motion/package.json b/examples/react/with-framer-motion/package.json index a0c6b715ee..540a3b5946 100644 --- a/examples/react/with-framer-motion/package.json +++ b/examples/react/with-framer-motion/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "redaxios": "^0.5.1", "framer-motion": "^11.13.3", "react": "^18.2.0", diff --git a/examples/react/with-trpc-react-query/package.json b/examples/react/with-trpc-react-query/package.json index 41883279d9..8ae2cab652 100644 --- a/examples/react/with-trpc-react-query/package.json +++ b/examples/react/with-trpc-react-query/package.json @@ -10,8 +10,8 @@ "dependencies": { "@tanstack/react-query": "^5.62.3", "@tanstack/react-query-devtools": "^5.62.3", - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "@tanstack/router-plugin": "^1.91.1", "@trpc/client": "11.0.0-rc.660", "@trpc/react-query": "11.0.0-rc.660", diff --git a/examples/react/with-trpc/package.json b/examples/react/with-trpc/package.json index 2a2238a696..d7780a1d70 100644 --- a/examples/react/with-trpc/package.json +++ b/examples/react/with-trpc/package.json @@ -8,8 +8,8 @@ "start": "vinxi start" }, "dependencies": { - "@tanstack/react-router": "^1.91.2", - "@tanstack/router-devtools": "^1.91.2", + "@tanstack/react-router": "^1.91.3", + "@tanstack/router-devtools": "^1.91.3", "@tanstack/router-plugin": "^1.91.1", "@trpc/client": "11.0.0-rc.660", "@trpc/server": "11.0.0-rc.660", diff --git a/packages/arktype-adapter/package.json b/packages/arktype-adapter/package.json index dee43724fc..7ee5fbeabe 100644 --- a/packages/arktype-adapter/package.json +++ b/packages/arktype-adapter/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/arktype-adapter", - "version": "1.91.2", + "version": "1.91.3", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/create-router/package.json b/packages/create-router/package.json index 6c04da53b5..ecaa0a61d4 100644 --- a/packages/create-router/package.json +++ b/packages/create-router/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/create-router", - "version": "1.91.2", + "version": "1.91.3", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/react-router-with-query/package.json b/packages/react-router-with-query/package.json index b49a05c884..dfb0eca965 100644 --- a/packages/react-router-with-query/package.json +++ b/packages/react-router-with-query/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/react-router-with-query", - "version": "1.91.2", + "version": "1.91.3", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/react-router/package.json b/packages/react-router/package.json index f55cdffab0..0c8b748525 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/react-router", - "version": "1.91.2", + "version": "1.91.3", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/router-devtools/package.json b/packages/router-devtools/package.json index 9eb8710711..a08522143e 100644 --- a/packages/router-devtools/package.json +++ b/packages/router-devtools/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/router-devtools", - "version": "1.91.2", + "version": "1.91.3", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/start/package.json b/packages/start/package.json index c03e27a0ca..172fbf00a8 100644 --- a/packages/start/package.json +++ b/packages/start/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/start", - "version": "1.91.2", + "version": "1.91.3", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/valibot-adapter/package.json b/packages/valibot-adapter/package.json index 3627904f0d..a05e3db1a5 100644 --- a/packages/valibot-adapter/package.json +++ b/packages/valibot-adapter/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/valibot-adapter", - "version": "1.91.2", + "version": "1.91.3", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/zod-adapter/package.json b/packages/zod-adapter/package.json index d6ffa242d0..367cad576d 100644 --- a/packages/zod-adapter/package.json +++ b/packages/zod-adapter/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/zod-adapter", - "version": "1.91.2", + "version": "1.91.3", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", From 915fbc02ec17ed799631df41ce03a584656b7092 Mon Sep 17 00:00:00 2001 From: AlexisPin <72150908+AlexisPin@users.noreply.github.com> Date: Thu, 19 Dec 2024 18:03:08 +0100 Subject: [PATCH 18/38] docs: minor typo in code-based-routing.md (#3043) --- docs/framework/react/guide/code-based-routing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/framework/react/guide/code-based-routing.md b/docs/framework/react/guide/code-based-routing.md index 398a5a24fb..c1e9a628e9 100644 --- a/docs/framework/react/guide/code-based-routing.md +++ b/docs/framework/react/guide/code-based-routing.md @@ -310,7 +310,7 @@ const routeTree = rootRoute.addChildren([ ]) ``` -Now both `/layout-a` and `/layout-b` will render the their contents inside of the `LayoutComponent`: +Now both `/layout-a` and `/layout-b` will render their contents inside of the `LayoutComponent`: ```tsx // URL: /layout-a From 1687da107cd3bc12e17085d7f36f30c1cffb33f8 Mon Sep 17 00:00:00 2001 From: timoconnellaus Date: Fri, 20 Dec 2024 10:36:02 +1100 Subject: [PATCH 19/38] feat(start): create-start cli (#2920) * create start cli * update lockfile * ci: apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- e2e/create-start/.gitignore | 10 + e2e/create-start/package.json | 16 + e2e/create-start/playwright.config.ts | 34 ++ .../tests/templates/barebones.test.ts | 38 ++ e2e/create-start/tsconfig.json | 10 + e2e/create-start/utils/setup.ts | 72 ++++ packages/create-start/.gitignore | 3 + packages/create-start/README.md | 0 packages/create-start/build.config.ts | 41 ++ packages/create-start/copyTemplates.mjs | 24 ++ packages/create-start/eslint.config.js | 16 + packages/create-start/index.js | 3 + packages/create-start/package.json | 75 ++++ packages/create-start/src/cli-entry.ts | 6 + packages/create-start/src/cli.ts | 133 +++++++ packages/create-start/src/constants.ts | 8 + packages/create-start/src/directory.ts | 73 ++++ packages/create-start/src/index.ts | 17 + packages/create-start/src/logo.ts | 44 +++ packages/create-start/src/module.ts | 286 ++++++++++++++ .../create-start/src/modules/core/index.ts | 272 +++++++++++++ .../src/modules/core/template/app.config.ts | 5 + .../src/modules/core/template/app/client.tsx | 10 + .../src/modules/core/template/app/router.tsx | 18 + .../core/template/app/routes/__root.tsx | 50 +++ .../src/modules/core/template/app/ssr.tsx | 15 + .../src/modules/core/template/tsconfig.json | 10 + packages/create-start/src/modules/git.ts | 147 +++++++ packages/create-start/src/modules/ide.ts | 88 +++++ .../create-start/src/modules/packageJson.ts | 183 +++++++++ .../src/modules/packageManager.ts | 109 ++++++ .../create-start/src/modules/vscode/index.ts | 46 +++ .../vscode/template/_dot_vscode/settings.json | 11 + .../src/templates/barebones/index.ts | 75 ++++ .../barebones/template/app/routes/index.tsx | 11 + packages/create-start/src/templates/index.ts | 75 ++++ packages/create-start/src/types.ts | 3 + packages/create-start/src/utils/debug.ts | 95 +++++ .../src/utils/getPackageManager.ts | 16 + .../src/utils/helpers/helperFactory.ts | 20 + .../create-start/src/utils/helpers/index.ts | 284 ++++++++++++++ packages/create-start/src/utils/runCmd.ts | 10 + .../src/utils/runPackageManagerCommand.ts | 26 ++ packages/create-start/src/utils/spawnCmd.ts | 39 ++ .../src/utils/validateProjectName.ts | 25 ++ packages/create-start/tests/e2e/cli.test.ts | 135 +++++++ .../tests/e2e/templates/barebones.test.ts | 79 ++++ packages/create-start/tsconfig.json | 9 + packages/create-start/vitest.config.ts | 14 + pnpm-lock.yaml | 366 +++++++++++++++++- scripts/publish.js | 4 + 51 files changed, 3140 insertions(+), 19 deletions(-) create mode 100644 e2e/create-start/.gitignore create mode 100644 e2e/create-start/package.json create mode 100644 e2e/create-start/playwright.config.ts create mode 100644 e2e/create-start/tests/templates/barebones.test.ts create mode 100644 e2e/create-start/tsconfig.json create mode 100644 e2e/create-start/utils/setup.ts create mode 100644 packages/create-start/.gitignore create mode 100644 packages/create-start/README.md create mode 100644 packages/create-start/build.config.ts create mode 100644 packages/create-start/copyTemplates.mjs create mode 100644 packages/create-start/eslint.config.js create mode 100755 packages/create-start/index.js create mode 100644 packages/create-start/package.json create mode 100644 packages/create-start/src/cli-entry.ts create mode 100644 packages/create-start/src/cli.ts create mode 100644 packages/create-start/src/constants.ts create mode 100644 packages/create-start/src/directory.ts create mode 100644 packages/create-start/src/index.ts create mode 100644 packages/create-start/src/logo.ts create mode 100644 packages/create-start/src/module.ts create mode 100644 packages/create-start/src/modules/core/index.ts create mode 100644 packages/create-start/src/modules/core/template/app.config.ts create mode 100644 packages/create-start/src/modules/core/template/app/client.tsx create mode 100644 packages/create-start/src/modules/core/template/app/router.tsx create mode 100644 packages/create-start/src/modules/core/template/app/routes/__root.tsx create mode 100644 packages/create-start/src/modules/core/template/app/ssr.tsx create mode 100644 packages/create-start/src/modules/core/template/tsconfig.json create mode 100644 packages/create-start/src/modules/git.ts create mode 100644 packages/create-start/src/modules/ide.ts create mode 100644 packages/create-start/src/modules/packageJson.ts create mode 100644 packages/create-start/src/modules/packageManager.ts create mode 100644 packages/create-start/src/modules/vscode/index.ts create mode 100644 packages/create-start/src/modules/vscode/template/_dot_vscode/settings.json create mode 100644 packages/create-start/src/templates/barebones/index.ts create mode 100644 packages/create-start/src/templates/barebones/template/app/routes/index.tsx create mode 100644 packages/create-start/src/templates/index.ts create mode 100644 packages/create-start/src/types.ts create mode 100644 packages/create-start/src/utils/debug.ts create mode 100644 packages/create-start/src/utils/getPackageManager.ts create mode 100644 packages/create-start/src/utils/helpers/helperFactory.ts create mode 100644 packages/create-start/src/utils/helpers/index.ts create mode 100644 packages/create-start/src/utils/runCmd.ts create mode 100644 packages/create-start/src/utils/runPackageManagerCommand.ts create mode 100644 packages/create-start/src/utils/spawnCmd.ts create mode 100644 packages/create-start/src/utils/validateProjectName.ts create mode 100644 packages/create-start/tests/e2e/cli.test.ts create mode 100644 packages/create-start/tests/e2e/templates/barebones.test.ts create mode 100644 packages/create-start/tsconfig.json create mode 100644 packages/create-start/vitest.config.ts diff --git a/e2e/create-start/.gitignore b/e2e/create-start/.gitignore new file mode 100644 index 0000000000..8354e4d50d --- /dev/null +++ b/e2e/create-start/.gitignore @@ -0,0 +1,10 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local + +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ \ No newline at end of file diff --git a/e2e/create-start/package.json b/e2e/create-start/package.json new file mode 100644 index 0000000000..e37cca89c2 --- /dev/null +++ b/e2e/create-start/package.json @@ -0,0 +1,16 @@ +{ + "name": "create-start-e2e", + "private": true, + "type": "module", + "scripts": { + "test:e2e": "playwright test --project=chromium" + }, + "devDependencies": { + "@playwright/test": "^1.48.2", + "@tanstack/create-start": "workspace:^", + "get-port-please": "^3.1.2", + "tempy": "^3.1.0", + "terminate": "^2.8.0", + "wait-port": "^1.1.0" + } +} diff --git a/e2e/create-start/playwright.config.ts b/e2e/create-start/playwright.config.ts new file mode 100644 index 0000000000..b07c8b268b --- /dev/null +++ b/e2e/create-start/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test' + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + + reporter: [['line']], + timeout: 60000, + use: { + trace: 'on-first-retry', + }, + workers: 1, + + // use: { + // /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://localhost:3001/', + // }, + + // webServer: { + // command: 'pnpm run dev', + // url: 'http://localhost:3001', + // reuseExistingServer: !process.env.CI, + // stdout: 'pipe', + // }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/create-start/tests/templates/barebones.test.ts b/e2e/create-start/tests/templates/barebones.test.ts new file mode 100644 index 0000000000..8778ef37d7 --- /dev/null +++ b/e2e/create-start/tests/templates/barebones.test.ts @@ -0,0 +1,38 @@ +import { temporaryDirectory } from 'tempy' +import { getRandomPort } from 'get-port-please' +import { unstable_scaffoldTemplate } from '@tanstack/create-start' +import { test } from '../../utils/setup' + +// Before running any tests - create the project in the temporary directory +const projectPath = temporaryDirectory() +await unstable_scaffoldTemplate({ + cfg: { + packageManager: { + installDeps: true, + packageManager: 'pnpm', + }, + git: { + setupGit: false, + }, + packageJson: { + type: 'new', + name: 'barebones-test', + }, + ide: { + ide: 'vscode', + }, + }, + targetPath: projectPath, + templateId: 'barebones', +}) + +const PORT = await getRandomPort() +test.use({ projectPath }) +test.use({ port: PORT }) +test.use({ baseURL: `http://localhost:${PORT}` }) + +test.describe('barebones template e2e', () => { + test('Navigating to index page', async ({ page }) => { + await page.goto('/') + }) +}) diff --git a/e2e/create-start/tsconfig.json b/e2e/create-start/tsconfig.json new file mode 100644 index 0000000000..0d2a31a7d7 --- /dev/null +++ b/e2e/create-start/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "target": "ESNext", + "moduleResolution": "Bundler", + "module": "ESNext" + } +} diff --git a/e2e/create-start/utils/setup.ts b/e2e/create-start/utils/setup.ts new file mode 100644 index 0000000000..bfb4c1fd53 --- /dev/null +++ b/e2e/create-start/utils/setup.ts @@ -0,0 +1,72 @@ +import { exec, execSync } from 'node:child_process' +import { test as baseTest } from '@playwright/test' +import terminate from 'terminate/promise' +import waitPort from 'wait-port' + +async function _setup( + projectPath: string, + port: number, +): Promise<{ + PID: number + ADDR: string + killProcess: () => Promise + deleteTempDir: () => void +}> { + const ADDR = `http://localhost:${port}` + + const childProcess = exec( + `VITE_SERVER_PORT=${port} pnpm vinxi dev --port ${port}`, + { + cwd: projectPath, + }, + ) + + childProcess.stdout?.on('data', (data) => { + const message = data.toString() + console.log('Stdout:', message) + }) + + childProcess.stderr?.on('data', (data) => { + console.error('Stderr:', data.toString()) + }) + + try { + await waitPort({ port, timeout: 30000 }) // Added timeout + } catch (err) { + console.error('Failed to start server:', err) + throw err + } + + const PID = childProcess.pid! + const killProcess = async () => { + console.log('Killing process') + try { + await terminate(PID) + } catch (err) { + console.error('Failed to kill process:', err) + } + } + const deleteTempDir = () => execSync(`rm -rf ${projectPath}`) + + return { PID, ADDR, killProcess, deleteTempDir } +} + +type SetupApp = Awaited> + +export const test = baseTest.extend<{ + setupApp: SetupApp + projectPath: string + port: number + ensureServer: void +}>({ + projectPath: ['', { option: true }], + port: [0, { option: true }], + ensureServer: [ + async ({ projectPath, port }, use) => { + const setup = await _setup(projectPath, port) + await use() + await setup.killProcess() + }, + { auto: true }, + ], +}) diff --git a/packages/create-start/.gitignore b/packages/create-start/.gitignore new file mode 100644 index 0000000000..cab449e017 --- /dev/null +++ b/packages/create-start/.gitignore @@ -0,0 +1,3 @@ +test-results +dist +node_modules \ No newline at end of file diff --git a/packages/create-start/README.md b/packages/create-start/README.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/create-start/build.config.ts b/packages/create-start/build.config.ts new file mode 100644 index 0000000000..1cf3d4a959 --- /dev/null +++ b/packages/create-start/build.config.ts @@ -0,0 +1,41 @@ +import { defineBuildConfig } from 'unbuild' + +// Separeate config required for dev because mkdist + cli-entry doesn't work +// with stub. It will create a .d.ts and .mjs file in the src folder +const dev = defineBuildConfig({ + entries: ['src/cli-entry'], + outDir: 'dist', + clean: true, + declaration: true, + rollup: { + inlineDependencies: true, + esbuild: { + target: 'node18', + minify: false, + }, + }, +}) + +const prod = defineBuildConfig({ + entries: [ + { + builder: 'mkdist', + cleanDist: true, + input: './src/', + pattern: ['**/*.{ts,tsx}', '!**/template/**'], + }, + ], + outDir: 'dist', + clean: true, + declaration: true, + rollup: { + inlineDependencies: true, + esbuild: { + target: 'node18', + minify: false, + }, + }, +}) + +const config = process.env.BUILD_ENV === 'production' ? prod : dev +export default config diff --git a/packages/create-start/copyTemplates.mjs b/packages/create-start/copyTemplates.mjs new file mode 100644 index 0000000000..1eb3915fad --- /dev/null +++ b/packages/create-start/copyTemplates.mjs @@ -0,0 +1,24 @@ +import fs from 'node:fs/promises' +import path from 'node:path' +import fg from 'fast-glob' +import url from 'node:url' + +const __filename = url.fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +async function copyTemplates() { + const templates = await fg('**/template/**', { + cwd: path.join(__dirname, 'src'), + onlyFiles: false, + }) + + for (const template of templates) { + const src = path.join(__dirname, 'src', template) + const dest = path.join(__dirname, 'dist', template) + + await fs.mkdir(path.dirname(dest), { recursive: true }) + await fs.cp(src, dest, { recursive: true }) + } +} + +copyTemplates().catch(console.error) diff --git a/packages/create-start/eslint.config.js b/packages/create-start/eslint.config.js new file mode 100644 index 0000000000..6e0bd1c039 --- /dev/null +++ b/packages/create-start/eslint.config.js @@ -0,0 +1,16 @@ +import rootConfig from '../../eslint.config.js' + +export default [ + ...rootConfig, + { + files: ['src/templates/**/template/**/*', 'src/modules/**/template/**/*'], + rules: { + '@typescript-eslint/ban-ts-comment': [ + 'error', + { + 'ts-nocheck': false, + }, + ], + }, + }, +] diff --git a/packages/create-start/index.js b/packages/create-start/index.js new file mode 100755 index 0000000000..9dd6223fc9 --- /dev/null +++ b/packages/create-start/index.js @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +import './dist/cli-entry.mjs' diff --git a/packages/create-start/package.json b/packages/create-start/package.json new file mode 100644 index 0000000000..0a1f6db22d --- /dev/null +++ b/packages/create-start/package.json @@ -0,0 +1,75 @@ +{ + "name": "@tanstack/create-start", + "version": "1.81.5", + "description": "Modern and scalable routing for React applications", + "author": "Tim O'Connell", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/TanStack/router.git", + "directory": "packages/create-router" + }, + "homepage": "https://tanstack.com/router", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "bin": { + "create-router": "index.js" + }, + "scripts": { + "dev": "BUILD_ENV=development unbuild --stub --watch", + "clean": "rimraf ./dist && rimraf ./coverage", + "test:eslint": "eslint ./src", + "generate-templates": "node ./dist/generate-templates/index.mjs", + "build": "BUILD_ENV=production unbuild && node ./copyTemplates.mjs", + "test": "vitest run", + "test:watch": "vitest watch", + "test:coverage": "vitest run --coverage" + }, + "type": "module", + "files": [ + "index.js", + "templates", + "dist" + ], + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "exports": { + ".": { + "import": "./src/index.ts" + } + }, + "dependencies": { + "gradient-string": "^3.0.0" + }, + "devDependencies": { + "@commander-js/extra-typings": "^12.1.0", + "@inquirer/prompts": "^5.5.0", + "@inquirer/type": "^3.0.1", + "@types/cross-spawn": "^6.0.6", + "@types/validate-npm-package-name": "^4.0.2", + "cross-spawn": "^7.0.5", + "fast-glob": "^3.3.2", + "picocolors": "^1.1.1", + "rollup-plugin-copy": "^3.5.0", + "tempy": "^3.1.0", + "tiny-invariant": "^1.3.3", + "unbuild": "^2.0.0", + "validate-npm-package-name": "^5.0.1", + "yocto-spinner": "^0.1.1", + "zod": "^3.23.8" + }, + "peerDependencies": { + "@tanstack/react-router": "workspace:^", + "@tanstack/router-devtools": "workspace:^", + "@tanstack/start": "workspace:^", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "vinxi": "0.4.3" + }, + "peerDependenciesMeta": {} +} diff --git a/packages/create-start/src/cli-entry.ts b/packages/create-start/src/cli-entry.ts new file mode 100644 index 0000000000..12e38723eb --- /dev/null +++ b/packages/create-start/src/cli-entry.ts @@ -0,0 +1,6 @@ +import { runCli } from './cli' + +runCli(process.argv).catch((error) => { + console.error(error) + process.exit(1) +}) diff --git a/packages/create-start/src/cli.ts b/packages/create-start/src/cli.ts new file mode 100644 index 0000000000..000322ef2c --- /dev/null +++ b/packages/create-start/src/cli.ts @@ -0,0 +1,133 @@ +import { Command, createOption } from '@commander-js/extra-typings' +import { packageManagerOption } from './modules/packageManager' +import { logo } from './logo' +import { + scaffoldTemplate, + templateCliOption, + templatePrompt, +} from './templates' +import { createDebugger, debugCliOption, initDebug } from './utils/debug' +import { getAbsolutePath, newProjectDirectoryPrompt } from './directory' +import { packageNameCliOption } from './modules/packageJson' +import { ideCliOption } from './modules/ide' +import type { TEMPLATE_NAME } from './templates' + +const logger = createDebugger('cli') + +const options = { + template: templateCliOption, + packageNameCliOption: packageNameCliOption, + packageManager: packageManagerOption, + directory: createOption('--directory ', 'The directory to use'), + installDeps: createOption( + '--install-deps', + 'Install dependencies after scaffolding', + ), + noInstallDeps: createOption( + '--no-install-deps', + 'Skip installing dependencies after scaffolding', + ), + initGit: createOption('--init-git', 'Initialise git'), + noInitGit: createOption('--no-init-git', 'Skip initialising git'), + hideLogo: createOption('--hide-logo', 'Hide the Tanstack Start logo'), + ide: ideCliOption, + debug: debugCliOption, +} + +const addNewProjectOptions = (command: Command) => { + return command + .addOption(options.template) + .addOption(options.packageNameCliOption) + .addOption(options.packageManager) + .addOption(options.directory) + .addOption(options.installDeps) + .addOption(options.noInstallDeps) + .addOption(options.initGit) + .addOption(options.noInitGit) + .addOption(options.hideLogo) + .addOption(options.ide) + .addOption(options.debug) +} + +// const addQueryCommand = addBaseOptions( +// new Command() +// .name('tanstack-query') +// .description('Add the Tanstack Query module'), +// ).action((options) => {}) + +// const addCommand = new Command() +// .name('add') +// .description('Add a module to your Tanstack Start project') + +const program = addNewProjectOptions( + new Command('create-start') + .name('create-start') + .description('Scaffold a Tanstack Start appliaction') + .command('default', { + isDefault: true, + hidden: true, + }), +) + // .addCommand(addCommand) + .action(async (options) => { + logger.info('Starting CLI action', { options }) + initDebug(options.debug) + + const templateId: TEMPLATE_NAME = + options.template ?? (await templatePrompt()) + logger.verbose('Template selected', { templateId }) + + const directory = options.directory ?? (await newProjectDirectoryPrompt()) + const targetPath = getAbsolutePath(directory) + logger.verbose('Target directory resolved', { directory, targetPath }) + + logger.info('Starting scaffold process', { + templateId, + targetPath, + packageManager: options.packageManager, + installDeps: options.installDeps, + packageName: options.packageName, + initGit: options.initGit, + ide: options.ide, + }) + + await scaffoldTemplate({ + cfg: { + packageManager: { + packageManager: options.packageManager, + installDeps: options.installDeps, + }, + packageJson: { + type: 'new', + name: options.packageName, + }, + git: { + setupGit: options.initGit, + }, + ide: { + ide: options.ide, + }, + }, + targetPath, + templateId, + }) + logger.info('Scaffold process complete') + }) + +export async function runCli(argv: Array) { + logger.info('CLI starting', { argv }) + if (!argv.includes('--hide-logo')) { + logo() + } + + return new Promise((resolve, reject) => { + logger.verbose('Parsing CLI arguments') + program + .parseAsync(argv) + .then(resolve) + .catch((error) => { + logger.error('CLI execution failed', error) + reject(error) + }) + }) +} diff --git a/packages/create-start/src/constants.ts b/packages/create-start/src/constants.ts new file mode 100644 index 0000000000..ef640a4203 --- /dev/null +++ b/packages/create-start/src/constants.ts @@ -0,0 +1,8 @@ +export const NAME = 'create-start' +export const SUPPORTED_PACKAGE_MANAGERS = [ + 'bun', + 'pnpm', + 'npm', + 'yarn', +] as const +export type PackageManager = (typeof SUPPORTED_PACKAGE_MANAGERS)[number] diff --git a/packages/create-start/src/directory.ts b/packages/create-start/src/directory.ts new file mode 100644 index 0000000000..297b29358e --- /dev/null +++ b/packages/create-start/src/directory.ts @@ -0,0 +1,73 @@ +import path from 'node:path' +import fs from 'node:fs/promises' +import { InvalidArgumentError, createOption } from '@commander-js/extra-typings' +import { input } from '@inquirer/prompts' + +export const getAbsolutePath = (relativePath: string) => { + return path.resolve(process.cwd(), relativePath) +} + +const doesPathExist = async (absolutePath: string) => { + try { + await fs.access(absolutePath) + return true + } catch { + return false + } +} + +const isFolderEmpty = async (absolutePath: string) => { + try { + const files = await fs.readdir(absolutePath) + return files.length === 0 + } catch { + return false + } +} + +const DEFAULT_NAME = 'my-tanstack-start-app' + +const generateDefaultName = async () => { + // Generate a unique default name e.g. my-tanstack-start-app, + // my-tanstack-start-app-1, my-tanstack-start-app-2 etc + + let folderName = DEFAULT_NAME + let absolutePath = getAbsolutePath(folderName) + let pathExists = await doesPathExist(absolutePath) + let counter = 1 + while (pathExists) { + folderName = `${DEFAULT_NAME}-${counter}` + absolutePath = getAbsolutePath(folderName) + pathExists = await doesPathExist(absolutePath) + counter++ + } + return `./${folderName}` +} + +const validateDirectory = async (directory: string) => { + const absolutePath = getAbsolutePath(directory) + const pathExists = await doesPathExist(absolutePath) + if (!pathExists) return true + const folderEmpty = await isFolderEmpty(absolutePath) + if (folderEmpty) return true + return 'The directory is not empty. New projects can only be scaffolded in empty directories' +} + +export const newProjectDirectoryCliOption = createOption( + '--directory ', + 'The directory to scaffold your app in', +).argParser(async (directory) => { + const validationResult = await validateDirectory(directory) + if (validationResult === true) return directory + throw new InvalidArgumentError(validationResult) +}) + +export const newProjectDirectoryPrompt = async () => { + return await input({ + message: 'Where should the project be created?', + default: await generateDefaultName(), + validate: async (path) => { + return await validateDirectory(path) + }, + }) +} diff --git a/packages/create-start/src/index.ts b/packages/create-start/src/index.ts new file mode 100644 index 0000000000..a73827cc6c --- /dev/null +++ b/packages/create-start/src/index.ts @@ -0,0 +1,17 @@ +import { createModule } from './module' +import { ideModule as unstable_ideModule } from './modules/ide' +import { gitModule as unstable_gitModule } from './modules/git' +import { coreModule as unstable_coreModule } from './modules/core' +import { packageJsonModule as unstable_packageJsonModule } from './modules/packageJson' +import { packageManagerModule as unstable_packageManagerModule } from './modules/packageManager' + +export { createModule as unstable_createModule } +export { scaffoldTemplate as unstable_scaffoldTemplate } from './templates' + +export const modules = { + unstable_ideModule, + unstable_gitModule, + unstable_coreModule, + unstable_packageJsonModule, + unstable_packageManagerModule, +} diff --git a/packages/create-start/src/logo.ts b/packages/create-start/src/logo.ts new file mode 100644 index 0000000000..9de211427d --- /dev/null +++ b/packages/create-start/src/logo.ts @@ -0,0 +1,44 @@ +import gradient from 'gradient-string' + +const LEFT_PADDING = 5 + +export const logo = () => { + const logoText = `|▗▄▄▄▖▗▄▖ ▗▖ ▗▖ ▗▄▄▖▗▄▄▄▖▗▄▖ ▗▄▄▖▗▖ ▗▖ + | █ ▐▌ ▐▌▐▛▚▖▐▌▐▌ █ ▐▌ ▐▌▐▌ ▐▌▗▞▘ + | █ ▐▛▀▜▌▐▌ ▝▜▌ ▝▀▚▖ █ ▐▛▀▜▌▐▌ ▐▛▚▖ + | █ ▐▌ ▐▌▐▌ ▐▌▗▄▄▞▘ █ ▐▌ ▐▌▝▚▄▄▖▐▌ ▐▌ + ` + + const startText = `| ▗▄▄▖▗▄▄▄▖▗▄▖ ▗▄▄▖▗▄▄▄▖ + | ▐▌ █ ▐▌ ▐▌▐▌ ▐▌ █ + | ▝▀▚▖ █ ▐▛▀▜▌▐▛▀▚▖ █ + | ▗▄▄▞▘ █ ▐▌ ▐▌▐▌ ▐▌ █ + ` + + const removeLeadngChars = (str: string) => { + return str + .split('\n') + .map((line) => line.replace(/^\s*\|/, '')) + .join('\n') + } + + const padLeft = (str: string) => { + return str + .split('\n') + .map((line) => ' '.repeat(LEFT_PADDING) + line) + .join('\n') + } + + // Create the gradients first + const logoGradient = gradient(['#00bba6', '#8a5eec']) + const startGradient = gradient(['#00bba6', '#00bba6']) + + // Then apply them to the processed text + const logo = logoGradient.multiline(padLeft(removeLeadngChars(logoText))) + const start = startGradient.multiline(padLeft(removeLeadngChars(startText))) + + console.log() + console.log(logo) + console.log(start) + console.log() +} diff --git a/packages/create-start/src/module.ts b/packages/create-start/src/module.ts new file mode 100644 index 0000000000..b6caf4154e --- /dev/null +++ b/packages/create-start/src/module.ts @@ -0,0 +1,286 @@ +import yoctoSpinner from 'yocto-spinner' +import { checkFolderExists, checkFolderIsEmpty } from './utils/helpers' +import { createDebugger } from './utils/debug' +import type { + ParseReturnType, + SafeParseReturnType, + ZodType, + input, + output, + z, +} from 'zod' +import type { Spinner } from 'yocto-spinner' + +const debug = createDebugger('module') + +type Schema = ZodType + +class ModuleBase { + private _baseSchema: TSchema + + constructor(baseSchema: TSchema) { + this._baseSchema = baseSchema + debug.info('Creating new module') + } + + init( + fn: (baseSchema: TSchema) => TInitSchema, + ): InitModule { + debug.verbose('Initializing module with schema transformer') + const schema = fn(this._baseSchema) + return new InitModule(this._baseSchema, schema) + } +} + +class InitModule { + private _baseSchema: TSchema + private _initSchema: TInitSchema + + constructor(baseSchema: TSchema, initSchema: TInitSchema) { + this._baseSchema = baseSchema + this._initSchema = initSchema + debug.verbose('Created init module') + } + + prompt( + fn: (initSchema: TInitSchema) => TPromptSchema, + ): PromptModule { + debug.verbose('Creating prompt module with schema transformer') + const schema = fn(this._initSchema) + return new PromptModule( + this._baseSchema, + this._initSchema, + schema, + ) + } +} + +class PromptModule< + TSchema extends Schema, + TInitSchema extends Schema, + TPromptSchema extends Schema, +> { + private _baseSchema: TSchema + private _initSchema: TInitSchema + private _promptSchema: TPromptSchema + + constructor( + baseSchema: TSchema, + initSchema: TInitSchema, + promptSchema: TPromptSchema, + ) { + this._baseSchema = baseSchema + this._initSchema = initSchema + this._promptSchema = promptSchema + debug.verbose('Created prompt module') + } + + validateAndApply< + TApplyFn extends ApplyFn, + TValidateFn extends ValidateFn, + >({ + validate, + apply, + spinnerConfigFn, + }: { + validate?: TValidateFn + apply: TApplyFn + spinnerConfigFn?: SpinnerConfigFn + }): FinalModule { + debug.verbose('Creating final module with validate and apply functions') + return new FinalModule< + TSchema, + TInitSchema, + TPromptSchema, + TValidateFn, + TApplyFn + >( + this._baseSchema, + this._initSchema, + this._promptSchema, + apply, + validate, + spinnerConfigFn, + ) + } +} + +type ApplyFn = (opts: { + targetPath: string + cfg: z.output +}) => void | Promise + +type ValidateFn = (opts: { + targetPath: string + cfg: z.output +}) => Promise> | Array + +type SpinnerOptions = { + success: string + error: string + inProgress: string +} + +type SpinnerConfigFn = ( + cfg: z.infer, +) => SpinnerOptions | undefined + +class FinalModule< + TSchema extends Schema, + TInitSchema extends Schema, + TPromptSchema extends Schema, + TValidateFn extends ValidateFn, + TApplyFn extends ApplyFn, +> { + public _baseSchema: TSchema + public _initSchema: TInitSchema + public _promptSchema: TPromptSchema + public _applyFn: TApplyFn + public _validateFn: TValidateFn | undefined + public _spinnerConfigFn: SpinnerConfigFn | undefined + + constructor( + baseSchema: TSchema, + initSchema: TInitSchema, + promptSchema: TPromptSchema, + applyFn: TApplyFn, + validateFn?: TValidateFn, + spinnerConfigFn?: SpinnerConfigFn, + ) { + this._baseSchema = baseSchema + this._initSchema = initSchema + this._promptSchema = promptSchema + this._applyFn = applyFn + this._validateFn = validateFn + if (spinnerConfigFn) this._spinnerConfigFn = spinnerConfigFn + debug.verbose('Created final module') + } + + async init(cfg: input): Promise> { + debug.verbose('Running init', { cfg }) + return await this._initSchema.parseAsync(cfg) + } + + public async initSafe( + cfg: input, + ): Promise, output>> { + debug.verbose('Running safe init', { cfg }) + return await this._initSchema.safeParseAsync(cfg) + } + + public async prompt( + cfg: input, + ): Promise, output>> { + debug.verbose('Running prompt', { cfg }) + return await this._promptSchema.parseAsync(cfg) + } + + public async validate( + cfg: input, + ): Promise, output>> { + debug.verbose('Running validate', { cfg }) + return await this._promptSchema.safeParseAsync(cfg) + } + + public async apply({ + cfg, + targetPath, + }: { + cfg: output + targetPath: string + }) { + debug.verbose('Running apply', { cfg, targetPath }) + const spinnerOptions = this._spinnerConfigFn?.(cfg) + await runWithSpinner({ + fn: async () => { + return await this._applyFn({ cfg, targetPath }) + }, + spinnerOptions, + }) + } + + public async execute({ + cfg, + targetPath, + type, + applyingMessage, + }: { + cfg: input + targetPath: string + type: 'new-project' | 'update' + applyingMessage?: string + }) { + debug.info('Executing module', { type, targetPath }) + + const targetExists = await checkFolderExists(targetPath) + const targetIsEmpty = await checkFolderIsEmpty(targetPath) + + debug.verbose('Target directory status', { targetExists, targetIsEmpty }) + + if (type === 'new-project') { + if (targetExists && !targetIsEmpty) { + debug.error('Target directory is not empty for new project') + console.error("The target folder isn't empty") + process.exit(0) + } + } + + if (type === 'update') { + if (!targetExists) { + debug.error('Target directory does not exist for update') + console.error("The target folder doesn't exist") + process.exit(0) + } + } + + debug.verbose('Parsing init state') + const initState = await this._initSchema.parseAsync(cfg) + + debug.verbose('Parsing prompt state') + const promptState = await this._promptSchema.parseAsync(initState) + + if (applyingMessage) { + console.log() + console.log(applyingMessage) + } + + debug.verbose('Applying module') + await this.apply({ cfg: promptState, targetPath }) + debug.info('Module execution complete') + } +} + +export function createModule( + baseSchema: TSchema, +): ModuleBase { + return new ModuleBase(baseSchema) +} + +export const runWithSpinner = async ({ + spinnerOptions, + fn, +}: { + spinnerOptions: SpinnerOptions | undefined + fn: () => Promise +}) => { + let spinner: Spinner + + if (spinnerOptions != undefined) { + spinner = yoctoSpinner({ + text: spinnerOptions.inProgress, + }).start() + } + + try { + await fn() + if (spinnerOptions) { + spinner!.success(spinnerOptions.success) + } + } catch (e) { + if (spinnerOptions) { + spinner!.error(spinnerOptions.error) + } + debug.error('Error in spinner operation', e) + throw e + } +} diff --git a/packages/create-start/src/modules/core/index.ts b/packages/create-start/src/modules/core/index.ts new file mode 100644 index 0000000000..97ea5a4321 --- /dev/null +++ b/packages/create-start/src/modules/core/index.ts @@ -0,0 +1,272 @@ +import { dirname } from 'node:path' +import { fileURLToPath } from 'node:url' +import { z } from 'zod' +import { packageJsonModule } from '../packageJson' +import { createModule, runWithSpinner } from '../../module' +import { ideModule } from '../ide' +import packageJson from '../../../package.json' assert { type: 'json' } +import { packageManagerModule } from '../packageManager' +import { initHelpers } from '../../utils/helpers' +import { gitModule } from '../git' +import { createDebugger } from '../../utils/debug' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +const debug = createDebugger('core-module') + +export const coreModule = createModule( + z.object({ + packageJson: packageJsonModule._initSchema.optional(), + ide: ideModule._initSchema.optional(), + packageManager: packageManagerModule._initSchema.optional(), + git: gitModule._initSchema.optional(), + }), +) + .init((schema) => + schema.transform(async (vals, ctx) => { + debug.verbose('Initializing core module schema', { vals }) + + const gitignore: z.infer['gitIgnore'] = [ + { + sectionName: 'Dependencies', + lines: ['node_modules/'], + }, + { + sectionName: 'Env', + lines: [ + '.env', + '.env.local', + '.env.development', + '.env.test', + '.env.production', + '.env.staging', + ], + }, + { + sectionName: 'System Files', + lines: ['.DS_Store', 'Thumbs.db'], + }, + ] + + vals.git = { + ...vals.git, + gitIgnore: [...(vals.git?.gitIgnore ?? []), ...gitignore], + } + + const packageJson: z.infer = { + type: 'new', + dependencies: await deps([ + '@tanstack/react-router', + '@tanstack/start', + 'react', + 'react-dom', + 'vinxi', + ]), + devDependencies: await deps(['@types/react', '@types/react']), + scripts: [ + { + name: 'dev', + script: 'vinxi dev', + }, + { + name: 'build', + script: 'vinxi build', + }, + { + name: 'start', + script: 'vinxi start', + }, + ], + ...vals.packageJson, + } + + debug.verbose('Parsing package manager schema') + const packageManager = + await packageManagerModule._initSchema.safeParseAsync( + vals.packageManager, + { + path: ['packageManager'], + }, + ) + + debug.verbose('Parsing IDE schema') + const ide = await ideModule._initSchema.safeParseAsync(vals.ide, { + path: ['ide'], + }) + + debug.verbose('Parsing git schema') + const git = await gitModule._initSchema.safeParseAsync(vals.git, { + path: ['git'], + }) + + if (!ide.success || !packageManager.success || !git.success) { + debug.error('Schema validation failed', null, { + ide: ide.success, + packageManager: packageManager.success, + git: git.success, + }) + ide.error?.issues.forEach((i) => ctx.addIssue(i)) + packageManager.error?.issues.forEach((i) => ctx.addIssue(i)) + git.error?.issues.forEach((i) => ctx.addIssue(i)) + throw Error('Failed validation') + } + + debug.verbose('Schema transformation complete') + return { + ...vals, + packageManager: packageManager.data, + ide: ide.data, + git: git.data, + packageJson, + } + }), + ) + .prompt((schema) => + schema.transform(async (vals, ctx) => { + debug.verbose('Running prompt transformations', { vals }) + + debug.verbose('Parsing IDE prompt schema') + const ide = await ideModule._promptSchema.safeParseAsync(vals.ide, { + path: ['ide'], + }) + + debug.verbose('Parsing package manager prompt schema') + const packageManager = + await packageManagerModule._promptSchema.safeParseAsync( + vals.packageManager, + { path: ['packageManager'] }, + ) + + debug.verbose('Parsing git prompt schema') + const git = await gitModule._promptSchema.safeParseAsync(vals.git, { + path: ['git'], + }) + + debug.verbose('Parsing package.json prompt schema') + const packageJson = await packageJsonModule._promptSchema.safeParseAsync( + vals.packageJson, + { + path: ['packageJson'], + }, + ) + + if ( + !ide.success || + !packageManager.success || + !git.success || + !packageJson.success + ) { + debug.error('Prompt validation failed', null, { + ide: ide.success, + packageManager: packageManager.success, + git: git.success, + packageJson: packageJson.success, + }) + ide.error?.issues.forEach((i) => ctx.addIssue(i)) + packageManager.error?.issues.forEach((i) => ctx.addIssue(i)) + git.error?.issues.forEach((i) => ctx.addIssue(i)) + throw Error('Failed validation') + } + + debug.verbose('Prompt transformations complete') + return { + packageJson: packageJson.data, + ide: ide.data, + packageManager: packageManager.data, + git: git.data, + } + }), + ) + .validateAndApply({ + validate: async ({ cfg, targetPath }) => { + debug.verbose('Validating core module', { targetPath }) + const _ = initHelpers(__dirname, targetPath) + + const issues = await _.getTemplateFilesThatWouldBeOverwritten({ + file: '**/*', + templateFolder: './template', + targetFolder: targetPath, + overwrite: false, + }) + + if (ideModule._validateFn) { + debug.verbose('Running IDE validation') + const ideIssues = await ideModule._validateFn({ + cfg: cfg.ide, + targetPath, + }) + issues.push(...ideIssues) + } + + debug.info('Validation complete', { issueCount: issues.length }) + return issues + }, + apply: async ({ cfg, targetPath }) => { + debug.info('Applying core module', { targetPath }) + const _ = initHelpers(__dirname, targetPath) + + debug.verbose('Copying core template files') + await runWithSpinner({ + spinnerOptions: { + inProgress: 'Copying core template files', + error: 'Failed to copy core template files', + success: 'Copied core template files', + }, + fn: async () => + await _.copyTemplateFiles({ + file: '**/*', + templateFolder: './template', + targetFolder: '.', + overwrite: false, + }), + }) + + debug.verbose('Applying package.json module') + await packageJsonModule.apply({ cfg: cfg.packageJson, targetPath }) + + debug.verbose('Applying IDE module') + await ideModule.apply({ cfg: cfg.ide, targetPath }) + + debug.verbose('Applying git module') + await gitModule._applyFn({ cfg: cfg.git, targetPath }) + + debug.verbose('Applying package manager module') + await packageManagerModule.apply({ + cfg: cfg.packageManager, + targetPath, + }) + + debug.info('Core module application complete') + }, + }) + +type DepNames< + T extends + (typeof packageJson)['peerDependencies'] = (typeof packageJson)['peerDependencies'], +> = keyof T + +const deps = async ( + depsArray: Array, +): Promise< + Exclude< + z.infer['dependencies'], + undefined + > +> => { + debug.verbose('Resolving dependencies', { deps: depsArray }) + const result = await Promise.all( + depsArray.map((d) => { + const version = + packageJson['peerDependencies'][d] === 'workspace:^' + ? 'latest' // Use latest in development + : packageJson['peerDependencies'][d] + return { + name: d, + version: version, + } + }), + ) + + return result +} diff --git a/packages/create-start/src/modules/core/template/app.config.ts b/packages/create-start/src/modules/core/template/app.config.ts new file mode 100644 index 0000000000..bb213f88d6 --- /dev/null +++ b/packages/create-start/src/modules/core/template/app.config.ts @@ -0,0 +1,5 @@ +// @ts-nocheck + +import { defineConfig } from '@tanstack/start/config' + +export default defineConfig({}) diff --git a/packages/create-start/src/modules/core/template/app/client.tsx b/packages/create-start/src/modules/core/template/app/client.tsx new file mode 100644 index 0000000000..abff2ff760 --- /dev/null +++ b/packages/create-start/src/modules/core/template/app/client.tsx @@ -0,0 +1,10 @@ +// @ts-nocheck + +/// +import { hydrateRoot } from 'react-dom/client' +import { StartClient } from '@tanstack/start' +import { createRouter } from './router' + +const router = createRouter() + +hydrateRoot(document, ) diff --git a/packages/create-start/src/modules/core/template/app/router.tsx b/packages/create-start/src/modules/core/template/app/router.tsx new file mode 100644 index 0000000000..227f00e224 --- /dev/null +++ b/packages/create-start/src/modules/core/template/app/router.tsx @@ -0,0 +1,18 @@ +// @ts-nocheck + +import { createRouter as createTanStackRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function createRouter() { + const router = createTanStackRouter({ + routeTree, + }) + + return router +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/packages/create-start/src/modules/core/template/app/routes/__root.tsx b/packages/create-start/src/modules/core/template/app/routes/__root.tsx new file mode 100644 index 0000000000..aa5bdec1a3 --- /dev/null +++ b/packages/create-start/src/modules/core/template/app/routes/__root.tsx @@ -0,0 +1,50 @@ +// @ts-nocheck + +import { + Outlet, + ScrollRestoration, + createRootRoute, +} from '@tanstack/react-router' +import { Meta, Scripts } from '@tanstack/start' +import type { ReactNode } from 'react' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { + charSet: 'utf-8', + }, + { + name: 'viewport', + content: 'width=device-width, initial-scale=1', + }, + { + title: 'TanStack Start Starter', + }, + ], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + ) +} + +function RootDocument({ children }: Readonly<{ children: ReactNode }>) { + return ( + + + + + + {children} + + + + + ) +} diff --git a/packages/create-start/src/modules/core/template/app/ssr.tsx b/packages/create-start/src/modules/core/template/app/ssr.tsx new file mode 100644 index 0000000000..1dc694297f --- /dev/null +++ b/packages/create-start/src/modules/core/template/app/ssr.tsx @@ -0,0 +1,15 @@ +// @ts-nocheck + +/// +import { + createStartHandler, + defaultStreamHandler, +} from '@tanstack/start/server' +import { getRouterManifest } from '@tanstack/start/router-manifest' + +import { createRouter } from './router' + +export default createStartHandler({ + createRouter, + getRouterManifest, +})(defaultStreamHandler) diff --git a/packages/create-start/src/modules/core/template/tsconfig.json b/packages/create-start/src/modules/core/template/tsconfig.json new file mode 100644 index 0000000000..edfdf0e833 --- /dev/null +++ b/packages/create-start/src/modules/core/template/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "moduleResolution": "Bundler", + "module": "ESNext", + "target": "ES2022", + "skipLibCheck": true, + "strictNullChecks": true + } +} diff --git a/packages/create-start/src/modules/git.ts b/packages/create-start/src/modules/git.ts new file mode 100644 index 0000000000..cbf62f00a2 --- /dev/null +++ b/packages/create-start/src/modules/git.ts @@ -0,0 +1,147 @@ +import { readFile, writeFile } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' +import { dirname, resolve } from 'node:path' +import { z } from 'zod' +import { select } from '@inquirer/prompts' +import { createModule } from '../module' +import { runCmd } from '../utils/runCmd' +import { createDebugger } from '../utils/debug' +import { checkFileExists, initHelpers } from '../utils/helpers' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +const debug = createDebugger('git-module') + +async function appendToGitignore( + targetPath: string, + newEntries: Array, + sectionName: string, +) { + const gitignorePath = resolve(targetPath, '.gitignore') + debug.verbose('Handling gitignore', { gitignorePath }) + + let existingContent = '' + const exists = await checkFileExists(gitignorePath) + + if (exists) { + existingContent = await readFile(gitignorePath, 'utf-8') + const lines = existingContent.split('\n') + + // Find existing section + const sectionStart = lines.findIndex( + (line) => line.trim() === `# ${sectionName}`, + ) + + if (sectionStart !== -1) { + // Section exists, find end (next comment or EOF) + let sectionEnd = lines.findIndex( + (line, i) => i > sectionStart && line.trim().startsWith('#'), + ) + if (sectionEnd === -1) sectionEnd = lines.length + + // Get existing entries in section + const sectionEntries = lines + .slice(sectionStart + 1, sectionEnd) + .map((line) => line.trim()) + .filter((line) => line !== '') + + // Filter out duplicates + newEntries = newEntries.filter( + (entry) => + !sectionEntries.some( + (existing) => existing.toLowerCase() === entry.trim().toLowerCase(), + ), + ) + + if (newEntries.length > 0) { + // Insert new entries at end of section + lines.splice(sectionEnd, 0, ...newEntries) + await writeFile(gitignorePath, lines.join('\n')) + debug.info('Updated existing section in gitignore file') + } + } else { + // Add new section at end + const newContent = `${existingContent}\n\n# ${sectionName}\n${newEntries.join('\n')}` + await writeFile(gitignorePath, newContent) + debug.info('Added new section to gitignore file') + } + } else { + // Create new file with section + const content = `# ${sectionName}\n${newEntries.join('\n')}` + await writeFile(gitignorePath, content) + debug.info('Created new gitignore file') + } +} + +export const gitModule = createModule( + z.object({ + setupGit: z.boolean().optional(), + gitIgnore: z + .object({ + sectionName: z.string(), + lines: z.string().array(), + }) + .array() + .optional(), + }), +) + .init((schema) => schema) // No init required + .prompt((schema) => + schema.transform(async (vals) => { + debug.verbose('Transforming git prompt schema', { vals }) + const setupGit = + vals.setupGit != undefined + ? vals.setupGit + : await select({ + message: 'Initialize git', + choices: [ + { name: 'yes', value: true }, + { name: 'no', value: false }, + ], + default: 'yes', + }) + debug.info('Git initialization choice made', { setupGit }) + return { + setupGit, + gitIgnore: vals.gitIgnore, + } + }), + ) + .validateAndApply({ + apply: async ({ cfg, targetPath }) => { + const _ = initHelpers(__dirname, targetPath) + debug.verbose('Applying git module', { cfg, targetPath }) + + if (cfg.gitIgnore && cfg.gitIgnore.length > 0) { + for (const gitIgnore of cfg.gitIgnore) { + await appendToGitignore( + _.getFullTargetPath(''), + gitIgnore.lines, + gitIgnore.sectionName, + ) + } + debug.info('Created / updated .gitignore') + } + + if (cfg.setupGit) { + debug.info('Initializing git repository') + try { + await runCmd('git', ['init'], {}, targetPath) + debug.info('Git repository initialized successfully') + } catch (error) { + debug.error('Failed to initialize git repository', error) + throw error + } + } else { + debug.info('Skipping git initialization') + } + }, + spinnerConfigFn: () => { + return { + success: 'Git initalized', + error: 'Failed to initialize git', + inProgress: 'Initializing git', + } + }, + }) diff --git a/packages/create-start/src/modules/ide.ts b/packages/create-start/src/modules/ide.ts new file mode 100644 index 0000000000..4294e5d6ed --- /dev/null +++ b/packages/create-start/src/modules/ide.ts @@ -0,0 +1,88 @@ +import { InvalidArgumentError, createOption } from '@commander-js/extra-typings' +import { z } from 'zod' +import { select } from '@inquirer/prompts' +import { createModule } from '../module' +import { createDebugger } from '../utils/debug' +import { vsCodeModule } from './vscode' + +const debug = createDebugger('ide-module') + +const ide = z.enum(['vscode', 'cursor', 'other']) + +const schema = z.object({ + ide: ide.optional(), +}) + +const SUPPORTED_IDES = ide.options +type SupportedIDE = z.infer +const DEFAULT_IDE = 'vscode' + +export const ideCliOption = createOption( + `--ide <${SUPPORTED_IDES.join('|')}>`, + `use this IDE (${SUPPORTED_IDES.join(', ')})`, +).argParser((value) => { + debug.verbose('Parsing IDE CLI option', { value }) + if (!SUPPORTED_IDES.includes(value as SupportedIDE)) { + debug.error('Invalid IDE option provided', null, { value }) + throw new InvalidArgumentError( + `Invalid IDE: ${value}. Only the following are allowed: ${SUPPORTED_IDES.join(', ')}`, + ) + } + return value as SupportedIDE +}) + +export const ideModule = createModule(schema) + .init((schema) => schema) + .prompt((schema) => + schema.transform(async (vals) => { + debug.verbose('Prompting for IDE selection', { currentValue: vals.ide }) + const ide = vals.ide + ? vals.ide + : await select({ + message: 'Select an IDE', + choices: SUPPORTED_IDES.map((i) => ({ value: i })), + default: DEFAULT_IDE, + }) + + debug.info('IDE selected', { ide }) + return { + ide, + } + }), + ) + .validateAndApply({ + validate: async ({ cfg, targetPath }) => { + debug.verbose('Validating IDE configuration', { + ide: cfg.ide, + targetPath, + }) + const issues: Array = [] + + if (cfg.ide === 'vscode') { + debug.verbose('Validating VSCode configuration') + const issuesVsCode = + (await vsCodeModule._validateFn?.({ cfg, targetPath })) ?? [] + issues.push(...issuesVsCode) + } + + if (issues.length > 0) { + debug.warn('IDE validation issues found', { issues }) + } + return issues + }, + apply: async ({ cfg, targetPath }) => { + debug.info('Applying IDE configuration', { ide: cfg.ide, targetPath }) + await vsCodeModule._applyFn({ cfg, targetPath }) + debug.info('IDE configuration applied successfully') + }, + spinnerConfigFn: (cfg) => { + debug.verbose('Configuring spinner for IDE setup', { ide: cfg.ide }) + return ['vscode'].includes(cfg.ide) + ? { + error: `Failed to set up ${cfg.ide}`, + inProgress: `Setting up ${cfg.ide}`, + success: `${cfg.ide} set up`, + } + : undefined + }, + }) diff --git a/packages/create-start/src/modules/packageJson.ts b/packages/create-start/src/modules/packageJson.ts new file mode 100644 index 0000000000..03642b3a32 --- /dev/null +++ b/packages/create-start/src/modules/packageJson.ts @@ -0,0 +1,183 @@ +import { dirname } from 'node:path' +import { fileURLToPath } from 'node:url' +import { z } from 'zod' +import { input } from '@inquirer/prompts' +import { InvalidArgumentError, createOption } from '@commander-js/extra-typings' +import { initHelpers } from '../utils/helpers' +import { createModule } from '../module' +import { validateProjectName } from '../utils/validateProjectName' +import { createDebugger } from '../utils/debug' + +const debug = createDebugger('package-json') + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +export const packageNameCliOption = createOption( + '--package-name ', + 'The name to use in the package.json', +).argParser((name) => { + const validation = validateProjectName(name) + if (!validation.valid) { + debug.error('Invalid package name', { name, validation }) + throw new InvalidArgumentError(`The project name ${name} is invalid`) + } + debug.verbose('Validated package name', { name }) + return name +}) + +const dependencies = z.array( + z.object({ + name: z.string(), + version: z.string(), + }), +) + +const script = z.object({ + name: z.string(), + script: z.string(), +}) + +const schema = z.object({ + type: z.enum(['new', 'update']), + name: z.string().optional(), + dependencies: dependencies.optional(), + devDependencies: dependencies.optional(), + scripts: z.array(script).optional(), +}) + +export const packageJsonModule = createModule(schema) + .init((schema) => schema) + .prompt((schema) => { + return schema.transform(async (vals) => { + debug.verbose('Transforming prompt schema', vals) + if (vals.type === 'new') { + const name = vals.name + ? vals.name + : await input({ + message: 'Enter the project name', + default: 'tanstack-start', + validate: (name) => { + const validation = validateProjectName(name) + if (validation.valid) { + debug.verbose('Valid project name entered', { name }) + return true + } + debug.warn('Invalid project name entered', { + name, + problems: validation.problems, + }) + return 'Invalid project name: ' + validation.problems[0] + }, + }) + + return { + ...vals, + name, + } + } else { + return vals + } + }) + }) + .validateAndApply({ + validate: async ({ cfg, targetPath }) => { + debug.verbose('Validating package.json', { cfg, targetPath }) + const issues: Array = [] + const _ = initHelpers(__dirname, targetPath) + + const packageJsonExists = await _.targetFileExists('./package.json') + debug.verbose('Package.json exists check', { exists: packageJsonExists }) + + if (cfg.type === 'new') { + if (packageJsonExists) { + debug.warn('Package.json already exists for new project') + issues.push('Package.json already exists') + } + } else { + if (!packageJsonExists) { + debug.warn('Package.json missing for update') + issues.push("Package.json doesn't exist to update") + } + } + + return issues + }, + apply: async ({ cfg, targetPath }) => { + debug.verbose('Applying package.json changes', { cfg, targetPath }) + const _ = initHelpers(__dirname, targetPath) + if (cfg.type === 'new') { + const packageJson = { + name: cfg.name, + version: '0.0.0', + private: true, + type: 'module', + } + + debug.verbose('Creating new package.json', packageJson) + await _.writeTargetfile( + './package.json', + JSON.stringify(packageJson, null, 2), + false, + ) + } + + let packageJson = JSON.parse(await _.readTargetFile('./package.json')) + debug.verbose('Current package.json contents', packageJson) + + const dependenciesRecord = createDepsRecord(cfg.dependencies ?? []) + const devDependenciesRecord = createDepsRecord(cfg.devDependencies ?? []) + const scriptsRecord = createScriptsRecord(cfg.scripts ?? []) + + packageJson = { + ...packageJson, + scripts: { + ...packageJson.scripts, + ...scriptsRecord, + }, + dependencies: { + ...packageJson.dependencies, + ...dependenciesRecord, + }, + devDependencies: { + ...packageJson.devDependencies, + ...devDependenciesRecord, + }, + } + + debug.verbose('Updated package.json contents', packageJson) + await _.writeTargetfile( + './package.json', + JSON.stringify(packageJson, null, 2), + true, + ) + }, + spinnerConfigFn: (cfg) => ({ + success: `${cfg.type === 'new' ? 'Created' : 'Updated'} package.json`, + error: `Failed to ${cfg.type === 'new' ? 'create' : 'update'} package.json`, + inProgress: `${cfg.type === 'new' ? 'Creating' : 'Updating'} package.json`, + }), + }) + +const createDepsRecord = (deps: Array<{ name: string; version: string }>) => + deps.reduce( + (acc: Record, dep: { name: string; version: string }) => ({ + ...acc, + [dep.name]: dep.version, + }), + {}, + ) + +const createScriptsRecord = ( + scripts: Array<{ name: string; script: string }>, +) => + scripts.reduce( + ( + acc: Record, + script: { name: string; script: string }, + ) => ({ + ...acc, + [script.name]: script.script, + }), + {}, + ) diff --git a/packages/create-start/src/modules/packageManager.ts b/packages/create-start/src/modules/packageManager.ts new file mode 100644 index 0000000000..2a0c2a52e4 --- /dev/null +++ b/packages/create-start/src/modules/packageManager.ts @@ -0,0 +1,109 @@ +import { z } from 'zod' +import { InvalidArgumentError, createOption } from '@commander-js/extra-typings' +import { select } from '@inquirer/prompts' +import { createModule } from '../module' +import { SUPPORTED_PACKAGE_MANAGERS } from '../constants' +import { getPackageManager } from '../utils/getPackageManager' +import { install } from '../utils/runPackageManagerCommand' +import { createDebugger } from '../utils/debug' + +const debug = createDebugger('packageManager') + +const schema = z.object({ + packageManager: z.enum(SUPPORTED_PACKAGE_MANAGERS), + installDeps: z.boolean(), +}) + +const DEFAULT_PACKAGE_MANAGER = 'npm' +const options = schema.shape.packageManager.options +type PackageManager = z.infer['packageManager'] + +export const packageManagerOption = createOption( + `--package-manager <${options.join('|')}>`, + `use this Package Manager (${options.join(', ')})`, +).argParser((value) => { + if (!options.includes(value as PackageManager)) { + debug.error('Invalid package manager provided', { value, allowed: options }) + throw new InvalidArgumentError( + `Invalid Package Manager: ${value}. Only the following are allowed: ${options.join(', ')}`, + ) + } + return value as PackageManager +}) + +export const packageManagerModule = createModule( + z.object({ + packageManager: z.enum(SUPPORTED_PACKAGE_MANAGERS).optional(), + installDeps: z.boolean().optional(), + }), +) + .init((schema) => + schema.transform((vals) => { + debug.verbose('Initializing package manager', vals) + const detectedPM = getPackageManager() + debug.verbose('Detected package manager', { detectedPM }) + return { + packageManager: vals.packageManager ?? detectedPM, + installDeps: vals.installDeps, + } + }), + ) + .prompt((schema) => + schema.transform(async (vals) => { + debug.verbose('Prompting for package manager options', vals) + const packageManager = + vals.packageManager != undefined + ? vals.packageManager + : await select({ + message: 'Select a package manager', + choices: options.map((pm) => ({ value: pm })), + default: getPackageManager() ?? DEFAULT_PACKAGE_MANAGER, + }) + + const installDeps = + vals.installDeps != undefined + ? vals.installDeps + : await select({ + message: 'Install dependencies', + choices: [ + { name: 'yes', value: true }, + { name: 'no', value: false }, + ], + default: 'yes', + }) + + debug.verbose('Package manager options selected', { + packageManager, + installDeps, + }) + return { + installDeps, + packageManager, + } + }), + ) + .validateAndApply({ + spinnerConfigFn: (cfg) => { + debug.verbose('Configuring spinner', cfg) + return cfg.installDeps + ? { + error: `Failed to install dependencies with ${cfg.packageManager}`, + inProgress: `Installing dependencies with ${cfg.packageManager}`, + success: `Installed dependencies with ${cfg.packageManager}`, + } + : undefined + }, + apply: async ({ cfg, targetPath }) => { + if (cfg.installDeps) { + debug.info('Installing dependencies', { + packageManager: cfg.packageManager, + targetPath, + }) + + await install(cfg.packageManager, targetPath) + debug.info('Dependencies installed successfully') + } else { + debug.info('Skipping dependency installation') + } + }, + }) diff --git a/packages/create-start/src/modules/vscode/index.ts b/packages/create-start/src/modules/vscode/index.ts new file mode 100644 index 0000000000..52c448c3c2 --- /dev/null +++ b/packages/create-start/src/modules/vscode/index.ts @@ -0,0 +1,46 @@ +import { dirname } from 'node:path' +import { fileURLToPath } from 'node:url' +import { z } from 'zod' +import { initHelpers } from '../../utils/helpers' +import { createModule } from '../../module' +import { createDebugger } from '../../utils/debug' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +const debug = createDebugger('vscode') + +export const vsCodeModule = createModule(z.object({})) + .init((schema) => schema) + .prompt((schema) => schema) + .validateAndApply({ + validate: async ({ targetPath }) => { + debug.verbose('Validating vscode module', { targetPath }) + const _ = initHelpers(__dirname, targetPath) + + const issues = await _.getTemplateFilesThatWouldBeOverwritten({ + file: '**/*', + templateFolder: './template', + targetFolder: targetPath, + overwrite: false, + }) + + debug.verbose('Validation complete', { issueCount: issues.length }) + return issues + }, + apply: async ({ targetPath }) => { + debug.info('Applying vscode module', { targetPath }) + // Copy the vscode template folders into the project + const _ = initHelpers(__dirname, targetPath) + + // TODO: Handle when the settings file already exists and merge settings + debug.verbose('Copying template files') + await _.copyTemplateFiles({ + file: '**/*', + templateFolder: './template', + targetFolder: '.', + overwrite: false, + }) + debug.info('VSCode module applied successfully') + }, + }) diff --git a/packages/create-start/src/modules/vscode/template/_dot_vscode/settings.json b/packages/create-start/src/modules/vscode/template/_dot_vscode/settings.json new file mode 100644 index 0000000000..00b5278e58 --- /dev/null +++ b/packages/create-start/src/modules/vscode/template/_dot_vscode/settings.json @@ -0,0 +1,11 @@ +{ + "files.watcherExclude": { + "**/routeTree.gen.ts": true + }, + "search.exclude": { + "**/routeTree.gen.ts": true + }, + "files.readonlyInclude": { + "**/routeTree.gen.ts": true + } +} diff --git a/packages/create-start/src/templates/barebones/index.ts b/packages/create-start/src/templates/barebones/index.ts new file mode 100644 index 0000000000..03dcb95fa5 --- /dev/null +++ b/packages/create-start/src/templates/barebones/index.ts @@ -0,0 +1,75 @@ +import { fileURLToPath } from 'node:url' +import { dirname } from 'node:path' +import { createModule, runWithSpinner } from '../../module' +import { coreModule } from '../../modules/core' +import { initHelpers } from '../../utils/helpers' +import { createDebugger } from '../../utils/debug' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +const debug = createDebugger('barebones-template') +const schema = coreModule._initSchema + +export const barebonesTemplate = createModule(schema) + .init((schema) => schema) + .prompt((schema) => + schema.transform(async (vals) => { + debug.verbose('Transforming prompt schema', { vals }) + const core = await coreModule._promptSchema.parseAsync(vals) + debug.verbose('Core module prompt complete') + + return { + ...core, + } + }), + ) + .validateAndApply({ + validate: async ({ cfg, targetPath }) => { + debug.verbose('Validating barebones template', { targetPath }) + const _ = initHelpers(__dirname, targetPath) + + const issues = await _.getTemplateFilesThatWouldBeOverwritten({ + file: '**/*', + templateFolder: './template', + targetFolder: targetPath, + overwrite: false, + }) + + debug.verbose('Template file conflicts found', { issues }) + + const coreIssues = + (await coreModule._validateFn?.({ cfg, targetPath })) ?? [] + debug.verbose('Core module validation issues', { coreIssues }) + + issues.push(...coreIssues) + + return issues + }, + apply: async ({ cfg, targetPath }) => { + debug.info('Applying barebones template', { targetPath }) + const _ = initHelpers(__dirname, targetPath) + + await runWithSpinner({ + spinnerOptions: { + inProgress: 'Copying barebones template files', + error: 'Failed to copy barebones template files', + success: 'Copied barebones template files', + }, + fn: async () => { + debug.verbose('Copying template files') + await _.copyTemplateFiles({ + file: '**/*', + templateFolder: './template', + targetFolder: '.', + overwrite: false, + }) + debug.verbose('Template files copied successfully') + }, + }) + + debug.verbose('Applying core module') + await coreModule._applyFn({ cfg, targetPath }) + debug.info('Barebones template applied successfully') + }, + }) diff --git a/packages/create-start/src/templates/barebones/template/app/routes/index.tsx b/packages/create-start/src/templates/barebones/template/app/routes/index.tsx new file mode 100644 index 0000000000..bf75abe499 --- /dev/null +++ b/packages/create-start/src/templates/barebones/template/app/routes/index.tsx @@ -0,0 +1,11 @@ +// @ts-nocheck + +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: RouteComponent, +}) + +function RouteComponent() { + return
Hello "/"!
+} diff --git a/packages/create-start/src/templates/index.ts b/packages/create-start/src/templates/index.ts new file mode 100644 index 0000000000..3fcbb7b5ec --- /dev/null +++ b/packages/create-start/src/templates/index.ts @@ -0,0 +1,75 @@ +import { select } from '@inquirer/prompts' +import { InvalidArgumentError, createOption } from '@commander-js/extra-typings' +import invariant from 'tiny-invariant' +import { createDebugger } from '../utils/debug' +import { barebonesTemplate } from './barebones' +import type { coreModule } from '../modules/core' +import type { z } from 'zod' + +const debug = createDebugger('templates') + +const templates = [ + { + id: 'barebones', + name: 'Barebones', + module: barebonesTemplate, + description: 'The bare minimum', + }, +] as const + +const templateIds = templates.map((t) => t.id) +export type TEMPLATE_NAME = (typeof templateIds)[number] +export const DEFAULT_TEMPLATE: TEMPLATE_NAME = 'barebones' + +export const templateCliOption = createOption( + '--template ', + 'Choose the template to use', +).argParser((value) => { + if (!templateIds.includes(value as TEMPLATE_NAME)) { + debug.error(`Invalid template specified: ${value}`) + throw new InvalidArgumentError( + `Invalid Template: ${value}. Only the following are allowed: ${templateIds.join(', ')}`, + ) + } + debug.verbose('Template validated from CLI', { template: value }) + return value as TEMPLATE_NAME +}) + +export const templatePrompt = async () => { + debug.info('Prompting for template selection') + const selection = await select({ + message: 'Which template would you like to use?', + choices: templates.map((t) => ({ + name: t.name, + value: t.id, + description: t.description, + })), + default: DEFAULT_TEMPLATE, + }) + debug.verbose('Template selected', { template: selection }) + return selection +} + +export const scaffoldTemplate = async ({ + templateId, + cfg, + targetPath, +}: { + templateId: TEMPLATE_NAME + cfg: z.input + targetPath: string +}) => { + debug.info('Starting template scaffolding', { templateId, targetPath }) + // const template = templates.find((f) => f.id === templateId) + const template = templates[0] // Remove this when we add more templates + invariant(template, `The template with ${templateId} is not valid`) + + debug.verbose('Executing template module', { template: template.id }) + await template.module.execute({ + cfg, + targetPath, + type: 'new-project', + applyingMessage: `Scaffolding the ${template.name} template`, + }) + debug.info('Template scaffolding complete') +} diff --git a/packages/create-start/src/types.ts b/packages/create-start/src/types.ts new file mode 100644 index 0000000000..b66c077f64 --- /dev/null +++ b/packages/create-start/src/types.ts @@ -0,0 +1,3 @@ +import type packageJson from '../package.json' + +export type PeerDependency = keyof typeof packageJson.peerDependencies diff --git a/packages/create-start/src/utils/debug.ts b/packages/create-start/src/utils/debug.ts new file mode 100644 index 0000000000..98a1548cc3 --- /dev/null +++ b/packages/create-start/src/utils/debug.ts @@ -0,0 +1,95 @@ +import { InvalidArgumentError, createOption } from '@commander-js/extra-typings' + +type Context = string +type LogLevel = 'info' | 'warn' | 'error' + +interface LogOptions { + context: Context + data?: Record +} + +let isDebugMode = false +let debugLevel = 0 // 1 = basic, 2 = verbose, 3 = trace + +const DEBUG_LEVELS = ['debug', 'trace', 'verbose'] as const +type DebugLevels = (typeof DEBUG_LEVELS)[number] + +export const debugCliOption = createOption( + `--debug <${DEBUG_LEVELS.join('|')}>`, + `Set a debug level (${DEBUG_LEVELS.join(', ')})`, +).argParser((value) => { + if (!DEBUG_LEVELS.includes(value as DebugLevels)) { + throw new InvalidArgumentError( + `Invalid IDE: ${value}. Only the following are allowed: ${DEBUG_LEVELS.join(', ')}`, + ) + } + return value as DebugLevels +}) + +export const initDebug = (level: undefined | 'debug' | 'trace' | 'verbose') => { + if (level === undefined) return + isDebugMode = true + if (level === 'debug') debugLevel = 1 + if (level === 'trace') debugLevel = 2 + if (level === 'verbose') debugLevel = 3 +} + +const formatData = (data?: Record): string => { + if (!data) return '' + return Object.entries(data) + .map(([key, value]) => `${key}=${JSON.stringify(value)}`) + .join(' ') +} + +const log = (level: LogLevel, message: string, options: LogOptions) => { + if (!isDebugMode) return + + const timestamp = new Date().toISOString() + const dataStr = formatData(options.data) + const logMessage = `[${timestamp}] [${level}] [${options.context}] ${message} ${dataStr}` + + switch (level) { + case 'error': + console.error(logMessage) + break + case 'warn': + console.warn(logMessage) + break + case 'info': + console.log(logMessage) + break + } +} + +export const createDebugger = (context: Context) => ({ + info: (message: string, data?: Record) => { + if (debugLevel < 1) return + log('info', message, { context, data }) + }, + warn: (message: string, data?: Record) => { + if (debugLevel < 2) return + log('warn', message, { context, data }) + }, + error: ( + message: string, + error?: Error | unknown, + data?: Record, + ) => { + if (debugLevel < 1) return + log('error', message, { + context, + data: { + ...data, + error: error instanceof Error ? error.message : error, + }, + }) + }, + verbose: (message: string, data?: Record) => { + if (debugLevel < 2) return + log('info', message, { context, data }) + }, + trace: (message: string, data?: Record) => { + if (debugLevel < 3) return + log('info', message, { context, data }) + }, +}) diff --git a/packages/create-start/src/utils/getPackageManager.ts b/packages/create-start/src/utils/getPackageManager.ts new file mode 100644 index 0000000000..4bcb7b8032 --- /dev/null +++ b/packages/create-start/src/utils/getPackageManager.ts @@ -0,0 +1,16 @@ +import { SUPPORTED_PACKAGE_MANAGERS } from '../constants' +import type { PackageManager } from '../constants' + +export function getPackageManager(): PackageManager | undefined { + const userAgent = process.env.npm_config_user_agent + + if (userAgent === undefined) { + return undefined + } + + const packageManager = SUPPORTED_PACKAGE_MANAGERS.find((manager) => + userAgent.startsWith(manager), + ) + + return packageManager +} diff --git a/packages/create-start/src/utils/helpers/helperFactory.ts b/packages/create-start/src/utils/helpers/helperFactory.ts new file mode 100644 index 0000000000..1f0fa6afb8 --- /dev/null +++ b/packages/create-start/src/utils/helpers/helperFactory.ts @@ -0,0 +1,20 @@ +export type Ctx = { + getFullModulePath: (relativePath: string) => string + getFullTargetPath: (relativePath: string) => string + targetFileExists: (relativePath: string) => Promise + moduleFileExists: (relativePath: string) => Promise + absoluteTargetFolder: string + absoluteModuleFolder: string +} + +type HelperFn any> = (args: { + modulePath: string + targetPath: string + ctx: Ctx +}) => T + +export const helperFactory = any>( + fn: HelperFn, +) => { + return fn +} diff --git a/packages/create-start/src/utils/helpers/index.ts b/packages/create-start/src/utils/helpers/index.ts new file mode 100644 index 0000000000..051bdf7a2c --- /dev/null +++ b/packages/create-start/src/utils/helpers/index.ts @@ -0,0 +1,284 @@ +import path, { resolve } from 'node:path' +import fs, { + access, + copyFile, + mkdir, + readFile, + readdir, + stat, + writeFile, +} from 'node:fs/promises' +import invariant from 'tiny-invariant' +import fastGlob from 'fast-glob' +import { createDebugger } from '../debug' + +import { helperFactory } from './helperFactory' +import type { Ctx } from './helperFactory' + +const debug = createDebugger('helpers') + +export const initHelpers = (modulePath: string, targetPath: string) => { + debug.info('Initializing helpers', { modulePath, targetPath }) + + const getFullModulePath = (relativePath: string) => { + const fullPath = path.join(modulePath, relativePath) + debug.trace('Getting full module path', { relativePath, fullPath }) + return fullPath + } + + const getFullTargetPath = (relativePath: string) => { + const fullPath = path.join(targetPath, relativePath) + debug.trace('Getting full target path', { relativePath, fullPath }) + return fullPath + } + + const targetFileExists = async (relativePath: string) => { + const path = resolve(targetPath, relativePath) + debug.trace('Checking if target file exists', { path }) + return await checkFileExists(path) + } + + const moduleFileExists = async (relativePath: string) => { + const path = resolve(modulePath, relativePath) + debug.trace('Checking if module file exists', { path }) + return await checkFileExists(path) + } + + const ctx: Ctx = { + targetFileExists, + moduleFileExists, + absoluteModuleFolder: getFullModulePath(modulePath), + absoluteTargetFolder: getFullTargetPath(targetPath), + getFullModulePath, + getFullTargetPath, + } + + debug.verbose('Created helper context', ctx) + + return { + ...ctx, + readTargetFile: createReadTargetFile({ + ctx, + modulePath, + targetPath, + }), + writeTargetfile: createWriteTargetFile({ + ctx, + modulePath, + targetPath, + }), + copyTemplateFiles: createCopyTemplateFiles({ + ctx, + modulePath, + targetPath, + }), + getTemplateFilesThatWouldBeOverwritten: + createGetTemplateFilesThatWouldBeOverwritten({ + ctx, + modulePath, + targetPath, + }), + } +} + +export const checkFileExists = async (path: string): Promise => { + debug.trace('Checking if file exists', { path }) + try { + await access(path, fs.constants.F_OK) + return true + } catch { + return false + } +} + +export const checkFolderExists = async (path: string): Promise => { + debug.trace('Checking if folder exists', { path }) + try { + await access(path, fs.constants.R_OK) + return true + } catch { + return false + } +} + +export const checkFolderIsEmpty = async (path: string): Promise => { + debug.trace('Checking if folder is empty', { path }) + try { + const files = await readdir(path) + return files.length === 0 + } catch { + return false + } +} + +const createReadTargetFile = helperFactory( + ({ ctx, targetPath }) => + async (relativePath: string) => { + debug.trace('Reading target file', { relativePath, targetPath }) + invariant( + await ctx.targetFileExists(relativePath), + `The file ${relativePath} doesn't exist`, + ) + const path = resolve(targetPath, relativePath) + return await readFile(path, 'utf-8') + }, +) + +export const createWriteTargetFile = helperFactory( + ({ targetPath }) => + async ( + relativePath: string, + content: string, + overwrite: boolean = false, + ) => { + debug.trace('Writing target file', { + relativePath, + targetPath, + overwrite, + }) + const path = resolve(targetPath, relativePath) + invariant( + !(!overwrite && (await checkFileExists(path))), + `File ${relativePath} already exists and overwrite is false`, + ) + await writeFile(path, content) + }, +) + +const DOT_PREFIX = '_dot_' + +const removeTsNoCheckHeader = async (filePath: string) => { + debug.trace('Removing ts-nocheck header', { filePath }) + // Template files will sometimes include // @ts-nocheck in the header of the file + // This is to avoid type checking in the template folders + // This function removes that header + + const content = await readFile(filePath, 'utf-8') + const lines = content.split('\n') + + let newContent = content + + if (lines[0]?.trim() === '// @ts-nocheck') { + newContent = lines.slice(1).join('\n').trimStart() + } + + await writeFile(filePath, newContent) +} + +async function copyDir(srcDir: string, destDir: string) { + debug.trace('Copying directory', { srcDir, destDir }) + await mkdir(destDir, { recursive: true }) + const files = await readdir(srcDir) + for (const file of files) { + const srcFile = resolve(srcDir, file) + const destFile = resolve(destDir, file) + await copy(srcFile, destFile) + } +} + +async function copy(src: string, dest: string) { + debug.trace('Copying file', { src, dest }) + const statResult = await stat(src) + const replacedDest = dest.replaceAll(DOT_PREFIX, '.') + if (statResult.isDirectory()) { + await copyDir(src, replacedDest) + } else { + await copyFile(src, replacedDest) + await removeTsNoCheckHeader(replacedDest) + } +} + +export const createGetTemplateFilesThatWouldBeOverwritten = helperFactory( + ({ ctx }) => + async ({ + file, + templateFolder, + targetFolder, + overwrite, + }: { + file: string + templateFolder: string + targetFolder: string + overwrite: boolean + }) => { + debug.verbose('Checking for files that would be overwritten', { + file, + templateFolder, + targetFolder, + overwrite, + }) + const overwrittenFiles: Array = [] + + if (overwrite) [] + + const absoluteTemplateFolder = ctx.getFullModulePath(templateFolder) + const absoluteTargetFolder = ctx.getFullTargetPath(targetFolder) + + const files = await fastGlob.glob(file, { + cwd: absoluteTemplateFolder, + onlyFiles: false, + }) + + for (const file of files) { + const exists = await checkFileExists( + resolve(absoluteTargetFolder, file), + ) + if (exists) { + debug.verbose('Found file that would be overwritten', { file }) + overwrittenFiles.push(file) + } + } + + return overwrittenFiles + }, +) + +export const createCopyTemplateFiles = helperFactory( + ({ ctx }) => + async ({ + file, + templateFolder, + targetFolder, + overwrite, + }: { + file: string + templateFolder: string + targetFolder: string + overwrite: boolean + }) => { + debug.verbose('Copying template files', { + file, + templateFolder, + targetFolder, + overwrite, + }) + const absoluteTemplateFolder = ctx.getFullModulePath(templateFolder) + const absoluteTargetFolder = ctx.getFullTargetPath(targetFolder) + + const templateFolderExists = checkFolderExists(absoluteTemplateFolder) + invariant( + templateFolderExists, + `The template folder ${templateFolder} doesn't exist`, + ) + + const files = await fastGlob.glob(file, { + cwd: absoluteTemplateFolder, + onlyFiles: false, + }) + + for (const file of files) { + if (overwrite) { + invariant( + await checkFileExists(resolve(absoluteTargetFolder, file)), + `The file ${file} couldn't be created because it would overwrite an existing file`, + ) + } + + debug.trace('Copying template file', { file }) + await copy( + resolve(absoluteTemplateFolder, file), + resolve(absoluteTargetFolder, file), + ) + } + }, +) diff --git a/packages/create-start/src/utils/runCmd.ts b/packages/create-start/src/utils/runCmd.ts new file mode 100644 index 0000000000..0b3634a294 --- /dev/null +++ b/packages/create-start/src/utils/runCmd.ts @@ -0,0 +1,10 @@ +import { spawnCommand } from './spawnCmd' + +export async function runCmd( + command: string, + args: Array, + env: NodeJS.ProcessEnv = {}, + cwd?: string, +) { + return spawnCommand(command, args, env, cwd) +} diff --git a/packages/create-start/src/utils/runPackageManagerCommand.ts b/packages/create-start/src/utils/runPackageManagerCommand.ts new file mode 100644 index 0000000000..9e7d054f20 --- /dev/null +++ b/packages/create-start/src/utils/runPackageManagerCommand.ts @@ -0,0 +1,26 @@ +import { spawnCommand } from './spawnCmd' +import type { PackageManager } from '../constants' + +export async function runPackageManagerCommand( + packageManager: PackageManager, + args: Array, + env: NodeJS.ProcessEnv = {}, + cwd?: string, +) { + return spawnCommand(packageManager, args, env, cwd) +} + +export async function install(packageManager: PackageManager, cwd?: string) { + return runPackageManagerCommand( + packageManager, + ['install'], + { + NODE_ENV: 'development', + }, + cwd, + ) +} + +export async function build(packageManager: PackageManager, cwd?: string) { + return runPackageManagerCommand(packageManager, ['run', 'build'], {}, cwd) +} diff --git a/packages/create-start/src/utils/spawnCmd.ts b/packages/create-start/src/utils/spawnCmd.ts new file mode 100644 index 0000000000..0525e2ae7b --- /dev/null +++ b/packages/create-start/src/utils/spawnCmd.ts @@ -0,0 +1,39 @@ +import spawn from 'cross-spawn' + +export async function spawnCommand( + command: string, + args: Array, + env: NodeJS.ProcessEnv = {}, + cwd?: string, +) { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { + env: { + ...process.env, + ...env, + }, + stdio: ['pipe', 'pipe', 'pipe'], + cwd, + }) + let stderrBuffer = '' + let stdoutBuffer = '' + + child.stderr?.on('data', (data) => { + stderrBuffer += data + }) + + child.stdout?.on('data', (data) => { + stdoutBuffer += data + }) + + child.on('close', (code) => { + if (code !== 0) { + reject( + `"${command} ${args.join(' ')}" failed ${stdoutBuffer} ${stderrBuffer}`, + ) + return + } + resolve() + }) + }) +} diff --git a/packages/create-start/src/utils/validateProjectName.ts b/packages/create-start/src/utils/validateProjectName.ts new file mode 100644 index 0000000000..dc1366ab4a --- /dev/null +++ b/packages/create-start/src/utils/validateProjectName.ts @@ -0,0 +1,25 @@ +import validate from 'validate-npm-package-name' + +type ValidatationResult = + | { + valid: true + } + | { + valid: false + problems: Array + } + +export function validateProjectName(name: string): ValidatationResult { + const nameValidation = validate(name) + if (nameValidation.validForNewPackages) { + return { valid: true } + } + + return { + valid: false, + problems: [ + ...(nameValidation.errors || []), + ...(nameValidation.warnings || []), + ], + } +} diff --git a/packages/create-start/tests/e2e/cli.test.ts b/packages/create-start/tests/e2e/cli.test.ts new file mode 100644 index 0000000000..c03a4fc767 --- /dev/null +++ b/packages/create-start/tests/e2e/cli.test.ts @@ -0,0 +1,135 @@ +import { readFile } from 'node:fs/promises' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' +import { temporaryDirectoryTask } from 'tempy' +import { runCli } from '../../src/cli' + +const constructCliArgs = ({ + template, + directory, + packageName, + packageManager, + installDeps, + initGit, + hideLogo, + ide, +}: { + template?: string + directory?: string + packageName?: string + packageManager?: string + installDeps?: boolean + initGit?: boolean + hideLogo?: boolean + ide?: string +}) => { + return [ + 'node', + 'cli.js', + '--template', + template ?? 'barebones', + '--directory', + directory ?? '', + '--package-name', + packageName ?? '', + '--package-manager', + packageManager ?? 'npm', + ...(installDeps === false ? ['--no-install-deps'] : []), + ...(initGit === false ? ['--no-init-git'] : []), + ...(hideLogo ? ['--hide-logo'] : []), + ...(ide ? ['--ide', ide] : []), + ] +} + +describe('cli e2e', () => { + it('should create a basic project structure using CLI', async () => { + await temporaryDirectoryTask(async (tempDir) => { + const args = constructCliArgs({ + template: 'barebones', + directory: tempDir, + packageName: 'test-package', + packageManager: 'npm', + installDeps: false, + hideLogo: true, + ide: 'vscode', + initGit: false, + }) + + await runCli(args) + + // Check core files exist + const expectedFiles = [ + '.gitignore', + 'package.json', + 'tsconfig.json', + 'app.config.ts', + 'app/client.tsx', + 'app/router.tsx', + 'app/routes/__root.tsx', + 'app/routes/index.tsx', + '.vscode/settings.json', + ] + + for (const file of expectedFiles) { + const filePath = join(tempDir, file) + const exists = await readFile(filePath) + expect(exists).toBeDefined() + } + }) + }) + + it('should create project in current directory when using "." as directory', async () => { + await temporaryDirectoryTask(async (tempDir) => { + const args = constructCliArgs({ + template: 'barebones', + directory: '.', + packageName: 'test-package', + packageManager: 'npm', + installDeps: false, + hideLogo: true, + ide: 'vscode', + initGit: false, + }) + + // Run CLI from the temporary directory + process.chdir(tempDir) + await runCli(args) + + // Check core files exist in current directory + const expectedFiles = [ + 'package.json', + 'tsconfig.json', + 'app.config.ts', + 'app/client.tsx', + 'app/router.tsx', + 'app/routes/__root.tsx', + 'app/routes/index.tsx', + '.vscode/settings.json', + ] + + for (const file of expectedFiles) { + const filePath = join(tempDir, file) + const exists = await readFile(filePath) + expect(exists).toBeDefined() + } + + // Reset working directory + process.chdir('..') + }) + }) + + it('should fail when using an incorrect directory name', async () => { + const args = constructCliArgs({ + template: 'barebones', + directory: '/invalid/directory/path', + packageName: 'test-package', + packageManager: 'npm', + installDeps: false, + hideLogo: true, + ide: 'vscode', + initGit: false, + }) + + await expect(runCli(args)).rejects.toThrow() + }) +}) diff --git a/packages/create-start/tests/e2e/templates/barebones.test.ts b/packages/create-start/tests/e2e/templates/barebones.test.ts new file mode 100644 index 0000000000..e46b37c122 --- /dev/null +++ b/packages/create-start/tests/e2e/templates/barebones.test.ts @@ -0,0 +1,79 @@ +import { join } from 'node:path' +import { readFile } from 'node:fs/promises' +import { describe, expect, it } from 'vitest' +import { temporaryDirectoryTask } from 'tempy' +import { barebonesTemplate } from '../../../src/templates/barebones' + +const base = (tmpDir: string) => ({ + cfg: { + packageManager: { + installDeps: false, + packageManager: 'npm' as const, + }, + git: { + setupGit: false, + }, + packageJson: { + type: 'new' as const, + name: 'test', + }, + ide: { + ide: 'vscode' as const, + }, + }, + targetPath: tmpDir, + type: 'new-project' as const, +}) + +describe('barebones template e2e', () => { + it('should create a basic project structure', async () => { + await temporaryDirectoryTask(async (tempDir) => { + await barebonesTemplate.execute(base(tempDir)) + + // Check core files exist + const expectedFiles = [ + 'package.json', + 'tsconfig.json', + 'app.config.ts', + 'app/client.tsx', + 'app/router.tsx', + 'app/routes/__root.tsx', + 'app/routes/index.tsx', + '.vscode/settings.json', + ] + + for (const file of expectedFiles) { + const filePath = join(tempDir, file) + const exists = await readFile(filePath) + expect(exists).toBeDefined() + } + }) + }) + + it('should have valid package.json contents', async () => { + await temporaryDirectoryTask(async (tempDir) => { + await barebonesTemplate.execute(base(tempDir)) + + const pkgJsonPath = join(tempDir, 'package.json') + const pkgJson = JSON.parse(await readFile(pkgJsonPath, 'utf-8')) + + expect(pkgJson.name).toBe('test') + expect(pkgJson.private).toBe(true) + expect(pkgJson.type).toBe('module') + expect(pkgJson.dependencies).toBeDefined() + expect(pkgJson.dependencies['@tanstack/react-router']).toBeDefined() + expect(pkgJson.dependencies['@tanstack/start']).toBeDefined() + expect(pkgJson.dependencies['react']).toBeDefined() + expect(pkgJson.dependencies['react-dom']).toBeDefined() + expect(pkgJson.dependencies['vinxi']).toBeDefined() + + expect(pkgJson.devDependencies).toBeDefined() + expect(pkgJson.devDependencies['@types/react']).toBeDefined() + + expect(pkgJson.scripts).toBeDefined() + expect(pkgJson.scripts.dev).toBe('vinxi dev') + expect(pkgJson.scripts.build).toBe('vinxi build') + expect(pkgJson.scripts.start).toBe('vinxi start') + }) + }) +}) diff --git a/packages/create-start/tsconfig.json b/packages/create-start/tsconfig.json new file mode 100644 index 0000000000..747f8933df --- /dev/null +++ b/packages/create-start/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "target": "ESNext", + "module": "ESNext" + }, + "moduleResolution": "Bundler", + "include": ["src", "tests", "build.config.ts"] +} diff --git a/packages/create-start/vitest.config.ts b/packages/create-start/vitest.config.ts new file mode 100644 index 0000000000..f5b965c618 --- /dev/null +++ b/packages/create-start/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vitest/config' +import packageJson from './package.json' + +const config = defineConfig({ + test: { + name: packageJson.name, + dir: './tests', + watch: false, + environment: 'node', + typecheck: { enabled: true }, + }, +}) + +export default config diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b4adb9bbf2..eb463fc375 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1999,10 +1999,10 @@ importers: version: 18.3.1 html-webpack-plugin: specifier: ^5.6.3 - version: 5.6.3(@rspack/core@1.1.5(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)) + version: 5.6.3(@rspack/core@1.1.5(@swc/helpers@0.5.15))(webpack@5.97.1) swc-loader: specifier: ^0.2.6 - version: 0.2.6(@swc/core@1.10.1(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)) + version: 0.2.6(@swc/core@1.10.1(@swc/helpers@0.5.15))(webpack@5.97.1) typescript: specifier: ^5.7.2 version: 5.7.2 @@ -3217,7 +3217,7 @@ importers: version: 4.3.4(vite@6.0.3(@types/node@22.10.1)(jiti@2.4.1)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)) html-webpack-plugin: specifier: ^5.6.0 - version: 5.6.3(@rspack/core@1.1.5(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)) + version: 5.6.3(@rspack/core@1.1.5(@swc/helpers@0.5.15))(webpack@5.97.1) react: specifier: ^18.3.1 version: 18.3.1 @@ -3226,7 +3226,7 @@ importers: version: 18.3.1(react@18.3.1) swc-loader: specifier: ^0.2.6 - version: 0.2.6(@swc/core@1.10.1(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)) + version: 0.2.6(@swc/core@1.10.1(@swc/helpers@0.5.15))(webpack@5.97.1) typescript: specifier: ^5.7.2 version: 5.7.2 @@ -3280,6 +3280,82 @@ importers: specifier: ^0.1.1 version: 0.1.1 + packages/create-start: + dependencies: + '@tanstack/react-router': + specifier: workspace:* + version: link:../react-router + '@tanstack/router-devtools': + specifier: workspace:* + version: link:../router-devtools + '@tanstack/start': + specifier: workspace:* + version: link:../start + '@types/react': + specifier: ^18.3.12 + version: 18.3.12 + '@types/react-dom': + specifier: ^18.3.1 + version: 18.3.1 + gradient-string: + specifier: ^3.0.0 + version: 3.0.0 + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + vinxi: + specifier: 0.4.3 + version: 0.4.3(@types/node@22.10.1)(ioredis@5.4.1)(terser@5.36.0)(typescript@5.7.2) + devDependencies: + '@commander-js/extra-typings': + specifier: ^12.1.0 + version: 12.1.0(commander@12.1.0) + '@inquirer/prompts': + specifier: ^5.5.0 + version: 5.5.0 + '@inquirer/type': + specifier: ^3.0.1 + version: 3.0.1(@types/node@22.10.1) + '@types/cross-spawn': + specifier: ^6.0.6 + version: 6.0.6 + '@types/validate-npm-package-name': + specifier: ^4.0.2 + version: 4.0.2 + cross-spawn: + specifier: ^7.0.5 + version: 7.0.6 + fast-glob: + specifier: ^3.3.2 + version: 3.3.2 + picocolors: + specifier: ^1.1.1 + version: 1.1.1 + rollup-plugin-copy: + specifier: ^3.5.0 + version: 3.5.0 + tempy: + specifier: ^3.1.0 + version: 3.1.0 + tiny-invariant: + specifier: ^1.3.3 + version: 1.3.3 + unbuild: + specifier: ^2.0.0 + version: 2.0.0(typescript@5.7.2)(vue-tsc@2.0.29(typescript@5.7.2)) + validate-npm-package-name: + specifier: ^5.0.1 + version: 5.0.1 + yocto-spinner: + specifier: ^0.1.1 + version: 0.1.1 + zod: + specifier: ^3.23.8 + version: 3.23.8 + packages/eslint-plugin-router: dependencies: '@typescript-eslint/utils': @@ -3913,6 +3989,11 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} + '@commander-js/extra-typings@12.1.0': + resolution: {integrity: sha512-wf/lwQvWAA0goIghcb91dQYpkLBcyhOhQNqG/VgWhnKzgt+UOMvra7EX/2fv70arm5RW+PUHoQHHDa6/p77Eqg==} + peerDependencies: + commander: ~12.1.0 + '@commitlint/parse@19.5.0': resolution: {integrity: sha512-cZ/IxfAlfWYhAQV0TwcbdR1Oc0/r0Ik1GEessDJ3Lbuma/MRO8FRQX76eurcXtmhJC//rj52ZSZuXUg0oIX0Fw==} engines: {node: '>=v18'} @@ -6051,6 +6132,12 @@ packages: '@types/express@4.17.21': resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} + '@types/fs-extra@8.1.5': + resolution: {integrity: sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==} + + '@types/glob@7.2.0': + resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -6075,6 +6162,9 @@ packages: '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/minimatch@5.1.2': + resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} + '@types/mute-stream@0.0.4': resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==} @@ -6123,6 +6213,9 @@ packages: '@types/statuses@2.0.5': resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==} + '@types/tinycolor2@1.4.6': + resolution: {integrity: sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==} + '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} @@ -6627,6 +6720,10 @@ packages: resolution: {integrity: sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==} engines: {node: '>=0.10.0'} + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -7076,9 +7173,21 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crossws@0.2.4: + resolution: {integrity: sha512-DAxroI2uSOgUKLz00NX6A8U/8EE3SZHmIND+10jkVSaypvyt57J5JEOxAQOL6lQxyzi/wZbTIwssU1uy69h5Vg==} + peerDependencies: + uWebSockets.js: '*' + peerDependenciesMeta: + uWebSockets.js: + optional: true + crossws@0.3.1: resolution: {integrity: sha512-HsZgeVYaG+b5zA+9PbIPGq4+J/CJynJuearykPsXx4V/eMhyQ5EDVg3Ak2FBZtVXCiOLu/U7IiwDHTr9MA+IKw==} + crypto-random-string@4.0.0: + resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==} + engines: {node: '>=12'} + css-declaration-sorter@7.2.0: resolution: {integrity: sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==} engines: {node: ^14 || ^16 || >=18} @@ -7875,6 +7984,10 @@ packages: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + fs-minipass@2.1.0: resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} engines: {node: '>= 8'} @@ -7987,6 +8100,10 @@ packages: resolution: {integrity: sha512-49TewVEz0UxZjr1WYYsWpPrhyC/B/pA8Bq0fUmet2n+eR7yn0IvNzNaoBwnK6mdkzcN+se7Ez9zUgULTz2QH4g==} engines: {node: '>=18'} + globby@10.0.1: + resolution: {integrity: sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==} + engines: {node: '>=8'} + globby@13.2.2: resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -8010,6 +8127,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + gradient-string@3.0.0: + resolution: {integrity: sha512-frdKI4Qi8Ihp4C6wZNB565de/THpIaw3DjP5ku87M+N9rNSGmPTjfkq61SdRXB7eCaL8O1hkKDvf6CDMtOzIAg==} + engines: {node: '>=14'} + graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} @@ -8021,6 +8142,9 @@ packages: resolution: {integrity: sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + h3@1.11.1: + resolution: {integrity: sha512-AbaH6IDnZN6nmbnJOH72y3c5Wwh9P97soSVdGSBbcDACRdkC0FEWf25pzx4f/NuOCK6quHmW18yF2Wx+G4Zi1A==} + h3@1.13.0: resolution: {integrity: sha512-vFEAu/yf8UMUcB4s43OaDaigcqpQd14yanmOsn+NcRX3/guSKncyE2rOYhq8RIchgJrPSs/QiIddnTTR1ddiAg==} @@ -8313,6 +8437,10 @@ packages: resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} engines: {node: '>=0.10.0'} + is-plain-object@3.0.1: + resolution: {integrity: sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==} + engines: {node: '>=0.10.0'} + is-plain-object@5.0.0: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} @@ -9709,6 +9837,10 @@ packages: engines: {node: 20 || >=22} hasBin: true + rollup-plugin-copy@3.5.0: + resolution: {integrity: sha512-wI8D5dvYovRMx/YYKtUNt3Yxaw4ORC9xo6Gt9t22kveWz1enG9QrhVlagzwrxSC455xD1dHMKhIJkbsQ7d48BA==} + engines: {node: '>=8.3'} + rollup-plugin-dts@6.1.1: resolution: {integrity: sha512-aSHRcJ6KG2IHIioYlvAOcEq6U99sVtqDDKVhnwt70rW6tsz3tv5OSjEiWcgzfsHdLyGXZ/3b/7b/+Za3Y6r1XA==} engines: {node: '>=16'} @@ -9886,6 +10018,10 @@ packages: resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==} engines: {node: '>=8'} + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + slash@4.0.0: resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} engines: {node: '>=12'} @@ -10101,6 +10237,14 @@ packages: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + temp-dir@3.0.0: + resolution: {integrity: sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==} + engines: {node: '>=14.16'} + + tempy@3.1.0: + resolution: {integrity: sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==} + engines: {node: '>=14.16'} + terser-webpack-plugin@5.3.10: resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} engines: {node: '>= 10.13.0'} @@ -10157,6 +10301,9 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + tinyexec@0.3.1: resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} @@ -10164,6 +10311,9 @@ packages: resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} engines: {node: '>=12.0.0'} + tinygradient@1.1.5: + resolution: {integrity: sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==} + tinypool@1.0.2: resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -10274,6 +10424,10 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} + type-fest@1.4.0: + resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} + engines: {node: '>=10'} + type-fest@2.19.0: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} @@ -10399,6 +10553,10 @@ packages: unimport@3.14.3: resolution: {integrity: sha512-yEJps4GW7jBdoQlxEV0ElBCJsJmH8FdZtk4oog0y++8hgLh0dGnDpE4oaTc0Lfx4N5rRJiGFUWHrBqC8CyUBmQ==} + unique-string@3.0.0: + resolution: {integrity: sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==} + engines: {node: '>=12'} + universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -10554,6 +10712,10 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vinxi@0.4.3: + resolution: {integrity: sha512-RgJz7RWftML5h/qfPsp3QKVc2FSlvV4+HevpE0yEY2j+PS/I2ULjoSsZDXaR8Ks2WYuFFDzQr8yrox7v8aqkng==} + hasBin: true + vinxi@0.5.1: resolution: {integrity: sha512-jvl2hJ0fyWwfDVQdDDHCJiVxqU4k0A6kFAnljS0kIjrGfhdTvKEWIoj0bcJgMyrKhxNMoZZGmHZsstQgjDIL3g==} hasBin: true @@ -11173,6 +11335,10 @@ snapshots: '@colors/colors@1.5.0': optional: true + '@commander-js/extra-typings@12.1.0(commander@12.1.0)': + dependencies: + commander: 12.1.0 + '@commitlint/parse@19.5.0': dependencies: '@commitlint/types': 19.5.0 @@ -13071,6 +13237,15 @@ snapshots: '@types/qs': 6.9.17 '@types/serve-static': 1.15.7 + '@types/fs-extra@8.1.5': + dependencies: + '@types/node': 22.10.1 + + '@types/glob@7.2.0': + dependencies: + '@types/minimatch': 5.1.2 + '@types/node': 22.10.1 + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -13093,6 +13268,8 @@ snapshots: '@types/mime@1.3.5': {} + '@types/minimatch@5.1.2': {} + '@types/mute-stream@0.0.4': dependencies: '@types/node': 22.10.1 @@ -13147,6 +13324,8 @@ snapshots: '@types/statuses@2.0.5': {} + '@types/tinycolor2@1.4.6': {} + '@types/tough-cookie@4.0.5': {} '@types/unist@3.0.3': {} @@ -13568,17 +13747,17 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 - '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4))': + '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4)(webpack@5.97.1)': dependencies: webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1) - '@webpack-cli/info@2.0.2(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4))': + '@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.97.1)': dependencies: webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1) - '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1))(webpack-dev-server@5.1.0(webpack-cli@5.1.4)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4))': + '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack-dev-server@5.1.0)(webpack@5.97.1)': dependencies: webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1) @@ -13788,6 +13967,8 @@ snapshots: array-slice@1.1.0: {} + array-union@2.1.0: {} + assertion-error@2.0.1: {} ast-types@0.16.1: @@ -14252,10 +14433,16 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crossws@0.2.4: {} + crossws@0.3.1: dependencies: uncrypto: 0.1.3 + crypto-random-string@4.0.0: + dependencies: + type-fest: 1.4.0 + css-declaration-sorter@7.2.0(postcss@8.4.49): dependencies: postcss: 8.4.49 @@ -15257,6 +15444,12 @@ snapshots: jsonfile: 4.0.0 universalify: 0.1.2 + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + fs-minipass@2.1.0: dependencies: minipass: 3.3.6 @@ -15385,6 +15578,17 @@ snapshots: globals@15.13.0: {} + globby@10.0.1: + dependencies: + '@types/glob': 7.2.0 + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + glob: 7.2.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + globby@13.2.2: dependencies: dir-glob: 3.0.1 @@ -15414,6 +15618,11 @@ snapshots: graceful-fs@4.2.11: {} + gradient-string@3.0.0: + dependencies: + chalk: 5.3.0 + tinygradient: 1.1.5 + graphemer@1.4.0: {} graphql@16.9.0: {} @@ -15422,6 +15631,21 @@ snapshots: dependencies: duplexer: 0.1.2 + h3@1.11.1: + dependencies: + cookie-es: 1.2.2 + crossws: 0.2.4 + defu: 6.1.4 + destr: 2.0.3 + iron-webcrypto: 1.2.1 + ohash: 1.1.4 + radix3: 1.1.2 + ufo: 1.5.4 + uncrypto: 0.1.3 + unenv: 1.10.0 + transitivePeerDependencies: + - uWebSockets.js + h3@1.13.0: dependencies: cookie-es: 1.2.2 @@ -15490,7 +15714,7 @@ snapshots: relateurl: 0.2.7 terser: 5.36.0 - html-webpack-plugin@5.6.3(@rspack/core@1.1.5(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)): + html-webpack-plugin@5.6.3(@rspack/core@1.1.5(@swc/helpers@0.5.15))(webpack@5.97.1): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -15713,6 +15937,8 @@ snapshots: dependencies: isobject: 3.0.1 + is-plain-object@3.0.1: {} + is-plain-object@5.0.0: {} is-potential-custom-element-name@1.0.1: {} @@ -17153,6 +17379,14 @@ snapshots: glob: 11.0.0 package-json-from-dist: 1.0.1 + rollup-plugin-copy@3.5.0: + dependencies: + '@types/fs-extra': 8.1.5 + colorette: 1.4.0 + fs-extra: 8.1.0 + globby: 10.0.1 + is-plain-object: 3.0.1 + rollup-plugin-dts@6.1.1(rollup@3.29.5)(typescript@5.7.2): dependencies: magic-string: 0.30.14 @@ -17371,6 +17605,8 @@ snapshots: dependencies: unicode-emoji-modifier-base: 1.0.0 + slash@3.0.0: {} + slash@4.0.0: {} slash@5.1.0: {} @@ -17551,7 +17787,7 @@ snapshots: csso: 5.0.5 picocolors: 1.1.1 - swc-loader@0.2.6(@swc/core@1.10.1(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)): + swc-loader@0.2.6(@swc/core@1.10.1(@swc/helpers@0.5.15))(webpack@5.97.1): dependencies: '@swc/core': 1.10.1(@swc/helpers@0.5.15) '@swc/counter': 0.1.3 @@ -17621,26 +17857,35 @@ snapshots: mkdirp: 1.0.4 yallist: 4.0.0 - terser-webpack-plugin@5.3.10(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)): + temp-dir@3.0.0: {} + + tempy@3.1.0: + dependencies: + is-stream: 3.0.0 + temp-dir: 3.0.0 + type-fest: 2.19.0 + unique-string: 3.0.0 + + terser-webpack-plugin@5.3.10(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.2 terser: 5.36.0 - webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4) + webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0) optionalDependencies: '@swc/core': 1.10.1(@swc/helpers@0.5.15) esbuild: 0.24.0 - terser-webpack-plugin@5.3.10(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)): + terser-webpack-plugin@5.3.10(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack@5.97.1): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.2 terser: 5.36.0 - webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0) + webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4) optionalDependencies: '@swc/core': 1.10.1(@swc/helpers@0.5.15) esbuild: 0.24.0 @@ -17678,6 +17923,8 @@ snapshots: tinybench@2.9.0: {} + tinycolor2@1.6.0: {} + tinyexec@0.3.1: {} tinyglobby@0.2.10: @@ -17685,6 +17932,11 @@ snapshots: fdir: 6.4.2(picomatch@4.0.2) picomatch: 4.0.2 + tinygradient@1.1.5: + dependencies: + '@types/tinycolor2': 1.4.6 + tinycolor2: 1.6.0 + tinypool@1.0.2: {} tinyrainbow@1.2.0: {} @@ -17772,6 +18024,8 @@ snapshots: type-fest@0.21.3: {} + type-fest@1.4.0: {} + type-fest@2.19.0: {} type-fest@4.30.0: {} @@ -17909,6 +18163,10 @@ snapshots: transitivePeerDependencies: - rollup + unique-string@3.0.0: + dependencies: + crypto-random-string: 4.0.0 + universalify@0.1.2: {} universalify@0.2.0: {} @@ -18022,6 +18280,76 @@ snapshots: vary@1.1.2: {} + vinxi@0.4.3(@types/node@22.10.1)(ioredis@5.4.1)(terser@5.36.0)(typescript@5.7.2): + dependencies: + '@babel/core': 7.26.0 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) + '@types/micromatch': 4.0.9 + '@vinxi/listhen': 1.5.6 + boxen: 7.1.1 + chokidar: 3.6.0 + citty: 0.1.6 + consola: 3.2.3 + crossws: 0.2.4 + dax-sh: 0.39.2 + defu: 6.1.4 + es-module-lexer: 1.5.4 + esbuild: 0.20.2 + fast-glob: 3.3.2 + get-port-please: 3.1.2 + h3: 1.11.1 + hookable: 5.5.3 + http-proxy: 1.18.1 + micromatch: 4.0.8 + nitropack: 2.10.4(typescript@5.7.2) + node-fetch-native: 1.6.4 + path-to-regexp: 6.3.0 + pathe: 1.1.2 + radix3: 1.1.2 + resolve: 1.22.8 + serve-placeholder: 2.0.2 + serve-static: 1.16.2 + ufo: 1.5.4 + unctx: 2.3.1 + unenv: 1.10.0 + unstorage: 1.13.1(ioredis@5.4.1) + vite: 5.4.11(@types/node@22.10.1)(terser@5.36.0) + zod: 3.23.8 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@electric-sql/pglite' + - '@libsql/client' + - '@netlify/blobs' + - '@planetscale/database' + - '@types/node' + - '@upstash/redis' + - '@vercel/kv' + - better-sqlite3 + - debug + - drizzle-orm + - encoding + - idb-keyval + - ioredis + - less + - lightningcss + - mysql2 + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - typescript + - uWebSockets.js + - xml2js + vinxi@0.5.1(@types/node@22.10.1)(ioredis@5.4.1)(jiti@2.4.1)(terser@5.36.0)(tsx@4.19.2)(typescript@5.7.2)(yaml@2.6.1): dependencies: '@babel/core': 7.26.0 @@ -18260,9 +18588,9 @@ snapshots: webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1): dependencies: '@discoveryjs/json-ext': 0.5.7 - '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)) - '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)) - '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1))(webpack-dev-server@5.1.0(webpack-cli@5.1.4)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)) + '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4)(webpack@5.97.1) + '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4)(webpack@5.97.1) + '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4)(webpack-dev-server@5.1.0)(webpack@5.97.1) colorette: 2.0.20 commander: 10.0.1 cross-spawn: 7.0.6 @@ -18276,7 +18604,7 @@ snapshots: optionalDependencies: webpack-dev-server: 5.1.0(webpack-cli@5.1.4)(webpack@5.97.1) - webpack-dev-middleware@7.4.2(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)): + webpack-dev-middleware@7.4.2(webpack@5.97.1): dependencies: colorette: 2.0.20 memfs: 4.14.1 @@ -18315,7 +18643,7 @@ snapshots: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.2(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)) + webpack-dev-middleware: 7.4.2(webpack@5.97.1) ws: 8.18.0 optionalDependencies: webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4) @@ -18388,7 +18716,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)) + terser-webpack-plugin: 5.3.10(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack@5.97.1) watchpack: 2.4.2 webpack-sources: 3.2.3 optionalDependencies: diff --git a/scripts/publish.js b/scripts/publish.js index 224c6fa6be..8dcd76481b 100644 --- a/scripts/publish.js +++ b/scripts/publish.js @@ -76,6 +76,10 @@ await publish({ name: '@tanstack/eslint-plugin-router', packageDir: 'packages/eslint-plugin-router', }, + { + name: '@tanstack/create-start', + packageDir: 'packages/create-start', + }, ], branchConfigs: { main: { From f2dbdff7e3ea58488c161595558592ab6b526ff4 Mon Sep 17 00:00:00 2001 From: Arnaud Kleinpeter Date: Fri, 20 Dec 2024 11:53:13 +0100 Subject: [PATCH 20/38] docs: Minor typo in middleware.md (#3042) --- docs/framework/react/start/middleware.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/framework/react/start/middleware.md b/docs/framework/react/start/middleware.md index 41d8b9e650..72b2c3b9be 100644 --- a/docs/framework/react/start/middleware.md +++ b/docs/framework/react/start/middleware.md @@ -5,7 +5,7 @@ title: Middleware ## What is Middleware? -Middleware allows you to customized the behavior of server functions created with `createServerFn` with things like shared validation, context, and much more. Middleware can even depend on other middleware to create a chain of operations that are executed hierarchically and in order. +Middleware allows you to customize the behavior of server functions created with `createServerFn` with things like shared validation, context, and much more. Middleware can even depend on other middleware to create a chain of operations that are executed hierarchically and in order. ## What kinds of things can I do with Middleware? From efed787726353ebc2c5eaeadd85fa6e683acafe7 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Fri, 20 Dec 2024 10:56:38 +0000 Subject: [PATCH 21/38] release: v1.92.0 --- packages/create-start/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-start/package.json b/packages/create-start/package.json index 0a1f6db22d..aae6a88bae 100644 --- a/packages/create-start/package.json +++ b/packages/create-start/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/create-start", - "version": "1.81.5", + "version": "1.92.0", "description": "Modern and scalable routing for React applications", "author": "Tim O'Connell", "license": "MIT", From 0310d21eb7cf5ba9b286fbe296541183c06aa4d1 Mon Sep 17 00:00:00 2001 From: Joshua Knauber <49953016+joshuaKnauber@users.noreply.github.com> Date: Sun, 22 Dec 2024 00:49:06 +0100 Subject: [PATCH 22/38] refactor(react-router): expose `scrollBehavior` on the `ScrollRestoration` component (#3053) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Sean Cassiere <33615041+SeanCassiere@users.noreply.github.com> --- .../framework/react/guide/scroll-restoration.md | 17 +++++++++++++++++ .../react-router/src/scroll-restoration.tsx | 7 ++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/framework/react/guide/scroll-restoration.md b/docs/framework/react/guide/scroll-restoration.md index 2f53c0519e..7b56faa245 100644 --- a/docs/framework/react/guide/scroll-restoration.md +++ b/docs/framework/react/guide/scroll-restoration.md @@ -137,3 +137,20 @@ function Component() { ) } ``` + +## Scroll Behavior + +To control the scroll behavior when navigating between pages, you can use the `scrollBehavior` option. This allows you to make the transition between pages instant instead of a smooth scroll. The global configuration of scroll restoration behavior has the same options as those supported by the browser, which are `smooth`, `instant`, and `auto` (see [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView#behavior) for more information). + +```tsx +import { ScrollRestoration } from '@tanstack/react-router' + +function Root() { + return ( + <> + + + + ) +} +``` diff --git a/packages/react-router/src/scroll-restoration.tsx b/packages/react-router/src/scroll-restoration.tsx index 26a3861e90..6d764bebcf 100644 --- a/packages/react-router/src/scroll-restoration.tsx +++ b/packages/react-router/src/scroll-restoration.tsx @@ -45,6 +45,7 @@ const cache: Cache = sessionsStorage export type ScrollRestorationOptions = { getKey?: (location: ParsedLocation) => string + scrollBehavior?: ScrollToOptions['behavior'] } /** @@ -154,7 +155,11 @@ export function useScrollRestoration(options?: ScrollRestorationOptions) { if (key === restoreKey) { if (elementSelector === windowKey) { windowRestored = true - window.scrollTo(entry.scrollX, entry.scrollY) + window.scrollTo({ + top: entry.scrollY, + left: entry.scrollX, + behavior: options?.scrollBehavior, + }) } else if (elementSelector) { const element = document.querySelector(elementSelector) if (element) { From f8b04e6ac2ab3085b9f60fedb60bdfcea22aef50 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Sat, 21 Dec 2024 23:54:12 +0000 Subject: [PATCH 23/38] release: v1.92.1 --- examples/react/authenticated-routes/package.json | 4 ++-- .../react/basic-default-search-params/package.json | 4 ++-- .../react/basic-file-based-codesplitting/package.json | 4 ++-- examples/react/basic-file-based/package.json | 4 ++-- .../react/basic-react-query-file-based/package.json | 4 ++-- examples/react/basic-react-query/package.json | 4 ++-- examples/react/basic-ssr-file-based/package.json | 6 +++--- .../react/basic-ssr-streaming-file-based/package.json | 6 +++--- examples/react/basic-virtual-file-based/package.json | 4 ++-- .../react/basic-virtual-inside-file-based/package.json | 4 ++-- examples/react/basic/package.json | 4 ++-- examples/react/deferred-data/package.json | 4 ++-- examples/react/kitchen-sink-file-based/package.json | 4 ++-- .../kitchen-sink-react-query-file-based/package.json | 4 ++-- examples/react/kitchen-sink-react-query/package.json | 4 ++-- examples/react/kitchen-sink/package.json | 4 ++-- examples/react/large-file-based/package.json | 4 ++-- examples/react/location-masking/package.json | 4 ++-- examples/react/navigation-blocking/package.json | 4 ++-- .../react/quickstart-esbuild-file-based/package.json | 4 ++-- examples/react/quickstart-file-based/package.json | 4 ++-- .../react/quickstart-rspack-file-based/package.json | 4 ++-- .../react/quickstart-webpack-file-based/package.json | 4 ++-- examples/react/quickstart/package.json | 4 ++-- .../react/router-monorepo-react-query/package.json | 4 ++-- .../packages/app/package.json | 2 +- .../packages/router/package.json | 2 +- examples/react/router-monorepo-simple/package.json | 4 ++-- .../router-monorepo-simple/packages/app/package.json | 2 +- .../packages/router/package.json | 2 +- examples/react/scroll-restoration/package.json | 4 ++-- examples/react/search-validator-adapters/package.json | 10 +++++----- examples/react/start-basic-auth/package.json | 6 +++--- examples/react/start-basic-react-query/package.json | 8 ++++---- examples/react/start-basic-rsc/package.json | 6 +++--- examples/react/start-basic/package.json | 6 +++--- examples/react/start-clerk-basic/package.json | 6 +++--- examples/react/start-convex-trellaux/package.json | 8 ++++---- examples/react/start-counter/package.json | 4 ++-- examples/react/start-large/package.json | 6 +++--- examples/react/start-supabase-basic/package.json | 6 +++--- examples/react/start-trellaux/package.json | 8 ++++---- examples/react/with-framer-motion/package.json | 4 ++-- examples/react/with-trpc-react-query/package.json | 4 ++-- examples/react/with-trpc/package.json | 4 ++-- packages/arktype-adapter/package.json | 2 +- packages/create-router/package.json | 2 +- packages/create-start/package.json | 2 +- packages/react-router-with-query/package.json | 2 +- packages/react-router/package.json | 2 +- packages/router-devtools/package.json | 2 +- packages/start/package.json | 2 +- packages/valibot-adapter/package.json | 2 +- packages/zod-adapter/package.json | 2 +- 54 files changed, 112 insertions(+), 112 deletions(-) diff --git a/examples/react/authenticated-routes/package.json b/examples/react/authenticated-routes/package.json index 1edb5629fa..d4835ade48 100644 --- a/examples/react/authenticated-routes/package.json +++ b/examples/react/authenticated-routes/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "@tanstack/router-plugin": "^1.91.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/basic-default-search-params/package.json b/examples/react/basic-default-search-params/package.json index 603825423e..8a1e90f37f 100644 --- a/examples/react/basic-default-search-params/package.json +++ b/examples/react/basic-default-search-params/package.json @@ -10,8 +10,8 @@ }, "dependencies": { "@tanstack/react-query": "^5.62.3", - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "redaxios": "^0.5.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/basic-file-based-codesplitting/package.json b/examples/react/basic-file-based-codesplitting/package.json index 2a2c43cfc2..88499cad41 100644 --- a/examples/react/basic-file-based-codesplitting/package.json +++ b/examples/react/basic-file-based-codesplitting/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "@tanstack/router-plugin": "^1.91.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/basic-file-based/package.json b/examples/react/basic-file-based/package.json index a74a8194f8..e82e62f253 100644 --- a/examples/react/basic-file-based/package.json +++ b/examples/react/basic-file-based/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "@tanstack/router-plugin": "^1.91.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/basic-react-query-file-based/package.json b/examples/react/basic-react-query-file-based/package.json index a1d75f0e89..b85ef3e317 100644 --- a/examples/react/basic-react-query-file-based/package.json +++ b/examples/react/basic-react-query-file-based/package.json @@ -11,8 +11,8 @@ "dependencies": { "@tanstack/react-query": "^5.62.3", "@tanstack/react-query-devtools": "^5.62.3", - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "@tanstack/router-plugin": "^1.91.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/basic-react-query/package.json b/examples/react/basic-react-query/package.json index 817956d94b..7a5fe86251 100644 --- a/examples/react/basic-react-query/package.json +++ b/examples/react/basic-react-query/package.json @@ -11,8 +11,8 @@ "dependencies": { "@tanstack/react-query": "^5.62.3", "@tanstack/react-query-devtools": "^5.62.3", - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "react": "^18.2.0", "react-dom": "^18.2.0", "redaxios": "^0.5.1" diff --git a/examples/react/basic-ssr-file-based/package.json b/examples/react/basic-ssr-file-based/package.json index 01a4f30898..d00940b5d4 100644 --- a/examples/react/basic-ssr-file-based/package.json +++ b/examples/react/basic-ssr-file-based/package.json @@ -11,10 +11,10 @@ "debug": "node --inspect-brk server" }, "dependencies": { - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "@tanstack/router-plugin": "^1.91.1", - "@tanstack/start": "^1.91.3", + "@tanstack/start": "^1.92.1", "get-port": "^7.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/basic-ssr-streaming-file-based/package.json b/examples/react/basic-ssr-streaming-file-based/package.json index 1259dae6b1..ab1ef65bae 100644 --- a/examples/react/basic-ssr-streaming-file-based/package.json +++ b/examples/react/basic-ssr-streaming-file-based/package.json @@ -11,10 +11,10 @@ "debug": "node --inspect-brk server" }, "dependencies": { - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "@tanstack/router-plugin": "^1.91.1", - "@tanstack/start": "^1.91.3", + "@tanstack/start": "^1.92.1", "get-port": "^7.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/basic-virtual-file-based/package.json b/examples/react/basic-virtual-file-based/package.json index 469479cae7..5dec2951fc 100644 --- a/examples/react/basic-virtual-file-based/package.json +++ b/examples/react/basic-virtual-file-based/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "@tanstack/router-plugin": "^1.91.1", "@tanstack/virtual-file-routes": "^1.87.6", "react": "^18.2.0", diff --git a/examples/react/basic-virtual-inside-file-based/package.json b/examples/react/basic-virtual-inside-file-based/package.json index d0902c695d..eccab24d2f 100644 --- a/examples/react/basic-virtual-inside-file-based/package.json +++ b/examples/react/basic-virtual-inside-file-based/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "@tanstack/router-plugin": "^1.91.1", "@tanstack/virtual-file-routes": "^1.87.6", "react": "^18.2.0", diff --git a/examples/react/basic/package.json b/examples/react/basic/package.json index a3570ae5a3..2a64646565 100644 --- a/examples/react/basic/package.json +++ b/examples/react/basic/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "react": "^18.2.0", "react-dom": "^18.2.0", "redaxios": "^0.5.1" diff --git a/examples/react/deferred-data/package.json b/examples/react/deferred-data/package.json index bae3fa1ffd..62b8d3825b 100644 --- a/examples/react/deferred-data/package.json +++ b/examples/react/deferred-data/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "redaxios": "^0.5.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/kitchen-sink-file-based/package.json b/examples/react/kitchen-sink-file-based/package.json index 25f5312334..83cd9c4ba3 100644 --- a/examples/react/kitchen-sink-file-based/package.json +++ b/examples/react/kitchen-sink-file-based/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "@tanstack/router-plugin": "^1.91.1", "immer": "^10.1.1", "react": "^18.2.0", diff --git a/examples/react/kitchen-sink-react-query-file-based/package.json b/examples/react/kitchen-sink-react-query-file-based/package.json index 4f20caaebd..61fb97c1e2 100644 --- a/examples/react/kitchen-sink-react-query-file-based/package.json +++ b/examples/react/kitchen-sink-react-query-file-based/package.json @@ -11,8 +11,8 @@ "dependencies": { "@tanstack/react-query": "^5.62.3", "@tanstack/react-query-devtools": "^5.62.3", - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "@tanstack/router-plugin": "^1.91.1", "immer": "^10.1.1", "react": "^18.2.0", diff --git a/examples/react/kitchen-sink-react-query/package.json b/examples/react/kitchen-sink-react-query/package.json index 963c9177a1..8618a5a005 100644 --- a/examples/react/kitchen-sink-react-query/package.json +++ b/examples/react/kitchen-sink-react-query/package.json @@ -11,8 +11,8 @@ "dependencies": { "@tanstack/react-query": "^5.62.3", "@tanstack/react-query-devtools": "^5.62.3", - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "redaxios": "^0.5.1", "immer": "^10.1.1", "react": "^18.2.0", diff --git a/examples/react/kitchen-sink/package.json b/examples/react/kitchen-sink/package.json index 40dc7ff942..08de509b7d 100644 --- a/examples/react/kitchen-sink/package.json +++ b/examples/react/kitchen-sink/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "redaxios": "^0.5.1", "immer": "^10.1.1", "react": "^18.2.0", diff --git a/examples/react/large-file-based/package.json b/examples/react/large-file-based/package.json index 5ec9fbae23..22da8997ba 100644 --- a/examples/react/large-file-based/package.json +++ b/examples/react/large-file-based/package.json @@ -12,8 +12,8 @@ }, "dependencies": { "@tanstack/react-query": "^5.62.3", - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "@tanstack/router-plugin": "^1.91.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/location-masking/package.json b/examples/react/location-masking/package.json index 153bbec3f0..aac761ba11 100644 --- a/examples/react/location-masking/package.json +++ b/examples/react/location-masking/package.json @@ -11,8 +11,8 @@ "dependencies": { "@radix-ui/react-dialog": "^1.1.2", "@tanstack/react-query": "^5.62.3", - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "redaxios": "^0.5.1", "react": "^18.2.0", "react-dom": "^18.2.0" diff --git a/examples/react/navigation-blocking/package.json b/examples/react/navigation-blocking/package.json index 85d0269d89..112c070b34 100644 --- a/examples/react/navigation-blocking/package.json +++ b/examples/react/navigation-blocking/package.json @@ -10,8 +10,8 @@ }, "dependencies": { "@tanstack/react-query": "^5.62.3", - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "redaxios": "^0.5.1", "react": "^18.2.0", "react-dom": "^18.2.0" diff --git a/examples/react/quickstart-esbuild-file-based/package.json b/examples/react/quickstart-esbuild-file-based/package.json index 9a75ed9e7d..2cd2dc8c5c 100644 --- a/examples/react/quickstart-esbuild-file-based/package.json +++ b/examples/react/quickstart-esbuild-file-based/package.json @@ -9,8 +9,8 @@ "start": "dev" }, "dependencies": { - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "@tanstack/router-plugin": "^1.91.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/quickstart-file-based/package.json b/examples/react/quickstart-file-based/package.json index 8a1cd02d0c..6f8c5be086 100644 --- a/examples/react/quickstart-file-based/package.json +++ b/examples/react/quickstart-file-based/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "@tanstack/router-plugin": "^1.91.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/quickstart-rspack-file-based/package.json b/examples/react/quickstart-rspack-file-based/package.json index b365b23269..51373d8d1d 100644 --- a/examples/react/quickstart-rspack-file-based/package.json +++ b/examples/react/quickstart-rspack-file-based/package.json @@ -8,8 +8,8 @@ "preview": "rsbuild preview" }, "dependencies": { - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/examples/react/quickstart-webpack-file-based/package.json b/examples/react/quickstart-webpack-file-based/package.json index ef05207b76..1e23f24858 100644 --- a/examples/react/quickstart-webpack-file-based/package.json +++ b/examples/react/quickstart-webpack-file-based/package.json @@ -7,8 +7,8 @@ "build": "webpack build && tsc --noEmit" }, "dependencies": { - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/examples/react/quickstart/package.json b/examples/react/quickstart/package.json index 948dc6e4f8..d8f72c824b 100644 --- a/examples/react/quickstart/package.json +++ b/examples/react/quickstart/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/examples/react/router-monorepo-react-query/package.json b/examples/react/router-monorepo-react-query/package.json index 3cddaf44b9..158a91d2a7 100644 --- a/examples/react/router-monorepo-react-query/package.json +++ b/examples/react/router-monorepo-react-query/package.json @@ -12,8 +12,8 @@ "dependencies": { "@tanstack/react-query": "^5.62.3", "@tanstack/react-query-devtools": "^5.62.3", - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "@tanstack/router-plugin": "^1.91.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/router-monorepo-react-query/packages/app/package.json b/examples/react/router-monorepo-react-query/packages/app/package.json index 5fa21820c1..ad0cf68a98 100644 --- a/examples/react/router-monorepo-react-query/packages/app/package.json +++ b/examples/react/router-monorepo-react-query/packages/app/package.json @@ -20,7 +20,7 @@ "@types/react-dom": "^18.2.18", "@vitejs/plugin-react": "^4.3.4", "typescript": "^5.7.2", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/router-devtools": "^1.92.1", "vite": "^6.0.3", "vite-plugin-dts": "^4.3.0" }, diff --git a/examples/react/router-monorepo-react-query/packages/router/package.json b/examples/react/router-monorepo-react-query/packages/router/package.json index 99426797a6..cc761dad3a 100644 --- a/examples/react/router-monorepo-react-query/packages/router/package.json +++ b/examples/react/router-monorepo-react-query/packages/router/package.json @@ -10,7 +10,7 @@ "dependencies": { "@tanstack/history": "^1.90.0", "@tanstack/react-query": "^5.62.3", - "@tanstack/react-router": "^1.91.3", + "@tanstack/react-router": "^1.92.1", "@tanstack/router-plugin": "^1.91.1", "@router-mono-react-query/post-query": "workspace:*", "redaxios": "^0.5.1", diff --git a/examples/react/router-monorepo-simple/package.json b/examples/react/router-monorepo-simple/package.json index 945416e9c6..994e30263e 100644 --- a/examples/react/router-monorepo-simple/package.json +++ b/examples/react/router-monorepo-simple/package.json @@ -8,8 +8,8 @@ "dev": "pnpm router build && pnpm post-feature build && pnpm app dev" }, "dependencies": { - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "@tanstack/router-plugin": "^1.91.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/router-monorepo-simple/packages/app/package.json b/examples/react/router-monorepo-simple/packages/app/package.json index e36a6963a7..15c400fe1a 100644 --- a/examples/react/router-monorepo-simple/packages/app/package.json +++ b/examples/react/router-monorepo-simple/packages/app/package.json @@ -19,7 +19,7 @@ "@types/react-dom": "^18.2.18", "@vitejs/plugin-react": "^4.3.4", "typescript": "^5.7.2", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/router-devtools": "^1.92.1", "vite": "^6.0.3", "vite-plugin-dts": "^4.3.0" }, diff --git a/examples/react/router-monorepo-simple/packages/router/package.json b/examples/react/router-monorepo-simple/packages/router/package.json index 6a36379c7c..6e6dad2ea7 100644 --- a/examples/react/router-monorepo-simple/packages/router/package.json +++ b/examples/react/router-monorepo-simple/packages/router/package.json @@ -9,7 +9,7 @@ "types": "./dist/index.d.ts", "dependencies": { "@tanstack/history": "^1.90.0", - "@tanstack/react-router": "^1.91.3", + "@tanstack/react-router": "^1.92.1", "@tanstack/router-plugin": "^1.91.1", "redaxios": "^0.5.1", "zod": "^3.23.8", diff --git a/examples/react/scroll-restoration/package.json b/examples/react/scroll-restoration/package.json index 3c162e97f2..5e5218ea8b 100644 --- a/examples/react/scroll-restoration/package.json +++ b/examples/react/scroll-restoration/package.json @@ -9,9 +9,9 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.91.3", + "@tanstack/react-router": "^1.92.1", "@tanstack/react-virtual": "^3.11.1", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/router-devtools": "^1.92.1", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/examples/react/search-validator-adapters/package.json b/examples/react/search-validator-adapters/package.json index e2c911e4b1..9fdd21c8dc 100644 --- a/examples/react/search-validator-adapters/package.json +++ b/examples/react/search-validator-adapters/package.json @@ -11,12 +11,12 @@ }, "dependencies": { "@tanstack/react-query": "^5.62.3", - "@tanstack/react-router": "^1.91.3", - "@tanstack/arktype-adapter": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/arktype-adapter": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "@tanstack/router-plugin": "^1.91.1", - "@tanstack/valibot-adapter": "^1.91.3", - "@tanstack/zod-adapter": "^1.91.3", + "@tanstack/valibot-adapter": "^1.92.1", + "@tanstack/zod-adapter": "^1.92.1", "arktype": "2.0.0-rc.26", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/start-basic-auth/package.json b/examples/react/start-basic-auth/package.json index 23c71bf022..7ca0fe9de5 100644 --- a/examples/react/start-basic-auth/package.json +++ b/examples/react/start-basic-auth/package.json @@ -11,9 +11,9 @@ }, "dependencies": { "@prisma/client": "5.22.0", - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", - "@tanstack/start": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", + "@tanstack/start": "^1.92.1", "prisma": "^5.22.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/examples/react/start-basic-react-query/package.json b/examples/react/start-basic-react-query/package.json index 4e96d66d9b..56a0d87b32 100644 --- a/examples/react/start-basic-react-query/package.json +++ b/examples/react/start-basic-react-query/package.json @@ -11,10 +11,10 @@ "dependencies": { "@tanstack/react-query": "^5.62.3", "@tanstack/react-query-devtools": "^5.62.3", - "@tanstack/react-router": "^1.91.3", - "@tanstack/react-router-with-query": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", - "@tanstack/start": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/react-router-with-query": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", + "@tanstack/start": "^1.92.1", "react": "^18.3.1", "react-dom": "^18.3.1", "redaxios": "^0.5.1", diff --git a/examples/react/start-basic-rsc/package.json b/examples/react/start-basic-rsc/package.json index 66a417119a..935cc46592 100644 --- a/examples/react/start-basic-rsc/package.json +++ b/examples/react/start-basic-rsc/package.json @@ -10,9 +10,9 @@ }, "dependencies": { "@babel/plugin-syntax-typescript": "^7.25.9", - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", - "@tanstack/start": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", + "@tanstack/start": "^1.92.1", "redaxios": "^0.5.1", "tailwind-merge": "^2.5.5", "vinxi": "0.5.1" diff --git a/examples/react/start-basic/package.json b/examples/react/start-basic/package.json index 3bef128dff..b8a7b3c550 100644 --- a/examples/react/start-basic/package.json +++ b/examples/react/start-basic/package.json @@ -9,9 +9,9 @@ "start": "vinxi start" }, "dependencies": { - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", - "@tanstack/start": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", + "@tanstack/start": "^1.92.1", "react": "^18.3.1", "react-dom": "^18.3.1", "redaxios": "^0.5.1", diff --git a/examples/react/start-clerk-basic/package.json b/examples/react/start-clerk-basic/package.json index 060e5a816a..9a13f584a9 100644 --- a/examples/react/start-clerk-basic/package.json +++ b/examples/react/start-clerk-basic/package.json @@ -10,9 +10,9 @@ }, "dependencies": { "@clerk/tanstack-start": "0.6.5", - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", - "@tanstack/start": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", + "@tanstack/start": "^1.92.1", "react": "^18.3.1", "react-dom": "^18.3.1", "redaxios": "^0.5.1", diff --git a/examples/react/start-convex-trellaux/package.json b/examples/react/start-convex-trellaux/package.json index e6a54c02b2..fd27572e32 100644 --- a/examples/react/start-convex-trellaux/package.json +++ b/examples/react/start-convex-trellaux/package.json @@ -13,10 +13,10 @@ "dependencies": { "@tanstack/react-query": "^5.62.3", "@tanstack/react-query-devtools": "^5.62.3", - "@tanstack/react-router": "^1.91.3", - "@tanstack/react-router-with-query": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", - "@tanstack/start": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/react-router-with-query": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", + "@tanstack/start": "^1.92.1", "@convex-dev/react-query": "0.0.0-alpha.8", "concurrently": "^8.2.2", "convex": "^1.17.3", diff --git a/examples/react/start-counter/package.json b/examples/react/start-counter/package.json index 628d3c0c5d..5842688c99 100644 --- a/examples/react/start-counter/package.json +++ b/examples/react/start-counter/package.json @@ -9,8 +9,8 @@ "start": "vinxi start" }, "dependencies": { - "@tanstack/react-router": "^1.91.3", - "@tanstack/start": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/start": "^1.92.1", "react": "^18.3.1", "react-dom": "^18.3.1", "vinxi": "0.5.1" diff --git a/examples/react/start-large/package.json b/examples/react/start-large/package.json index e41bca4756..0cbfa90e74 100644 --- a/examples/react/start-large/package.json +++ b/examples/react/start-large/package.json @@ -12,9 +12,9 @@ }, "dependencies": { "@tanstack/react-query": "^5.62.3", - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", - "@tanstack/start": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", + "@tanstack/start": "^1.92.1", "react": "^18.3.1", "react-dom": "^18.3.1", "redaxios": "^0.5.1", diff --git a/examples/react/start-supabase-basic/package.json b/examples/react/start-supabase-basic/package.json index f93e6b3564..be902ddf6a 100644 --- a/examples/react/start-supabase-basic/package.json +++ b/examples/react/start-supabase-basic/package.json @@ -15,9 +15,9 @@ "dependencies": { "@supabase/ssr": "^0.5.2", "@supabase/supabase-js": "^2.47.3", - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", - "@tanstack/start": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", + "@tanstack/start": "^1.92.1", "react": "^18.3.1", "react-dom": "^18.3.1", "vinxi": "0.5.1" diff --git a/examples/react/start-trellaux/package.json b/examples/react/start-trellaux/package.json index fca0779f77..9fd0ad825b 100644 --- a/examples/react/start-trellaux/package.json +++ b/examples/react/start-trellaux/package.json @@ -11,10 +11,10 @@ "dependencies": { "@tanstack/react-query": "^5.62.3", "@tanstack/react-query-devtools": "^5.62.3", - "@tanstack/react-router": "^1.91.3", - "@tanstack/react-router-with-query": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", - "@tanstack/start": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/react-router-with-query": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", + "@tanstack/start": "^1.92.1", "ky": "^1.7.2", "msw": "^2.6.8", "react": "^18.3.1", diff --git a/examples/react/with-framer-motion/package.json b/examples/react/with-framer-motion/package.json index 540a3b5946..5985d7fd3d 100644 --- a/examples/react/with-framer-motion/package.json +++ b/examples/react/with-framer-motion/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "redaxios": "^0.5.1", "framer-motion": "^11.13.3", "react": "^18.2.0", diff --git a/examples/react/with-trpc-react-query/package.json b/examples/react/with-trpc-react-query/package.json index 8ae2cab652..f704182d45 100644 --- a/examples/react/with-trpc-react-query/package.json +++ b/examples/react/with-trpc-react-query/package.json @@ -10,8 +10,8 @@ "dependencies": { "@tanstack/react-query": "^5.62.3", "@tanstack/react-query-devtools": "^5.62.3", - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "@tanstack/router-plugin": "^1.91.1", "@trpc/client": "11.0.0-rc.660", "@trpc/react-query": "11.0.0-rc.660", diff --git a/examples/react/with-trpc/package.json b/examples/react/with-trpc/package.json index d7780a1d70..9a1d3f7142 100644 --- a/examples/react/with-trpc/package.json +++ b/examples/react/with-trpc/package.json @@ -8,8 +8,8 @@ "start": "vinxi start" }, "dependencies": { - "@tanstack/react-router": "^1.91.3", - "@tanstack/router-devtools": "^1.91.3", + "@tanstack/react-router": "^1.92.1", + "@tanstack/router-devtools": "^1.92.1", "@tanstack/router-plugin": "^1.91.1", "@trpc/client": "11.0.0-rc.660", "@trpc/server": "11.0.0-rc.660", diff --git a/packages/arktype-adapter/package.json b/packages/arktype-adapter/package.json index 7ee5fbeabe..48e04c31a0 100644 --- a/packages/arktype-adapter/package.json +++ b/packages/arktype-adapter/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/arktype-adapter", - "version": "1.91.3", + "version": "1.92.1", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/create-router/package.json b/packages/create-router/package.json index ecaa0a61d4..b90b35cd46 100644 --- a/packages/create-router/package.json +++ b/packages/create-router/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/create-router", - "version": "1.91.3", + "version": "1.92.1", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/create-start/package.json b/packages/create-start/package.json index aae6a88bae..59d81a77bd 100644 --- a/packages/create-start/package.json +++ b/packages/create-start/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/create-start", - "version": "1.92.0", + "version": "1.92.1", "description": "Modern and scalable routing for React applications", "author": "Tim O'Connell", "license": "MIT", diff --git a/packages/react-router-with-query/package.json b/packages/react-router-with-query/package.json index dfb0eca965..0d20acfa09 100644 --- a/packages/react-router-with-query/package.json +++ b/packages/react-router-with-query/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/react-router-with-query", - "version": "1.91.3", + "version": "1.92.1", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 0c8b748525..8424810a68 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/react-router", - "version": "1.91.3", + "version": "1.92.1", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/router-devtools/package.json b/packages/router-devtools/package.json index a08522143e..4cd65076ef 100644 --- a/packages/router-devtools/package.json +++ b/packages/router-devtools/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/router-devtools", - "version": "1.91.3", + "version": "1.92.1", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/start/package.json b/packages/start/package.json index 172fbf00a8..9e1c949e91 100644 --- a/packages/start/package.json +++ b/packages/start/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/start", - "version": "1.91.3", + "version": "1.92.1", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/valibot-adapter/package.json b/packages/valibot-adapter/package.json index a05e3db1a5..eed9a2906a 100644 --- a/packages/valibot-adapter/package.json +++ b/packages/valibot-adapter/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/valibot-adapter", - "version": "1.91.3", + "version": "1.92.1", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/zod-adapter/package.json b/packages/zod-adapter/package.json index 367cad576d..eea1adbe6a 100644 --- a/packages/zod-adapter/package.json +++ b/packages/zod-adapter/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/zod-adapter", - "version": "1.91.3", + "version": "1.92.1", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", From 11f2b966d22620d972070a93bdccbee32128b7e0 Mon Sep 17 00:00:00 2001 From: alakhpc Date: Sun, 22 Dec 2024 15:50:11 -0500 Subject: [PATCH 24/38] fix(start): returning `null` from server functions (#3048) Co-authored-by: SeanCassiere <33615041+SeanCassiere@users.noreply.github.com> --- .../-server-fns/allow-fn-return-null.tsx | 63 +++++++++++++++++++ e2e/start/basic/app/routes/server-fns.tsx | 2 + e2e/start/basic/app/styles/app.css | 4 ++ e2e/start/basic/tests/base.spec.ts | 20 ++++++ packages/start/src/client/createServerFn.ts | 5 +- packages/start/src/server-handler/index.tsx | 13 ++++ 6 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 e2e/start/basic/app/routes/-server-fns/allow-fn-return-null.tsx diff --git a/e2e/start/basic/app/routes/-server-fns/allow-fn-return-null.tsx b/e2e/start/basic/app/routes/-server-fns/allow-fn-return-null.tsx new file mode 100644 index 0000000000..297498e45a --- /dev/null +++ b/e2e/start/basic/app/routes/-server-fns/allow-fn-return-null.tsx @@ -0,0 +1,63 @@ +/** + * This exported component checks whether the server function can + * return null without throwing an error or returning something else. + * @link https://github.com/TanStack/router/issues/2776 + */ + +import * as React from 'react' +import { createServerFn } from '@tanstack/start' + +const $allow_return_null_getFn = createServerFn().handler(async () => { + return null +}) +const $allow_return_null_postFn = createServerFn({ method: 'POST' }).handler( + async () => { + return null + }, +) + +export function AllowServerFnReturnNull() { + const [getServerResult, setGetServerResult] = React.useState('-') + const [postServerResult, setPostServerResult] = React.useState('-') + + return ( +
+

Allow ServerFn to return `null`

+

+ This component checks whether the server function can return null + without throwing an error. +

+
+ It should return{' '} + +
{JSON.stringify(null)}
+
+
+

+ {`GET: $allow_return_null_getFn returns`} +
+ + {JSON.stringify(getServerResult)} + +

+

+ {`POST: $allow_return_null_postFn returns`} +
+ + {JSON.stringify(postServerResult)} + +

+ +
+ ) +} diff --git a/e2e/start/basic/app/routes/server-fns.tsx b/e2e/start/basic/app/routes/server-fns.tsx index c829feed9d..21939570b9 100644 --- a/e2e/start/basic/app/routes/server-fns.tsx +++ b/e2e/start/basic/app/routes/server-fns.tsx @@ -3,6 +3,7 @@ import { createFileRoute } from '@tanstack/react-router' import { ConsistentServerFnCalls } from './-server-fns/consistent-fn-calls' import { MultipartServerFnCall } from './-server-fns/multipart-formdata-fn-call' +import { AllowServerFnReturnNull } from './-server-fns/allow-fn-return-null' export const Route = createFileRoute('/server-fns')({ component: RouteComponent, @@ -13,6 +14,7 @@ function RouteComponent() { <> + ) } diff --git a/e2e/start/basic/app/styles/app.css b/e2e/start/basic/app/styles/app.css index d6426ccb72..e858b17319 100644 --- a/e2e/start/basic/app/styles/app.css +++ b/e2e/start/basic/app/styles/app.css @@ -3,6 +3,10 @@ @tailwind utilities; @layer base { + html { + color-scheme: light dark; + } + html, body { @apply text-gray-900 bg-gray-50 dark:bg-gray-950 dark:text-gray-200; diff --git a/e2e/start/basic/tests/base.spec.ts b/e2e/start/basic/tests/base.spec.ts index 2f2414bccc..80c30f9d07 100644 --- a/e2e/start/basic/tests/base.spec.ts +++ b/e2e/start/basic/tests/base.spec.ts @@ -192,3 +192,23 @@ test('env-only functions can only be called on the server or client respectively 'client got: hello', ) }) + +test.only('Server function can return null for GET and POST calls', async ({ + page, +}) => { + await page.goto('/server-fns') + + await page.waitForLoadState('networkidle') + await page.getByTestId('test-allow-server-fn-return-null-btn').click() + await page.waitForLoadState('networkidle') + + // GET call + await expect( + page.getByTestId('allow_return_null_getFn-response'), + ).toContainText(JSON.stringify(null)) + + // POST call + await expect( + page.getByTestId('allow_return_null_postFn-response'), + ).toContainText(JSON.stringify(null)) +}) diff --git a/packages/start/src/client/createServerFn.ts b/packages/start/src/client/createServerFn.ts index 651c7033b4..7bed1b8533 100644 --- a/packages/start/src/client/createServerFn.ts +++ b/packages/start/src/client/createServerFn.ts @@ -368,7 +368,10 @@ const applyMiddleware = ( context, sendContext, headers, - result: userResult?.result ?? (mCtx as any).result, + result: + userResult?.result !== undefined + ? userResult.result + : (mCtx as any).result, } as MiddlewareResult & { method: Method }) diff --git a/packages/start/src/server-handler/index.tsx b/packages/start/src/server-handler/index.tsx index 54151ba682..7bd7fe3dbc 100644 --- a/packages/start/src/server-handler/index.tsx +++ b/packages/start/src/server-handler/index.tsx @@ -4,6 +4,7 @@ import { pathToFileURL } from 'node:url' import { defaultTransformer, isNotFound, + isPlainObject, isRedirect, } from '@tanstack/react-router' import invariant from 'tiny-invariant' @@ -101,6 +102,12 @@ export async function handleServerRequest(request: Request, _event?: H3Event) { if (result instanceof Response) { return result + } else if ( + isPlainObject(result) && + 'result' in result && + result.result instanceof Response + ) { + return result.result } // TODO: RSCs @@ -138,6 +145,12 @@ export async function handleServerRequest(request: Request, _event?: H3Event) { } catch (error: any) { if (error instanceof Response) { return error + } else if ( + isPlainObject(error) && + 'result' in error && + error.result instanceof Response + ) { + return error.result } // Currently this server-side context has no idea how to From 3a538f74cafdcbde39586b62a33bd91df8b0a733 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Sun, 22 Dec 2024 20:58:48 +0000 Subject: [PATCH 25/38] release: v1.92.2 --- examples/react/basic-ssr-file-based/package.json | 2 +- examples/react/basic-ssr-streaming-file-based/package.json | 2 +- examples/react/start-basic-auth/package.json | 2 +- examples/react/start-basic-react-query/package.json | 2 +- examples/react/start-basic-rsc/package.json | 2 +- examples/react/start-basic/package.json | 2 +- examples/react/start-clerk-basic/package.json | 2 +- examples/react/start-convex-trellaux/package.json | 2 +- examples/react/start-counter/package.json | 2 +- examples/react/start-large/package.json | 2 +- examples/react/start-supabase-basic/package.json | 2 +- examples/react/start-trellaux/package.json | 2 +- packages/create-start/package.json | 2 +- packages/start/package.json | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/examples/react/basic-ssr-file-based/package.json b/examples/react/basic-ssr-file-based/package.json index d00940b5d4..d3ecb76910 100644 --- a/examples/react/basic-ssr-file-based/package.json +++ b/examples/react/basic-ssr-file-based/package.json @@ -14,7 +14,7 @@ "@tanstack/react-router": "^1.92.1", "@tanstack/router-devtools": "^1.92.1", "@tanstack/router-plugin": "^1.91.1", - "@tanstack/start": "^1.92.1", + "@tanstack/start": "^1.92.2", "get-port": "^7.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/basic-ssr-streaming-file-based/package.json b/examples/react/basic-ssr-streaming-file-based/package.json index ab1ef65bae..badb2005a9 100644 --- a/examples/react/basic-ssr-streaming-file-based/package.json +++ b/examples/react/basic-ssr-streaming-file-based/package.json @@ -14,7 +14,7 @@ "@tanstack/react-router": "^1.92.1", "@tanstack/router-devtools": "^1.92.1", "@tanstack/router-plugin": "^1.91.1", - "@tanstack/start": "^1.92.1", + "@tanstack/start": "^1.92.2", "get-port": "^7.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/start-basic-auth/package.json b/examples/react/start-basic-auth/package.json index 7ca0fe9de5..717bccd91a 100644 --- a/examples/react/start-basic-auth/package.json +++ b/examples/react/start-basic-auth/package.json @@ -13,7 +13,7 @@ "@prisma/client": "5.22.0", "@tanstack/react-router": "^1.92.1", "@tanstack/router-devtools": "^1.92.1", - "@tanstack/start": "^1.92.1", + "@tanstack/start": "^1.92.2", "prisma": "^5.22.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/examples/react/start-basic-react-query/package.json b/examples/react/start-basic-react-query/package.json index 56a0d87b32..938527a9f5 100644 --- a/examples/react/start-basic-react-query/package.json +++ b/examples/react/start-basic-react-query/package.json @@ -14,7 +14,7 @@ "@tanstack/react-router": "^1.92.1", "@tanstack/react-router-with-query": "^1.92.1", "@tanstack/router-devtools": "^1.92.1", - "@tanstack/start": "^1.92.1", + "@tanstack/start": "^1.92.2", "react": "^18.3.1", "react-dom": "^18.3.1", "redaxios": "^0.5.1", diff --git a/examples/react/start-basic-rsc/package.json b/examples/react/start-basic-rsc/package.json index 935cc46592..39f979f0f0 100644 --- a/examples/react/start-basic-rsc/package.json +++ b/examples/react/start-basic-rsc/package.json @@ -12,7 +12,7 @@ "@babel/plugin-syntax-typescript": "^7.25.9", "@tanstack/react-router": "^1.92.1", "@tanstack/router-devtools": "^1.92.1", - "@tanstack/start": "^1.92.1", + "@tanstack/start": "^1.92.2", "redaxios": "^0.5.1", "tailwind-merge": "^2.5.5", "vinxi": "0.5.1" diff --git a/examples/react/start-basic/package.json b/examples/react/start-basic/package.json index b8a7b3c550..58ce670527 100644 --- a/examples/react/start-basic/package.json +++ b/examples/react/start-basic/package.json @@ -11,7 +11,7 @@ "dependencies": { "@tanstack/react-router": "^1.92.1", "@tanstack/router-devtools": "^1.92.1", - "@tanstack/start": "^1.92.1", + "@tanstack/start": "^1.92.2", "react": "^18.3.1", "react-dom": "^18.3.1", "redaxios": "^0.5.1", diff --git a/examples/react/start-clerk-basic/package.json b/examples/react/start-clerk-basic/package.json index 9a13f584a9..8ed5e6d6a7 100644 --- a/examples/react/start-clerk-basic/package.json +++ b/examples/react/start-clerk-basic/package.json @@ -12,7 +12,7 @@ "@clerk/tanstack-start": "0.6.5", "@tanstack/react-router": "^1.92.1", "@tanstack/router-devtools": "^1.92.1", - "@tanstack/start": "^1.92.1", + "@tanstack/start": "^1.92.2", "react": "^18.3.1", "react-dom": "^18.3.1", "redaxios": "^0.5.1", diff --git a/examples/react/start-convex-trellaux/package.json b/examples/react/start-convex-trellaux/package.json index fd27572e32..efa76bb986 100644 --- a/examples/react/start-convex-trellaux/package.json +++ b/examples/react/start-convex-trellaux/package.json @@ -16,7 +16,7 @@ "@tanstack/react-router": "^1.92.1", "@tanstack/react-router-with-query": "^1.92.1", "@tanstack/router-devtools": "^1.92.1", - "@tanstack/start": "^1.92.1", + "@tanstack/start": "^1.92.2", "@convex-dev/react-query": "0.0.0-alpha.8", "concurrently": "^8.2.2", "convex": "^1.17.3", diff --git a/examples/react/start-counter/package.json b/examples/react/start-counter/package.json index 5842688c99..4276dc8289 100644 --- a/examples/react/start-counter/package.json +++ b/examples/react/start-counter/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@tanstack/react-router": "^1.92.1", - "@tanstack/start": "^1.92.1", + "@tanstack/start": "^1.92.2", "react": "^18.3.1", "react-dom": "^18.3.1", "vinxi": "0.5.1" diff --git a/examples/react/start-large/package.json b/examples/react/start-large/package.json index 0cbfa90e74..2d6011c718 100644 --- a/examples/react/start-large/package.json +++ b/examples/react/start-large/package.json @@ -14,7 +14,7 @@ "@tanstack/react-query": "^5.62.3", "@tanstack/react-router": "^1.92.1", "@tanstack/router-devtools": "^1.92.1", - "@tanstack/start": "^1.92.1", + "@tanstack/start": "^1.92.2", "react": "^18.3.1", "react-dom": "^18.3.1", "redaxios": "^0.5.1", diff --git a/examples/react/start-supabase-basic/package.json b/examples/react/start-supabase-basic/package.json index be902ddf6a..05cc827acd 100644 --- a/examples/react/start-supabase-basic/package.json +++ b/examples/react/start-supabase-basic/package.json @@ -17,7 +17,7 @@ "@supabase/supabase-js": "^2.47.3", "@tanstack/react-router": "^1.92.1", "@tanstack/router-devtools": "^1.92.1", - "@tanstack/start": "^1.92.1", + "@tanstack/start": "^1.92.2", "react": "^18.3.1", "react-dom": "^18.3.1", "vinxi": "0.5.1" diff --git a/examples/react/start-trellaux/package.json b/examples/react/start-trellaux/package.json index 9fd0ad825b..6a9d5a55bc 100644 --- a/examples/react/start-trellaux/package.json +++ b/examples/react/start-trellaux/package.json @@ -14,7 +14,7 @@ "@tanstack/react-router": "^1.92.1", "@tanstack/react-router-with-query": "^1.92.1", "@tanstack/router-devtools": "^1.92.1", - "@tanstack/start": "^1.92.1", + "@tanstack/start": "^1.92.2", "ky": "^1.7.2", "msw": "^2.6.8", "react": "^18.3.1", diff --git a/packages/create-start/package.json b/packages/create-start/package.json index 59d81a77bd..6dc150e430 100644 --- a/packages/create-start/package.json +++ b/packages/create-start/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/create-start", - "version": "1.92.1", + "version": "1.92.2", "description": "Modern and scalable routing for React applications", "author": "Tim O'Connell", "license": "MIT", diff --git a/packages/start/package.json b/packages/start/package.json index 9e1c949e91..1146c20b0f 100644 --- a/packages/start/package.json +++ b/packages/start/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/start", - "version": "1.92.1", + "version": "1.92.2", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", From ce467d1b9b97a8813494df4f8f032619ad339f83 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Sun, 22 Dec 2024 23:25:53 -0700 Subject: [PATCH 26/38] fix: allow serverFn errors to also have context (#3037) * fix: allow serverFn errors to also have context * fix: allow errors, undefined from server functions * remove logs * fix: transformer and tests * ci: apply automated fixes * no logs * fix conficts --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- packages/react-router/src/transformer.ts | 131 ++++++++++--- .../react-router/tests/transformer.test.tsx | 13 +- packages/start/src/client/createMiddleware.ts | 21 ++- packages/start/src/client/createServerFn.ts | 172 ++++++++++-------- packages/start/src/client/serverFnFetcher.tsx | 34 +--- packages/start/src/server-handler/index.tsx | 2 +- 6 files changed, 233 insertions(+), 140 deletions(-) diff --git a/packages/react-router/src/transformer.ts b/packages/react-router/src/transformer.ts index 7a915aba29..f58f03bb59 100644 --- a/packages/react-router/src/transformer.ts +++ b/packages/react-router/src/transformer.ts @@ -3,49 +3,128 @@ import { isPlainObject } from './utils' export interface RouterTransformer { stringify: (obj: unknown) => string parse: (str: string) => unknown + encode: (value: T) => T + decode: (value: T) => T } export const defaultTransformer: RouterTransformer = { stringify: (value: any) => - JSON.stringify(value, function replacer(key, value) { - const keyVal = this[key] - const transformer = transformers.find((t) => t.stringifyCondition(keyVal)) + JSON.stringify(value, function replacer(key, val) { + const ogVal = this[key] + const transformer = transformers.find((t) => t.stringifyCondition(ogVal)) if (transformer) { - return transformer.stringify(keyVal) + return transformer.stringify(ogVal) } - return value + return val }), parse: (value: string) => - JSON.parse(value, function parser(key, value) { - const keyVal = this[key] - const transformer = transformers.find((t) => t.parseCondition(keyVal)) + JSON.parse(value, function parser(key, val) { + const ogVal = this[key] + if (isPlainObject(ogVal)) { + const transformer = transformers.find((t) => t.parseCondition(ogVal)) - if (transformer) { - return transformer.parse(keyVal) + if (transformer) { + return transformer.parse(ogVal) + } } - return value + return val }), -} + encode: (value: any) => { + // When encodign, dive first + if (Array.isArray(value)) { + return value.map((v) => defaultTransformer.encode(v)) + } -const transformers = [ - { - // Dates - stringifyCondition: (value: any) => value instanceof Date, - stringify: (value: any) => ({ $date: value.toISOString() }), - parseCondition: (value: any) => isPlainObject(value) && value.$date, - parse: (value: any) => new Date(value.$date), + if (isPlainObject(value)) { + return Object.fromEntries( + Object.entries(value).map(([key, v]) => [ + key, + defaultTransformer.encode(v), + ]), + ) + } + + const transformer = transformers.find((t) => t.stringifyCondition(value)) + if (transformer) { + return transformer.stringify(value) + } + + return value }, - { - // undefined - stringifyCondition: (value: any) => value === undefined, - stringify: () => ({ $undefined: '' }), - parseCondition: (value: any) => - isPlainObject(value) && value.$undefined === '', - parse: () => undefined, + decode: (value: any) => { + // Attempt transform first + if (isPlainObject(value)) { + const transformer = transformers.find((t) => t.parseCondition(value)) + if (transformer) { + return transformer.parse(value) + } + } + + if (Array.isArray(value)) { + return value.map((v) => defaultTransformer.decode(v)) + } + + if (isPlainObject(value)) { + return Object.fromEntries( + Object.entries(value).map(([key, v]) => [ + key, + defaultTransformer.decode(v), + ]), + ) + } + + return value }, +} + +const createTransformer = ( + key: T, + check: (value: any) => boolean, + toValue: (value: any) => any = (v) => v, + fromValue: (value: any) => any = (v) => v, +) => ({ + key, + stringifyCondition: check, + stringify: (value: any) => ({ [`$${key}`]: toValue(value) }), + parseCondition: (value: any) => Object.hasOwn(value, `$${key}`), + parse: (value: any) => fromValue(value[`$${key}`]), +}) + +// Keep these ordered by predicted frequency +const transformers = [ + createTransformer( + // Key + 'undefined', + // Check + (v) => v === undefined, + // To + () => 0, + // From + () => undefined, + ), + createTransformer( + // Key + 'date', + // Check + (v) => v instanceof Date, + // To + (v) => v.toISOString(), + // From + (v) => new Date(v), + ), + createTransformer( + // Key + 'error', + // Check + (v) => v instanceof Error, + // To + (v) => ({ ...v, message: v.message, stack: v.stack, cause: v.cause }), + // From + (v) => Object.assign(new Error(v.message), v), + ), ] as const export type TransformerStringify = T extends TSerializable diff --git a/packages/react-router/tests/transformer.test.tsx b/packages/react-router/tests/transformer.test.tsx index cf7e6c67a5..f0278b6582 100644 --- a/packages/react-router/tests/transformer.test.tsx +++ b/packages/react-router/tests/transformer.test.tsx @@ -11,9 +11,9 @@ describe('transformer.stringify', () => { }) test('should stringify undefined', () => { - expect(defaultTransformer.stringify(undefined)).toMatchInlineSnapshot(` - "{"$undefined":""}" - `) + expect(defaultTransformer.stringify(undefined)).toMatchInlineSnapshot( + `"{"$undefined":0}"`, + ) }) test('should stringify object foo="bar"', () => { @@ -23,10 +23,9 @@ describe('transformer.stringify', () => { }) test('should stringify object foo=undefined', () => { - expect(defaultTransformer.stringify({ foo: undefined })) - .toMatchInlineSnapshot(` - "{"foo":{"$undefined":""}}" - `) + expect( + defaultTransformer.stringify({ foo: undefined }), + ).toMatchInlineSnapshot(`"{"foo":{"$undefined":0}}"`) }) test('should stringify object foo=Date', () => { diff --git a/packages/start/src/client/createMiddleware.ts b/packages/start/src/client/createMiddleware.ts index 6eefb7751f..33d7f7126a 100644 --- a/packages/start/src/client/createMiddleware.ts +++ b/packages/start/src/client/createMiddleware.ts @@ -124,6 +124,9 @@ export interface MiddlewareServerFnOptions< data: Expand> context: Expand> next: MiddlewareServerNextFn + method: Method + filename: string + functionId: string } export type MiddlewareServerFn< @@ -134,9 +137,11 @@ export type MiddlewareServerFn< TNewClientAfterContext, > = ( options: MiddlewareServerFnOptions, -) => - | Promise> - | ServerResultWithContext +) => MiddlewareServerFnResult + +export type MiddlewareServerFnResult = + | Promise> + | ServerResultWithContext export type MiddlewareClientNextFn = < TNewServerContext = undefined, @@ -156,6 +161,8 @@ export interface MiddlewareClientFnOptions< sendContext?: unknown // cc Chris Horobin method: Method next: MiddlewareClientNextFn + filename: string + functionId: string } export type MiddlewareClientFn< @@ -165,7 +172,9 @@ export type MiddlewareClientFn< TClientContext, > = ( options: MiddlewareClientFnOptions, -) => +) => MiddlewareClientFnResult + +export type MiddlewareClientFnResult = | Promise> | ClientResultWithContext @@ -208,7 +217,9 @@ export type MiddlewareClientAfterFn< TClientContext, TClientAfterContext >, -) => +) => MiddlewareClientAfterFnResult + +export type MiddlewareClientAfterFnResult = | Promise> | ClientAfterResultWithContext diff --git a/packages/start/src/client/createServerFn.ts b/packages/start/src/client/createServerFn.ts index 7bed1b8533..191d3bd84b 100644 --- a/packages/start/src/client/createServerFn.ts +++ b/packages/start/src/client/createServerFn.ts @@ -1,5 +1,9 @@ import invariant from 'tiny-invariant' -import { defaultTransformer } from '@tanstack/react-router' +import { + defaultTransformer, + isNotFound, + isRedirect, +} from '@tanstack/react-router' import { mergeHeaders } from './headers' import { globalMiddleware } from './registerGlobalMiddleware' import type { @@ -17,6 +21,8 @@ import type { MergeAllServerContext, MergeAllValidatorInputs, MergeAllValidatorOutputs, + MiddlewareClientFnResult, + MiddlewareServerFnResult, } from './createMiddleware' export interface JsonResponse extends Response { @@ -27,6 +33,7 @@ export type CompiledFetcherFnOptions = { method: Method data: unknown headers?: HeadersInit + context?: any } export type Fetcher = { @@ -35,6 +42,7 @@ export type Fetcher = { method: Method data: unknown headers?: HeadersInit + context?: any }) => Promise } & FetcherImpl @@ -86,7 +94,9 @@ export interface ServerFnCtx { } export type CompiledFetcherFn = { - (opts: CompiledFetcherFnOptions & ServerFnBaseOptions): Promise + ( + opts: CompiledFetcherFnOptions & ServerFnBaseOptions, + ): Promise url: string } @@ -250,18 +260,25 @@ export function createServerFn< ...extractedFn, // The extracted function on the server-side calls // this function - __executeServer: (opts: any) => { + __executeServer: async (opts: any) => { const parsedOpts = opts instanceof FormData ? extractFormDataContext(opts) : opts - return executeMiddleware(resolvedMiddleware, 'server', { - ...extractedFn, - ...parsedOpts, - }).then((d) => ({ + const result = await executeMiddleware( + resolvedMiddleware, + 'server', + { + ...extractedFn, + ...parsedOpts, + }, + ).then((d) => ({ // Only send the result and sendContext back to the client result: d.result, + error: d.error, context: d.sendContext, })) + + return result }, }, ) as any @@ -325,55 +342,43 @@ export type MiddlewareOptions = { context?: any } -export type MiddlewareResult = { - context: any - sendContext: any - data: any - result: unknown +export type MiddlewareResult = MiddlewareOptions & { + result?: unknown + error?: unknown } -const applyMiddleware = ( - middlewareFn: NonNullable< - | AnyMiddleware['options']['client'] - | AnyMiddleware['options']['server'] - | AnyMiddleware['options']['clientAfter'] - >, - mCtx: MiddlewareOptions, - nextFn: (ctx: MiddlewareOptions) => Promise, -) => { - return middlewareFn({ - data: mCtx.data, - context: mCtx.context, - sendContext: mCtx.sendContext, - method: mCtx.method, - next: ((userResult: any) => { - // Take the user provided context - // and merge it with the current context - const context = { - ...mCtx.context, - ...userResult?.context, - } - - const sendContext = { - ...mCtx.sendContext, - ...(userResult?.sendContext ?? {}), - } +export type NextFn = (ctx: MiddlewareResult) => Promise - const headers = mergeHeaders(mCtx.headers, userResult?.headers) +export type MiddlewareFn = ( + ctx: MiddlewareOptions & { + next: NextFn + }, +) => Promise +const applyMiddleware = async ( + middlewareFn: MiddlewareFn, + ctx: MiddlewareOptions, + nextFn: NextFn, +) => { + return middlewareFn({ + ...ctx, + next: (async (userCtx: MiddlewareResult | undefined = {} as any) => { // Return the next middleware return nextFn({ - method: mCtx.method, - data: mCtx.data, - context, - sendContext, - headers, + ...ctx, + ...userCtx, + context: { + ...ctx.context, + ...userCtx.context, + }, + sendContext: { + ...ctx.sendContext, + ...(userCtx.sendContext ?? {}), + }, + headers: mergeHeaders(ctx.headers, userCtx.headers), result: - userResult?.result !== undefined - ? userResult.result - : (mCtx as any).result, - } as MiddlewareResult & { - method: Method + userCtx.result !== undefined ? userCtx.result : (ctx as any).result, + error: userCtx.error ?? (ctx as any).error, }) }) as any, }) @@ -415,13 +420,13 @@ async function executeMiddleware( ...middlewares, ]) - const next = async (ctx: MiddlewareOptions): Promise => { + const next: NextFn = async (ctx) => { // Get the next middleware const nextMiddleware = flattenedMiddlewares.shift() // If there are no more middlewares, return the context if (!nextMiddleware) { - return ctx as any + return ctx } if ( @@ -432,33 +437,47 @@ async function executeMiddleware( ctx.data = await execValidator(nextMiddleware.options.validator, ctx.data) } - const middlewareFn = + const middlewareFn = ( env === 'client' ? nextMiddleware.options.client : nextMiddleware.options.server + ) as MiddlewareFn | undefined if (middlewareFn) { // Execute the middleware - return applyMiddleware( - middlewareFn, - ctx, - async (userCtx): Promise => { - // If there is a clientAfter function and we are on the client - if (env === 'client' && nextMiddleware.options.clientAfter) { - // We need to await the next middleware and get the result - const result = await next(userCtx) - // Then we can execute the clientAfter function - return applyMiddleware( - nextMiddleware.options.clientAfter, - result as any, - // Identity, because there "next" is just returning - (d: any) => d, - ) as any + return applyMiddleware(middlewareFn, ctx, async (newCtx) => { + // If there is a clientAfter function and we are on the client + const clientAfter = nextMiddleware.options.clientAfter as + | MiddlewareFn + | undefined + + if (env === 'client' && clientAfter) { + // We need to await the next middleware and get the result + const result = await next(newCtx) + + // Then we can execute the clientAfter function + return applyMiddleware( + clientAfter, + { + ...newCtx, + ...result, + }, + // Identity, because there "next" is just returning + (d: any) => d, + ) + } + + return next(newCtx).catch((error) => { + if (isRedirect(error) || isNotFound(error)) { + return { + ...newCtx, + error, + } } - return next(userCtx) - }, - ) as any + throw error + }) + }) } return next(ctx) @@ -468,7 +487,7 @@ async function executeMiddleware( return next({ ...opts, headers: opts.headers || {}, - sendContext: (opts as any).sendContext || {}, + sendContext: opts.sendContext || {}, context: opts.context || {}, }) } @@ -484,21 +503,22 @@ function serverFnBaseToMiddleware( client: async ({ next, sendContext, ...ctx }) => { // Execute the extracted function // but not before serializing the context - const res = await options.extractedFn?.({ + const serverCtx = await options.extractedFn?.({ ...ctx, // switch the sendContext over to context context: sendContext, - } as any) + }) - return next(res) + return next(serverCtx) as unknown as MiddlewareClientFnResult }, server: async ({ next, ...ctx }) => { // Execute the server function - const result = await options.serverFn?.(ctx as any) + const result = await options.serverFn?.(ctx) return next({ + ...ctx, result, - } as any) + } as any) as unknown as MiddlewareServerFnResult }, }, } diff --git a/packages/start/src/client/serverFnFetcher.tsx b/packages/start/src/client/serverFnFetcher.tsx index 7aac71c7f1..1f9305e17e 100644 --- a/packages/start/src/client/serverFnFetcher.tsx +++ b/packages/start/src/client/serverFnFetcher.tsx @@ -65,12 +65,13 @@ export async function serverFnFetcher( // Check if the response is JSON if (response.headers.get('content-type')?.includes('application/json')) { - const text = await response.text() - const json = text ? defaultTransformer.parse(text) : undefined + // Even though the response is JSON, we need to decode it + // because the server may have transformed it + const json = defaultTransformer.decode(await response.json()) // If the response is a redirect or not found, throw it // for the router to handle - if (isRedirect(json) || isNotFound(json)) { + if (isRedirect(json) || isNotFound(json) || json instanceof Error) { throw json } @@ -96,14 +97,13 @@ export async function serverFnFetcher( // If the response is JSON, return it parsed const contentType = response.headers.get('content-type') - const text = await response.text() if (contentType && contentType.includes('application/json')) { - return text ? JSON.parse(text) : undefined + return defaultTransformer.decode(await response.json()) } else { // Otherwise, return the text as a fallback // If the user wants more than this, they can pass a // request instead - return text + return response.text() } } @@ -132,27 +132,11 @@ async function handleResponseErrors(response: Response) { const contentType = response.headers.get('content-type') const isJson = contentType && contentType.includes('application/json') - const body = await (async () => { - if (isJson) { - return await response.json() - } - return await response.text() - })() - - const message = `Request failed with status ${response.status}` - if (isJson) { - throw new Error( - JSON.stringify({ - message, - body, - }), - ) - } else { - throw new Error( - [message, `${JSON.stringify(body, null, 2)}`].join('\n\n'), - ) + throw defaultTransformer.decode(await response.json()) } + + throw new Error(await response.text()) } return response diff --git a/packages/start/src/server-handler/index.tsx b/packages/start/src/server-handler/index.tsx index 7bd7fe3dbc..66345c7c38 100644 --- a/packages/start/src/server-handler/index.tsx +++ b/packages/start/src/server-handler/index.tsx @@ -166,7 +166,7 @@ export async function handleServerRequest(request: Request, _event?: H3Event) { console.error(error) console.info() - return new Response(JSON.stringify(error), { + return new Response(defaultTransformer.stringify(error), { status: 500, headers: { 'Content-Type': 'application/json', From d4574efc7793ad14bab900490328c093f4a170ed Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Mon, 23 Dec 2024 06:27:48 +0000 Subject: [PATCH 27/38] release: v1.92.3 --- examples/react/authenticated-routes/package.json | 4 ++-- .../react/basic-default-search-params/package.json | 4 ++-- .../react/basic-file-based-codesplitting/package.json | 4 ++-- examples/react/basic-file-based/package.json | 4 ++-- .../react/basic-react-query-file-based/package.json | 4 ++-- examples/react/basic-react-query/package.json | 4 ++-- examples/react/basic-ssr-file-based/package.json | 6 +++--- .../react/basic-ssr-streaming-file-based/package.json | 6 +++--- examples/react/basic-virtual-file-based/package.json | 4 ++-- .../react/basic-virtual-inside-file-based/package.json | 4 ++-- examples/react/basic/package.json | 4 ++-- examples/react/deferred-data/package.json | 4 ++-- examples/react/kitchen-sink-file-based/package.json | 4 ++-- .../kitchen-sink-react-query-file-based/package.json | 4 ++-- examples/react/kitchen-sink-react-query/package.json | 4 ++-- examples/react/kitchen-sink/package.json | 4 ++-- examples/react/large-file-based/package.json | 4 ++-- examples/react/location-masking/package.json | 4 ++-- examples/react/navigation-blocking/package.json | 4 ++-- .../react/quickstart-esbuild-file-based/package.json | 4 ++-- examples/react/quickstart-file-based/package.json | 4 ++-- .../react/quickstart-rspack-file-based/package.json | 4 ++-- .../react/quickstart-webpack-file-based/package.json | 4 ++-- examples/react/quickstart/package.json | 4 ++-- .../react/router-monorepo-react-query/package.json | 4 ++-- .../packages/app/package.json | 2 +- .../packages/router/package.json | 2 +- examples/react/router-monorepo-simple/package.json | 4 ++-- .../router-monorepo-simple/packages/app/package.json | 2 +- .../packages/router/package.json | 2 +- examples/react/scroll-restoration/package.json | 4 ++-- examples/react/search-validator-adapters/package.json | 10 +++++----- examples/react/start-basic-auth/package.json | 6 +++--- examples/react/start-basic-react-query/package.json | 8 ++++---- examples/react/start-basic-rsc/package.json | 6 +++--- examples/react/start-basic/package.json | 6 +++--- examples/react/start-clerk-basic/package.json | 6 +++--- examples/react/start-convex-trellaux/package.json | 8 ++++---- examples/react/start-counter/package.json | 4 ++-- examples/react/start-large/package.json | 6 +++--- examples/react/start-supabase-basic/package.json | 6 +++--- examples/react/start-trellaux/package.json | 8 ++++---- examples/react/with-framer-motion/package.json | 4 ++-- examples/react/with-trpc-react-query/package.json | 4 ++-- examples/react/with-trpc/package.json | 4 ++-- packages/arktype-adapter/package.json | 2 +- packages/create-router/package.json | 2 +- packages/create-start/package.json | 2 +- packages/react-router-with-query/package.json | 2 +- packages/react-router/package.json | 2 +- packages/router-devtools/package.json | 2 +- packages/start/package.json | 2 +- packages/valibot-adapter/package.json | 2 +- packages/zod-adapter/package.json | 2 +- 54 files changed, 112 insertions(+), 112 deletions(-) diff --git a/examples/react/authenticated-routes/package.json b/examples/react/authenticated-routes/package.json index d4835ade48..3a22a3ede6 100644 --- a/examples/react/authenticated-routes/package.json +++ b/examples/react/authenticated-routes/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "@tanstack/router-plugin": "^1.91.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/basic-default-search-params/package.json b/examples/react/basic-default-search-params/package.json index 8a1e90f37f..5e221da329 100644 --- a/examples/react/basic-default-search-params/package.json +++ b/examples/react/basic-default-search-params/package.json @@ -10,8 +10,8 @@ }, "dependencies": { "@tanstack/react-query": "^5.62.3", - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "redaxios": "^0.5.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/basic-file-based-codesplitting/package.json b/examples/react/basic-file-based-codesplitting/package.json index 88499cad41..c8f8a655bd 100644 --- a/examples/react/basic-file-based-codesplitting/package.json +++ b/examples/react/basic-file-based-codesplitting/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "@tanstack/router-plugin": "^1.91.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/basic-file-based/package.json b/examples/react/basic-file-based/package.json index e82e62f253..3e483e581e 100644 --- a/examples/react/basic-file-based/package.json +++ b/examples/react/basic-file-based/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "@tanstack/router-plugin": "^1.91.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/basic-react-query-file-based/package.json b/examples/react/basic-react-query-file-based/package.json index b85ef3e317..45a3906ea5 100644 --- a/examples/react/basic-react-query-file-based/package.json +++ b/examples/react/basic-react-query-file-based/package.json @@ -11,8 +11,8 @@ "dependencies": { "@tanstack/react-query": "^5.62.3", "@tanstack/react-query-devtools": "^5.62.3", - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "@tanstack/router-plugin": "^1.91.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/basic-react-query/package.json b/examples/react/basic-react-query/package.json index 7a5fe86251..e349fa2b39 100644 --- a/examples/react/basic-react-query/package.json +++ b/examples/react/basic-react-query/package.json @@ -11,8 +11,8 @@ "dependencies": { "@tanstack/react-query": "^5.62.3", "@tanstack/react-query-devtools": "^5.62.3", - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "react": "^18.2.0", "react-dom": "^18.2.0", "redaxios": "^0.5.1" diff --git a/examples/react/basic-ssr-file-based/package.json b/examples/react/basic-ssr-file-based/package.json index d3ecb76910..5f1d5e2496 100644 --- a/examples/react/basic-ssr-file-based/package.json +++ b/examples/react/basic-ssr-file-based/package.json @@ -11,10 +11,10 @@ "debug": "node --inspect-brk server" }, "dependencies": { - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "@tanstack/router-plugin": "^1.91.1", - "@tanstack/start": "^1.92.2", + "@tanstack/start": "^1.92.3", "get-port": "^7.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/basic-ssr-streaming-file-based/package.json b/examples/react/basic-ssr-streaming-file-based/package.json index badb2005a9..2364221dcb 100644 --- a/examples/react/basic-ssr-streaming-file-based/package.json +++ b/examples/react/basic-ssr-streaming-file-based/package.json @@ -11,10 +11,10 @@ "debug": "node --inspect-brk server" }, "dependencies": { - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "@tanstack/router-plugin": "^1.91.1", - "@tanstack/start": "^1.92.2", + "@tanstack/start": "^1.92.3", "get-port": "^7.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/basic-virtual-file-based/package.json b/examples/react/basic-virtual-file-based/package.json index 5dec2951fc..de6cc6a20f 100644 --- a/examples/react/basic-virtual-file-based/package.json +++ b/examples/react/basic-virtual-file-based/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "@tanstack/router-plugin": "^1.91.1", "@tanstack/virtual-file-routes": "^1.87.6", "react": "^18.2.0", diff --git a/examples/react/basic-virtual-inside-file-based/package.json b/examples/react/basic-virtual-inside-file-based/package.json index eccab24d2f..e71a68f1ed 100644 --- a/examples/react/basic-virtual-inside-file-based/package.json +++ b/examples/react/basic-virtual-inside-file-based/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "@tanstack/router-plugin": "^1.91.1", "@tanstack/virtual-file-routes": "^1.87.6", "react": "^18.2.0", diff --git a/examples/react/basic/package.json b/examples/react/basic/package.json index 2a64646565..452b892363 100644 --- a/examples/react/basic/package.json +++ b/examples/react/basic/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "react": "^18.2.0", "react-dom": "^18.2.0", "redaxios": "^0.5.1" diff --git a/examples/react/deferred-data/package.json b/examples/react/deferred-data/package.json index 62b8d3825b..9fc38639c5 100644 --- a/examples/react/deferred-data/package.json +++ b/examples/react/deferred-data/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "redaxios": "^0.5.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/kitchen-sink-file-based/package.json b/examples/react/kitchen-sink-file-based/package.json index 83cd9c4ba3..d34d50e463 100644 --- a/examples/react/kitchen-sink-file-based/package.json +++ b/examples/react/kitchen-sink-file-based/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "@tanstack/router-plugin": "^1.91.1", "immer": "^10.1.1", "react": "^18.2.0", diff --git a/examples/react/kitchen-sink-react-query-file-based/package.json b/examples/react/kitchen-sink-react-query-file-based/package.json index 61fb97c1e2..612a90fd08 100644 --- a/examples/react/kitchen-sink-react-query-file-based/package.json +++ b/examples/react/kitchen-sink-react-query-file-based/package.json @@ -11,8 +11,8 @@ "dependencies": { "@tanstack/react-query": "^5.62.3", "@tanstack/react-query-devtools": "^5.62.3", - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "@tanstack/router-plugin": "^1.91.1", "immer": "^10.1.1", "react": "^18.2.0", diff --git a/examples/react/kitchen-sink-react-query/package.json b/examples/react/kitchen-sink-react-query/package.json index 8618a5a005..6d14f9cc5b 100644 --- a/examples/react/kitchen-sink-react-query/package.json +++ b/examples/react/kitchen-sink-react-query/package.json @@ -11,8 +11,8 @@ "dependencies": { "@tanstack/react-query": "^5.62.3", "@tanstack/react-query-devtools": "^5.62.3", - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "redaxios": "^0.5.1", "immer": "^10.1.1", "react": "^18.2.0", diff --git a/examples/react/kitchen-sink/package.json b/examples/react/kitchen-sink/package.json index 08de509b7d..777685081a 100644 --- a/examples/react/kitchen-sink/package.json +++ b/examples/react/kitchen-sink/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "redaxios": "^0.5.1", "immer": "^10.1.1", "react": "^18.2.0", diff --git a/examples/react/large-file-based/package.json b/examples/react/large-file-based/package.json index 22da8997ba..29f8e2688c 100644 --- a/examples/react/large-file-based/package.json +++ b/examples/react/large-file-based/package.json @@ -12,8 +12,8 @@ }, "dependencies": { "@tanstack/react-query": "^5.62.3", - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "@tanstack/router-plugin": "^1.91.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/location-masking/package.json b/examples/react/location-masking/package.json index aac761ba11..ee9ad09d91 100644 --- a/examples/react/location-masking/package.json +++ b/examples/react/location-masking/package.json @@ -11,8 +11,8 @@ "dependencies": { "@radix-ui/react-dialog": "^1.1.2", "@tanstack/react-query": "^5.62.3", - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "redaxios": "^0.5.1", "react": "^18.2.0", "react-dom": "^18.2.0" diff --git a/examples/react/navigation-blocking/package.json b/examples/react/navigation-blocking/package.json index 112c070b34..de693d0609 100644 --- a/examples/react/navigation-blocking/package.json +++ b/examples/react/navigation-blocking/package.json @@ -10,8 +10,8 @@ }, "dependencies": { "@tanstack/react-query": "^5.62.3", - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "redaxios": "^0.5.1", "react": "^18.2.0", "react-dom": "^18.2.0" diff --git a/examples/react/quickstart-esbuild-file-based/package.json b/examples/react/quickstart-esbuild-file-based/package.json index 2cd2dc8c5c..194f6f1cdc 100644 --- a/examples/react/quickstart-esbuild-file-based/package.json +++ b/examples/react/quickstart-esbuild-file-based/package.json @@ -9,8 +9,8 @@ "start": "dev" }, "dependencies": { - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "@tanstack/router-plugin": "^1.91.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/quickstart-file-based/package.json b/examples/react/quickstart-file-based/package.json index 6f8c5be086..3a746212e7 100644 --- a/examples/react/quickstart-file-based/package.json +++ b/examples/react/quickstart-file-based/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "@tanstack/router-plugin": "^1.91.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/quickstart-rspack-file-based/package.json b/examples/react/quickstart-rspack-file-based/package.json index 51373d8d1d..fe0218a0c4 100644 --- a/examples/react/quickstart-rspack-file-based/package.json +++ b/examples/react/quickstart-rspack-file-based/package.json @@ -8,8 +8,8 @@ "preview": "rsbuild preview" }, "dependencies": { - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/examples/react/quickstart-webpack-file-based/package.json b/examples/react/quickstart-webpack-file-based/package.json index 1e23f24858..3183bdbf85 100644 --- a/examples/react/quickstart-webpack-file-based/package.json +++ b/examples/react/quickstart-webpack-file-based/package.json @@ -7,8 +7,8 @@ "build": "webpack build && tsc --noEmit" }, "dependencies": { - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/examples/react/quickstart/package.json b/examples/react/quickstart/package.json index d8f72c824b..2278e32a46 100644 --- a/examples/react/quickstart/package.json +++ b/examples/react/quickstart/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/examples/react/router-monorepo-react-query/package.json b/examples/react/router-monorepo-react-query/package.json index 158a91d2a7..6862a25160 100644 --- a/examples/react/router-monorepo-react-query/package.json +++ b/examples/react/router-monorepo-react-query/package.json @@ -12,8 +12,8 @@ "dependencies": { "@tanstack/react-query": "^5.62.3", "@tanstack/react-query-devtools": "^5.62.3", - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "@tanstack/router-plugin": "^1.91.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/router-monorepo-react-query/packages/app/package.json b/examples/react/router-monorepo-react-query/packages/app/package.json index ad0cf68a98..fa5c7021c4 100644 --- a/examples/react/router-monorepo-react-query/packages/app/package.json +++ b/examples/react/router-monorepo-react-query/packages/app/package.json @@ -20,7 +20,7 @@ "@types/react-dom": "^18.2.18", "@vitejs/plugin-react": "^4.3.4", "typescript": "^5.7.2", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/router-devtools": "^1.92.3", "vite": "^6.0.3", "vite-plugin-dts": "^4.3.0" }, diff --git a/examples/react/router-monorepo-react-query/packages/router/package.json b/examples/react/router-monorepo-react-query/packages/router/package.json index cc761dad3a..8a52d53c2a 100644 --- a/examples/react/router-monorepo-react-query/packages/router/package.json +++ b/examples/react/router-monorepo-react-query/packages/router/package.json @@ -10,7 +10,7 @@ "dependencies": { "@tanstack/history": "^1.90.0", "@tanstack/react-query": "^5.62.3", - "@tanstack/react-router": "^1.92.1", + "@tanstack/react-router": "^1.92.3", "@tanstack/router-plugin": "^1.91.1", "@router-mono-react-query/post-query": "workspace:*", "redaxios": "^0.5.1", diff --git a/examples/react/router-monorepo-simple/package.json b/examples/react/router-monorepo-simple/package.json index 994e30263e..193a42300a 100644 --- a/examples/react/router-monorepo-simple/package.json +++ b/examples/react/router-monorepo-simple/package.json @@ -8,8 +8,8 @@ "dev": "pnpm router build && pnpm post-feature build && pnpm app dev" }, "dependencies": { - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "@tanstack/router-plugin": "^1.91.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/router-monorepo-simple/packages/app/package.json b/examples/react/router-monorepo-simple/packages/app/package.json index 15c400fe1a..6ddd548fc7 100644 --- a/examples/react/router-monorepo-simple/packages/app/package.json +++ b/examples/react/router-monorepo-simple/packages/app/package.json @@ -19,7 +19,7 @@ "@types/react-dom": "^18.2.18", "@vitejs/plugin-react": "^4.3.4", "typescript": "^5.7.2", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/router-devtools": "^1.92.3", "vite": "^6.0.3", "vite-plugin-dts": "^4.3.0" }, diff --git a/examples/react/router-monorepo-simple/packages/router/package.json b/examples/react/router-monorepo-simple/packages/router/package.json index 6e6dad2ea7..e38ebaa073 100644 --- a/examples/react/router-monorepo-simple/packages/router/package.json +++ b/examples/react/router-monorepo-simple/packages/router/package.json @@ -9,7 +9,7 @@ "types": "./dist/index.d.ts", "dependencies": { "@tanstack/history": "^1.90.0", - "@tanstack/react-router": "^1.92.1", + "@tanstack/react-router": "^1.92.3", "@tanstack/router-plugin": "^1.91.1", "redaxios": "^0.5.1", "zod": "^3.23.8", diff --git a/examples/react/scroll-restoration/package.json b/examples/react/scroll-restoration/package.json index 5e5218ea8b..ea2d9437c3 100644 --- a/examples/react/scroll-restoration/package.json +++ b/examples/react/scroll-restoration/package.json @@ -9,9 +9,9 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.92.1", + "@tanstack/react-router": "^1.92.3", "@tanstack/react-virtual": "^3.11.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/router-devtools": "^1.92.3", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/examples/react/search-validator-adapters/package.json b/examples/react/search-validator-adapters/package.json index 9fdd21c8dc..253916415c 100644 --- a/examples/react/search-validator-adapters/package.json +++ b/examples/react/search-validator-adapters/package.json @@ -11,12 +11,12 @@ }, "dependencies": { "@tanstack/react-query": "^5.62.3", - "@tanstack/react-router": "^1.92.1", - "@tanstack/arktype-adapter": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/arktype-adapter": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "@tanstack/router-plugin": "^1.91.1", - "@tanstack/valibot-adapter": "^1.92.1", - "@tanstack/zod-adapter": "^1.92.1", + "@tanstack/valibot-adapter": "^1.92.3", + "@tanstack/zod-adapter": "^1.92.3", "arktype": "2.0.0-rc.26", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/start-basic-auth/package.json b/examples/react/start-basic-auth/package.json index 717bccd91a..927957ea4a 100644 --- a/examples/react/start-basic-auth/package.json +++ b/examples/react/start-basic-auth/package.json @@ -11,9 +11,9 @@ }, "dependencies": { "@prisma/client": "5.22.0", - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", - "@tanstack/start": "^1.92.2", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", + "@tanstack/start": "^1.92.3", "prisma": "^5.22.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/examples/react/start-basic-react-query/package.json b/examples/react/start-basic-react-query/package.json index 938527a9f5..b798625a80 100644 --- a/examples/react/start-basic-react-query/package.json +++ b/examples/react/start-basic-react-query/package.json @@ -11,10 +11,10 @@ "dependencies": { "@tanstack/react-query": "^5.62.3", "@tanstack/react-query-devtools": "^5.62.3", - "@tanstack/react-router": "^1.92.1", - "@tanstack/react-router-with-query": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", - "@tanstack/start": "^1.92.2", + "@tanstack/react-router": "^1.92.3", + "@tanstack/react-router-with-query": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", + "@tanstack/start": "^1.92.3", "react": "^18.3.1", "react-dom": "^18.3.1", "redaxios": "^0.5.1", diff --git a/examples/react/start-basic-rsc/package.json b/examples/react/start-basic-rsc/package.json index 39f979f0f0..7230171dd2 100644 --- a/examples/react/start-basic-rsc/package.json +++ b/examples/react/start-basic-rsc/package.json @@ -10,9 +10,9 @@ }, "dependencies": { "@babel/plugin-syntax-typescript": "^7.25.9", - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", - "@tanstack/start": "^1.92.2", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", + "@tanstack/start": "^1.92.3", "redaxios": "^0.5.1", "tailwind-merge": "^2.5.5", "vinxi": "0.5.1" diff --git a/examples/react/start-basic/package.json b/examples/react/start-basic/package.json index 58ce670527..b5d09b6e54 100644 --- a/examples/react/start-basic/package.json +++ b/examples/react/start-basic/package.json @@ -9,9 +9,9 @@ "start": "vinxi start" }, "dependencies": { - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", - "@tanstack/start": "^1.92.2", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", + "@tanstack/start": "^1.92.3", "react": "^18.3.1", "react-dom": "^18.3.1", "redaxios": "^0.5.1", diff --git a/examples/react/start-clerk-basic/package.json b/examples/react/start-clerk-basic/package.json index 8ed5e6d6a7..91051811e2 100644 --- a/examples/react/start-clerk-basic/package.json +++ b/examples/react/start-clerk-basic/package.json @@ -10,9 +10,9 @@ }, "dependencies": { "@clerk/tanstack-start": "0.6.5", - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", - "@tanstack/start": "^1.92.2", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", + "@tanstack/start": "^1.92.3", "react": "^18.3.1", "react-dom": "^18.3.1", "redaxios": "^0.5.1", diff --git a/examples/react/start-convex-trellaux/package.json b/examples/react/start-convex-trellaux/package.json index efa76bb986..485f1552ca 100644 --- a/examples/react/start-convex-trellaux/package.json +++ b/examples/react/start-convex-trellaux/package.json @@ -13,10 +13,10 @@ "dependencies": { "@tanstack/react-query": "^5.62.3", "@tanstack/react-query-devtools": "^5.62.3", - "@tanstack/react-router": "^1.92.1", - "@tanstack/react-router-with-query": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", - "@tanstack/start": "^1.92.2", + "@tanstack/react-router": "^1.92.3", + "@tanstack/react-router-with-query": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", + "@tanstack/start": "^1.92.3", "@convex-dev/react-query": "0.0.0-alpha.8", "concurrently": "^8.2.2", "convex": "^1.17.3", diff --git a/examples/react/start-counter/package.json b/examples/react/start-counter/package.json index 4276dc8289..4fe177a88c 100644 --- a/examples/react/start-counter/package.json +++ b/examples/react/start-counter/package.json @@ -9,8 +9,8 @@ "start": "vinxi start" }, "dependencies": { - "@tanstack/react-router": "^1.92.1", - "@tanstack/start": "^1.92.2", + "@tanstack/react-router": "^1.92.3", + "@tanstack/start": "^1.92.3", "react": "^18.3.1", "react-dom": "^18.3.1", "vinxi": "0.5.1" diff --git a/examples/react/start-large/package.json b/examples/react/start-large/package.json index 2d6011c718..ea21aebf88 100644 --- a/examples/react/start-large/package.json +++ b/examples/react/start-large/package.json @@ -12,9 +12,9 @@ }, "dependencies": { "@tanstack/react-query": "^5.62.3", - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", - "@tanstack/start": "^1.92.2", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", + "@tanstack/start": "^1.92.3", "react": "^18.3.1", "react-dom": "^18.3.1", "redaxios": "^0.5.1", diff --git a/examples/react/start-supabase-basic/package.json b/examples/react/start-supabase-basic/package.json index 05cc827acd..be27eaade4 100644 --- a/examples/react/start-supabase-basic/package.json +++ b/examples/react/start-supabase-basic/package.json @@ -15,9 +15,9 @@ "dependencies": { "@supabase/ssr": "^0.5.2", "@supabase/supabase-js": "^2.47.3", - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", - "@tanstack/start": "^1.92.2", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", + "@tanstack/start": "^1.92.3", "react": "^18.3.1", "react-dom": "^18.3.1", "vinxi": "0.5.1" diff --git a/examples/react/start-trellaux/package.json b/examples/react/start-trellaux/package.json index 6a9d5a55bc..4a5d2d0435 100644 --- a/examples/react/start-trellaux/package.json +++ b/examples/react/start-trellaux/package.json @@ -11,10 +11,10 @@ "dependencies": { "@tanstack/react-query": "^5.62.3", "@tanstack/react-query-devtools": "^5.62.3", - "@tanstack/react-router": "^1.92.1", - "@tanstack/react-router-with-query": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", - "@tanstack/start": "^1.92.2", + "@tanstack/react-router": "^1.92.3", + "@tanstack/react-router-with-query": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", + "@tanstack/start": "^1.92.3", "ky": "^1.7.2", "msw": "^2.6.8", "react": "^18.3.1", diff --git a/examples/react/with-framer-motion/package.json b/examples/react/with-framer-motion/package.json index 5985d7fd3d..e29bc41b32 100644 --- a/examples/react/with-framer-motion/package.json +++ b/examples/react/with-framer-motion/package.json @@ -9,8 +9,8 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "redaxios": "^0.5.1", "framer-motion": "^11.13.3", "react": "^18.2.0", diff --git a/examples/react/with-trpc-react-query/package.json b/examples/react/with-trpc-react-query/package.json index f704182d45..321af31d93 100644 --- a/examples/react/with-trpc-react-query/package.json +++ b/examples/react/with-trpc-react-query/package.json @@ -10,8 +10,8 @@ "dependencies": { "@tanstack/react-query": "^5.62.3", "@tanstack/react-query-devtools": "^5.62.3", - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "@tanstack/router-plugin": "^1.91.1", "@trpc/client": "11.0.0-rc.660", "@trpc/react-query": "11.0.0-rc.660", diff --git a/examples/react/with-trpc/package.json b/examples/react/with-trpc/package.json index 9a1d3f7142..52038d25e4 100644 --- a/examples/react/with-trpc/package.json +++ b/examples/react/with-trpc/package.json @@ -8,8 +8,8 @@ "start": "vinxi start" }, "dependencies": { - "@tanstack/react-router": "^1.92.1", - "@tanstack/router-devtools": "^1.92.1", + "@tanstack/react-router": "^1.92.3", + "@tanstack/router-devtools": "^1.92.3", "@tanstack/router-plugin": "^1.91.1", "@trpc/client": "11.0.0-rc.660", "@trpc/server": "11.0.0-rc.660", diff --git a/packages/arktype-adapter/package.json b/packages/arktype-adapter/package.json index 48e04c31a0..65a4d5a96f 100644 --- a/packages/arktype-adapter/package.json +++ b/packages/arktype-adapter/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/arktype-adapter", - "version": "1.92.1", + "version": "1.92.3", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/create-router/package.json b/packages/create-router/package.json index b90b35cd46..91c6a3ab56 100644 --- a/packages/create-router/package.json +++ b/packages/create-router/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/create-router", - "version": "1.92.1", + "version": "1.92.3", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/create-start/package.json b/packages/create-start/package.json index 6dc150e430..882762306d 100644 --- a/packages/create-start/package.json +++ b/packages/create-start/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/create-start", - "version": "1.92.2", + "version": "1.92.3", "description": "Modern and scalable routing for React applications", "author": "Tim O'Connell", "license": "MIT", diff --git a/packages/react-router-with-query/package.json b/packages/react-router-with-query/package.json index 0d20acfa09..25d405a53d 100644 --- a/packages/react-router-with-query/package.json +++ b/packages/react-router-with-query/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/react-router-with-query", - "version": "1.92.1", + "version": "1.92.3", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 8424810a68..8cdcb21928 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/react-router", - "version": "1.92.1", + "version": "1.92.3", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/router-devtools/package.json b/packages/router-devtools/package.json index 4cd65076ef..05f47940ea 100644 --- a/packages/router-devtools/package.json +++ b/packages/router-devtools/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/router-devtools", - "version": "1.92.1", + "version": "1.92.3", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/start/package.json b/packages/start/package.json index 1146c20b0f..708658c571 100644 --- a/packages/start/package.json +++ b/packages/start/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/start", - "version": "1.92.2", + "version": "1.92.3", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/valibot-adapter/package.json b/packages/valibot-adapter/package.json index eed9a2906a..f519f21db6 100644 --- a/packages/valibot-adapter/package.json +++ b/packages/valibot-adapter/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/valibot-adapter", - "version": "1.92.1", + "version": "1.92.3", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/zod-adapter/package.json b/packages/zod-adapter/package.json index eea1adbe6a..0ebe4b4319 100644 --- a/packages/zod-adapter/package.json +++ b/packages/zod-adapter/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/zod-adapter", - "version": "1.92.1", + "version": "1.92.3", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", From dd3ab7bc0b3b9a8b0e7ca71654840b83a3a9ca24 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Mon, 23 Dec 2024 22:46:50 -0700 Subject: [PATCH 28/38] move directive plugin to own package --- .../README.md | 0 .../eslint.config.js | 0 .../package.json | 0 .../src/compilers.ts | 592 +++++++++ .../directive-functions-plugin/src/index.ts | 60 + .../src/logger.ts | 0 .../tests/compiler.test.ts | 0 .../tsconfig.json | 0 .../vite.config.ts | 0 packages/server-functions-plugin/README.md | 5 + .../server-functions-plugin/eslint.config.js | 5 + packages/server-functions-plugin/package.json | 90 ++ .../src/compilers.ts | 23 +- .../src/index.ts | 56 +- .../server-functions-plugin/src/logger.ts | 59 + .../tests/compiler.test.ts | 1083 +++++++++++++++++ .../server-functions-plugin/tsconfig.json | 8 + .../server-functions-plugin/vite.config.ts | 20 + .../server-functions-vite-plugin/src/ast.ts | 20 - packages/start/src/client-runtime/index.tsx | 2 +- packages/start/src/config/index.ts | 2 +- packages/start/src/server-runtime/index.tsx | 2 +- packages/start/src/ssr-runtime/index.tsx | 2 +- pnpm-lock.yaml | 108 +- 24 files changed, 2035 insertions(+), 102 deletions(-) rename packages/{server-functions-vite-plugin => directive-functions-plugin}/README.md (100%) rename packages/{server-functions-vite-plugin => directive-functions-plugin}/eslint.config.js (100%) rename packages/{server-functions-vite-plugin => directive-functions-plugin}/package.json (100%) create mode 100644 packages/directive-functions-plugin/src/compilers.ts create mode 100644 packages/directive-functions-plugin/src/index.ts rename packages/{server-functions-vite-plugin => directive-functions-plugin}/src/logger.ts (100%) rename packages/{server-functions-vite-plugin => directive-functions-plugin}/tests/compiler.test.ts (100%) rename packages/{server-functions-vite-plugin => directive-functions-plugin}/tsconfig.json (100%) rename packages/{server-functions-vite-plugin => directive-functions-plugin}/vite.config.ts (100%) create mode 100644 packages/server-functions-plugin/README.md create mode 100644 packages/server-functions-plugin/eslint.config.js create mode 100644 packages/server-functions-plugin/package.json rename packages/{server-functions-vite-plugin => server-functions-plugin}/src/compilers.ts (97%) rename packages/{server-functions-vite-plugin => server-functions-plugin}/src/index.ts (80%) create mode 100644 packages/server-functions-plugin/src/logger.ts create mode 100644 packages/server-functions-plugin/tests/compiler.test.ts create mode 100644 packages/server-functions-plugin/tsconfig.json create mode 100644 packages/server-functions-plugin/vite.config.ts delete mode 100644 packages/server-functions-vite-plugin/src/ast.ts diff --git a/packages/server-functions-vite-plugin/README.md b/packages/directive-functions-plugin/README.md similarity index 100% rename from packages/server-functions-vite-plugin/README.md rename to packages/directive-functions-plugin/README.md diff --git a/packages/server-functions-vite-plugin/eslint.config.js b/packages/directive-functions-plugin/eslint.config.js similarity index 100% rename from packages/server-functions-vite-plugin/eslint.config.js rename to packages/directive-functions-plugin/eslint.config.js diff --git a/packages/server-functions-vite-plugin/package.json b/packages/directive-functions-plugin/package.json similarity index 100% rename from packages/server-functions-vite-plugin/package.json rename to packages/directive-functions-plugin/package.json diff --git a/packages/directive-functions-plugin/src/compilers.ts b/packages/directive-functions-plugin/src/compilers.ts new file mode 100644 index 0000000000..5b9f47a3af --- /dev/null +++ b/packages/directive-functions-plugin/src/compilers.ts @@ -0,0 +1,592 @@ +import path from 'node:path' +import * as babel from '@babel/core' +import _generate from '@babel/generator' +import { parse } from '@babel/parser' +import { isIdentifier, isVariableDeclarator } from '@babel/types' +import { codeFrameColumns } from '@babel/code-frame' +import { deadCodeElimination } from 'babel-dead-code-elimination' +import type { ParseResult } from '@babel/parser' + +let generate = _generate + +if ('default' in generate) { + generate = generate.default as typeof generate +} + +export interface DirectiveFn { + nodePath: SupportedFunctionPath + functionName: string + functionId: string + referenceName: string + splitFilename: string + filename: string + chunkName: string +} + +export type SupportedFunctionPath = + | babel.NodePath + | babel.NodePath + | babel.NodePath + +export type ReplacerFn = (opts: { + fn: string + splitImportFn: string + filename: string + functionId: string + isSplitFn: boolean +}) => string + +// const debug = process.env.TSR_VITE_DEBUG === 'true' + +export type CompileDirectivesOpts = ParseAstOptions & { + directive: string + directiveLabel: string + getRuntimeCode?: (opts: { + directiveFnsById: Record + }) => string + replacer: ReplacerFn + devSplitImporter: string +} + +export type ParseAstOptions = { + code: string + filename: string + root: string +} + +export function parseAst(opts: ParseAstOptions): ParseResult { + return parse(opts.code, { + plugins: ['jsx', 'typescript'], + sourceType: 'module', + ...{ + root: opts.root, + filename: opts.filename, + sourceMaps: true, + }, + }) +} + +export function compileDirectives(opts: CompileDirectivesOpts) { + const [_, searchParamsStr] = opts.filename.split('?') + const searchParams = new URLSearchParams(searchParamsStr) + const directiveSplitParam = `tsr-directive-${opts.directive.replace(/[^a-zA-Z0-9]/g, '-')}-split` + const functionName = searchParams.get(directiveSplitParam) + + const ast = parseAst(opts) + const directiveFnsById = findDirectives(ast, { + ...opts, + splitFunctionName: functionName, + directiveSplitParam, + }) + + const directiveFnsByFunctionName = Object.fromEntries( + Object.entries(directiveFnsById).map(([, fn]) => [fn.functionName, fn]), + ) + + // Add runtime code if there are directives + // Add runtime code if there are directives + if (Object.keys(directiveFnsById).length > 0) { + // Add a vite import to the top of the file + ast.program.body.unshift( + babel.types.importDeclaration( + [babel.types.importDefaultSpecifier(babel.types.identifier('vite'))], + babel.types.stringLiteral('vite'), + ), + ) + + if (opts.getRuntimeCode) { + const runtimeImport = babel.template.statement( + opts.getRuntimeCode({ directiveFnsById }), + )() + ast.program.body.unshift(runtimeImport) + } + } + + // If there is a functionName, we need to remove all exports + // then make sure that our function is exported under the + // directive name + if (functionName) { + const directiveFn = directiveFnsByFunctionName[functionName] + + if (!directiveFn) { + throw new Error(`${opts.directiveLabel} ${functionName} not found`) + } + + safeRemoveExports(ast) + + ast.program.body.push( + babel.types.exportDefaultDeclaration( + babel.types.identifier(directiveFn.referenceName), + ), + ) + } + + deadCodeElimination(ast) + + const compiledResult = generate(ast, { + sourceMaps: true, + sourceFileName: opts.filename, + minified: process.env.NODE_ENV === 'production', + }) + + return { + compiledResult, + directiveFnsById, + } +} + +function findNearestVariableName( + path: babel.NodePath, + directiveLabel: 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( + `${directiveLabel} in ClassMethod or ObjectMethod not supported`, + ) + } + + return '' + })() + + if (name) { + nameParts.unshift(name) + } + + currentPath = currentPath.parentPath + } + + return nameParts.length > 0 ? nameParts.join('_') : 'anonymous' +} + +function makeFileLocationUrlSafe(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 +} + +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 + directiveLabel: string + replacer?: ReplacerFn + splitFunctionName?: string | null + directiveSplitParam: string + devSplitImporter: string + }, +) { + const directiveFnsById: Record = {} + const functionNameCounts: Record = {} + + 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, + ) + + // 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) + } + }, + ExportNamedDeclaration(path) { + if (babel.types.isFunctionDeclaration(path.node.declaration)) { + compileDirective(path.get('declaration') as SupportedFunctionPath) + } + }, + }) + } else { + // Find all directives + babel.traverse(ast, { + DirectiveLiteral(nodePath) { + if (nodePath.node.value === opts.directive) { + const directiveFn = nodePath.findParent((p) => p.isFunction()) as + | SupportedFunctionPath + | undefined + + if (!directiveFn) return + + // Handle class and object methods which are not supported + const isGenerator = + directiveFn.isFunction() && directiveFn.node.generator + + const isClassMethod = directiveFn.isClassMethod() + const isObjectMethod = directiveFn.isObjectMethod() + + if (isClassMethod || isObjectMethod || isGenerator) { + throw codeFrameError( + opts.code, + directiveFn.node.loc, + `"${opts.directive}" in ${isClassMethod ? 'class' : isObjectMethod ? 'object method' : 'generator function'} 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.directiveLabel}s 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.directiveLabel}s must be function declarations or function expressions`, + ) + } + + compileDirective(directiveFn) + } + }, + }) + } + + return directiveFnsById + + function compileDirective(directiveFn: SupportedFunctionPath) { + // 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, + ) + } + + // Find the nearest variable name + let functionName = findNearestVariableName(directiveFn, opts.directiveLabel) + + // Count the number of functions with the same baseLabel + functionNameCounts[functionName] = + (functionNameCounts[functionName] || 0) + 1 + + // If there are multiple functions with the same fnName, + // append a unique identifier to the functionId + functionName = + functionNameCounts[functionName]! > 1 + ? `${functionName}_${functionNameCounts[functionName]! - 1}` + : functionName + + // Move the function to program level while preserving its position + // in the program body + const programBody = programPath.node.body + + const topParent = + directiveFn.findParent((p) => !!p.parentPath?.isProgram()) || directiveFn + + const topParentIndex = programBody.indexOf(topParent.node as any) + + // Determine the reference name for the function + let referenceName = makeIdentifierSafe(functionName) + + // Crawl the scope to refresh all the bindings + programPath.scope.crawl() + + // If we find this referece in the scope, we need to make it unique + while (programPath.scope.hasBinding(referenceName)) { + const [realReferenceName, count] = referenceName.split(/_(\d+)$/) + referenceName = realReferenceName + `_${Number(count || '0') + 1}` + } + + // if (referenceCounts.get(referenceName) === 0) { + + // referenceName += `_${(referenceCounts.get(referenceName) || 0) + 1}` + + // If the reference name came from the function declaration, + // // We need to update the function name to match the reference name + // if (babel.types.isFunctionDeclaration(directiveFn.node)) { + // console.log('updating function name', directiveFn.node.id!.name) + // directiveFn.node.id!.name = referenceName + // } + + // 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()) { + // Then place the function at the top level + programBody.splice( + topParentIndex, + 0, + babel.types.variableDeclaration('const', [ + babel.types.variableDeclarator( + babel.types.identifier(referenceName), + babel.types.toExpression(directiveFn.node as any), + ), + ]), + ) + + // If it's an exported named function, we need to swap it with an + // export const originalFunctionName = referenceName + if ( + babel.types.isExportNamedDeclaration(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(referenceName), + ), + ]), + ), + ) + + directiveFn.remove() + } else { + directiveFn.replaceWith(babel.types.identifier(referenceName)) + } + + directiveFn = programPath.get( + `body.${topParentIndex}.declarations.0.init`, + ) as SupportedFunctionPath + } + + const functionId = makeFileLocationUrlSafe( + `${opts.filename.replace( + path.extname(opts.filename), + '', + )}--${functionName}`.replace(opts.root, ''), + ) + + const [filename, searchParamsStr] = opts.filename.split('?') + const searchParams = new URLSearchParams(searchParamsStr) + searchParams.set(opts.directiveSplitParam, functionName) + const splitFilename = `${filename}?${searchParams.toString()}` + + // If a replacer is provided, replace the function with the replacer + if (opts.replacer) { + const replacer = opts.replacer({ + fn: '$$fn$$', + splitImportFn: '$$splitImportFn$$', + // splitFilename, + filename: filename!, + functionId: functionId, + isSplitFn: functionName === opts.splitFunctionName, + }) + + const replacement = babel.template.expression(replacer, { + placeholderPattern: false, + placeholderWhitelist: new Set(['$$fn$$', '$$splitImportFn$$']), + })({ + ...(replacer.includes('$$fn$$') + ? { $$fn$$: babel.types.toExpression(directiveFn.node) } + : {}), + ...(replacer.includes('$$splitImportFn$$') + ? { + $$splitImportFn$$: + process.env.NODE_ENV === 'production' + ? `(...args) => import(${JSON.stringify(splitFilename)}).then(module => module.default(...args))` + : `(...args) => ${opts.devSplitImporter}(${JSON.stringify(splitFilename)}).then(module => module.default(...args))`, + } + : {}), + }) + + directiveFn.replaceWith(replacement) + } + + // Finally register the directive to + // our map of directives + directiveFnsById[functionId] = { + nodePath: directiveFn, + referenceName, + functionName: functionName || '', + functionId: functionId, + splitFilename, + filename: opts.filename, + chunkName: fileNameToChunkName(opts.root, splitFilename), + } + } +} + +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) => { + const programBody = ast.program.body + + const removeExport = ( + path: + | babel.NodePath + | babel.NodePath, + ) => { + // If the value is a function declaration, class declaration, or variable declaration, + // That means it has a name and can remain in the file, just unexported. + if ( + babel.types.isFunctionDeclaration(path.node.declaration) || + babel.types.isClassDeclaration(path.node.declaration) || + babel.types.isVariableDeclaration(path.node.declaration) + ) { + // If the value is a function declaration, class declaration, or variable declaration, + // That means it has a name and can remain in the file, just unexported. + if ( + babel.types.isFunctionDeclaration(path.node.declaration) || + babel.types.isClassDeclaration(path.node.declaration) || + babel.types.isVariableDeclaration(path.node.declaration) + ) { + // Move the declaration to the top level at the same index + const insertIndex = programBody.findIndex( + (node) => node === path.node.declaration, + ) + programBody.splice(insertIndex, 0, path.node.declaration as any) + } + } + + // Otherwise, remove the export declaration + path.remove() + } + + // Before we add our export, remove any other exports. + // Don't remove the thing they export, just the export declaration + babel.traverse(ast, { + ExportDefaultDeclaration(path) { + removeExport(path) + }, + ExportNamedDeclaration(path) { + removeExport(path) + }, + }) +} + +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 new file mode 100644 index 0000000000..801a3abeb0 --- /dev/null +++ b/packages/directive-functions-plugin/src/index.ts @@ -0,0 +1,60 @@ +import { fileURLToPath, pathToFileURL } from 'node:url' + +import { compileDirectives } from './compilers' +import { logDiff } from './logger' +import type { CompileDirectivesOpts, DirectiveFn } from './compilers' +import type { Plugin } from 'vite' + +const debug = Boolean(process.env.TSR_VITE_DEBUG) + +export type DirectiveFunctionsViteOptions = Pick< + CompileDirectivesOpts, + 'directive' | 'directiveLabel' | 'getRuntimeCode' | 'replacer' +> + +const createDirectiveRx = (directive: string) => + new RegExp(`"${directive}"|'${directive}'`, 'g') + +export function TanStackDirectiveFunctionsPlugin( + opts: DirectiveFunctionsViteOptions & { + onDirectiveFnsById?: (directiveFnsById: Record) => void + }, +): Plugin { + let ROOT: string = process.cwd() + + const directiveRx = createDirectiveRx(opts.directive) + + return { + name: 'tanstack-start-directive-vite-plugin', + enforce: 'pre', + configResolved: (config) => { + ROOT = config.root + }, + transform(code, id) { + const url = pathToFileURL(id) + url.searchParams.delete('v') + id = fileURLToPath(url).replace(/\\/g, '/') + + if (!directiveRx.test(code)) { + return null + } + + const { compiledResult, directiveFnsById } = compileDirectives({ + ...opts, + code, + root: ROOT, + filename: id, + // globalThis.app currently refers to Vinxi's app instance. In the future, it can just be the + // vite dev server instance we get from Nitro. + devSplitImporter: `(globalThis.app.getRouter('server').internals.devServer.ssrLoadModule)`, + }) + + opts.onDirectiveFnsById?.(directiveFnsById) + + if (debug) console.info('Directive Input/Output') + if (debug) logDiff(code, compiledResult.code) + + return compiledResult + }, + } +} diff --git a/packages/server-functions-vite-plugin/src/logger.ts b/packages/directive-functions-plugin/src/logger.ts similarity index 100% rename from packages/server-functions-vite-plugin/src/logger.ts rename to packages/directive-functions-plugin/src/logger.ts diff --git a/packages/server-functions-vite-plugin/tests/compiler.test.ts b/packages/directive-functions-plugin/tests/compiler.test.ts similarity index 100% rename from packages/server-functions-vite-plugin/tests/compiler.test.ts rename to packages/directive-functions-plugin/tests/compiler.test.ts diff --git a/packages/server-functions-vite-plugin/tsconfig.json b/packages/directive-functions-plugin/tsconfig.json similarity index 100% rename from packages/server-functions-vite-plugin/tsconfig.json rename to packages/directive-functions-plugin/tsconfig.json diff --git a/packages/server-functions-vite-plugin/vite.config.ts b/packages/directive-functions-plugin/vite.config.ts similarity index 100% rename from packages/server-functions-vite-plugin/vite.config.ts rename to packages/directive-functions-plugin/vite.config.ts diff --git a/packages/server-functions-plugin/README.md b/packages/server-functions-plugin/README.md new file mode 100644 index 0000000000..798fa094a3 --- /dev/null +++ b/packages/server-functions-plugin/README.md @@ -0,0 +1,5 @@ + + +# TanStack Start Vite Plugin + +See https://tanstack.com/router/latest/docs/framework/react/guide/file-based-routing diff --git a/packages/server-functions-plugin/eslint.config.js b/packages/server-functions-plugin/eslint.config.js new file mode 100644 index 0000000000..8ce6ad05fc --- /dev/null +++ b/packages/server-functions-plugin/eslint.config.js @@ -0,0 +1,5 @@ +// @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 new file mode 100644 index 0000000000..319df7bbfd --- /dev/null +++ b/packages/server-functions-plugin/package.json @@ -0,0 +1,90 @@ +{ + "name": "@tanstack/server-functions-plugin", + "version": "1.87.3", + "description": "Modern and scalable routing for React applications", + "author": "Tanner Linsley", + "license": "MIT", + "repository": { + "type": "git", + "url": "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:ts52": "node ../../node_modules/typescript52/lib/tsc.js", + "test:types:ts53": "node ../../node_modules/typescript53/lib/tsc.js", + "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": "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": { + "@tanstack/directive-functions-plugin": "workspace:*", + "@babel/code-frame": "7.26.2", + "@babel/core": "^7.26.0", + "@babel/generator": "^7.26.3", + "@babel/parser": "^7.26.3", + "@babel/plugin-syntax-jsx": "^7.25.9", + "@babel/plugin-syntax-typescript": "^7.25.9", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.26.4", + "@babel/types": "^7.26.3", + "@types/babel__code-frame": "^7.0.6", + "@types/babel__core": "^7.20.5", + "@types/babel__generator": "^7.6.8", + "@types/babel__template": "^7.4.4", + "@types/babel__traverse": "^7.20.6", + "@types/diff": "^6.0.0", + "babel-dead-code-elimination": "^1.0.6", + "chalk": "^5.3.0", + "dedent": "^1.5.3", + "diff": "^7.0.0", + "tiny-invariant": "^1.3.3" + } +} diff --git a/packages/server-functions-vite-plugin/src/compilers.ts b/packages/server-functions-plugin/src/compilers.ts similarity index 97% rename from packages/server-functions-vite-plugin/src/compilers.ts rename to packages/server-functions-plugin/src/compilers.ts index eb16b459bc..d01d482074 100644 --- a/packages/server-functions-vite-plugin/src/compilers.ts +++ b/packages/server-functions-plugin/src/compilers.ts @@ -1,12 +1,11 @@ import path from 'node:path' import * as babel from '@babel/core' import _generate from '@babel/generator' - +import { parse } from '@babel/parser' import { isIdentifier, isVariableDeclarator } from '@babel/types' import { codeFrameColumns } from '@babel/code-frame' import { deadCodeElimination } from 'babel-dead-code-elimination' -import { parseAst } from './ast' -import type { ParseAstOptions } from './ast' +import type { ParseResult } from '@babel/parser' let generate = _generate @@ -49,6 +48,24 @@ export type CompileDirectivesOpts = ParseAstOptions & { devSplitImporter: string } +export type ParseAstOptions = { + code: string + filename: string + root: string +} + +export function parseAst(opts: ParseAstOptions): ParseResult { + return parse(opts.code, { + plugins: ['jsx', 'typescript'], + sourceType: 'module', + ...{ + root: opts.root, + filename: opts.filename, + sourceMaps: true, + }, + }) +} + export function compileDirectives(opts: CompileDirectivesOpts) { const [_, searchParamsStr] = opts.filename.split('?') const searchParams = new URLSearchParams(searchParamsStr) diff --git a/packages/server-functions-vite-plugin/src/index.ts b/packages/server-functions-plugin/src/index.ts similarity index 80% rename from packages/server-functions-vite-plugin/src/index.ts rename to packages/server-functions-plugin/src/index.ts index 7f42dd8db7..1ae9eae0b4 100644 --- a/packages/server-functions-vite-plugin/src/index.ts +++ b/packages/server-functions-plugin/src/index.ts @@ -1,15 +1,10 @@ -import { fileURLToPath, pathToFileURL } from 'node:url' - import { mkdirSync, readFileSync, writeFileSync } from 'node:fs' import path from 'node:path' -import { compileDirectives } from './compilers' -import { logDiff } from './logger' +import { TanStackDirectiveFunctionsPlugin } from '@tanstack/directive-functions-plugin' import type { CompileDirectivesOpts, DirectiveFn } from './compilers' import type { Plugin } from 'vite' -const debug = Boolean(process.env.TSR_VITE_DEBUG) - -export type ServerFunctionsViteOptions = Pick< +export type DirectiveFunctionsViteOptions = Pick< CompileDirectivesOpts, 'directive' | 'directiveLabel' | 'getRuntimeCode' | 'replacer' > @@ -21,9 +16,6 @@ export type CreateServerRpcFn = ( splitImportFn: string, ) => any -const createDirectiveRx = (directive: string) => - new RegExp(`"${directive}"|'${directive}'`, 'g') - export function createTanStackServerFnPlugin(_opts?: {}): { client: Array ssr: Array @@ -199,47 +191,3 @@ export function createTanStackServerFnPlugin(_opts?: {}): { ], } } - -export function TanStackDirectiveFunctionsPlugin( - opts: ServerFunctionsViteOptions & { - onDirectiveFnsById?: (directiveFnsById: Record) => void - }, -): Plugin { - let ROOT: string = process.cwd() - - const directiveRx = createDirectiveRx(opts.directive) - - return { - name: 'tanstack-start-directive-vite-plugin', - enforce: 'pre', - configResolved: (config) => { - ROOT = config.root - }, - transform(code, id) { - const url = pathToFileURL(id) - url.searchParams.delete('v') - id = fileURLToPath(url).replace(/\\/g, '/') - - if (!directiveRx.test(code)) { - return null - } - - const { compiledResult, directiveFnsById } = compileDirectives({ - ...opts, - code, - root: ROOT, - filename: id, - // globalThis.app currently refers to Vinxi's app instance. In the future, it can just be the - // vite dev server instance we get from Nitro. - devSplitImporter: `(globalThis.app.getRouter('server').internals.devServer.ssrLoadModule)`, - }) - - opts.onDirectiveFnsById?.(directiveFnsById) - - if (debug) console.info('Directive Input/Output') - if (debug) logDiff(code, compiledResult.code) - - return compiledResult - }, - } -} diff --git a/packages/server-functions-plugin/src/logger.ts b/packages/server-functions-plugin/src/logger.ts new file mode 100644 index 0000000000..fd777080c3 --- /dev/null +++ b/packages/server-functions-plugin/src/logger.ts @@ -0,0 +1,59 @@ +import chalk from 'chalk' +import { diffWords } from 'diff' + +export function logDiff(oldStr: string, newStr: string) { + const differences = diffWords(oldStr, newStr) + + let output = '' + let unchangedLines = '' + + function processUnchangedLines(lines: string): string { + const lineArray = lines.split('\n') + if (lineArray.length > 4) { + return [ + chalk.dim(lineArray[0]), + chalk.dim(lineArray[1]), + '', + chalk.dim.bold(`... (${lineArray.length - 4} lines) ...`), + '', + chalk.dim(lineArray[lineArray.length - 2]), + chalk.dim(lineArray[lineArray.length - 1]), + ].join('\n') + } + return chalk.dim(lines) + } + + differences.forEach((part, index) => { + const nextPart = differences[index + 1] + + if (part.added) { + if (unchangedLines) { + output += processUnchangedLines(unchangedLines) + unchangedLines = '' + } + output += chalk.green.bold(part.value) + if (nextPart?.removed) output += ' ' + } else if (part.removed) { + if (unchangedLines) { + output += processUnchangedLines(unchangedLines) + unchangedLines = '' + } + output += chalk.red.bold(part.value) + if (nextPart?.added) output += ' ' + } else { + unchangedLines += part.value + } + }) + + // Process any remaining unchanged lines at the end + if (unchangedLines) { + output += processUnchangedLines(unchangedLines) + } + + if (output) { + console.log('\nDiff:') + console.log(output) + } else { + console.log('No changes') + } +} diff --git a/packages/server-functions-plugin/tests/compiler.test.ts b/packages/server-functions-plugin/tests/compiler.test.ts new file mode 100644 index 0000000000..7092e1e348 --- /dev/null +++ b/packages/server-functions-plugin/tests/compiler.test.ts @@ -0,0 +1,1083 @@ +import { describe, expect, test } from 'vitest' + +import { compileDirectives } from '../src/compilers' +import type { CompileDirectivesOpts } from '../src/compilers' + +const clientConfig: Omit = { + directive: 'use server', + directiveLabel: 'Server function', + root: './test-files', + filename: 'test.ts', + getRuntimeCode: () => 'import { createClientRpc } from "my-rpc-lib-client"', + replacer: (opts) => + `createClientRpc({ + filename: ${JSON.stringify(opts.filename)}, + functionId: ${JSON.stringify(opts.functionId)}, + })`, + devSplitImporter: `devImport`, +} + +const ssrConfig: Omit = { + directive: 'use server', + directiveLabel: 'Server function', + root: './test-files', + filename: 'test.ts', + getRuntimeCode: () => 'import { createSsrRpc } from "my-rpc-lib-server"', + replacer: (opts) => + `createSsrRpc({ + filename: ${JSON.stringify(opts.filename)}, + functionId: ${JSON.stringify(opts.functionId)}, + })`, + devSplitImporter: `devImport`, +} + +const serverConfig: Omit = { + directive: 'use server', + directiveLabel: 'Server function', + root: './test-files', + filename: 'test.ts', + getRuntimeCode: () => 'import { createServerRpc } from "my-rpc-lib-server"', + 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({ + fn: ${opts.isSplitFn ? opts.fn : opts.splitImportFn}, + filename: ${JSON.stringify(opts.filename)}, + functionId: ${JSON.stringify(opts.functionId)}, + })`, + devSplitImporter: `devImport`, +} + +describe('server function compilation', () => { + const code = ` + export const namedFunction = wrapper(function namedFunction() { + 'use server' + return 'hello' + }) + + export const arrowFunction = wrapper(() => { + 'use server' + return 'hello' + }) + + export const anonymousFunction = wrapper(function () { + 'use server' + return 'hello' + }) + + 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() + } + + function usedFn() { + return 'hello' + } + + function unusedFn() { + return 'hello' + } + + const namedDefaultExport = 'namedDefaultExport' + export default namedDefaultExport + + const usedButNotExported = 'usedButNotExported' + + const namedExport = 'namedExport' + + export { + namedExport + } + + ` + + test('basic function declaration nested in other variable', () => { + const client = compileDirectives({ + ...clientConfig, + code, + }) + const ssr = compileDirectives({ ...ssrConfig, code }) + const splitFiles = Object.entries(ssr.directiveFnsById) + .map(([_fnId, directiveFn]) => { + return `// ${directiveFn.functionId}\n\n${ + compileDirectives({ + ...serverConfig, + code, + filename: directiveFn.splitFilename, + }).compiledResult.code + }` + }) + .join('\n\n\n') + + expect(client.compiledResult.code).toMatchInlineSnapshot(` + "import { createClientRpc } from "my-rpc-lib-client"; + const namedFunction_wrapper_namedFunction = createClientRpc({ + filename: "test.ts", + functionId: "test--namedFunction_wrapper_namedFunction" + }); + export const namedFunction = wrapper(namedFunction_wrapper_namedFunction); + const arrowFunction_wrapper = createClientRpc({ + filename: "test.ts", + functionId: "test--arrowFunction_wrapper" + }); + export const arrowFunction = wrapper(arrowFunction_wrapper); + const anonymousFunction_wrapper = createClientRpc({ + filename: "test.ts", + functionId: "test--anonymousFunction_wrapper" + }); + export const anonymousFunction = wrapper(anonymousFunction_wrapper); + const multipleDirectives_multipleDirectives = createClientRpc({ + filename: "test.ts", + functionId: "test--multipleDirectives_multipleDirectives" + }); + export const multipleDirectives = multipleDirectives_multipleDirectives; + const iife_1 = createClientRpc({ + filename: "test.ts", + functionId: "test--iife" + }); + export const iife = iife_1(); + const defaultExportFn_1 = createClientRpc({ + filename: "test.ts", + functionId: "test--defaultExportFn" + }); + export default defaultExportFn_1; + const namedExportFn_1 = createClientRpc({ + filename: "test.ts", + functionId: "test--namedExportFn" + }); + export const namedExportFn = namedExportFn_1; + const exportedArrowFunction_wrapper = createClientRpc({ + filename: "test.ts", + functionId: "test--exportedArrowFunction_wrapper" + }); + export const exportedArrowFunction = wrapper(exportedArrowFunction_wrapper); + const namedExportConst_1 = createClientRpc({ + filename: "test.ts", + functionId: "test--namedExportConst" + }); + export const namedExportConst = namedExportConst_1; + const namedDefaultExport = 'namedDefaultExport'; + export default namedDefaultExport; + const namedExport = 'namedExport'; + export { namedExport };" + `) + + expect(ssr.compiledResult.code).toMatchInlineSnapshot(` + "import { createSsrRpc } from "my-rpc-lib-server"; + const namedFunction_wrapper_namedFunction = createSsrRpc({ + filename: "test.ts", + functionId: "test--namedFunction_wrapper_namedFunction" + }); + export const namedFunction = wrapper(namedFunction_wrapper_namedFunction); + const arrowFunction_wrapper = createSsrRpc({ + filename: "test.ts", + functionId: "test--arrowFunction_wrapper" + }); + export const arrowFunction = wrapper(arrowFunction_wrapper); + const anonymousFunction_wrapper = createSsrRpc({ + filename: "test.ts", + functionId: "test--anonymousFunction_wrapper" + }); + export const anonymousFunction = wrapper(anonymousFunction_wrapper); + const multipleDirectives_multipleDirectives = createSsrRpc({ + filename: "test.ts", + functionId: "test--multipleDirectives_multipleDirectives" + }); + export const multipleDirectives = multipleDirectives_multipleDirectives; + const iife_1 = createSsrRpc({ + filename: "test.ts", + functionId: "test--iife" + }); + export const iife = iife_1(); + const defaultExportFn_1 = createSsrRpc({ + filename: "test.ts", + functionId: "test--defaultExportFn" + }); + export default defaultExportFn_1; + const namedExportFn_1 = createSsrRpc({ + filename: "test.ts", + functionId: "test--namedExportFn" + }); + export const namedExportFn = namedExportFn_1; + const exportedArrowFunction_wrapper = createSsrRpc({ + filename: "test.ts", + functionId: "test--exportedArrowFunction_wrapper" + }); + export const exportedArrowFunction = wrapper(exportedArrowFunction_wrapper); + const namedExportConst_1 = createSsrRpc({ + filename: "test.ts", + functionId: "test--namedExportConst" + }); + export const namedExportConst = namedExportConst_1; + const namedDefaultExport = 'namedDefaultExport'; + export default namedDefaultExport; + const namedExport = 'namedExport'; + export { namedExport };" + `) + + expect(splitFiles).toMatchInlineSnapshot( + ` + "// test--namedFunction_wrapper_namedFunction + + import { createServerRpc } from "my-rpc-lib-server"; + const namedFunction_wrapper_namedFunction = createServerRpc({ + fn: function namedFunction() { + return 'hello'; + }, + filename: "test.ts", + functionId: "test--namedFunction_wrapper_namedFunction" + }); + export default namedFunction_wrapper_namedFunction; + + + // test--arrowFunction_wrapper + + import { createServerRpc } from "my-rpc-lib-server"; + const arrowFunction_wrapper = createServerRpc({ + fn: () => { + return 'hello'; + }, + filename: "test.ts", + functionId: "test--arrowFunction_wrapper" + }); + export default arrowFunction_wrapper; + + + // test--anonymousFunction_wrapper + + import { createServerRpc } from "my-rpc-lib-server"; + const anonymousFunction_wrapper = createServerRpc({ + fn: function () { + return 'hello'; + }, + filename: "test.ts", + functionId: "test--anonymousFunction_wrapper" + }); + export default anonymousFunction_wrapper; + + + // test--multipleDirectives_multipleDirectives + + import { createServerRpc } from "my-rpc-lib-server"; + const multipleDirectives_multipleDirectives = createServerRpc({ + fn: function multipleDirectives() { + 'use strict'; + + return 'hello'; + }, + filename: "test.ts", + functionId: "test--multipleDirectives_multipleDirectives" + }); + export default multipleDirectives_multipleDirectives; + + + // test--iife + + import { createServerRpc } from "my-rpc-lib-server"; + const iife_1 = createServerRpc({ + fn: function () { + return 'hello'; + }, + filename: "test.ts", + functionId: "test--iife" + }); + export default iife_1; + + + // test--defaultExportFn + + import { createServerRpc } from "my-rpc-lib-server"; + const defaultExportFn_1 = createServerRpc({ + fn: function defaultExportFn() { + return 'hello'; + }, + filename: "test.ts", + functionId: "test--defaultExportFn" + }); + export default defaultExportFn_1; + + + // test--namedExportFn + + import { createServerRpc } from "my-rpc-lib-server"; + const namedExportFn_1 = createServerRpc({ + fn: function namedExportFn() { + return 'hello'; + }, + filename: "test.ts", + functionId: "test--namedExportFn" + }); + export default namedExportFn_1; + + + // test--exportedArrowFunction_wrapper + + import { createServerRpc } from "my-rpc-lib-server"; + const exportedArrowFunction_wrapper = createServerRpc({ + fn: () => { + return 'hello'; + }, + filename: "test.ts", + functionId: "test--exportedArrowFunction_wrapper" + }); + export default exportedArrowFunction_wrapper; + + + // test--namedExportConst + + import { createServerRpc } from "my-rpc-lib-server"; + const namedExportConst_1 = createServerRpc({ + fn: () => { + return usedFn(); + }, + filename: "test.ts", + functionId: "test--namedExportConst" + }); + function usedFn() { + return 'hello'; + } + export default namedExportConst_1;" + `, + ) + }) + + test('Does not support function declarations nested in other blocks', () => { + const code = ` + outer(() => { + function useServer() { + 'use server' + return 'hello' + } + }) + ` + + expect(() => + compileDirectives({ ...clientConfig, code }), + ).toThrowErrorMatchingInlineSnapshot( + ` + [Error: 1 | + > 2 | outer(() => { + | ^^ + > 3 | function useServer() { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 4 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 5 | return 'hello' + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 6 | } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 7 | }) + | ^^^^^^^ Server functions cannot be nested in other blocks or functions + 8 | ] + `, + ) + expect(() => + compileDirectives({ ...serverConfig, code }), + ).toThrowErrorMatchingInlineSnapshot( + ` + [Error: 1 | + > 2 | outer(() => { + | ^^ + > 3 | function useServer() { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 4 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 5 | return 'hello' + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 6 | } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 7 | }) + | ^^^^^^^ Server functions cannot be nested in other blocks or functions + 8 | ] + `, + ) + expect(() => + compileDirectives({ + ...serverConfig, + code, + filename: serverConfig.filename + `?tsr-serverfn-split=temp`, + }), + ).toThrowErrorMatchingInlineSnapshot( + ` + [Error: 1 | + > 2 | outer(() => { + | ^^ + > 3 | function useServer() { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 4 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 5 | return 'hello' + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 6 | } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 7 | }) + | ^^^^^^^ Server functions cannot be nested in other blocks or functions + 8 | ] + `, + ) + }) + + 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 })) + .toThrowErrorMatchingInlineSnapshot(` + [Error: 1 | + 2 | class TestClass { + > 3 | method() { + | ^^^^^^^^^^^ + > 4 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 5 | return 'hello' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 6 | } + | ^^^^^^^^^ "use server" in class not supported + 7 | + 8 | static staticMethod() { + 9 | 'use server'] + `) + expect(() => compileDirectives({ ...serverConfig, code })) + .toThrowErrorMatchingInlineSnapshot(` + [Error: 1 | + 2 | class TestClass { + > 3 | method() { + | ^^^^^^^^^^^ + > 4 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 5 | return 'hello' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 6 | } + | ^^^^^^^^^ "use server" in class not supported + 7 | + 8 | static staticMethod() { + 9 | 'use server'] + `) + expect(() => + compileDirectives({ + ...serverConfig, + code, + filename: serverConfig.filename + `?tsr-serverfn-split=temp`, + }), + ).toThrowErrorMatchingInlineSnapshot(` + [Error: 1 | + 2 | class TestClass { + > 3 | method() { + | ^^^^^^^^^^^ + > 4 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 5 | return 'hello' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 6 | } + | ^^^^^^^^^ "use server" in class not supported + 7 | + 8 | static staticMethod() { + 9 | 'use server'] + `) + }) + + test('does not support object methods', () => { + const code = ` + const obj = { + method() { + 'use server' + return 'hello' + }, + } + ` + + expect(() => compileDirectives({ ...clientConfig, code })) + .toThrowErrorMatchingInlineSnapshot(` + [Error: 1 | + 2 | const obj = { + > 3 | method() { + | ^^^^^^^^^^^ + > 4 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 5 | return 'hello' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 6 | }, + | ^^^^^^^^^ "use server" in object method not supported + 7 | } + 8 | ] + `) + expect(() => compileDirectives({ ...serverConfig, code })) + .toThrowErrorMatchingInlineSnapshot(` + [Error: 1 | + 2 | const obj = { + > 3 | method() { + | ^^^^^^^^^^^ + > 4 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 5 | return 'hello' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 6 | }, + | ^^^^^^^^^ "use server" in object method not supported + 7 | } + 8 | ] + `) + expect(() => + compileDirectives({ + ...serverConfig, + code, + filename: serverConfig.filename + `?tsr-serverfn-split=temp`, + }), + ).toThrowErrorMatchingInlineSnapshot(` + [Error: 1 | + 2 | const obj = { + > 3 | method() { + | ^^^^^^^^^^^ + > 4 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 5 | return 'hello' + | ^^^^^^^^^^^^^^^^^^^^^^ + > 6 | }, + | ^^^^^^^^^ "use server" in object method not supported + 7 | } + 8 | ] + `) + }) + + test('does not support generator functions', () => { + const code = ` + function* generatorServer() { + 'use server' + yield 'hello' + } + + async function* asyncGeneratorServer() { + 'use server' + yield 'hello' + } + ` + + expect(() => compileDirectives({ ...clientConfig, code })) + .toThrowErrorMatchingInlineSnapshot(` + [Error: 1 | + > 2 | function* generatorServer() { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 3 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^ + > 4 | yield 'hello' + | ^^^^^^^^^^^^^^^^^^^^ + > 5 | } + | ^^^^^^^ "use server" in generator function not supported + 6 | + 7 | async function* asyncGeneratorServer() { + 8 | 'use server'] + `) + expect(() => compileDirectives({ ...serverConfig, code })) + .toThrowErrorMatchingInlineSnapshot(` + [Error: 1 | + > 2 | function* generatorServer() { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 3 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^ + > 4 | yield 'hello' + | ^^^^^^^^^^^^^^^^^^^^ + > 5 | } + | ^^^^^^^ "use server" in generator function not supported + 6 | + 7 | async function* asyncGeneratorServer() { + 8 | 'use server'] + `) + expect(() => + compileDirectives({ + ...serverConfig, + code, + filename: serverConfig.filename + `?tsr-serverfn-split=temp`, + }), + ).toThrowErrorMatchingInlineSnapshot(` + [Error: 1 | + > 2 | function* generatorServer() { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 3 | 'use server' + | ^^^^^^^^^^^^^^^^^^^^ + > 4 | yield 'hello' + | ^^^^^^^^^^^^^^^^^^^^ + > 5 | } + | ^^^^^^^ "use server" in generator function not supported + 6 | + 7 | async function* asyncGeneratorServer() { + 8 | 'use server'] + `) + }) + + test('multiple directiveFnsById', () => { + const code = ` + function multiDirective() { + 'use strict' + 'use server' + return 'hello' + } + ` + + const client = compileDirectives({ ...clientConfig, code }) + const ssr = compileDirectives({ ...ssrConfig, code }) + const splitFiles = Object.entries(ssr.directiveFnsById) + .map(([_fnId, directiveFn]) => { + return `// ${directiveFn.functionId}\n\n${ + compileDirectives({ + ...serverConfig, + code, + filename: directiveFn.splitFilename, + }).compiledResult.code + }` + }) + .join('\n\n\n') + + expect(client.compiledResult.code).toMatchInlineSnapshot(` + "import { createClientRpc } from "my-rpc-lib-client"; + createClientRpc({ + filename: "test.ts", + functionId: "test--multiDirective" + });" + `) + expect(ssr.compiledResult.code).toMatchInlineSnapshot(` + "import { createSsrRpc } from "my-rpc-lib-server"; + createSsrRpc({ + filename: "test.ts", + functionId: "test--multiDirective" + });" + `) + expect(splitFiles).toMatchInlineSnapshot(` + "// test--multiDirective + + import { createServerRpc } from "my-rpc-lib-server"; + createServerRpc({ + fn: function multiDirective() { + 'use strict'; + + return 'hello'; + }, + filename: "test.ts", + functionId: "test--multiDirective" + }); + export default 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 splitFiles = Object.entries(ssr.directiveFnsById) + .map(([_fnId, directiveFn]) => { + return `// ${directiveFn.functionId}\n\n${ + compileDirectives({ + ...serverConfig, + code, + filename: directiveFn.splitFilename, + }).compiledResult.code + }` + }) + .join('\n\n\n') + + expect(client.compiledResult.code).toMatchInlineSnapshot(` + "import { createClientRpc } from "my-rpc-lib-client"; + const iife_1 = createClientRpc({ + filename: "test.ts", + functionId: "test--iife" + }); + export const iife = iife_1();" + `) + + expect(ssr.compiledResult.code).toMatchInlineSnapshot(` + "import { createSsrRpc } from "my-rpc-lib-server"; + const iife_1 = createSsrRpc({ + filename: "test.ts", + functionId: "test--iife" + }); + export const iife = iife_1();" + `) + + expect(splitFiles).toMatchInlineSnapshot(` + "// test--iife + + import { createServerRpc } from "my-rpc-lib-server"; + const iife_1 = createServerRpc({ + fn: function () { + return 'hello'; + }, + filename: "test.ts", + functionId: "test--iife" + }); + export default 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 splitFiles = Object.entries(ssr.directiveFnsById) + .map(([_fnId, directiveFn]) => { + return `// ${directiveFn.functionId}\n\n${ + compileDirectives({ + ...serverConfig, + code, + filename: directiveFn.splitFilename, + }).compiledResult.code + }` + }) + .join('\n\n\n') + + expect(client.compiledResult.code).toMatchInlineSnapshot(` + "import { createClientRpc } from "my-rpc-lib-client"; + const outer_useServer = createClientRpc({ + filename: "test.ts", + functionId: "test--outer_useServer" + }); + outer(outer_useServer); + const outer_useServer_1 = createClientRpc({ + filename: "test.ts", + functionId: "test--outer_useServer_1" + }); + outer(outer_useServer_1);" + `) + + expect(ssr.compiledResult.code).toMatchInlineSnapshot(` + "import { createSsrRpc } from "my-rpc-lib-server"; + const outer_useServer = createSsrRpc({ + filename: "test.ts", + functionId: "test--outer_useServer" + }); + outer(outer_useServer); + const outer_useServer_1 = createSsrRpc({ + filename: "test.ts", + functionId: "test--outer_useServer_1" + }); + outer(outer_useServer_1);" + `) + + expect(splitFiles).toMatchInlineSnapshot(` + "// test--outer_useServer + + import { createServerRpc } from "my-rpc-lib-server"; + const outer_useServer = createServerRpc({ + fn: function useServer() { + return 'hello'; + }, + filename: "test.ts", + functionId: "test--outer_useServer" + }); + outer(outer_useServer); + const outer_useServer_1 = createServerRpc({ + fn: (...args) => devImport("test.ts?tsr-directive-use-server-split=test--outer_useServer_1").then(module => module.default(...args)), + filename: "test.ts", + functionId: "test--outer_useServer_1" + }); + outer(outer_useServer_1); + export default outer_useServer; + + + // test--outer_useServer_1 + + import { createServerRpc } from "my-rpc-lib-server"; + const outer_useServer = createServerRpc({ + fn: (...args) => devImport("test.ts?tsr-directive-use-server-split=test--outer_useServer").then(module => module.default(...args)), + filename: "test.ts", + functionId: "test--outer_useServer" + }); + outer(outer_useServer); + const outer_useServer_1 = createServerRpc({ + fn: function useServer() { + return 'hello'; + }, + filename: "test.ts", + functionId: "test--outer_useServer_1" + }); + outer(outer_useServer_1); + export default 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 splitFiles = Object.entries(ssr.directiveFnsById) + .map(([_fnId, directive]) => { + return `// ${directive.functionName}\n\n${ + compileDirectives({ + ...serverConfig, + code, + filename: directive.splitFilename, + }).compiledResult.code + }` + }) + .join('\n\n\n') + + expect(client.compiledResult.code).toMatchInlineSnapshot(` + "'use server'; + + import { createClientRpc } from "my-rpc-lib-client"; + const useServer_1 = createClientRpc({ + filename: "test.ts", + functionId: "test--useServer" + }); + export const useServer = useServer_1; + const defaultExport_1 = createClientRpc({ + filename: "test.ts", + functionId: "test--defaultExport" + }); + export default defaultExport_1;" + `) + expect(ssr.compiledResult.code).toMatchInlineSnapshot(` + "'use server'; + + import { createSsrRpc } from "my-rpc-lib-server"; + const useServer_1 = createSsrRpc({ + filename: "test.ts", + functionId: "test--useServer" + }); + export const useServer = useServer_1; + const defaultExport_1 = createSsrRpc({ + filename: "test.ts", + functionId: "test--defaultExport" + }); + export default defaultExport_1;" + `) + expect(splitFiles).toMatchInlineSnapshot(` + "// useServer + + 'use server'; + + import { createServerRpc } from "my-rpc-lib-server"; + const useServer_1 = createServerRpc({ + fn: function useServer() { + return usedInUseServer(); + }, + filename: "test.ts", + functionId: "test--useServer" + }); + function usedInUseServer() { + return 'hello'; + } + export default useServer_1; + + + // defaultExport + + 'use server'; + + import { createServerRpc } from "my-rpc-lib-server"; + const defaultExport_1 = createServerRpc({ + fn: function defaultExport() { + return 'hello'; + }, + filename: "test.ts", + functionId: "test--defaultExport" + }); + export default defaultExport_1;" + `) + }) + + test('createServerFn with identifier', () => { + // The following code is the client output of the tanstack-start-vite-plugin + // that compiles `createServerFn` calls to automatically add the `use server` + // directive in the right places. + const clientOrSsrCode = `import { createServerFn } from '@tanstack/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-vite-plugin + // that compiles `createServerFn` calls to automatically add the `use server` + // directive in the right places. + const serverCode = `import { createServerFn } from '@tanstack/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 splitFiles = Object.entries(ssr.directiveFnsById) + .map(([_fnId, directiveFn]) => { + return `// ${directiveFn.functionId}\n\n${ + compileDirectives({ + ...serverConfig, + code: serverCode, + filename: directiveFn.splitFilename, + }).compiledResult.code + }` + }) + .join('\n\n\n') + + expect(client.compiledResult.code).toMatchInlineSnapshot(` + "import { createClientRpc } from "my-rpc-lib-client"; + import { createServerFn } from '@tanstack/start'; + const myServerFn_createServerFn_handler = createClientRpc({ + filename: "test.ts", + functionId: "test--myServerFn_createServerFn_handler" + }); + export const myServerFn = createServerFn().handler(myServerFn_createServerFn_handler); + const myServerFn2_createServerFn_handler = createClientRpc({ + filename: "test.ts", + functionId: "test--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/start'; + const myServerFn_createServerFn_handler = createSsrRpc({ + filename: "test.ts", + functionId: "test--myServerFn_createServerFn_handler" + }); + export const myServerFn = createServerFn().handler(myServerFn_createServerFn_handler); + const myServerFn2_createServerFn_handler = createSsrRpc({ + filename: "test.ts", + functionId: "test--myServerFn2_createServerFn_handler" + }); + export const myServerFn2 = createServerFn().handler(myServerFn2_createServerFn_handler);" + `) + expect(splitFiles).toMatchInlineSnapshot(` + "// test--myServerFn_createServerFn_handler + + import { createServerRpc } from "my-rpc-lib-server"; + import { createServerFn } from '@tanstack/start'; + const myFunc = () => { + return 'hello from the server'; + }; + const myServerFn_createServerFn_handler = createServerRpc({ + fn: opts => { + return myServerFn.__executeServer(opts); + }, + filename: "test.ts", + functionId: "test--myServerFn_createServerFn_handler" + }); + const myServerFn = createServerFn().handler(myServerFn_createServerFn_handler, myFunc); + export default myServerFn_createServerFn_handler; + + + // test--myServerFn2_createServerFn_handler + + import { createServerRpc } from "my-rpc-lib-server"; + import { createServerFn } from '@tanstack/start'; + const myFunc = () => { + return 'hello from the server'; + }; + const myServerFn_createServerFn_handler = createServerRpc({ + fn: (...args) => devImport("test.ts?tsr-directive-use-server-split=test--myServerFn_createServerFn_handler").then(module => module.default(...args)), + filename: "test.ts", + functionId: "test--myServerFn_createServerFn_handler" + }); + const myFunc2 = () => { + return myServerFn({ + data: 'hello 2 from the server' + }); + }; + const myServerFn2_createServerFn_handler = createServerRpc({ + fn: opts => { + return myServerFn2.__executeServer(opts); + }, + filename: "test.ts", + functionId: "test--myServerFn2_createServerFn_handler" + }); + const myServerFn = createServerFn().handler(myServerFn_createServerFn_handler, myFunc); + const myServerFn2 = createServerFn().handler(myServerFn2_createServerFn_handler, myFunc2); + export default myServerFn2_createServerFn_handler;" + `) + }) +}) diff --git a/packages/server-functions-plugin/tsconfig.json b/packages/server-functions-plugin/tsconfig.json new file mode 100644 index 0000000000..37d21ef6ca --- /dev/null +++ b/packages/server-functions-plugin/tsconfig.json @@ -0,0 +1,8 @@ +{ + "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 new file mode 100644 index 0000000000..5389f0f739 --- /dev/null +++ b/packages/server-functions-plugin/vite.config.ts @@ -0,0 +1,20 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import { tanstackViteConfig } from '@tanstack/config/vite' +import packageJson from './package.json' + +const config = defineConfig({ + test: { + 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-vite-plugin/src/ast.ts b/packages/server-functions-vite-plugin/src/ast.ts deleted file mode 100644 index c353b97abc..0000000000 --- a/packages/server-functions-vite-plugin/src/ast.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { parse } from '@babel/parser' -import type { ParseResult } from '@babel/parser' - -export type ParseAstOptions = { - code: string - filename: string - root: string -} - -export function parseAst(opts: ParseAstOptions): ParseResult { - return parse(opts.code, { - plugins: ['jsx', 'typescript'], - sourceType: 'module', - ...{ - root: opts.root, - filename: opts.filename, - sourceMaps: true, - }, - }) -} diff --git a/packages/start/src/client-runtime/index.tsx b/packages/start/src/client-runtime/index.tsx index c4bf7b8536..d1a495d5c8 100644 --- a/packages/start/src/client-runtime/index.tsx +++ b/packages/start/src/client-runtime/index.tsx @@ -1,6 +1,6 @@ import { serverFnFetcher } from '../client' import { getBaseUrl } from './getBaseUrl' -import type { CreateClientRpcFn } from '@tanstack/directive-functions-plugin' +import type { CreateClientRpcFn } from '@tanstack/server-functions-plugin' export const createClientRpc: CreateClientRpcFn = (functionId) => { const base = getBaseUrl(window.location.origin, functionId) diff --git a/packages/start/src/config/index.ts b/packages/start/src/config/index.ts index beb35e419e..6572f430f1 100644 --- a/packages/start/src/config/index.ts +++ b/packages/start/src/config/index.ts @@ -14,7 +14,7 @@ import { createApp } from 'vinxi' import { config } from 'vinxi/plugins/config' // // @ts-expect-error // import { serverComponents } from '@vinxi/server-components/plugin' -import { createTanStackServerFnPlugin } from '@tanstack/directive-functions-plugin' +import { createTanStackServerFnPlugin } from '@tanstack/server-functions-plugin' import { tanstackStartVinxiFileRouter } from './vinxi-file-router.js' import { checkDeploymentPresetInput, diff --git a/packages/start/src/server-runtime/index.tsx b/packages/start/src/server-runtime/index.tsx index 4434abec24..fb85d2a5b8 100644 --- a/packages/start/src/server-runtime/index.tsx +++ b/packages/start/src/server-runtime/index.tsx @@ -1,5 +1,5 @@ import { getBaseUrl } from '../client-runtime/getBaseUrl' -import type { CreateServerRpcFn } from '@tanstack/directive-functions-plugin' +import type { CreateServerRpcFn } from '@tanstack/server-functions-plugin' const fakeHost = 'http://localhost:3000' diff --git a/packages/start/src/ssr-runtime/index.tsx b/packages/start/src/ssr-runtime/index.tsx index 5547b756ee..f52946a130 100644 --- a/packages/start/src/ssr-runtime/index.tsx +++ b/packages/start/src/ssr-runtime/index.tsx @@ -4,7 +4,7 @@ import invariant from 'tiny-invariant' import { serverFnFetcher } from '../client' import { getBaseUrl } from '../client-runtime/getBaseUrl' import { handleServerRequest } from '../server-handler/index' -import type { CreateSsrRpcFn } from '@tanstack/directive-functions-plugin' +import type { CreateSsrRpcFn } from '@tanstack/server-functions-plugin' export function createIncomingMessage( url: string, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eb463fc375..35d8641131 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1999,10 +1999,10 @@ importers: version: 18.3.1 html-webpack-plugin: specifier: ^5.6.3 - version: 5.6.3(@rspack/core@1.1.5(@swc/helpers@0.5.15))(webpack@5.97.1) + version: 5.6.3(@rspack/core@1.1.5(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)) swc-loader: specifier: ^0.2.6 - version: 0.2.6(@swc/core@1.10.1(@swc/helpers@0.5.15))(webpack@5.97.1) + version: 0.2.6(@swc/core@1.10.1(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)) typescript: specifier: ^5.7.2 version: 5.7.2 @@ -3217,7 +3217,7 @@ importers: version: 4.3.4(vite@6.0.3(@types/node@22.10.1)(jiti@2.4.1)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)) html-webpack-plugin: specifier: ^5.6.0 - version: 5.6.3(@rspack/core@1.1.5(@swc/helpers@0.5.15))(webpack@5.97.1) + version: 5.6.3(@rspack/core@1.1.5(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)) react: specifier: ^18.3.1 version: 18.3.1 @@ -3226,7 +3226,7 @@ importers: version: 18.3.1(react@18.3.1) swc-loader: specifier: ^0.2.6 - version: 0.2.6(@swc/core@1.10.1(@swc/helpers@0.5.15))(webpack@5.97.1) + version: 0.2.6(@swc/core@1.10.1(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)) typescript: specifier: ^5.7.2 version: 5.7.2 @@ -3356,6 +3356,69 @@ importers: specifier: ^3.23.8 version: 3.23.8 + packages/directive-functions-plugin: + dependencies: + '@babel/code-frame': + specifier: 7.26.2 + version: 7.26.2 + '@babel/core': + specifier: ^7.26.0 + version: 7.26.0 + '@babel/generator': + specifier: ^7.26.3 + version: 7.26.3 + '@babel/parser': + specifier: ^7.26.3 + version: 7.26.3 + '@babel/plugin-syntax-jsx': + specifier: ^7.25.9 + version: 7.25.9(@babel/core@7.26.0) + '@babel/plugin-syntax-typescript': + specifier: ^7.25.9 + version: 7.25.9(@babel/core@7.26.0) + '@babel/template': + specifier: ^7.25.9 + version: 7.25.9 + '@babel/traverse': + specifier: ^7.26.4 + version: 7.26.4 + '@babel/types': + specifier: ^7.26.3 + version: 7.26.3 + '@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__generator': + specifier: ^7.6.8 + version: 7.6.8 + '@types/babel__template': + specifier: ^7.4.4 + version: 7.4.4 + '@types/babel__traverse': + specifier: ^7.20.6 + version: 7.20.6 + '@types/diff': + specifier: ^6.0.0 + version: 6.0.0 + babel-dead-code-elimination: + specifier: ^1.0.6 + version: 1.0.8 + chalk: + specifier: ^5.3.0 + version: 5.3.0 + dedent: + specifier: ^1.5.3 + version: 1.5.3 + diff: + specifier: ^7.0.0 + version: 7.0.0 + tiny-invariant: + specifier: ^1.3.3 + version: 1.3.3 + packages/eslint-plugin-router: dependencies: '@typescript-eslint/utils': @@ -3575,7 +3638,7 @@ importers: specifier: workspace:* version: link:../router-plugin - packages/server-functions-vite-plugin: + packages/server-functions-plugin: dependencies: '@babel/code-frame': specifier: 7.26.2 @@ -3604,6 +3667,9 @@ importers: '@babel/types': specifier: ^7.26.3 version: 7.26.3 + '@tanstack/directive-functions-plugin': + specifier: workspace:* + version: link:../directive-functions-plugin '@types/babel__code-frame': specifier: ^7.0.6 version: 7.0.6 @@ -3642,7 +3708,7 @@ importers: dependencies: '@tanstack/directive-functions-plugin': specifier: workspace:^ - version: link:../server-functions-vite-plugin + version: link:../directive-functions-plugin '@tanstack/react-cross-context': specifier: workspace:* version: link:../react-cross-context @@ -13747,17 +13813,17 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 - '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4)(webpack@5.97.1)': + '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4))': dependencies: webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1) - '@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.97.1)': + '@webpack-cli/info@2.0.2(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4))': dependencies: webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1) - '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack-dev-server@5.1.0)(webpack@5.97.1)': + '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1))(webpack-dev-server@5.1.0(webpack-cli@5.1.4)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4))': dependencies: webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1) @@ -15714,7 +15780,7 @@ snapshots: relateurl: 0.2.7 terser: 5.36.0 - html-webpack-plugin@5.6.3(@rspack/core@1.1.5(@swc/helpers@0.5.15))(webpack@5.97.1): + html-webpack-plugin@5.6.3(@rspack/core@1.1.5(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -17787,7 +17853,7 @@ snapshots: csso: 5.0.5 picocolors: 1.1.1 - swc-loader@0.2.6(@swc/core@1.10.1(@swc/helpers@0.5.15))(webpack@5.97.1): + swc-loader@0.2.6(@swc/core@1.10.1(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)): dependencies: '@swc/core': 1.10.1(@swc/helpers@0.5.15) '@swc/counter': 0.1.3 @@ -17866,26 +17932,26 @@ snapshots: type-fest: 2.19.0 unique-string: 3.0.0 - terser-webpack-plugin@5.3.10(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)): + terser-webpack-plugin@5.3.10(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.2 terser: 5.36.0 - webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0) + webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4) optionalDependencies: '@swc/core': 1.10.1(@swc/helpers@0.5.15) esbuild: 0.24.0 - terser-webpack-plugin@5.3.10(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack@5.97.1): + terser-webpack-plugin@5.3.10(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.2 terser: 5.36.0 - webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4) + webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0) optionalDependencies: '@swc/core': 1.10.1(@swc/helpers@0.5.15) esbuild: 0.24.0 @@ -18588,9 +18654,9 @@ snapshots: webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1): dependencies: '@discoveryjs/json-ext': 0.5.7 - '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4)(webpack@5.97.1) - '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4)(webpack@5.97.1) - '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4)(webpack-dev-server@5.1.0)(webpack@5.97.1) + '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)) + '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)) + '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1))(webpack-dev-server@5.1.0(webpack-cli@5.1.4)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)) colorette: 2.0.20 commander: 10.0.1 cross-spawn: 7.0.6 @@ -18604,7 +18670,7 @@ snapshots: optionalDependencies: webpack-dev-server: 5.1.0(webpack-cli@5.1.4)(webpack@5.97.1) - webpack-dev-middleware@7.4.2(webpack@5.97.1): + webpack-dev-middleware@7.4.2(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)): dependencies: colorette: 2.0.20 memfs: 4.14.1 @@ -18643,7 +18709,7 @@ snapshots: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.2(webpack@5.97.1) + webpack-dev-middleware: 7.4.2(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)) ws: 8.18.0 optionalDependencies: webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4) @@ -18716,7 +18782,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack@5.97.1) + terser-webpack-plugin: 5.3.10(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)) watchpack: 2.4.2 webpack-sources: 3.2.3 optionalDependencies: From 45b39c76b52bf912d23cf6c284cf0b7af7bc022f Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Mon, 6 Jan 2025 16:54:17 -0700 Subject: [PATCH 29/38] fixes, tests --- package.json | 4 +- packages/directive-functions-plugin/README.md | 2 +- .../src/compilers.ts | 6 +- .../directive-functions-plugin/src/index.ts | 2 + .../tests/compiler.test.ts | 144 +++++----- packages/server-functions-plugin/README.md | 2 +- packages/server-functions-plugin/src/index.ts | 7 +- .../tests/compiler.test.ts | 6 +- packages/start/package.json | 1 + pnpm-lock.yaml | 257 ++++++++++++++---- 10 files changed, 291 insertions(+), 140 deletions(-) diff --git a/package.json b/package.json index dd757b42f6..74ba451a5f 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,9 @@ "@tanstack/arktype-adapter": "workspace:*", "@tanstack/start": "workspace:*", "@tanstack/start-vite-plugin": "workspace:*", - "@tanstack/eslint-plugin-router": "workspace:*" + "@tanstack/eslint-plugin-router": "workspace:*", + "@tanstack/server-functions-plugin": "workspace:*", + "@tanstack/directive-functions-plugin": "workspace:*" } } } diff --git a/packages/directive-functions-plugin/README.md b/packages/directive-functions-plugin/README.md index 798fa094a3..aa886079e6 100644 --- a/packages/directive-functions-plugin/README.md +++ b/packages/directive-functions-plugin/README.md @@ -1,5 +1,5 @@ -# TanStack Start Vite Plugin +# TanStack Directive Functions Plugin See https://tanstack.com/router/latest/docs/framework/react/guide/file-based-routing diff --git a/packages/directive-functions-plugin/src/compilers.ts b/packages/directive-functions-plugin/src/compilers.ts index 5b9f47a3af..0ce812f3f1 100644 --- a/packages/directive-functions-plugin/src/compilers.ts +++ b/packages/directive-functions-plugin/src/compilers.ts @@ -1,4 +1,3 @@ -import path from 'node:path' import * as babel from '@babel/core' import _generate from '@babel/generator' import { parse } from '@babel/parser' @@ -454,10 +453,7 @@ export function findDirectives( } const functionId = makeFileLocationUrlSafe( - `${opts.filename.replace( - path.extname(opts.filename), - '', - )}--${functionName}`.replace(opts.root, ''), + `${opts.filename}--${functionName}`.replace(opts.root, ''), ) const [filename, searchParamsStr] = opts.filename.split('?') diff --git a/packages/directive-functions-plugin/src/index.ts b/packages/directive-functions-plugin/src/index.ts index 801a3abeb0..8de8ca6cf8 100644 --- a/packages/directive-functions-plugin/src/index.ts +++ b/packages/directive-functions-plugin/src/index.ts @@ -7,6 +7,8 @@ import type { Plugin } from 'vite' const debug = Boolean(process.env.TSR_VITE_DEBUG) +export type { DirectiveFn, CompileDirectivesOpts } from './compilers' + export type DirectiveFunctionsViteOptions = Pick< CompileDirectivesOpts, 'directive' | 'directiveLabel' | 'getRuntimeCode' | 'replacer' diff --git a/packages/directive-functions-plugin/tests/compiler.test.ts b/packages/directive-functions-plugin/tests/compiler.test.ts index 7092e1e348..f22b1676d3 100644 --- a/packages/directive-functions-plugin/tests/compiler.test.ts +++ b/packages/directive-functions-plugin/tests/compiler.test.ts @@ -143,47 +143,47 @@ describe('server function compilation', () => { "import { createClientRpc } from "my-rpc-lib-client"; const namedFunction_wrapper_namedFunction = createClientRpc({ filename: "test.ts", - functionId: "test--namedFunction_wrapper_namedFunction" + functionId: "test_ts--namedFunction_wrapper_namedFunction" }); export const namedFunction = wrapper(namedFunction_wrapper_namedFunction); const arrowFunction_wrapper = createClientRpc({ filename: "test.ts", - functionId: "test--arrowFunction_wrapper" + functionId: "test_ts--arrowFunction_wrapper" }); export const arrowFunction = wrapper(arrowFunction_wrapper); const anonymousFunction_wrapper = createClientRpc({ filename: "test.ts", - functionId: "test--anonymousFunction_wrapper" + functionId: "test_ts--anonymousFunction_wrapper" }); export const anonymousFunction = wrapper(anonymousFunction_wrapper); const multipleDirectives_multipleDirectives = createClientRpc({ filename: "test.ts", - functionId: "test--multipleDirectives_multipleDirectives" + functionId: "test_ts--multipleDirectives_multipleDirectives" }); export const multipleDirectives = multipleDirectives_multipleDirectives; const iife_1 = createClientRpc({ filename: "test.ts", - functionId: "test--iife" + functionId: "test_ts--iife" }); export const iife = iife_1(); const defaultExportFn_1 = createClientRpc({ filename: "test.ts", - functionId: "test--defaultExportFn" + functionId: "test_ts--defaultExportFn" }); export default defaultExportFn_1; const namedExportFn_1 = createClientRpc({ filename: "test.ts", - functionId: "test--namedExportFn" + functionId: "test_ts--namedExportFn" }); export const namedExportFn = namedExportFn_1; const exportedArrowFunction_wrapper = createClientRpc({ filename: "test.ts", - functionId: "test--exportedArrowFunction_wrapper" + functionId: "test_ts--exportedArrowFunction_wrapper" }); export const exportedArrowFunction = wrapper(exportedArrowFunction_wrapper); const namedExportConst_1 = createClientRpc({ filename: "test.ts", - functionId: "test--namedExportConst" + functionId: "test_ts--namedExportConst" }); export const namedExportConst = namedExportConst_1; const namedDefaultExport = 'namedDefaultExport'; @@ -196,47 +196,47 @@ describe('server function compilation', () => { "import { createSsrRpc } from "my-rpc-lib-server"; const namedFunction_wrapper_namedFunction = createSsrRpc({ filename: "test.ts", - functionId: "test--namedFunction_wrapper_namedFunction" + functionId: "test_ts--namedFunction_wrapper_namedFunction" }); export const namedFunction = wrapper(namedFunction_wrapper_namedFunction); const arrowFunction_wrapper = createSsrRpc({ filename: "test.ts", - functionId: "test--arrowFunction_wrapper" + functionId: "test_ts--arrowFunction_wrapper" }); export const arrowFunction = wrapper(arrowFunction_wrapper); const anonymousFunction_wrapper = createSsrRpc({ filename: "test.ts", - functionId: "test--anonymousFunction_wrapper" + functionId: "test_ts--anonymousFunction_wrapper" }); export const anonymousFunction = wrapper(anonymousFunction_wrapper); const multipleDirectives_multipleDirectives = createSsrRpc({ filename: "test.ts", - functionId: "test--multipleDirectives_multipleDirectives" + functionId: "test_ts--multipleDirectives_multipleDirectives" }); export const multipleDirectives = multipleDirectives_multipleDirectives; const iife_1 = createSsrRpc({ filename: "test.ts", - functionId: "test--iife" + functionId: "test_ts--iife" }); export const iife = iife_1(); const defaultExportFn_1 = createSsrRpc({ filename: "test.ts", - functionId: "test--defaultExportFn" + functionId: "test_ts--defaultExportFn" }); export default defaultExportFn_1; const namedExportFn_1 = createSsrRpc({ filename: "test.ts", - functionId: "test--namedExportFn" + functionId: "test_ts--namedExportFn" }); export const namedExportFn = namedExportFn_1; const exportedArrowFunction_wrapper = createSsrRpc({ filename: "test.ts", - functionId: "test--exportedArrowFunction_wrapper" + functionId: "test_ts--exportedArrowFunction_wrapper" }); export const exportedArrowFunction = wrapper(exportedArrowFunction_wrapper); const namedExportConst_1 = createSsrRpc({ filename: "test.ts", - functionId: "test--namedExportConst" + functionId: "test_ts--namedExportConst" }); export const namedExportConst = namedExportConst_1; const namedDefaultExport = 'namedDefaultExport'; @@ -247,7 +247,7 @@ describe('server function compilation', () => { expect(splitFiles).toMatchInlineSnapshot( ` - "// test--namedFunction_wrapper_namedFunction + "// test_ts--namedFunction_wrapper_namedFunction import { createServerRpc } from "my-rpc-lib-server"; const namedFunction_wrapper_namedFunction = createServerRpc({ @@ -255,12 +255,12 @@ describe('server function compilation', () => { return 'hello'; }, filename: "test.ts", - functionId: "test--namedFunction_wrapper_namedFunction" + functionId: "test_ts_tsr-directive-use-server-split_namedFunction_wrapper_namedFunction--namedFunction_wrapper_namedFunction" }); export default namedFunction_wrapper_namedFunction; - // test--arrowFunction_wrapper + // test_ts--arrowFunction_wrapper import { createServerRpc } from "my-rpc-lib-server"; const arrowFunction_wrapper = createServerRpc({ @@ -268,12 +268,12 @@ describe('server function compilation', () => { return 'hello'; }, filename: "test.ts", - functionId: "test--arrowFunction_wrapper" + functionId: "test_ts_tsr-directive-use-server-split_arrowFunction_wrapper--arrowFunction_wrapper" }); export default arrowFunction_wrapper; - // test--anonymousFunction_wrapper + // test_ts--anonymousFunction_wrapper import { createServerRpc } from "my-rpc-lib-server"; const anonymousFunction_wrapper = createServerRpc({ @@ -281,12 +281,12 @@ describe('server function compilation', () => { return 'hello'; }, filename: "test.ts", - functionId: "test--anonymousFunction_wrapper" + functionId: "test_ts_tsr-directive-use-server-split_anonymousFunction_wrapper--anonymousFunction_wrapper" }); export default anonymousFunction_wrapper; - // test--multipleDirectives_multipleDirectives + // test_ts--multipleDirectives_multipleDirectives import { createServerRpc } from "my-rpc-lib-server"; const multipleDirectives_multipleDirectives = createServerRpc({ @@ -296,12 +296,12 @@ describe('server function compilation', () => { return 'hello'; }, filename: "test.ts", - functionId: "test--multipleDirectives_multipleDirectives" + functionId: "test_ts_tsr-directive-use-server-split_multipleDirectives_multipleDirectives--multipleDirectives_multipleDirectives" }); export default multipleDirectives_multipleDirectives; - // test--iife + // test_ts--iife import { createServerRpc } from "my-rpc-lib-server"; const iife_1 = createServerRpc({ @@ -309,12 +309,12 @@ describe('server function compilation', () => { return 'hello'; }, filename: "test.ts", - functionId: "test--iife" + functionId: "test_ts_tsr-directive-use-server-split_iife--iife" }); export default iife_1; - // test--defaultExportFn + // test_ts--defaultExportFn import { createServerRpc } from "my-rpc-lib-server"; const defaultExportFn_1 = createServerRpc({ @@ -322,12 +322,12 @@ describe('server function compilation', () => { return 'hello'; }, filename: "test.ts", - functionId: "test--defaultExportFn" + functionId: "test_ts_tsr-directive-use-server-split_defaultExportFn--defaultExportFn" }); export default defaultExportFn_1; - // test--namedExportFn + // test_ts--namedExportFn import { createServerRpc } from "my-rpc-lib-server"; const namedExportFn_1 = createServerRpc({ @@ -335,12 +335,12 @@ describe('server function compilation', () => { return 'hello'; }, filename: "test.ts", - functionId: "test--namedExportFn" + functionId: "test_ts_tsr-directive-use-server-split_namedExportFn--namedExportFn" }); export default namedExportFn_1; - // test--exportedArrowFunction_wrapper + // test_ts--exportedArrowFunction_wrapper import { createServerRpc } from "my-rpc-lib-server"; const exportedArrowFunction_wrapper = createServerRpc({ @@ -348,12 +348,12 @@ describe('server function compilation', () => { return 'hello'; }, filename: "test.ts", - functionId: "test--exportedArrowFunction_wrapper" + functionId: "test_ts_tsr-directive-use-server-split_exportedArrowFunction_wrapper--exportedArrowFunction_wrapper" }); export default exportedArrowFunction_wrapper; - // test--namedExportConst + // test_ts--namedExportConst import { createServerRpc } from "my-rpc-lib-server"; const namedExportConst_1 = createServerRpc({ @@ -361,7 +361,7 @@ describe('server function compilation', () => { return usedFn(); }, filename: "test.ts", - functionId: "test--namedExportConst" + functionId: "test_ts_tsr-directive-use-server-split_namedExportConst--namedExportConst" }); function usedFn() { return 'hello'; @@ -671,18 +671,18 @@ describe('server function compilation', () => { "import { createClientRpc } from "my-rpc-lib-client"; createClientRpc({ filename: "test.ts", - functionId: "test--multiDirective" + functionId: "test_ts--multiDirective" });" `) expect(ssr.compiledResult.code).toMatchInlineSnapshot(` "import { createSsrRpc } from "my-rpc-lib-server"; createSsrRpc({ filename: "test.ts", - functionId: "test--multiDirective" + functionId: "test_ts--multiDirective" });" `) expect(splitFiles).toMatchInlineSnapshot(` - "// test--multiDirective + "// test_ts--multiDirective import { createServerRpc } from "my-rpc-lib-server"; createServerRpc({ @@ -692,7 +692,7 @@ describe('server function compilation', () => { return 'hello'; }, filename: "test.ts", - functionId: "test--multiDirective" + functionId: "test_ts_tsr-directive-use-server-split_multiDirective--multiDirective" }); export default multiDirective_1;" `) @@ -724,7 +724,7 @@ describe('server function compilation', () => { "import { createClientRpc } from "my-rpc-lib-client"; const iife_1 = createClientRpc({ filename: "test.ts", - functionId: "test--iife" + functionId: "test_ts--iife" }); export const iife = iife_1();" `) @@ -733,13 +733,13 @@ describe('server function compilation', () => { "import { createSsrRpc } from "my-rpc-lib-server"; const iife_1 = createSsrRpc({ filename: "test.ts", - functionId: "test--iife" + functionId: "test_ts--iife" }); export const iife = iife_1();" `) expect(splitFiles).toMatchInlineSnapshot(` - "// test--iife + "// test_ts--iife import { createServerRpc } from "my-rpc-lib-server"; const iife_1 = createServerRpc({ @@ -747,7 +747,7 @@ describe('server function compilation', () => { return 'hello'; }, filename: "test.ts", - functionId: "test--iife" + functionId: "test_ts_tsr-directive-use-server-split_iife--iife" }); export default iife_1;" `) @@ -785,12 +785,12 @@ describe('server function compilation', () => { "import { createClientRpc } from "my-rpc-lib-client"; const outer_useServer = createClientRpc({ filename: "test.ts", - functionId: "test--outer_useServer" + functionId: "test_ts--outer_useServer" }); outer(outer_useServer); const outer_useServer_1 = createClientRpc({ filename: "test.ts", - functionId: "test--outer_useServer_1" + functionId: "test_ts--outer_useServer_1" }); outer(outer_useServer_1);" `) @@ -799,18 +799,18 @@ describe('server function compilation', () => { "import { createSsrRpc } from "my-rpc-lib-server"; const outer_useServer = createSsrRpc({ filename: "test.ts", - functionId: "test--outer_useServer" + functionId: "test_ts--outer_useServer" }); outer(outer_useServer); const outer_useServer_1 = createSsrRpc({ filename: "test.ts", - functionId: "test--outer_useServer_1" + functionId: "test_ts--outer_useServer_1" }); outer(outer_useServer_1);" `) expect(splitFiles).toMatchInlineSnapshot(` - "// test--outer_useServer + "// test_ts--outer_useServer import { createServerRpc } from "my-rpc-lib-server"; const outer_useServer = createServerRpc({ @@ -818,25 +818,25 @@ describe('server function compilation', () => { return 'hello'; }, filename: "test.ts", - functionId: "test--outer_useServer" + functionId: "test_ts_tsr-directive-use-server-split_outer_useServer--outer_useServer" }); outer(outer_useServer); const outer_useServer_1 = createServerRpc({ - fn: (...args) => devImport("test.ts?tsr-directive-use-server-split=test--outer_useServer_1").then(module => module.default(...args)), + fn: (...args) => devImport("test.ts?tsr-directive-use-server-split=outer_useServer_1").then(module => module.default(...args)), filename: "test.ts", - functionId: "test--outer_useServer_1" + functionId: "test_ts_tsr-directive-use-server-split_outer_useServer--outer_useServer_1" }); outer(outer_useServer_1); export default outer_useServer; - // test--outer_useServer_1 + // test_ts--outer_useServer_1 import { createServerRpc } from "my-rpc-lib-server"; const outer_useServer = createServerRpc({ - fn: (...args) => devImport("test.ts?tsr-directive-use-server-split=test--outer_useServer").then(module => module.default(...args)), + fn: (...args) => devImport("test.ts?tsr-directive-use-server-split=outer_useServer").then(module => module.default(...args)), filename: "test.ts", - functionId: "test--outer_useServer" + functionId: "test_ts_tsr-directive-use-server-split_outer_useServer_1--outer_useServer" }); outer(outer_useServer); const outer_useServer_1 = createServerRpc({ @@ -844,7 +844,7 @@ describe('server function compilation', () => { return 'hello'; }, filename: "test.ts", - functionId: "test--outer_useServer_1" + functionId: "test_ts_tsr-directive-use-server-split_outer_useServer_1--outer_useServer_1" }); outer(outer_useServer_1); export default outer_useServer_1;" @@ -892,12 +892,12 @@ describe('server function compilation', () => { import { createClientRpc } from "my-rpc-lib-client"; const useServer_1 = createClientRpc({ filename: "test.ts", - functionId: "test--useServer" + functionId: "test_ts--useServer" }); export const useServer = useServer_1; const defaultExport_1 = createClientRpc({ filename: "test.ts", - functionId: "test--defaultExport" + functionId: "test_ts--defaultExport" }); export default defaultExport_1;" `) @@ -907,12 +907,12 @@ describe('server function compilation', () => { import { createSsrRpc } from "my-rpc-lib-server"; const useServer_1 = createSsrRpc({ filename: "test.ts", - functionId: "test--useServer" + functionId: "test_ts--useServer" }); export const useServer = useServer_1; const defaultExport_1 = createSsrRpc({ filename: "test.ts", - functionId: "test--defaultExport" + functionId: "test_ts--defaultExport" }); export default defaultExport_1;" `) @@ -927,7 +927,7 @@ describe('server function compilation', () => { return usedInUseServer(); }, filename: "test.ts", - functionId: "test--useServer" + functionId: "test_ts_tsr-directive-use-server-split_useServer--useServer" }); function usedInUseServer() { return 'hello'; @@ -945,7 +945,7 @@ describe('server function compilation', () => { return 'hello'; }, filename: "test.ts", - functionId: "test--defaultExport" + functionId: "test_ts_tsr-directive-use-server-split_defaultExport--defaultExport" }); export default defaultExport_1;" `) @@ -1009,12 +1009,12 @@ describe('server function compilation', () => { import { createServerFn } from '@tanstack/start'; const myServerFn_createServerFn_handler = createClientRpc({ filename: "test.ts", - functionId: "test--myServerFn_createServerFn_handler" + functionId: "test_ts--myServerFn_createServerFn_handler" }); export const myServerFn = createServerFn().handler(myServerFn_createServerFn_handler); const myServerFn2_createServerFn_handler = createClientRpc({ filename: "test.ts", - functionId: "test--myServerFn2_createServerFn_handler" + functionId: "test_ts--myServerFn2_createServerFn_handler" }); export const myServerFn2 = createServerFn().handler(myServerFn2_createServerFn_handler);" `) @@ -1023,17 +1023,17 @@ describe('server function compilation', () => { import { createServerFn } from '@tanstack/start'; const myServerFn_createServerFn_handler = createSsrRpc({ filename: "test.ts", - functionId: "test--myServerFn_createServerFn_handler" + functionId: "test_ts--myServerFn_createServerFn_handler" }); export const myServerFn = createServerFn().handler(myServerFn_createServerFn_handler); const myServerFn2_createServerFn_handler = createSsrRpc({ filename: "test.ts", - functionId: "test--myServerFn2_createServerFn_handler" + functionId: "test_ts--myServerFn2_createServerFn_handler" }); export const myServerFn2 = createServerFn().handler(myServerFn2_createServerFn_handler);" `) expect(splitFiles).toMatchInlineSnapshot(` - "// test--myServerFn_createServerFn_handler + "// test_ts--myServerFn_createServerFn_handler import { createServerRpc } from "my-rpc-lib-server"; import { createServerFn } from '@tanstack/start'; @@ -1045,13 +1045,13 @@ describe('server function compilation', () => { return myServerFn.__executeServer(opts); }, filename: "test.ts", - functionId: "test--myServerFn_createServerFn_handler" + functionId: "test_ts_tsr-directive-use-server-split_myServerFn_createServerFn_handler--myServerFn_createServerFn_handler" }); const myServerFn = createServerFn().handler(myServerFn_createServerFn_handler, myFunc); export default myServerFn_createServerFn_handler; - // test--myServerFn2_createServerFn_handler + // test_ts--myServerFn2_createServerFn_handler import { createServerRpc } from "my-rpc-lib-server"; import { createServerFn } from '@tanstack/start'; @@ -1059,9 +1059,9 @@ describe('server function compilation', () => { return 'hello from the server'; }; const myServerFn_createServerFn_handler = createServerRpc({ - fn: (...args) => devImport("test.ts?tsr-directive-use-server-split=test--myServerFn_createServerFn_handler").then(module => module.default(...args)), + fn: (...args) => devImport("test.ts?tsr-directive-use-server-split=myServerFn_createServerFn_handler").then(module => module.default(...args)), filename: "test.ts", - functionId: "test--myServerFn_createServerFn_handler" + functionId: "test_ts_tsr-directive-use-server-split_myServerFn2_createServerFn_handler--myServerFn_createServerFn_handler" }); const myFunc2 = () => { return myServerFn({ @@ -1073,7 +1073,7 @@ describe('server function compilation', () => { return myServerFn2.__executeServer(opts); }, filename: "test.ts", - functionId: "test--myServerFn2_createServerFn_handler" + functionId: "test_ts_tsr-directive-use-server-split_myServerFn2_createServerFn_handler--myServerFn2_createServerFn_handler" }); const myServerFn = createServerFn().handler(myServerFn_createServerFn_handler, myFunc); const myServerFn2 = createServerFn().handler(myServerFn2_createServerFn_handler, myFunc2); diff --git a/packages/server-functions-plugin/README.md b/packages/server-functions-plugin/README.md index 798fa094a3..b8b55fe5b0 100644 --- a/packages/server-functions-plugin/README.md +++ b/packages/server-functions-plugin/README.md @@ -1,5 +1,5 @@ -# TanStack Start Vite Plugin +# TanStack Server Functions Plugin See https://tanstack.com/router/latest/docs/framework/react/guide/file-based-routing diff --git a/packages/server-functions-plugin/src/index.ts b/packages/server-functions-plugin/src/index.ts index 1ae9eae0b4..cacacba2de 100644 --- a/packages/server-functions-plugin/src/index.ts +++ b/packages/server-functions-plugin/src/index.ts @@ -1,14 +1,9 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs' import path from 'node:path' import { TanStackDirectiveFunctionsPlugin } from '@tanstack/directive-functions-plugin' -import type { CompileDirectivesOpts, DirectiveFn } from './compilers' +import type { DirectiveFn } from '@tanstack/directive-functions-plugin' import type { Plugin } from 'vite' -export type DirectiveFunctionsViteOptions = Pick< - CompileDirectivesOpts, - 'directive' | 'directiveLabel' | 'getRuntimeCode' | 'replacer' -> - export type CreateClientRpcFn = (functionId: string) => any export type CreateSsrRpcFn = (functionId: string) => any export type CreateServerRpcFn = ( diff --git a/packages/server-functions-plugin/tests/compiler.test.ts b/packages/server-functions-plugin/tests/compiler.test.ts index 7092e1e348..e918201e24 100644 --- a/packages/server-functions-plugin/tests/compiler.test.ts +++ b/packages/server-functions-plugin/tests/compiler.test.ts @@ -822,7 +822,7 @@ describe('server function compilation', () => { }); outer(outer_useServer); const outer_useServer_1 = createServerRpc({ - fn: (...args) => devImport("test.ts?tsr-directive-use-server-split=test--outer_useServer_1").then(module => module.default(...args)), + fn: (...args) => devImport("test.ts?tsr-directive-use-server-split=outer_useServer_1").then(module => module.default(...args)), filename: "test.ts", functionId: "test--outer_useServer_1" }); @@ -834,7 +834,7 @@ describe('server function compilation', () => { import { createServerRpc } from "my-rpc-lib-server"; const outer_useServer = createServerRpc({ - fn: (...args) => devImport("test.ts?tsr-directive-use-server-split=test--outer_useServer").then(module => module.default(...args)), + fn: (...args) => devImport("test.ts?tsr-directive-use-server-split=outer_useServer").then(module => module.default(...args)), filename: "test.ts", functionId: "test--outer_useServer" }); @@ -1059,7 +1059,7 @@ describe('server function compilation', () => { return 'hello from the server'; }; const myServerFn_createServerFn_handler = createServerRpc({ - fn: (...args) => devImport("test.ts?tsr-directive-use-server-split=test--myServerFn_createServerFn_handler").then(module => module.default(...args)), + fn: (...args) => devImport("test.ts?tsr-directive-use-server-split=myServerFn_createServerFn_handler").then(module => module.default(...args)), filename: "test.ts", functionId: "test--myServerFn_createServerFn_handler" }); diff --git a/packages/start/package.json b/packages/start/package.json index 2a1f856f50..ff632fcfc7 100644 --- a/packages/start/package.json +++ b/packages/start/package.json @@ -138,6 +138,7 @@ "@tanstack/router-generator": "workspace:^", "@tanstack/router-plugin": "workspace:^", "@tanstack/start-vite-plugin": "workspace:^", + "@tanstack/server-functions-plugin": "workspace:^", "@vinxi/react": "0.2.5", "@vinxi/react-server-dom": "^0.0.3", "@vinxi/server-components": "0.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9539189e7d..11251e2a3b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,6 +25,8 @@ overrides: '@tanstack/start': workspace:* '@tanstack/start-vite-plugin': workspace:* '@tanstack/eslint-plugin-router': workspace:* + '@tanstack/server-functions-plugin': workspace:* + '@tanstack/directive-functions-plugin': workspace:* importers: @@ -2004,10 +2006,10 @@ importers: version: 18.3.1 html-webpack-plugin: specifier: ^5.6.3 - version: 5.6.3(@rspack/core@1.1.8(@swc/helpers@0.5.15))(webpack@5.97.1) + version: 5.6.3(@rspack/core@1.1.8(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@5.1.4)) swc-loader: specifier: ^0.2.6 - version: 0.2.6(@swc/core@1.10.1(@swc/helpers@0.5.15))(webpack@5.97.1) + version: 0.2.6(@swc/core@1.10.1(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@5.1.4)) typescript: specifier: ^5.7.2 version: 5.7.2 @@ -3234,7 +3236,7 @@ importers: version: 7.0.6 html-webpack-plugin: specifier: ^5.6.0 - version: 5.6.3(@rspack/core@1.1.8(@swc/helpers@0.5.15))(webpack@5.97.1) + version: 5.6.3(@rspack/core@1.1.8(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@5.1.4)) picocolors: specifier: ^1.1.1 version: 1.1.1 @@ -3246,7 +3248,7 @@ importers: version: 18.3.1(react@18.3.1) swc-loader: specifier: ^0.2.6 - version: 0.2.6(@swc/core@1.10.1(@swc/helpers@0.5.15))(webpack@5.97.1) + version: 0.2.6(@swc/core@1.10.1(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@5.1.4)) tinyglobby: specifier: ^0.2.10 version: 0.2.10 @@ -3370,6 +3372,69 @@ importers: specifier: ^3.0.1 version: 3.0.1(typescript@5.7.2)(vue-tsc@2.0.29(typescript@5.7.2)) + packages/directive-functions-plugin: + dependencies: + '@babel/code-frame': + specifier: 7.26.2 + version: 7.26.2 + '@babel/core': + specifier: ^7.26.0 + version: 7.26.0 + '@babel/generator': + specifier: ^7.26.3 + version: 7.26.3 + '@babel/parser': + specifier: ^7.26.3 + version: 7.26.3 + '@babel/plugin-syntax-jsx': + specifier: ^7.25.9 + version: 7.25.9(@babel/core@7.26.0) + '@babel/plugin-syntax-typescript': + specifier: ^7.25.9 + version: 7.25.9(@babel/core@7.26.0) + '@babel/template': + specifier: ^7.25.9 + version: 7.25.9 + '@babel/traverse': + specifier: ^7.26.4 + version: 7.26.4 + '@babel/types': + specifier: ^7.26.3 + version: 7.26.3 + '@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__generator': + specifier: ^7.6.8 + version: 7.6.8 + '@types/babel__template': + specifier: ^7.4.4 + version: 7.4.4 + '@types/babel__traverse': + specifier: ^7.20.6 + version: 7.20.6 + '@types/diff': + specifier: ^6.0.0 + version: 6.0.0 + babel-dead-code-elimination: + specifier: ^1.0.6 + version: 1.0.8 + chalk: + specifier: ^5.3.0 + version: 5.3.0 + dedent: + specifier: ^1.5.3 + version: 1.5.3 + diff: + specifier: ^7.0.0 + version: 7.0.0 + tiny-invariant: + specifier: ^1.3.3 + version: 1.3.3 + packages/eslint-plugin-router: dependencies: '@typescript-eslint/utils': @@ -3589,6 +3654,72 @@ importers: specifier: workspace:* version: link:../router-plugin + packages/server-functions-plugin: + dependencies: + '@babel/code-frame': + specifier: 7.26.2 + version: 7.26.2 + '@babel/core': + specifier: ^7.26.0 + version: 7.26.0 + '@babel/generator': + specifier: ^7.26.3 + version: 7.26.3 + '@babel/parser': + specifier: ^7.26.3 + version: 7.26.3 + '@babel/plugin-syntax-jsx': + specifier: ^7.25.9 + version: 7.25.9(@babel/core@7.26.0) + '@babel/plugin-syntax-typescript': + specifier: ^7.25.9 + version: 7.25.9(@babel/core@7.26.0) + '@babel/template': + specifier: ^7.25.9 + version: 7.25.9 + '@babel/traverse': + specifier: ^7.26.4 + version: 7.26.4 + '@babel/types': + specifier: ^7.26.3 + version: 7.26.3 + '@tanstack/directive-functions-plugin': + specifier: workspace:* + version: link:../directive-functions-plugin + '@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__generator': + specifier: ^7.6.8 + version: 7.6.8 + '@types/babel__template': + specifier: ^7.4.4 + version: 7.4.4 + '@types/babel__traverse': + specifier: ^7.20.6 + version: 7.20.6 + '@types/diff': + specifier: ^6.0.0 + version: 6.0.0 + babel-dead-code-elimination: + specifier: ^1.0.6 + version: 1.0.8 + chalk: + specifier: ^5.3.0 + version: 5.3.0 + dedent: + specifier: ^1.5.3 + version: 1.5.3 + diff: + specifier: ^7.0.0 + version: 7.0.0 + tiny-invariant: + specifier: ^1.3.3 + version: 1.3.3 + packages/start: dependencies: '@tanstack/react-cross-context': @@ -3603,6 +3734,9 @@ importers: '@tanstack/router-plugin': specifier: workspace:* version: link:../router-plugin + '@tanstack/server-functions-plugin': + specifier: workspace:* + version: link:../server-functions-plugin '@tanstack/start-vite-plugin': specifier: workspace:* version: link:../start-vite-plugin @@ -6137,6 +6271,9 @@ packages: '@types/cross-spawn@6.0.6': resolution: {integrity: sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==} + '@types/diff@6.0.0': + resolution: {integrity: sha512-dhVCYGv3ZSbzmQaBSagrv1WJ6rXCdkyTcDyoNu1MD8JohI7pR7k8wdZEm+mvdxRKXyHVwckFzWU1vJc+Z29MlA==} + '@types/doctrine@0.0.9': resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} @@ -7338,6 +7475,14 @@ packages: decimal.js@10.4.3: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + dedent@1.5.3: + resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -7431,6 +7576,10 @@ packages: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + diff@7.0.0: + resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==} + engines: {node: '>=0.3.1'} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -11196,7 +11345,7 @@ snapshots: '@babel/traverse': 7.26.4 '@babel/types': 7.26.3 convert-source-map: 2.0.0 - debug: 4.3.7(supports-color@9.4.0) + debug: 4.3.7 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -11291,7 +11440,7 @@ snapshots: '@babel/parser': 7.26.3 '@babel/template': 7.25.9 '@babel/types': 7.26.3 - debug: 4.3.7(supports-color@9.4.0) + debug: 4.4.0(supports-color@9.4.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -11940,7 +12089,7 @@ snapshots: '@eslint/config-array@0.19.0': dependencies: '@eslint/object-schema': 2.1.4 - debug: 4.3.7(supports-color@9.4.0) + debug: 4.4.0(supports-color@9.4.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -11950,7 +12099,7 @@ snapshots: '@eslint/eslintrc@3.2.0': dependencies: ajv: 6.12.6 - debug: 4.3.7(supports-color@9.4.0) + debug: 4.4.0(supports-color@9.4.0) espree: 10.3.0 globals: 14.0.0 ignore: 5.3.2 @@ -12167,7 +12316,7 @@ snapshots: '@kwsites/file-exists@1.1.1': dependencies: - debug: 4.3.7(supports-color@9.4.0) + debug: 4.4.0(supports-color@9.4.0) transitivePeerDependencies: - supports-color @@ -13417,6 +13566,8 @@ snapshots: dependencies: '@types/node': 22.10.2 + '@types/diff@6.0.0': {} + '@types/doctrine@0.0.9': {} '@types/eslint-scope@3.7.7': @@ -13588,7 +13739,7 @@ snapshots: '@typescript-eslint/types': 8.18.2 '@typescript-eslint/typescript-estree': 8.18.2(typescript@5.7.2) '@typescript-eslint/visitor-keys': 8.18.2 - debug: 4.3.7(supports-color@9.4.0) + debug: 4.4.0(supports-color@9.4.0) eslint: 9.17.0(jiti@2.4.1) typescript: 5.7.2 transitivePeerDependencies: @@ -13621,7 +13772,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.18.2(typescript@5.7.2) '@typescript-eslint/utils': 8.18.2(eslint@9.17.0(jiti@2.4.1))(typescript@5.7.2) - debug: 4.3.7(supports-color@9.4.0) + debug: 4.4.0(supports-color@9.4.0) eslint: 9.17.0(jiti@2.4.1) ts-api-utils: 1.4.3(typescript@5.7.2) typescript: 5.7.2 @@ -13636,7 +13787,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.18.2 '@typescript-eslint/visitor-keys': 8.18.2 - debug: 4.3.7(supports-color@9.4.0) + debug: 4.4.0(supports-color@9.4.0) fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 @@ -13959,17 +14110,17 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 - '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4)(webpack@5.97.1)': + '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@5.1.4))': dependencies: webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1) - '@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.97.1)': + '@webpack-cli/info@2.0.2(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@5.1.4))': dependencies: webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1) - '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack-dev-server@5.1.0)(webpack@5.97.1)': + '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1))(webpack-dev-server@5.1.0(webpack-cli@5.1.4)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@5.1.4))': dependencies: webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1) @@ -14027,13 +14178,13 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.3.7(supports-color@9.4.0) + debug: 4.4.0(supports-color@9.4.0) transitivePeerDependencies: - supports-color agent-base@7.1.1(supports-color@9.4.0): dependencies: - debug: 4.3.7(supports-color@9.4.0) + debug: 4.4.0(supports-color@9.4.0) transitivePeerDependencies: - supports-color @@ -14786,18 +14937,20 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.3.7(supports-color@9.4.0): + debug@4.3.7: dependencies: ms: 2.1.3 - optionalDependencies: - supports-color: 9.4.0 - debug@4.4.0: + debug@4.4.0(supports-color@9.4.0): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 9.4.0 decimal.js@10.4.3: {} + dedent@1.5.3: {} + deep-eql@5.0.2: {} deep-is@0.1.4: {} @@ -14857,6 +15010,8 @@ snapshots: diff-sequences@29.6.3: {} + diff@7.0.0: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -14985,7 +15140,7 @@ snapshots: esbuild-register@3.6.0(esbuild@0.24.2): dependencies: - debug: 4.3.7(supports-color@9.4.0) + debug: 4.4.0(supports-color@9.4.0) esbuild: 0.24.2 transitivePeerDependencies: - supports-color @@ -15160,7 +15315,7 @@ snapshots: '@types/doctrine': 0.0.9 '@typescript-eslint/scope-manager': 8.18.0 '@typescript-eslint/utils': 8.18.2(eslint@9.17.0(jiti@2.4.1))(typescript@5.7.2) - debug: 4.3.7(supports-color@9.4.0) + debug: 4.4.0(supports-color@9.4.0) doctrine: 3.0.0 enhanced-resolve: 5.17.1 eslint: 9.17.0(jiti@2.4.1) @@ -15361,7 +15516,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.3.7(supports-color@9.4.0) + debug: 4.3.7 escape-string-regexp: 4.0.0 eslint-scope: 8.2.0 eslint-visitor-keys: 4.2.0 @@ -15905,7 +16060,7 @@ snapshots: relateurl: 0.2.7 terser: 5.36.0 - html-webpack-plugin@5.6.3(@rspack/core@1.1.8(@swc/helpers@0.5.15))(webpack@5.97.1): + html-webpack-plugin@5.6.3(@rspack/core@1.1.8(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@5.1.4)): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -15945,7 +16100,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.1(supports-color@9.4.0) - debug: 4.3.7(supports-color@9.4.0) + debug: 4.4.0(supports-color@9.4.0) transitivePeerDependencies: - supports-color @@ -15974,14 +16129,14 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.3.7(supports-color@9.4.0) + debug: 4.4.0(supports-color@9.4.0) transitivePeerDependencies: - supports-color https-proxy-agent@7.0.5(supports-color@9.4.0): dependencies: agent-base: 7.1.1(supports-color@9.4.0) - debug: 4.3.7(supports-color@9.4.0) + debug: 4.4.0(supports-color@9.4.0) transitivePeerDependencies: - supports-color @@ -16050,7 +16205,7 @@ snapshots: dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 - debug: 4.3.7(supports-color@9.4.0) + debug: 4.4.0(supports-color@9.4.0) denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -17836,7 +17991,7 @@ snapshots: dependencies: '@kwsites/file-exists': 1.1.1 '@kwsites/promise-deferred': 1.1.1 - debug: 4.3.7(supports-color@9.4.0) + debug: 4.4.0(supports-color@9.4.0) transitivePeerDependencies: - supports-color @@ -17882,7 +18037,7 @@ snapshots: spdy-transport@3.0.0: dependencies: - debug: 4.3.7(supports-color@9.4.0) + debug: 4.4.0(supports-color@9.4.0) detect-node: 2.1.0 hpack.js: 2.1.6 obuf: 1.1.2 @@ -17893,7 +18048,7 @@ snapshots: spdy@4.0.2: dependencies: - debug: 4.3.7(supports-color@9.4.0) + debug: 4.4.0(supports-color@9.4.0) handle-thing: 2.0.1 http-deceiver: 1.2.7 select-hose: 2.0.0 @@ -18024,7 +18179,7 @@ snapshots: csso: 5.0.5 picocolors: 1.1.1 - swc-loader@0.2.6(@swc/core@1.10.1(@swc/helpers@0.5.15))(webpack@5.97.1): + swc-loader@0.2.6(@swc/core@1.10.1(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@5.1.4)): dependencies: '@swc/core': 1.10.1(@swc/helpers@0.5.15) '@swc/counter': 0.1.3 @@ -18132,26 +18287,26 @@ snapshots: type-fest: 2.19.0 unique-string: 3.0.0 - terser-webpack-plugin@5.3.10(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)): + terser-webpack-plugin@5.3.10(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@5.1.4)): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.2 terser: 5.36.0 - webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2) + webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@5.1.4) optionalDependencies: '@swc/core': 1.10.1(@swc/helpers@0.5.15) esbuild: 0.24.2 - terser-webpack-plugin@5.3.10(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack@5.97.1): + terser-webpack-plugin@5.3.10(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.2 terser: 5.36.0 - webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@5.1.4) + webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2) optionalDependencies: '@swc/core': 1.10.1(@swc/helpers@0.5.15) esbuild: 0.24.2 @@ -18699,7 +18854,7 @@ snapshots: vite-node@2.1.8(@types/node@22.10.2)(jiti@2.4.1)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1): dependencies: cac: 6.7.14 - debug: 4.3.7(supports-color@9.4.0) + debug: 4.4.0(supports-color@9.4.0) es-module-lexer: 1.5.4 pathe: 1.1.2 vite: 6.0.3(@types/node@22.10.2)(jiti@2.4.1)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1) @@ -18724,7 +18879,7 @@ snapshots: '@volar/typescript': 2.4.10 '@vue/language-core': 2.0.29(typescript@5.7.2) compare-versions: 6.1.1 - debug: 4.3.7(supports-color@9.4.0) + debug: 4.4.0(supports-color@9.4.0) kolorist: 1.8.0 local-pkg: 0.5.1 magic-string: 0.30.14 @@ -18744,7 +18899,7 @@ snapshots: '@volar/typescript': 2.4.11 '@vue/language-core': 2.1.10(typescript@5.7.2) compare-versions: 6.1.1 - debug: 4.4.0 + debug: 4.4.0(supports-color@9.4.0) kolorist: 1.8.0 local-pkg: 0.5.1 magic-string: 0.30.17 @@ -18763,7 +18918,7 @@ snapshots: '@volar/typescript': 2.4.11 '@vue/language-core': 2.1.10(typescript@5.7.2) compare-versions: 6.1.1 - debug: 4.4.0 + debug: 4.4.0(supports-color@9.4.0) kolorist: 1.8.0 local-pkg: 0.5.1 magic-string: 0.30.17 @@ -18781,7 +18936,7 @@ snapshots: vite-tsconfig-paths@5.1.4(typescript@5.7.2)(vite@6.0.3(@types/node@22.10.1)(jiti@2.4.1)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)): dependencies: - debug: 4.3.7(supports-color@9.4.0) + debug: 4.3.7 globrex: 0.1.2 tsconfck: 3.1.4(typescript@5.7.2) optionalDependencies: @@ -18792,7 +18947,7 @@ snapshots: vite-tsconfig-paths@5.1.4(typescript@5.7.2)(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.1)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)): dependencies: - debug: 4.3.7(supports-color@9.4.0) + debug: 4.3.7 globrex: 0.1.2 tsconfck: 3.1.4(typescript@5.7.2) optionalDependencies: @@ -18837,7 +18992,7 @@ snapshots: '@vitest/spy': 2.1.8 '@vitest/utils': 2.1.8 chai: 5.1.2 - debug: 4.3.7(supports-color@9.4.0) + debug: 4.3.7 expect-type: 1.1.0 magic-string: 0.30.14 pathe: 1.1.2 @@ -18870,7 +19025,7 @@ snapshots: vue-eslint-parser@9.4.3(eslint@9.17.0(jiti@2.4.1)): dependencies: - debug: 4.3.7(supports-color@9.4.0) + debug: 4.4.0(supports-color@9.4.0) eslint: 9.17.0(jiti@2.4.1) eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 @@ -18914,9 +19069,9 @@ snapshots: webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1): dependencies: '@discoveryjs/json-ext': 0.5.7 - '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4)(webpack@5.97.1) - '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4)(webpack@5.97.1) - '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4)(webpack-dev-server@5.1.0)(webpack@5.97.1) + '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@5.1.4)) + '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@5.1.4)) + '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1))(webpack-dev-server@5.1.0(webpack-cli@5.1.4)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@5.1.4)) colorette: 2.0.20 commander: 10.0.1 cross-spawn: 7.0.6 @@ -18930,7 +19085,7 @@ snapshots: optionalDependencies: webpack-dev-server: 5.1.0(webpack-cli@5.1.4)(webpack@5.97.1) - webpack-dev-middleware@7.4.2(webpack@5.97.1): + webpack-dev-middleware@7.4.2(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@5.1.4)): dependencies: colorette: 2.0.20 memfs: 4.14.1 @@ -18969,7 +19124,7 @@ snapshots: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.2(webpack@5.97.1) + webpack-dev-middleware: 7.4.2(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@5.1.4)) ws: 8.18.0 optionalDependencies: webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@5.1.4) @@ -19042,7 +19197,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack@5.97.1) + terser-webpack-plugin: 5.3.10(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@5.1.4)) watchpack: 2.4.2 webpack-sources: 3.2.3 optionalDependencies: From d82e9df02f8c34bda950db3e8b0b471dcab9d4d8 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Mon, 6 Jan 2025 17:00:57 -0700 Subject: [PATCH 30/38] suggested fixes from sean --- packages/server-functions-plugin/src/index.ts | 2 +- packages/start/src/server-handler/index.tsx | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/server-functions-plugin/src/index.ts b/packages/server-functions-plugin/src/index.ts index cacacba2de..13674c2cae 100644 --- a/packages/server-functions-plugin/src/index.ts +++ b/packages/server-functions-plugin/src/index.ts @@ -138,7 +138,7 @@ export function createTanStackServerFnPlugin(_opts?: {}): { (() => { let serverFunctionsManifest: Record return { - name: 'tanstack-start-server-fn-vite-plugin-build-', + name: 'tanstack-start-server-fn-vite-plugin-build', enforce: 'post', apply: 'build', config(config) { diff --git a/packages/start/src/server-handler/index.tsx b/packages/start/src/server-handler/index.tsx index 66345c7c38..cf4a2cccd3 100644 --- a/packages/start/src/server-handler/index.tsx +++ b/packages/start/src/server-handler/index.tsx @@ -24,12 +24,24 @@ async function handleServerAction(event: H3Event) { return handleServerRequest(toWebRequest(event), event) } +function sanitizeBase(base: string | undefined) { + if (!base) { + throw new Error( + '🚨 process.env.TSS_SERVER_BASE is required in start/server-handler/index', + ) + } + + return base.replace(/^\/|\/$/g, '') +} + export async function handleServerRequest(request: Request, _event?: H3Event) { const method = request.method const url = new URL(request.url, 'http://localhost:3000') // extract the serverFnId from the url as host/_server/:serverFnId // Define a regex to match the path and extract the :thing part - const regex = /\/_server\/([^/?#]+)/ + const regex = new RegExp( + `/${sanitizeBase(process.env.TSS_SERVER_BASE)}/([^/?#]+)`, + ) // Execute the regex const match = url.pathname.match(regex) @@ -48,12 +60,15 @@ export async function handleServerRequest(request: Request, _event?: H3Event) { console.info(`\nServerFn Request: ${serverFnId}`) let action: Function + // In dev, we (for now) use Vinxi to get the "server" server-side router + // Then we use that router's devServer.ssrLoadModule to get the serverFn if (process.env.NODE_ENV === 'development') { action = (await (globalThis as any).app .getRouter('server') .internals.devServer.ssrLoadModule(serverFnInfo.splitFilename) .then((d: any) => d.default)) as Function } else { + // In prod, we use the serverFn's chunkName to get the serverFn const router = (globalThis as any).app.getRouter('server') const filePath = join( router.outDir, From 7733c0cd80b6c7e294d6909a65e92b13e91305ff Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Mon, 6 Jan 2025 22:27:32 -0700 Subject: [PATCH 31/38] fix tests --- .../tests/compiler.test.ts | 205 +--- .../tests/compiler.test.ts | 1083 ----------------- .../tests/index.test.ts | 5 + 3 files changed, 17 insertions(+), 1276 deletions(-) delete mode 100644 packages/server-functions-plugin/tests/compiler.test.ts create mode 100644 packages/server-functions-plugin/tests/index.test.ts diff --git a/packages/directive-functions-plugin/tests/compiler.test.ts b/packages/directive-functions-plugin/tests/compiler.test.ts index f22b1676d3..f822aecdd2 100644 --- a/packages/directive-functions-plugin/tests/compiler.test.ts +++ b/packages/directive-functions-plugin/tests/compiler.test.ts @@ -381,70 +381,15 @@ describe('server function compilation', () => { }) ` - expect(() => - compileDirectives({ ...clientConfig, code }), - ).toThrowErrorMatchingInlineSnapshot( - ` - [Error: 1 | - > 2 | outer(() => { - | ^^ - > 3 | function useServer() { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 4 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 5 | return 'hello' - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 6 | } - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 7 | }) - | ^^^^^^^ Server functions cannot be nested in other blocks or functions - 8 | ] - `, - ) - expect(() => - compileDirectives({ ...serverConfig, code }), - ).toThrowErrorMatchingInlineSnapshot( - ` - [Error: 1 | - > 2 | outer(() => { - | ^^ - > 3 | function useServer() { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 4 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 5 | return 'hello' - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 6 | } - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 7 | }) - | ^^^^^^^ Server functions cannot be nested in other blocks or functions - 8 | ] - `, - ) + expect(() => compileDirectives({ ...clientConfig, code })).toThrow() + expect(() => compileDirectives({ ...serverConfig, code })).toThrow() expect(() => compileDirectives({ ...serverConfig, code, filename: serverConfig.filename + `?tsr-serverfn-split=temp`, }), - ).toThrowErrorMatchingInlineSnapshot( - ` - [Error: 1 | - > 2 | outer(() => { - | ^^ - > 3 | function useServer() { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 4 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 5 | return 'hello' - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 6 | } - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 7 | }) - | ^^^^^^^ Server functions cannot be nested in other blocks or functions - 8 | ] - `, - ) + ).toThrow() }) test('does not support class methods', () => { @@ -462,59 +407,15 @@ describe('server function compilation', () => { } ` - expect(() => compileDirectives({ ...clientConfig, code })) - .toThrowErrorMatchingInlineSnapshot(` - [Error: 1 | - 2 | class TestClass { - > 3 | method() { - | ^^^^^^^^^^^ - > 4 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 5 | return 'hello' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 6 | } - | ^^^^^^^^^ "use server" in class not supported - 7 | - 8 | static staticMethod() { - 9 | 'use server'] - `) - expect(() => compileDirectives({ ...serverConfig, code })) - .toThrowErrorMatchingInlineSnapshot(` - [Error: 1 | - 2 | class TestClass { - > 3 | method() { - | ^^^^^^^^^^^ - > 4 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 5 | return 'hello' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 6 | } - | ^^^^^^^^^ "use server" in class not supported - 7 | - 8 | static staticMethod() { - 9 | 'use server'] - `) + expect(() => compileDirectives({ ...clientConfig, code })).toThrow() + expect(() => compileDirectives({ ...serverConfig, code })).toThrow() expect(() => compileDirectives({ ...serverConfig, code, filename: serverConfig.filename + `?tsr-serverfn-split=temp`, }), - ).toThrowErrorMatchingInlineSnapshot(` - [Error: 1 | - 2 | class TestClass { - > 3 | method() { - | ^^^^^^^^^^^ - > 4 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 5 | return 'hello' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 6 | } - | ^^^^^^^^^ "use server" in class not supported - 7 | - 8 | static staticMethod() { - 9 | 'use server'] - `) + ).toThrow() }) test('does not support object methods', () => { @@ -527,56 +428,15 @@ describe('server function compilation', () => { } ` - expect(() => compileDirectives({ ...clientConfig, code })) - .toThrowErrorMatchingInlineSnapshot(` - [Error: 1 | - 2 | const obj = { - > 3 | method() { - | ^^^^^^^^^^^ - > 4 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 5 | return 'hello' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 6 | }, - | ^^^^^^^^^ "use server" in object method not supported - 7 | } - 8 | ] - `) - expect(() => compileDirectives({ ...serverConfig, code })) - .toThrowErrorMatchingInlineSnapshot(` - [Error: 1 | - 2 | const obj = { - > 3 | method() { - | ^^^^^^^^^^^ - > 4 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 5 | return 'hello' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 6 | }, - | ^^^^^^^^^ "use server" in object method not supported - 7 | } - 8 | ] - `) + expect(() => compileDirectives({ ...clientConfig, code })).toThrow() + expect(() => compileDirectives({ ...serverConfig, code })).toThrow() expect(() => compileDirectives({ ...serverConfig, code, filename: serverConfig.filename + `?tsr-serverfn-split=temp`, }), - ).toThrowErrorMatchingInlineSnapshot(` - [Error: 1 | - 2 | const obj = { - > 3 | method() { - | ^^^^^^^^^^^ - > 4 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 5 | return 'hello' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 6 | }, - | ^^^^^^^^^ "use server" in object method not supported - 7 | } - 8 | ] - `) + ).toThrow() }) test('does not support generator functions', () => { @@ -592,56 +452,15 @@ describe('server function compilation', () => { } ` - expect(() => compileDirectives({ ...clientConfig, code })) - .toThrowErrorMatchingInlineSnapshot(` - [Error: 1 | - > 2 | function* generatorServer() { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 3 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^ - > 4 | yield 'hello' - | ^^^^^^^^^^^^^^^^^^^^ - > 5 | } - | ^^^^^^^ "use server" in generator function not supported - 6 | - 7 | async function* asyncGeneratorServer() { - 8 | 'use server'] - `) - expect(() => compileDirectives({ ...serverConfig, code })) - .toThrowErrorMatchingInlineSnapshot(` - [Error: 1 | - > 2 | function* generatorServer() { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 3 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^ - > 4 | yield 'hello' - | ^^^^^^^^^^^^^^^^^^^^ - > 5 | } - | ^^^^^^^ "use server" in generator function not supported - 6 | - 7 | async function* asyncGeneratorServer() { - 8 | 'use server'] - `) + expect(() => compileDirectives({ ...clientConfig, code })).toThrow() + expect(() => compileDirectives({ ...serverConfig, code })).toThrow() expect(() => compileDirectives({ ...serverConfig, code, filename: serverConfig.filename + `?tsr-serverfn-split=temp`, }), - ).toThrowErrorMatchingInlineSnapshot(` - [Error: 1 | - > 2 | function* generatorServer() { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 3 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^ - > 4 | yield 'hello' - | ^^^^^^^^^^^^^^^^^^^^ - > 5 | } - | ^^^^^^^ "use server" in generator function not supported - 6 | - 7 | async function* asyncGeneratorServer() { - 8 | 'use server'] - `) + ).toThrow() }) test('multiple directiveFnsById', () => { diff --git a/packages/server-functions-plugin/tests/compiler.test.ts b/packages/server-functions-plugin/tests/compiler.test.ts deleted file mode 100644 index e918201e24..0000000000 --- a/packages/server-functions-plugin/tests/compiler.test.ts +++ /dev/null @@ -1,1083 +0,0 @@ -import { describe, expect, test } from 'vitest' - -import { compileDirectives } from '../src/compilers' -import type { CompileDirectivesOpts } from '../src/compilers' - -const clientConfig: Omit = { - directive: 'use server', - directiveLabel: 'Server function', - root: './test-files', - filename: 'test.ts', - getRuntimeCode: () => 'import { createClientRpc } from "my-rpc-lib-client"', - replacer: (opts) => - `createClientRpc({ - filename: ${JSON.stringify(opts.filename)}, - functionId: ${JSON.stringify(opts.functionId)}, - })`, - devSplitImporter: `devImport`, -} - -const ssrConfig: Omit = { - directive: 'use server', - directiveLabel: 'Server function', - root: './test-files', - filename: 'test.ts', - getRuntimeCode: () => 'import { createSsrRpc } from "my-rpc-lib-server"', - replacer: (opts) => - `createSsrRpc({ - filename: ${JSON.stringify(opts.filename)}, - functionId: ${JSON.stringify(opts.functionId)}, - })`, - devSplitImporter: `devImport`, -} - -const serverConfig: Omit = { - directive: 'use server', - directiveLabel: 'Server function', - root: './test-files', - filename: 'test.ts', - getRuntimeCode: () => 'import { createServerRpc } from "my-rpc-lib-server"', - 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({ - fn: ${opts.isSplitFn ? opts.fn : opts.splitImportFn}, - filename: ${JSON.stringify(opts.filename)}, - functionId: ${JSON.stringify(opts.functionId)}, - })`, - devSplitImporter: `devImport`, -} - -describe('server function compilation', () => { - const code = ` - export const namedFunction = wrapper(function namedFunction() { - 'use server' - return 'hello' - }) - - export const arrowFunction = wrapper(() => { - 'use server' - return 'hello' - }) - - export const anonymousFunction = wrapper(function () { - 'use server' - return 'hello' - }) - - 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() - } - - function usedFn() { - return 'hello' - } - - function unusedFn() { - return 'hello' - } - - const namedDefaultExport = 'namedDefaultExport' - export default namedDefaultExport - - const usedButNotExported = 'usedButNotExported' - - const namedExport = 'namedExport' - - export { - namedExport - } - - ` - - test('basic function declaration nested in other variable', () => { - const client = compileDirectives({ - ...clientConfig, - code, - }) - const ssr = compileDirectives({ ...ssrConfig, code }) - const splitFiles = Object.entries(ssr.directiveFnsById) - .map(([_fnId, directiveFn]) => { - return `// ${directiveFn.functionId}\n\n${ - compileDirectives({ - ...serverConfig, - code, - filename: directiveFn.splitFilename, - }).compiledResult.code - }` - }) - .join('\n\n\n') - - expect(client.compiledResult.code).toMatchInlineSnapshot(` - "import { createClientRpc } from "my-rpc-lib-client"; - const namedFunction_wrapper_namedFunction = createClientRpc({ - filename: "test.ts", - functionId: "test--namedFunction_wrapper_namedFunction" - }); - export const namedFunction = wrapper(namedFunction_wrapper_namedFunction); - const arrowFunction_wrapper = createClientRpc({ - filename: "test.ts", - functionId: "test--arrowFunction_wrapper" - }); - export const arrowFunction = wrapper(arrowFunction_wrapper); - const anonymousFunction_wrapper = createClientRpc({ - filename: "test.ts", - functionId: "test--anonymousFunction_wrapper" - }); - export const anonymousFunction = wrapper(anonymousFunction_wrapper); - const multipleDirectives_multipleDirectives = createClientRpc({ - filename: "test.ts", - functionId: "test--multipleDirectives_multipleDirectives" - }); - export const multipleDirectives = multipleDirectives_multipleDirectives; - const iife_1 = createClientRpc({ - filename: "test.ts", - functionId: "test--iife" - }); - export const iife = iife_1(); - const defaultExportFn_1 = createClientRpc({ - filename: "test.ts", - functionId: "test--defaultExportFn" - }); - export default defaultExportFn_1; - const namedExportFn_1 = createClientRpc({ - filename: "test.ts", - functionId: "test--namedExportFn" - }); - export const namedExportFn = namedExportFn_1; - const exportedArrowFunction_wrapper = createClientRpc({ - filename: "test.ts", - functionId: "test--exportedArrowFunction_wrapper" - }); - export const exportedArrowFunction = wrapper(exportedArrowFunction_wrapper); - const namedExportConst_1 = createClientRpc({ - filename: "test.ts", - functionId: "test--namedExportConst" - }); - export const namedExportConst = namedExportConst_1; - const namedDefaultExport = 'namedDefaultExport'; - export default namedDefaultExport; - const namedExport = 'namedExport'; - export { namedExport };" - `) - - expect(ssr.compiledResult.code).toMatchInlineSnapshot(` - "import { createSsrRpc } from "my-rpc-lib-server"; - const namedFunction_wrapper_namedFunction = createSsrRpc({ - filename: "test.ts", - functionId: "test--namedFunction_wrapper_namedFunction" - }); - export const namedFunction = wrapper(namedFunction_wrapper_namedFunction); - const arrowFunction_wrapper = createSsrRpc({ - filename: "test.ts", - functionId: "test--arrowFunction_wrapper" - }); - export const arrowFunction = wrapper(arrowFunction_wrapper); - const anonymousFunction_wrapper = createSsrRpc({ - filename: "test.ts", - functionId: "test--anonymousFunction_wrapper" - }); - export const anonymousFunction = wrapper(anonymousFunction_wrapper); - const multipleDirectives_multipleDirectives = createSsrRpc({ - filename: "test.ts", - functionId: "test--multipleDirectives_multipleDirectives" - }); - export const multipleDirectives = multipleDirectives_multipleDirectives; - const iife_1 = createSsrRpc({ - filename: "test.ts", - functionId: "test--iife" - }); - export const iife = iife_1(); - const defaultExportFn_1 = createSsrRpc({ - filename: "test.ts", - functionId: "test--defaultExportFn" - }); - export default defaultExportFn_1; - const namedExportFn_1 = createSsrRpc({ - filename: "test.ts", - functionId: "test--namedExportFn" - }); - export const namedExportFn = namedExportFn_1; - const exportedArrowFunction_wrapper = createSsrRpc({ - filename: "test.ts", - functionId: "test--exportedArrowFunction_wrapper" - }); - export const exportedArrowFunction = wrapper(exportedArrowFunction_wrapper); - const namedExportConst_1 = createSsrRpc({ - filename: "test.ts", - functionId: "test--namedExportConst" - }); - export const namedExportConst = namedExportConst_1; - const namedDefaultExport = 'namedDefaultExport'; - export default namedDefaultExport; - const namedExport = 'namedExport'; - export { namedExport };" - `) - - expect(splitFiles).toMatchInlineSnapshot( - ` - "// test--namedFunction_wrapper_namedFunction - - import { createServerRpc } from "my-rpc-lib-server"; - const namedFunction_wrapper_namedFunction = createServerRpc({ - fn: function namedFunction() { - return 'hello'; - }, - filename: "test.ts", - functionId: "test--namedFunction_wrapper_namedFunction" - }); - export default namedFunction_wrapper_namedFunction; - - - // test--arrowFunction_wrapper - - import { createServerRpc } from "my-rpc-lib-server"; - const arrowFunction_wrapper = createServerRpc({ - fn: () => { - return 'hello'; - }, - filename: "test.ts", - functionId: "test--arrowFunction_wrapper" - }); - export default arrowFunction_wrapper; - - - // test--anonymousFunction_wrapper - - import { createServerRpc } from "my-rpc-lib-server"; - const anonymousFunction_wrapper = createServerRpc({ - fn: function () { - return 'hello'; - }, - filename: "test.ts", - functionId: "test--anonymousFunction_wrapper" - }); - export default anonymousFunction_wrapper; - - - // test--multipleDirectives_multipleDirectives - - import { createServerRpc } from "my-rpc-lib-server"; - const multipleDirectives_multipleDirectives = createServerRpc({ - fn: function multipleDirectives() { - 'use strict'; - - return 'hello'; - }, - filename: "test.ts", - functionId: "test--multipleDirectives_multipleDirectives" - }); - export default multipleDirectives_multipleDirectives; - - - // test--iife - - import { createServerRpc } from "my-rpc-lib-server"; - const iife_1 = createServerRpc({ - fn: function () { - return 'hello'; - }, - filename: "test.ts", - functionId: "test--iife" - }); - export default iife_1; - - - // test--defaultExportFn - - import { createServerRpc } from "my-rpc-lib-server"; - const defaultExportFn_1 = createServerRpc({ - fn: function defaultExportFn() { - return 'hello'; - }, - filename: "test.ts", - functionId: "test--defaultExportFn" - }); - export default defaultExportFn_1; - - - // test--namedExportFn - - import { createServerRpc } from "my-rpc-lib-server"; - const namedExportFn_1 = createServerRpc({ - fn: function namedExportFn() { - return 'hello'; - }, - filename: "test.ts", - functionId: "test--namedExportFn" - }); - export default namedExportFn_1; - - - // test--exportedArrowFunction_wrapper - - import { createServerRpc } from "my-rpc-lib-server"; - const exportedArrowFunction_wrapper = createServerRpc({ - fn: () => { - return 'hello'; - }, - filename: "test.ts", - functionId: "test--exportedArrowFunction_wrapper" - }); - export default exportedArrowFunction_wrapper; - - - // test--namedExportConst - - import { createServerRpc } from "my-rpc-lib-server"; - const namedExportConst_1 = createServerRpc({ - fn: () => { - return usedFn(); - }, - filename: "test.ts", - functionId: "test--namedExportConst" - }); - function usedFn() { - return 'hello'; - } - export default namedExportConst_1;" - `, - ) - }) - - test('Does not support function declarations nested in other blocks', () => { - const code = ` - outer(() => { - function useServer() { - 'use server' - return 'hello' - } - }) - ` - - expect(() => - compileDirectives({ ...clientConfig, code }), - ).toThrowErrorMatchingInlineSnapshot( - ` - [Error: 1 | - > 2 | outer(() => { - | ^^ - > 3 | function useServer() { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 4 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 5 | return 'hello' - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 6 | } - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 7 | }) - | ^^^^^^^ Server functions cannot be nested in other blocks or functions - 8 | ] - `, - ) - expect(() => - compileDirectives({ ...serverConfig, code }), - ).toThrowErrorMatchingInlineSnapshot( - ` - [Error: 1 | - > 2 | outer(() => { - | ^^ - > 3 | function useServer() { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 4 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 5 | return 'hello' - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 6 | } - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 7 | }) - | ^^^^^^^ Server functions cannot be nested in other blocks or functions - 8 | ] - `, - ) - expect(() => - compileDirectives({ - ...serverConfig, - code, - filename: serverConfig.filename + `?tsr-serverfn-split=temp`, - }), - ).toThrowErrorMatchingInlineSnapshot( - ` - [Error: 1 | - > 2 | outer(() => { - | ^^ - > 3 | function useServer() { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 4 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 5 | return 'hello' - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 6 | } - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 7 | }) - | ^^^^^^^ Server functions cannot be nested in other blocks or functions - 8 | ] - `, - ) - }) - - 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 })) - .toThrowErrorMatchingInlineSnapshot(` - [Error: 1 | - 2 | class TestClass { - > 3 | method() { - | ^^^^^^^^^^^ - > 4 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 5 | return 'hello' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 6 | } - | ^^^^^^^^^ "use server" in class not supported - 7 | - 8 | static staticMethod() { - 9 | 'use server'] - `) - expect(() => compileDirectives({ ...serverConfig, code })) - .toThrowErrorMatchingInlineSnapshot(` - [Error: 1 | - 2 | class TestClass { - > 3 | method() { - | ^^^^^^^^^^^ - > 4 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 5 | return 'hello' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 6 | } - | ^^^^^^^^^ "use server" in class not supported - 7 | - 8 | static staticMethod() { - 9 | 'use server'] - `) - expect(() => - compileDirectives({ - ...serverConfig, - code, - filename: serverConfig.filename + `?tsr-serverfn-split=temp`, - }), - ).toThrowErrorMatchingInlineSnapshot(` - [Error: 1 | - 2 | class TestClass { - > 3 | method() { - | ^^^^^^^^^^^ - > 4 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 5 | return 'hello' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 6 | } - | ^^^^^^^^^ "use server" in class not supported - 7 | - 8 | static staticMethod() { - 9 | 'use server'] - `) - }) - - test('does not support object methods', () => { - const code = ` - const obj = { - method() { - 'use server' - return 'hello' - }, - } - ` - - expect(() => compileDirectives({ ...clientConfig, code })) - .toThrowErrorMatchingInlineSnapshot(` - [Error: 1 | - 2 | const obj = { - > 3 | method() { - | ^^^^^^^^^^^ - > 4 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 5 | return 'hello' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 6 | }, - | ^^^^^^^^^ "use server" in object method not supported - 7 | } - 8 | ] - `) - expect(() => compileDirectives({ ...serverConfig, code })) - .toThrowErrorMatchingInlineSnapshot(` - [Error: 1 | - 2 | const obj = { - > 3 | method() { - | ^^^^^^^^^^^ - > 4 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 5 | return 'hello' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 6 | }, - | ^^^^^^^^^ "use server" in object method not supported - 7 | } - 8 | ] - `) - expect(() => - compileDirectives({ - ...serverConfig, - code, - filename: serverConfig.filename + `?tsr-serverfn-split=temp`, - }), - ).toThrowErrorMatchingInlineSnapshot(` - [Error: 1 | - 2 | const obj = { - > 3 | method() { - | ^^^^^^^^^^^ - > 4 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 5 | return 'hello' - | ^^^^^^^^^^^^^^^^^^^^^^ - > 6 | }, - | ^^^^^^^^^ "use server" in object method not supported - 7 | } - 8 | ] - `) - }) - - test('does not support generator functions', () => { - const code = ` - function* generatorServer() { - 'use server' - yield 'hello' - } - - async function* asyncGeneratorServer() { - 'use server' - yield 'hello' - } - ` - - expect(() => compileDirectives({ ...clientConfig, code })) - .toThrowErrorMatchingInlineSnapshot(` - [Error: 1 | - > 2 | function* generatorServer() { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 3 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^ - > 4 | yield 'hello' - | ^^^^^^^^^^^^^^^^^^^^ - > 5 | } - | ^^^^^^^ "use server" in generator function not supported - 6 | - 7 | async function* asyncGeneratorServer() { - 8 | 'use server'] - `) - expect(() => compileDirectives({ ...serverConfig, code })) - .toThrowErrorMatchingInlineSnapshot(` - [Error: 1 | - > 2 | function* generatorServer() { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 3 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^ - > 4 | yield 'hello' - | ^^^^^^^^^^^^^^^^^^^^ - > 5 | } - | ^^^^^^^ "use server" in generator function not supported - 6 | - 7 | async function* asyncGeneratorServer() { - 8 | 'use server'] - `) - expect(() => - compileDirectives({ - ...serverConfig, - code, - filename: serverConfig.filename + `?tsr-serverfn-split=temp`, - }), - ).toThrowErrorMatchingInlineSnapshot(` - [Error: 1 | - > 2 | function* generatorServer() { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - > 3 | 'use server' - | ^^^^^^^^^^^^^^^^^^^^ - > 4 | yield 'hello' - | ^^^^^^^^^^^^^^^^^^^^ - > 5 | } - | ^^^^^^^ "use server" in generator function not supported - 6 | - 7 | async function* asyncGeneratorServer() { - 8 | 'use server'] - `) - }) - - test('multiple directiveFnsById', () => { - const code = ` - function multiDirective() { - 'use strict' - 'use server' - return 'hello' - } - ` - - const client = compileDirectives({ ...clientConfig, code }) - const ssr = compileDirectives({ ...ssrConfig, code }) - const splitFiles = Object.entries(ssr.directiveFnsById) - .map(([_fnId, directiveFn]) => { - return `// ${directiveFn.functionId}\n\n${ - compileDirectives({ - ...serverConfig, - code, - filename: directiveFn.splitFilename, - }).compiledResult.code - }` - }) - .join('\n\n\n') - - expect(client.compiledResult.code).toMatchInlineSnapshot(` - "import { createClientRpc } from "my-rpc-lib-client"; - createClientRpc({ - filename: "test.ts", - functionId: "test--multiDirective" - });" - `) - expect(ssr.compiledResult.code).toMatchInlineSnapshot(` - "import { createSsrRpc } from "my-rpc-lib-server"; - createSsrRpc({ - filename: "test.ts", - functionId: "test--multiDirective" - });" - `) - expect(splitFiles).toMatchInlineSnapshot(` - "// test--multiDirective - - import { createServerRpc } from "my-rpc-lib-server"; - createServerRpc({ - fn: function multiDirective() { - 'use strict'; - - return 'hello'; - }, - filename: "test.ts", - functionId: "test--multiDirective" - }); - export default 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 splitFiles = Object.entries(ssr.directiveFnsById) - .map(([_fnId, directiveFn]) => { - return `// ${directiveFn.functionId}\n\n${ - compileDirectives({ - ...serverConfig, - code, - filename: directiveFn.splitFilename, - }).compiledResult.code - }` - }) - .join('\n\n\n') - - expect(client.compiledResult.code).toMatchInlineSnapshot(` - "import { createClientRpc } from "my-rpc-lib-client"; - const iife_1 = createClientRpc({ - filename: "test.ts", - functionId: "test--iife" - }); - export const iife = iife_1();" - `) - - expect(ssr.compiledResult.code).toMatchInlineSnapshot(` - "import { createSsrRpc } from "my-rpc-lib-server"; - const iife_1 = createSsrRpc({ - filename: "test.ts", - functionId: "test--iife" - }); - export const iife = iife_1();" - `) - - expect(splitFiles).toMatchInlineSnapshot(` - "// test--iife - - import { createServerRpc } from "my-rpc-lib-server"; - const iife_1 = createServerRpc({ - fn: function () { - return 'hello'; - }, - filename: "test.ts", - functionId: "test--iife" - }); - export default 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 splitFiles = Object.entries(ssr.directiveFnsById) - .map(([_fnId, directiveFn]) => { - return `// ${directiveFn.functionId}\n\n${ - compileDirectives({ - ...serverConfig, - code, - filename: directiveFn.splitFilename, - }).compiledResult.code - }` - }) - .join('\n\n\n') - - expect(client.compiledResult.code).toMatchInlineSnapshot(` - "import { createClientRpc } from "my-rpc-lib-client"; - const outer_useServer = createClientRpc({ - filename: "test.ts", - functionId: "test--outer_useServer" - }); - outer(outer_useServer); - const outer_useServer_1 = createClientRpc({ - filename: "test.ts", - functionId: "test--outer_useServer_1" - }); - outer(outer_useServer_1);" - `) - - expect(ssr.compiledResult.code).toMatchInlineSnapshot(` - "import { createSsrRpc } from "my-rpc-lib-server"; - const outer_useServer = createSsrRpc({ - filename: "test.ts", - functionId: "test--outer_useServer" - }); - outer(outer_useServer); - const outer_useServer_1 = createSsrRpc({ - filename: "test.ts", - functionId: "test--outer_useServer_1" - }); - outer(outer_useServer_1);" - `) - - expect(splitFiles).toMatchInlineSnapshot(` - "// test--outer_useServer - - import { createServerRpc } from "my-rpc-lib-server"; - const outer_useServer = createServerRpc({ - fn: function useServer() { - return 'hello'; - }, - filename: "test.ts", - functionId: "test--outer_useServer" - }); - outer(outer_useServer); - const outer_useServer_1 = createServerRpc({ - fn: (...args) => devImport("test.ts?tsr-directive-use-server-split=outer_useServer_1").then(module => module.default(...args)), - filename: "test.ts", - functionId: "test--outer_useServer_1" - }); - outer(outer_useServer_1); - export default outer_useServer; - - - // test--outer_useServer_1 - - import { createServerRpc } from "my-rpc-lib-server"; - const outer_useServer = createServerRpc({ - fn: (...args) => devImport("test.ts?tsr-directive-use-server-split=outer_useServer").then(module => module.default(...args)), - filename: "test.ts", - functionId: "test--outer_useServer" - }); - outer(outer_useServer); - const outer_useServer_1 = createServerRpc({ - fn: function useServer() { - return 'hello'; - }, - filename: "test.ts", - functionId: "test--outer_useServer_1" - }); - outer(outer_useServer_1); - export default 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 splitFiles = Object.entries(ssr.directiveFnsById) - .map(([_fnId, directive]) => { - return `// ${directive.functionName}\n\n${ - compileDirectives({ - ...serverConfig, - code, - filename: directive.splitFilename, - }).compiledResult.code - }` - }) - .join('\n\n\n') - - expect(client.compiledResult.code).toMatchInlineSnapshot(` - "'use server'; - - import { createClientRpc } from "my-rpc-lib-client"; - const useServer_1 = createClientRpc({ - filename: "test.ts", - functionId: "test--useServer" - }); - export const useServer = useServer_1; - const defaultExport_1 = createClientRpc({ - filename: "test.ts", - functionId: "test--defaultExport" - }); - export default defaultExport_1;" - `) - expect(ssr.compiledResult.code).toMatchInlineSnapshot(` - "'use server'; - - import { createSsrRpc } from "my-rpc-lib-server"; - const useServer_1 = createSsrRpc({ - filename: "test.ts", - functionId: "test--useServer" - }); - export const useServer = useServer_1; - const defaultExport_1 = createSsrRpc({ - filename: "test.ts", - functionId: "test--defaultExport" - }); - export default defaultExport_1;" - `) - expect(splitFiles).toMatchInlineSnapshot(` - "// useServer - - 'use server'; - - import { createServerRpc } from "my-rpc-lib-server"; - const useServer_1 = createServerRpc({ - fn: function useServer() { - return usedInUseServer(); - }, - filename: "test.ts", - functionId: "test--useServer" - }); - function usedInUseServer() { - return 'hello'; - } - export default useServer_1; - - - // defaultExport - - 'use server'; - - import { createServerRpc } from "my-rpc-lib-server"; - const defaultExport_1 = createServerRpc({ - fn: function defaultExport() { - return 'hello'; - }, - filename: "test.ts", - functionId: "test--defaultExport" - }); - export default defaultExport_1;" - `) - }) - - test('createServerFn with identifier', () => { - // The following code is the client output of the tanstack-start-vite-plugin - // that compiles `createServerFn` calls to automatically add the `use server` - // directive in the right places. - const clientOrSsrCode = `import { createServerFn } from '@tanstack/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-vite-plugin - // that compiles `createServerFn` calls to automatically add the `use server` - // directive in the right places. - const serverCode = `import { createServerFn } from '@tanstack/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 splitFiles = Object.entries(ssr.directiveFnsById) - .map(([_fnId, directiveFn]) => { - return `// ${directiveFn.functionId}\n\n${ - compileDirectives({ - ...serverConfig, - code: serverCode, - filename: directiveFn.splitFilename, - }).compiledResult.code - }` - }) - .join('\n\n\n') - - expect(client.compiledResult.code).toMatchInlineSnapshot(` - "import { createClientRpc } from "my-rpc-lib-client"; - import { createServerFn } from '@tanstack/start'; - const myServerFn_createServerFn_handler = createClientRpc({ - filename: "test.ts", - functionId: "test--myServerFn_createServerFn_handler" - }); - export const myServerFn = createServerFn().handler(myServerFn_createServerFn_handler); - const myServerFn2_createServerFn_handler = createClientRpc({ - filename: "test.ts", - functionId: "test--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/start'; - const myServerFn_createServerFn_handler = createSsrRpc({ - filename: "test.ts", - functionId: "test--myServerFn_createServerFn_handler" - }); - export const myServerFn = createServerFn().handler(myServerFn_createServerFn_handler); - const myServerFn2_createServerFn_handler = createSsrRpc({ - filename: "test.ts", - functionId: "test--myServerFn2_createServerFn_handler" - }); - export const myServerFn2 = createServerFn().handler(myServerFn2_createServerFn_handler);" - `) - expect(splitFiles).toMatchInlineSnapshot(` - "// test--myServerFn_createServerFn_handler - - import { createServerRpc } from "my-rpc-lib-server"; - import { createServerFn } from '@tanstack/start'; - const myFunc = () => { - return 'hello from the server'; - }; - const myServerFn_createServerFn_handler = createServerRpc({ - fn: opts => { - return myServerFn.__executeServer(opts); - }, - filename: "test.ts", - functionId: "test--myServerFn_createServerFn_handler" - }); - const myServerFn = createServerFn().handler(myServerFn_createServerFn_handler, myFunc); - export default myServerFn_createServerFn_handler; - - - // test--myServerFn2_createServerFn_handler - - import { createServerRpc } from "my-rpc-lib-server"; - import { createServerFn } from '@tanstack/start'; - const myFunc = () => { - return 'hello from the server'; - }; - const myServerFn_createServerFn_handler = createServerRpc({ - fn: (...args) => devImport("test.ts?tsr-directive-use-server-split=myServerFn_createServerFn_handler").then(module => module.default(...args)), - filename: "test.ts", - functionId: "test--myServerFn_createServerFn_handler" - }); - const myFunc2 = () => { - return myServerFn({ - data: 'hello 2 from the server' - }); - }; - const myServerFn2_createServerFn_handler = createServerRpc({ - fn: opts => { - return myServerFn2.__executeServer(opts); - }, - filename: "test.ts", - functionId: "test--myServerFn2_createServerFn_handler" - }); - const myServerFn = createServerFn().handler(myServerFn_createServerFn_handler, myFunc); - const myServerFn2 = createServerFn().handler(myServerFn2_createServerFn_handler, myFunc2); - export default myServerFn2_createServerFn_handler;" - `) - }) -}) diff --git a/packages/server-functions-plugin/tests/index.test.ts b/packages/server-functions-plugin/tests/index.test.ts new file mode 100644 index 0000000000..a2d03d603e --- /dev/null +++ b/packages/server-functions-plugin/tests/index.test.ts @@ -0,0 +1,5 @@ +import { expect, test } from 'vitest' + +test('test', () => { + expect(1).toBe(1) +}) From d37a786326f9ea57a87a33c3f2e395359d2fbd47 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Mon, 6 Jan 2025 22:36:09 -0700 Subject: [PATCH 32/38] fix: import specifier --- packages/start/package.json | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/start/package.json b/packages/start/package.json index ff632fcfc7..963a70cd06 100644 --- a/packages/start/package.json +++ b/packages/start/package.json @@ -82,16 +82,6 @@ "default": "./dist/cjs/api/index.cjs" } }, - "./client-runtime": { - "import": { - "types": "./dist/esm/client-runtime/index.d.ts", - "default": "./dist/esm/client-runtime/index.js" - }, - "require": { - "types": "./dist/cjs/client-runtime/index.d.cts", - "default": "./dist/cjs/client-runtime/index.cjs" - } - }, "./config": { "import": { "types": "./dist/esm/config/index.d.ts", @@ -104,16 +94,20 @@ "default": "./dist/esm/router-manifest/index.js" } }, - "./server-runtime": { + "./client-runtime": { "import": { - "types": "./dist/esm/server-runtime/index.d.ts", - "default": "./dist/esm/server-runtime/index.js" + "types": "./dist/esm/client-runtime/index.d.ts", + "default": "./dist/esm/client-runtime/index.js" + }, + "require": { + "types": "./dist/cjs/client-runtime/index.d.cts", + "default": "./dist/cjs/client-runtime/index.cjs" } }, - "./react-server-runtime": { + "./server-runtime": { "import": { - "types": "./dist/esm/react-server-runtime/index.d.ts", - "default": "./dist/esm/react-server-runtime/index.js" + "types": "./dist/esm/server-runtime/index.d.ts", + "default": "./dist/esm/server-runtime/index.js" } }, "./server-handler": { @@ -122,6 +116,12 @@ "default": "./dist/esm/server-handler/index.js" } }, + "./ssr-runtime": { + "import": { + "types": "./dist/esm/ssr-runtime/index.d.ts", + "default": "./dist/esm/ssr-runtime/index.js" + } + }, "./package.json": "./package.json" }, "sideEffects": false, From 497a83ff215f65be9dbbd24f4cc205d2272fc450 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Tue, 7 Jan 2025 00:29:54 -0700 Subject: [PATCH 33/38] fix: dev imports --- .../src/compilers.ts | 9 +- .../directive-functions-plugin/src/index.ts | 2 +- .../tests/compiler.test.ts | 12 +- .../server-functions-plugin/src/compilers.ts | 592 ------------------ packages/server-functions-plugin/src/index.ts | 51 +- .../server-functions-plugin/src/logger.ts | 59 -- packages/start/src/server-handler/index.tsx | 24 +- 7 files changed, 58 insertions(+), 691 deletions(-) delete mode 100644 packages/server-functions-plugin/src/compilers.ts delete mode 100644 packages/server-functions-plugin/src/logger.ts diff --git a/packages/directive-functions-plugin/src/compilers.ts b/packages/directive-functions-plugin/src/compilers.ts index 0ce812f3f1..01eaefb28a 100644 --- a/packages/directive-functions-plugin/src/compilers.ts +++ b/packages/directive-functions-plugin/src/compilers.ts @@ -44,7 +44,7 @@ export type CompileDirectivesOpts = ParseAstOptions & { directiveFnsById: Record }) => string replacer: ReplacerFn - devSplitImporter: string + // devSplitImporter: string } export type ParseAstOptions = { @@ -247,7 +247,7 @@ export function findDirectives( replacer?: ReplacerFn splitFunctionName?: string | null directiveSplitParam: string - devSplitImporter: string + // devSplitImporter: string }, ) { const directiveFnsById: Record = {} @@ -481,10 +481,7 @@ export function findDirectives( : {}), ...(replacer.includes('$$splitImportFn$$') ? { - $$splitImportFn$$: - process.env.NODE_ENV === 'production' - ? `(...args) => import(${JSON.stringify(splitFilename)}).then(module => module.default(...args))` - : `(...args) => ${opts.devSplitImporter}(${JSON.stringify(splitFilename)}).then(module => module.default(...args))`, + $$splitImportFn$$: `(...args) => import(${JSON.stringify(splitFilename)}).then(module => module.default(...args))`, } : {}), }) diff --git a/packages/directive-functions-plugin/src/index.ts b/packages/directive-functions-plugin/src/index.ts index 8de8ca6cf8..d1bce43f7d 100644 --- a/packages/directive-functions-plugin/src/index.ts +++ b/packages/directive-functions-plugin/src/index.ts @@ -12,6 +12,7 @@ export type { DirectiveFn, CompileDirectivesOpts } from './compilers' export type DirectiveFunctionsViteOptions = Pick< CompileDirectivesOpts, 'directive' | 'directiveLabel' | 'getRuntimeCode' | 'replacer' + // | 'devSplitImporter' > const createDirectiveRx = (directive: string) => @@ -48,7 +49,6 @@ export function TanStackDirectiveFunctionsPlugin( filename: id, // globalThis.app currently refers to Vinxi's app instance. In the future, it can just be the // vite dev server instance we get from Nitro. - devSplitImporter: `(globalThis.app.getRouter('server').internals.devServer.ssrLoadModule)`, }) opts.onDirectiveFnsById?.(directiveFnsById) diff --git a/packages/directive-functions-plugin/tests/compiler.test.ts b/packages/directive-functions-plugin/tests/compiler.test.ts index f822aecdd2..1ee2f0ee7d 100644 --- a/packages/directive-functions-plugin/tests/compiler.test.ts +++ b/packages/directive-functions-plugin/tests/compiler.test.ts @@ -14,7 +14,7 @@ const clientConfig: Omit = { filename: ${JSON.stringify(opts.filename)}, functionId: ${JSON.stringify(opts.functionId)}, })`, - devSplitImporter: `devImport`, + // devSplitImporter: `devImport`, } const ssrConfig: Omit = { @@ -28,7 +28,7 @@ const ssrConfig: Omit = { filename: ${JSON.stringify(opts.filename)}, functionId: ${JSON.stringify(opts.functionId)}, })`, - devSplitImporter: `devImport`, + // devSplitImporter: `devImport`, } const serverConfig: Omit = { @@ -49,7 +49,7 @@ const serverConfig: Omit = { filename: ${JSON.stringify(opts.filename)}, functionId: ${JSON.stringify(opts.functionId)}, })`, - devSplitImporter: `devImport`, + // devSplitImporter: `devImport`, } describe('server function compilation', () => { @@ -641,7 +641,7 @@ describe('server function compilation', () => { }); outer(outer_useServer); const outer_useServer_1 = createServerRpc({ - fn: (...args) => devImport("test.ts?tsr-directive-use-server-split=outer_useServer_1").then(module => module.default(...args)), + fn: (...args) => import("test.ts?tsr-directive-use-server-split=outer_useServer_1").then(module => module.default(...args)), filename: "test.ts", functionId: "test_ts_tsr-directive-use-server-split_outer_useServer--outer_useServer_1" }); @@ -653,7 +653,7 @@ describe('server function compilation', () => { import { createServerRpc } from "my-rpc-lib-server"; const outer_useServer = createServerRpc({ - fn: (...args) => devImport("test.ts?tsr-directive-use-server-split=outer_useServer").then(module => module.default(...args)), + fn: (...args) => import("test.ts?tsr-directive-use-server-split=outer_useServer").then(module => module.default(...args)), filename: "test.ts", functionId: "test_ts_tsr-directive-use-server-split_outer_useServer_1--outer_useServer" }); @@ -878,7 +878,7 @@ describe('server function compilation', () => { return 'hello from the server'; }; const myServerFn_createServerFn_handler = createServerRpc({ - fn: (...args) => devImport("test.ts?tsr-directive-use-server-split=myServerFn_createServerFn_handler").then(module => module.default(...args)), + fn: (...args) => import("test.ts?tsr-directive-use-server-split=myServerFn_createServerFn_handler").then(module => module.default(...args)), filename: "test.ts", functionId: "test_ts_tsr-directive-use-server-split_myServerFn2_createServerFn_handler--myServerFn_createServerFn_handler" }); diff --git a/packages/server-functions-plugin/src/compilers.ts b/packages/server-functions-plugin/src/compilers.ts deleted file mode 100644 index d01d482074..0000000000 --- a/packages/server-functions-plugin/src/compilers.ts +++ /dev/null @@ -1,592 +0,0 @@ -import path from 'node:path' -import * as babel from '@babel/core' -import _generate from '@babel/generator' -import { parse } from '@babel/parser' -import { isIdentifier, isVariableDeclarator } from '@babel/types' -import { codeFrameColumns } from '@babel/code-frame' -import { deadCodeElimination } from 'babel-dead-code-elimination' -import type { ParseResult } from '@babel/parser' - -let generate = _generate - -if ('default' in generate) { - generate = generate.default as typeof generate -} - -export interface DirectiveFn { - nodePath: SupportedFunctionPath - functionName: string - functionId: string - referenceName: string - splitFilename: string - filename: string - chunkName: string -} - -export type SupportedFunctionPath = - | babel.NodePath - | babel.NodePath - | babel.NodePath - -export type ReplacerFn = (opts: { - fn: string - splitImportFn: string - filename: string - functionId: string - isSplitFn: boolean -}) => string - -// const debug = process.env.TSR_VITE_DEBUG === 'true' - -export type CompileDirectivesOpts = ParseAstOptions & { - directive: string - directiveLabel: string - getRuntimeCode?: (opts: { - directiveFnsById: Record - }) => string - replacer: ReplacerFn - devSplitImporter: string -} - -export type ParseAstOptions = { - code: string - filename: string - root: string -} - -export function parseAst(opts: ParseAstOptions): ParseResult { - return parse(opts.code, { - plugins: ['jsx', 'typescript'], - sourceType: 'module', - ...{ - root: opts.root, - filename: opts.filename, - sourceMaps: true, - }, - }) -} - -export function compileDirectives(opts: CompileDirectivesOpts) { - const [_, searchParamsStr] = opts.filename.split('?') - const searchParams = new URLSearchParams(searchParamsStr) - const directiveSplitParam = `tsr-directive-${opts.directive.replace(/[^a-zA-Z0-9]/g, '-')}-split` - const functionName = searchParams.get(directiveSplitParam) - - const ast = parseAst(opts) - const directiveFnsById = findDirectives(ast, { - ...opts, - splitFunctionName: functionName, - directiveSplitParam, - }) - - const directiveFnsByFunctionName = Object.fromEntries( - Object.entries(directiveFnsById).map(([id, fn]) => [fn.functionName, fn]), - ) - - // Add runtime code if there are directives - // Add runtime code if there are directives - if (Object.keys(directiveFnsById).length > 0) { - // Add a vite import to the top of the file - ast.program.body.unshift( - babel.types.importDeclaration( - [babel.types.importDefaultSpecifier(babel.types.identifier('vite'))], - babel.types.stringLiteral('vite'), - ), - ) - - if (opts.getRuntimeCode) { - const runtimeImport = babel.template.statement( - opts.getRuntimeCode({ directiveFnsById }), - )() - ast.program.body.unshift(runtimeImport) - } - } - - // If there is a functionName, we need to remove all exports - // then make sure that our function is exported under the - // directive name - if (functionName) { - const directiveFn = directiveFnsByFunctionName[functionName] - - if (!directiveFn) { - throw new Error(`${opts.directiveLabel} ${functionName} not found`) - } - - safeRemoveExports(ast) - - ast.program.body.push( - babel.types.exportDefaultDeclaration( - babel.types.identifier(directiveFn.referenceName), - ), - ) - } - - deadCodeElimination(ast) - - const compiledResult = generate(ast, { - sourceMaps: true, - sourceFileName: opts.filename, - minified: process.env.NODE_ENV === 'production', - }) - - return { - compiledResult, - directiveFnsById, - } -} - -function findNearestVariableName( - path: babel.NodePath, - directiveLabel: 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( - `${directiveLabel} in ClassMethod or ObjectMethod not supported`, - ) - } - - return '' - })() - - if (name) { - nameParts.unshift(name) - } - - currentPath = currentPath.parentPath - } - - return nameParts.length > 0 ? nameParts.join('_') : 'anonymous' -} - -function makeFileLocationUrlSafe(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 -} - -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 - directiveLabel: string - replacer?: ReplacerFn - splitFunctionName?: string | null - directiveSplitParam: string - devSplitImporter: string - }, -) { - const directiveFnsById: Record = {} - const functionNameCounts: Record = {} - - 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, - ) - - // 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) - } - }, - ExportNamedDeclaration(path) { - if (babel.types.isFunctionDeclaration(path.node.declaration)) { - compileDirective(path.get('declaration') as SupportedFunctionPath) - } - }, - }) - } else { - // Find all directives - babel.traverse(ast, { - DirectiveLiteral(nodePath) { - if (nodePath.node.value === opts.directive) { - const directiveFn = nodePath.findParent((p) => p.isFunction()) as - | SupportedFunctionPath - | undefined - - if (!directiveFn) return - - // Handle class and object methods which are not supported - const isGenerator = - directiveFn.isFunction() && directiveFn.node.generator - - const isClassMethod = directiveFn.isClassMethod() - const isObjectMethod = directiveFn.isObjectMethod() - - if (isClassMethod || isObjectMethod || isGenerator) { - throw codeFrameError( - opts.code, - directiveFn.node.loc, - `"${opts.directive}" in ${isClassMethod ? 'class' : isObjectMethod ? 'object method' : 'generator function'} 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.directiveLabel}s 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.directiveLabel}s must be function declarations or function expressions`, - ) - } - - compileDirective(directiveFn) - } - }, - }) - } - - return directiveFnsById - - function compileDirective(directiveFn: SupportedFunctionPath) { - // 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, - ) - } - - // Find the nearest variable name - let functionName = findNearestVariableName(directiveFn, opts.directiveLabel) - - // Count the number of functions with the same baseLabel - functionNameCounts[functionName] = - (functionNameCounts[functionName] || 0) + 1 - - // If there are multiple functions with the same fnName, - // append a unique identifier to the functionId - functionName = - functionNameCounts[functionName]! > 1 - ? `${functionName}_${functionNameCounts[functionName]! - 1}` - : functionName - - // Move the function to program level while preserving its position - // in the program body - const programBody = programPath.node.body - - const topParent = - directiveFn.findParent((p) => !!p.parentPath?.isProgram()) || directiveFn - - const topParentIndex = programBody.indexOf(topParent.node as any) - - // Determine the reference name for the function - let referenceName = makeIdentifierSafe(functionName) - - // Crawl the scope to refresh all the bindings - programPath.scope.crawl() - - // If we find this referece in the scope, we need to make it unique - while (programPath.scope.hasBinding(referenceName)) { - const [realReferenceName, count] = referenceName.split(/_(\d+)$/) - referenceName = realReferenceName + `_${Number(count || '0') + 1}` - } - - // if (referenceCounts.get(referenceName) === 0) { - - // referenceName += `_${(referenceCounts.get(referenceName) || 0) + 1}` - - // If the reference name came from the function declaration, - // // We need to update the function name to match the reference name - // if (babel.types.isFunctionDeclaration(directiveFn.node)) { - // console.log('updating function name', directiveFn.node.id!.name) - // directiveFn.node.id!.name = referenceName - // } - - // 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()) { - // Then place the function at the top level - programBody.splice( - topParentIndex, - 0, - babel.types.variableDeclaration('const', [ - babel.types.variableDeclarator( - babel.types.identifier(referenceName), - babel.types.toExpression(directiveFn.node as any), - ), - ]), - ) - - // If it's an exported named function, we need to swap it with an - // export const originalFunctionName = referenceName - if ( - babel.types.isExportNamedDeclaration(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(referenceName), - ), - ]), - ), - ) - - directiveFn.remove() - } else { - directiveFn.replaceWith(babel.types.identifier(referenceName)) - } - - directiveFn = programPath.get( - `body.${topParentIndex}.declarations.0.init`, - ) as SupportedFunctionPath - } - - const functionId = makeFileLocationUrlSafe( - `${opts.filename.replace( - path.extname(opts.filename), - '', - )}--${functionName}`.replace(opts.root, ''), - ) - - const [filename, searchParamsStr] = opts.filename.split('?') - const searchParams = new URLSearchParams(searchParamsStr) - searchParams.set(opts.directiveSplitParam, functionName) - const splitFilename = `${filename}?${searchParams.toString()}` - - // If a replacer is provided, replace the function with the replacer - if (opts.replacer) { - const replacer = opts.replacer({ - fn: '$$fn$$', - splitImportFn: '$$splitImportFn$$', - // splitFilename, - filename: filename!, - functionId: functionId, - isSplitFn: functionName === opts.splitFunctionName, - }) - - const replacement = babel.template.expression(replacer, { - placeholderPattern: false, - placeholderWhitelist: new Set(['$$fn$$', '$$splitImportFn$$']), - })({ - ...(replacer.includes('$$fn$$') - ? { $$fn$$: babel.types.toExpression(directiveFn.node) } - : {}), - ...(replacer.includes('$$splitImportFn$$') - ? { - $$splitImportFn$$: - process.env.NODE_ENV === 'production' - ? `(...args) => import(${JSON.stringify(splitFilename)}).then(module => module.default(...args))` - : `(...args) => ${opts.devSplitImporter}(${JSON.stringify(splitFilename)}).then(module => module.default(...args))`, - } - : {}), - }) - - directiveFn.replaceWith(replacement) - } - - // Finally register the directive to - // our map of directives - directiveFnsById[functionId] = { - nodePath: directiveFn, - referenceName, - functionName: functionName || '', - functionId: functionId, - splitFilename, - filename: opts.filename, - chunkName: fileNameToChunkName(opts.root, splitFilename), - } - } -} - -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) => { - const programBody = ast.program.body - - const removeExport = ( - path: - | babel.NodePath - | babel.NodePath, - ) => { - // If the value is a function declaration, class declaration, or variable declaration, - // That means it has a name and can remain in the file, just unexported. - if ( - babel.types.isFunctionDeclaration(path.node.declaration) || - babel.types.isClassDeclaration(path.node.declaration) || - babel.types.isVariableDeclaration(path.node.declaration) - ) { - // If the value is a function declaration, class declaration, or variable declaration, - // That means it has a name and can remain in the file, just unexported. - if ( - babel.types.isFunctionDeclaration(path.node.declaration) || - babel.types.isClassDeclaration(path.node.declaration) || - babel.types.isVariableDeclaration(path.node.declaration) - ) { - // Move the declaration to the top level at the same index - const insertIndex = programBody.findIndex( - (node) => node === path.node.declaration, - ) - programBody.splice(insertIndex, 0, path.node.declaration as any) - } - } - - // Otherwise, remove the export declaration - path.remove() - } - - // Before we add our export, remove any other exports. - // Don't remove the thing they export, just the export declaration - babel.traverse(ast, { - ExportDefaultDeclaration(path) { - removeExport(path) - }, - ExportNamedDeclaration(path) { - removeExport(path) - }, - }) -} - -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/server-functions-plugin/src/index.ts b/packages/server-functions-plugin/src/index.ts index 13674c2cae..c26ec19b4d 100644 --- a/packages/server-functions-plugin/src/index.ts +++ b/packages/server-functions-plugin/src/index.ts @@ -11,6 +11,11 @@ export type CreateServerRpcFn = ( splitImportFn: string, ) => any +declare global { + // eslint-disable-next-line no-var + var TSR_directiveFnsById: Record +} + export function createTanStackServerFnPlugin(_opts?: {}): { client: Array ssr: Array @@ -19,7 +24,15 @@ export function createTanStackServerFnPlugin(_opts?: {}): { const ROOT = process.cwd() const manifestFilename = 'node_modules/.tanstack-start/server-functions-manifest.json' - const directiveFnsById: Record = {} + + globalThis.TSR_directiveFnsById = {} + + const onDirectiveFnsById = (d: Record) => { + // When directives are compiled, save them so we + // can create a manifest + console.log('onDirectiveFnsById', d) + Object.assign(globalThis.TSR_directiveFnsById, d) + } const directiveFnsByIdToManifest = ( directiveFnsById: Record, @@ -49,9 +62,13 @@ export function createTanStackServerFnPlugin(_opts?: {}): { }, load(id) { if (id === 'tsr:server-fn-manifest') { - return `export default ${JSON.stringify( - directiveFnsByIdToManifest(directiveFnsById), - )}` + if (process.env.NODE_ENV === 'production') { + return `export default ${JSON.stringify( + directiveFnsByIdToManifest(globalThis.TSR_directiveFnsById), + )}` + } + + return `export default globalThis.TSR_directiveFnsById` } return null @@ -70,11 +87,8 @@ export function createTanStackServerFnPlugin(_opts?: {}): { replacer: (opts) => // On the client, all we need is the function ID `createClientRpc(${JSON.stringify(opts.functionId)})`, - onDirectiveFnsById: (d) => { - // When directives are compiled, save them so we - // can create a manifest - Object.assign(directiveFnsById, d) - }, + onDirectiveFnsById, + // devSplitImporter: `(globalThis.app.getRouter('server').internals.devServer.ssrLoadModule)`, }), // Now that we have the directiveFnsById, we need to create a new // virtual module that can be used to import that manifest @@ -88,7 +102,9 @@ export function createTanStackServerFnPlugin(_opts?: {}): { mkdirSync(path.dirname(manifestFilename), { recursive: true }) writeFileSync( path.join(ROOT, manifestFilename), - JSON.stringify(directiveFnsByIdToManifest(directiveFnsById)), + JSON.stringify( + directiveFnsByIdToManifest(globalThis.TSR_directiveFnsById), + ), ) }, }, @@ -106,11 +122,8 @@ export function createTanStackServerFnPlugin(_opts?: {}): { // is split into a worker. Similar to the client, we'll use the ID // to call into the worker using a local http event. `createSsrRpc(${JSON.stringify(opts.functionId)})`, - onDirectiveFnsById: (d) => { - // When directives are compiled, save them so we - // can create a manifest - Object.assign(directiveFnsById, d) - }, + onDirectiveFnsById, + // devSplitImporter: `(globalThis.app.getRouter('server').internals.devServer.ssrLoadModule)`, }), ], server: [ @@ -129,14 +142,12 @@ export function createTanStackServerFnPlugin(_opts?: {}): { // to create a new chunk/entry for the server function and also // replace other function references to it with the import statement `createServerRpc(${JSON.stringify(opts.functionId)}, ${opts.isSplitFn ? opts.fn : opts.splitImportFn})`, - onDirectiveFnsById: (d) => { - // When directives are compiled, save them so we - // can create a manifest - Object.assign(directiveFnsById, d) - }, + onDirectiveFnsById, + // devSplitImporter: `(globalThis.app.getRouter('server').internals.devServer.ssrLoadModule)`, }), (() => { let serverFunctionsManifest: Record + return { name: 'tanstack-start-server-fn-vite-plugin-build', enforce: 'post', diff --git a/packages/server-functions-plugin/src/logger.ts b/packages/server-functions-plugin/src/logger.ts deleted file mode 100644 index fd777080c3..0000000000 --- a/packages/server-functions-plugin/src/logger.ts +++ /dev/null @@ -1,59 +0,0 @@ -import chalk from 'chalk' -import { diffWords } from 'diff' - -export function logDiff(oldStr: string, newStr: string) { - const differences = diffWords(oldStr, newStr) - - let output = '' - let unchangedLines = '' - - function processUnchangedLines(lines: string): string { - const lineArray = lines.split('\n') - if (lineArray.length > 4) { - return [ - chalk.dim(lineArray[0]), - chalk.dim(lineArray[1]), - '', - chalk.dim.bold(`... (${lineArray.length - 4} lines) ...`), - '', - chalk.dim(lineArray[lineArray.length - 2]), - chalk.dim(lineArray[lineArray.length - 1]), - ].join('\n') - } - return chalk.dim(lines) - } - - differences.forEach((part, index) => { - const nextPart = differences[index + 1] - - if (part.added) { - if (unchangedLines) { - output += processUnchangedLines(unchangedLines) - unchangedLines = '' - } - output += chalk.green.bold(part.value) - if (nextPart?.removed) output += ' ' - } else if (part.removed) { - if (unchangedLines) { - output += processUnchangedLines(unchangedLines) - unchangedLines = '' - } - output += chalk.red.bold(part.value) - if (nextPart?.added) output += ' ' - } else { - unchangedLines += part.value - } - }) - - // Process any remaining unchanged lines at the end - if (unchangedLines) { - output += processUnchangedLines(unchangedLines) - } - - if (output) { - console.log('\nDiff:') - console.log(output) - } else { - console.log('No changes') - } -} diff --git a/packages/start/src/server-handler/index.tsx b/packages/start/src/server-handler/index.tsx index cf4a2cccd3..7555b1d28f 100644 --- a/packages/start/src/server-handler/index.tsx +++ b/packages/start/src/server-handler/index.tsx @@ -50,23 +50,28 @@ export async function handleServerRequest(request: Request, _event?: H3Event) { payload?: any } - invariant(typeof serverFnId === 'string', 'Invalid server action') + if (typeof serverFnId !== 'string') { + throw new Error('Invalid server action param for serverFnId: ' + serverFnId) + } const serverFnInfo = serverFnManifest[serverFnId] - invariant(serverFnInfo, 'Server function not found') + if (!serverFnInfo) { + console.log('serverFnManifest', serverFnManifest) + throw new Error('Server function info not found for ' + serverFnId) + } if (process.env.NODE_ENV === 'development') console.info(`\nServerFn Request: ${serverFnId}`) - let action: Function + let action: Function | undefined // In dev, we (for now) use Vinxi to get the "server" server-side router // Then we use that router's devServer.ssrLoadModule to get the serverFn if (process.env.NODE_ENV === 'development') { - action = (await (globalThis as any).app + action = await (globalThis as any).app .getRouter('server') .internals.devServer.ssrLoadModule(serverFnInfo.splitFilename) - .then((d: any) => d.default)) as Function + .then((d: any) => d.default) } else { // In prod, we use the serverFn's chunkName to get the serverFn const router = (globalThis as any).app.getRouter('server') @@ -76,10 +81,15 @@ export async function handleServerRequest(request: Request, _event?: H3Event) { serverFnInfo.chunkName + '.mjs', ) const url = pathToFileURL(filePath).toString() - action = (await import(url).then((d) => d.default)) as Function + action = (await import(/* @vite-ignore */ url).then( + (d) => d.default, + )) as Function } - invariant(action, 'Server function not found') + if (!action) { + console.log('serverFnManifest', serverFnManifest) + throw new Error('Server function fn not resolved for ' + serverFnId) + } const response = await (async () => { try { From 83a1bc8d1216f3bd86e095aa5e4ad716edf07c7c Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Tue, 7 Jan 2025 16:45:41 -0700 Subject: [PATCH 34/38] fix: simpler config, better logging, more reliable plugin coordination --- .nvmrc | 2 +- packages/router-plugin/package.json | 17 +- .../src/core/code-splitter/compilers.ts | 64 ++--- .../src/core/router-code-splitter-plugin.ts | 31 +-- packages/router-plugin/src/logger.ts | 59 +++++ packages/server-functions-plugin/src/index.ts | 2 - packages/start-vite-plugin/package.json | 17 +- packages/start-vite-plugin/src/index.ts | 31 +-- packages/start-vite-plugin/src/logger.ts | 59 +++++ packages/start/src/client/createServerFn.ts | 13 +- packages/start/src/config/index.ts | 220 +++++++++--------- pnpm-lock.yaml | 18 ++ 12 files changed, 326 insertions(+), 207 deletions(-) create mode 100644 packages/router-plugin/src/logger.ts create mode 100644 packages/start-vite-plugin/src/logger.ts diff --git a/.nvmrc b/.nvmrc index b8e593f521..3516580bbb 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.15.1 +20.17.0 diff --git a/packages/router-plugin/package.json b/packages/router-plugin/package.json index 1bed491454..dbc2564fa6 100644 --- a/packages/router-plugin/package.json +++ b/packages/router-plugin/package.json @@ -103,24 +103,27 @@ "node": ">=12" }, "dependencies": { + "@babel/core": "^7.26.0", + "@babel/generator": "^7.26.3", + "@babel/parser": "^7.26.3", "@babel/plugin-syntax-jsx": "^7.25.9", + "@babel/plugin-syntax-typescript": "^7.25.9", "@babel/template": "^7.25.9", + "@babel/traverse": "^7.26.4", + "@babel/types": "^7.26.3", "@tanstack/router-generator": "workspace:^", "@tanstack/virtual-file-routes": "workspace:^", "@types/babel__core": "^7.20.5", "@types/babel__generator": "^7.6.8", "@types/babel__template": "^7.4.4", "@types/babel__traverse": "^7.20.6", + "@types/diff": "^6.0.0", "babel-dead-code-elimination": "^1.0.8", + "chalk": "^5.3.0", "chokidar": "^3.6.0", + "diff": "^7.0.0", "unplugin": "^1.16.0", - "zod": "^3.23.8", - "@babel/generator": "^7.26.3", - "@babel/core": "^7.26.0", - "@babel/parser": "^7.26.3", - "@babel/plugin-syntax-typescript": "^7.25.9", - "@babel/traverse": "^7.26.4", - "@babel/types": "^7.26.3" + "zod": "^3.23.8" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", diff --git a/packages/router-plugin/src/core/code-splitter/compilers.ts b/packages/router-plugin/src/core/code-splitter/compilers.ts index ba2485b7d8..d05ce6e2cc 100644 --- a/packages/router-plugin/src/core/code-splitter/compilers.ts +++ b/packages/router-plugin/src/core/code-splitter/compilers.ts @@ -5,6 +5,7 @@ import * as template from '@babel/template' import { deadCodeElimination } from 'babel-dead-code-elimination' import { splitPrefix } from '../constants' +import { logDiff } from '../../logger' import { parseAst } from './ast' import type { ParseAstOptions } from './ast' @@ -37,6 +38,23 @@ interface State { splitModulesById: SplitModulesById } +function addSplitSearchParamToFilename(filename: string) { + const [bareFilename, ...searchParams] = filename.split('?') + const filenameSearchParams = new URLSearchParams(searchParams.join('&')) + filenameSearchParams.set(splitPrefix, '') + return `${bareFilename}?${filenameSearchParams.toString()}` +} + +function removeSplitSearchParamFromFilename(filename: string) { + const [bareFilename, ...searchParams] = filename.split('?') + const filenameSearchParams = new URLSearchParams(searchParams.join('&')) + filenameSearchParams.delete(splitPrefix) + if (filenameSearchParams.size === 0) { + return bareFilename! + } + return `${bareFilename}?${filenameSearchParams.toString()}` +} + export function compileCodeSplitReferenceRoute(opts: ParseAstOptions) { const ast = parseAst(opts) @@ -45,7 +63,11 @@ export function compileCodeSplitReferenceRoute(opts: ParseAstOptions) { enter(programPath, programState) { const state = programState as unknown as State - const splitUrl = `${splitPrefix}:${opts.filename}?${splitPrefix}` + // We need to extract the existing search params from the filename, if any + // and add the splitPrefix to them, then write them back to the filename + const splitUrl = `${splitPrefix}:${addSplitSearchParamToFilename( + opts.filename, + )}` /** * If the component for the route is being imported from @@ -251,21 +273,16 @@ export function compileCodeSplitReferenceRoute(opts: ParseAstOptions) { }, }) - if (debug) console.info('') - if (debug) console.info('Dead Code Elimination Input 1:') - if (debug) console.info(generate(ast, { sourceMaps: true }).code) - if (debug) console.info('') - if (debug) console.info('') - if (debug) console.info('') + const beforeDCE = debug ? generate(ast, { sourceMaps: true }).code : '' deadCodeElimination(ast) - if (debug) console.info('') - if (debug) console.info('Dead Code Elimination Output 1:') - if (debug) console.info(generate(ast, { sourceMaps: true }).code) - if (debug) console.info('') - if (debug) console.info('') - if (debug) console.info('') + const afterDCE = debug ? generate(ast, { sourceMaps: true }).code : '' + + if (debug) { + console.info('Code Splitting DCE Input/Output') + logDiff(beforeDCE, afterDCE) + } return generate(ast, { sourceMaps: true, @@ -491,7 +508,7 @@ export function compileCodeSplitVirtualRoute(opts: ParseAstOptions) { ), ), t.stringLiteral( - opts.filename.split(`?${splitPrefix}`)[0] as string, + removeSplitSearchParamFromFilename(opts.filename), ), ), ) @@ -503,21 +520,16 @@ export function compileCodeSplitVirtualRoute(opts: ParseAstOptions) { }, }) - if (debug) console.info('') - if (debug) console.info('Dead Code Elimination Input 2:') - if (debug) console.info(generate(ast, { sourceMaps: true }).code) - if (debug) console.info('') - if (debug) console.info('') - if (debug) console.info('') + const beforeDCE = debug ? generate(ast, { sourceMaps: true }).code : '' deadCodeElimination(ast) - if (debug) console.info('') - if (debug) console.info('Dead Code Elimination Output 2:') - if (debug) console.info(generate(ast, { sourceMaps: true }).code) - if (debug) console.info('') - if (debug) console.info('') - if (debug) console.info('') + const afterDCE = debug ? generate(ast, { sourceMaps: true }).code : '' + + if (debug) { + console.info('Code Splitting DCE Input/Output') + logDiff(beforeDCE, afterDCE) + } // if there are exported identifiers, then we need to add a warning // to the file to let the user know that the exported identifiers diff --git a/packages/router-plugin/src/core/router-code-splitter-plugin.ts b/packages/router-plugin/src/core/router-code-splitter-plugin.ts index 1437cb2e7c..1727386d5a 100644 --- a/packages/router-plugin/src/core/router-code-splitter-plugin.ts +++ b/packages/router-plugin/src/core/router-code-splitter-plugin.ts @@ -1,6 +1,7 @@ import { isAbsolute, join, normalize } from 'node:path' import { fileURLToPath, pathToFileURL } from 'node:url' +import { logDiff } from '../logger' import { getConfig } from './config' import { compileCodeSplitReferenceRoute, @@ -70,27 +71,16 @@ export const unpluginRouterCodeSplitterFactory: UnpluginFactory< const handleSplittingFile = (code: string, id: string) => { if (debug) console.info('Splitting route: ', id) - if (debug) console.info('') - if (debug) console.info('Split Route Input: ', id) - if (debug) console.info('') - if (debug) console.info(code) - if (debug) console.info('') - if (debug) console.info('') - if (debug) console.info('') - const compiledVirtualRoute = compileCodeSplitVirtualRoute({ code, root: ROOT, filename: id, }) - if (debug) console.info('') - if (debug) console.info('Split Route Output: ', id) - if (debug) console.info('') - if (debug) console.info(compiledVirtualRoute.code) - if (debug) console.info('') - if (debug) console.info('') - if (debug) console.info('') + if (debug) { + console.info('Code Splitting Input/Output: ', id) + logDiff(code, compiledVirtualRoute.code) + } return compiledVirtualRoute } @@ -104,13 +94,10 @@ export const unpluginRouterCodeSplitterFactory: UnpluginFactory< filename: id, }) - if (debug) console.info('') - if (debug) console.info('Handling createRoute output: ', id) - if (debug) console.info('') - if (debug) console.info(compiledReferenceRoute.code) - if (debug) console.info('') - if (debug) console.info('') - if (debug) console.info('') + if (debug) { + console.info('Router Compiler Input/Output: ', id) + logDiff(code, compiledReferenceRoute.code) + } return compiledReferenceRoute } diff --git a/packages/router-plugin/src/logger.ts b/packages/router-plugin/src/logger.ts new file mode 100644 index 0000000000..fd777080c3 --- /dev/null +++ b/packages/router-plugin/src/logger.ts @@ -0,0 +1,59 @@ +import chalk from 'chalk' +import { diffWords } from 'diff' + +export function logDiff(oldStr: string, newStr: string) { + const differences = diffWords(oldStr, newStr) + + let output = '' + let unchangedLines = '' + + function processUnchangedLines(lines: string): string { + const lineArray = lines.split('\n') + if (lineArray.length > 4) { + return [ + chalk.dim(lineArray[0]), + chalk.dim(lineArray[1]), + '', + chalk.dim.bold(`... (${lineArray.length - 4} lines) ...`), + '', + chalk.dim(lineArray[lineArray.length - 2]), + chalk.dim(lineArray[lineArray.length - 1]), + ].join('\n') + } + return chalk.dim(lines) + } + + differences.forEach((part, index) => { + const nextPart = differences[index + 1] + + if (part.added) { + if (unchangedLines) { + output += processUnchangedLines(unchangedLines) + unchangedLines = '' + } + output += chalk.green.bold(part.value) + if (nextPart?.removed) output += ' ' + } else if (part.removed) { + if (unchangedLines) { + output += processUnchangedLines(unchangedLines) + unchangedLines = '' + } + output += chalk.red.bold(part.value) + if (nextPart?.added) output += ' ' + } else { + unchangedLines += part.value + } + }) + + // Process any remaining unchanged lines at the end + if (unchangedLines) { + output += processUnchangedLines(unchangedLines) + } + + if (output) { + console.log('\nDiff:') + console.log(output) + } else { + console.log('No changes') + } +} diff --git a/packages/server-functions-plugin/src/index.ts b/packages/server-functions-plugin/src/index.ts index c26ec19b4d..b5f9e36926 100644 --- a/packages/server-functions-plugin/src/index.ts +++ b/packages/server-functions-plugin/src/index.ts @@ -30,7 +30,6 @@ export function createTanStackServerFnPlugin(_opts?: {}): { const onDirectiveFnsById = (d: Record) => { // When directives are compiled, save them so we // can create a manifest - console.log('onDirectiveFnsById', d) Object.assign(globalThis.TSR_directiveFnsById, d) } @@ -190,7 +189,6 @@ export function createTanStackServerFnPlugin(_opts?: {}): { }) as any), ...serverFnEntries, } - console.log(config.build.rollupOptions.input) }, } })(), diff --git a/packages/start-vite-plugin/package.json b/packages/start-vite-plugin/package.json index 8a75f4bb67..cda7d16928 100644 --- a/packages/start-vite-plugin/package.json +++ b/packages/start-vite-plugin/package.json @@ -65,20 +65,23 @@ }, "dependencies": { "@babel/code-frame": "7.26.2", + "@babel/core": "^7.26.0", "@babel/generator": "^7.26.3", + "@babel/parser": "^7.26.3", "@babel/plugin-syntax-jsx": "^7.25.9", + "@babel/plugin-syntax-typescript": "^7.25.9", "@babel/template": "^7.25.9", + "@babel/traverse": "^7.26.4", + "@babel/types": "^7.26.3", + "@types/babel__code-frame": "^7.0.6", "@types/babel__core": "^7.20.5", "@types/babel__generator": "^7.6.8", - "@types/babel__code-frame": "^7.0.6", "@types/babel__template": "^7.4.4", "@types/babel__traverse": "^7.20.6", - "tiny-invariant": "^1.3.3", + "@types/diff": "^6.0.0", "babel-dead-code-elimination": "^1.0.8", - "@babel/core": "^7.26.0", - "@babel/parser": "^7.26.3", - "@babel/plugin-syntax-typescript": "^7.25.9", - "@babel/traverse": "^7.26.4", - "@babel/types": "^7.26.3" + "chalk": "^5.3.0", + "diff": "^7.0.0", + "tiny-invariant": "^1.3.3" } } diff --git a/packages/start-vite-plugin/src/index.ts b/packages/start-vite-plugin/src/index.ts index 1245d7d082..1c60d20997 100644 --- a/packages/start-vite-plugin/src/index.ts +++ b/packages/start-vite-plugin/src/index.ts @@ -2,6 +2,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url' import { compileEliminateDeadCode, compileStartOutput } from './compilers' +import { logDiff } from './logger' import type { Plugin } from 'vite' const debug = Boolean(process.env.TSR_VITE_DEBUG) @@ -69,13 +70,10 @@ plugins: [ env: opts.env, }) - if (debug) console.info('') - if (debug) console.info('Compiled createServerFn Output') - if (debug) console.info('') - if (debug) console.info(compiled.code) - if (debug) console.info('') - if (debug) console.info('') - if (debug) console.info('') + if (debug) { + console.info('Start Output Input/Output: ', id) + logDiff(code, compiled.code) + } return compiled }, @@ -101,14 +99,6 @@ export function TanStackStartViteDeadCodeElimination( if (transformFuncs.some((fn) => code.includes(fn))) { if (debug) console.info('Handling dead code elimination: ', id) - if (debug) console.info('') - if (debug) console.info('Dead Code Elimination Input:') - if (debug) console.info('') - if (debug) console.info(code) - if (debug) console.info('') - if (debug) console.info('') - if (debug) console.info('') - const compiled = compileEliminateDeadCode({ code, root: ROOT, @@ -116,13 +106,10 @@ export function TanStackStartViteDeadCodeElimination( env: opts.env, }) - if (debug) console.info('') - if (debug) console.info('Dead Code Elimination Output:') - if (debug) console.info('') - if (debug) console.info(compiled.code) - if (debug) console.info('') - if (debug) console.info('') - if (debug) console.info('') + if (debug) { + console.info('Start DCE Input/Output: ', id) + logDiff(code, compiled.code) + } return compiled } diff --git a/packages/start-vite-plugin/src/logger.ts b/packages/start-vite-plugin/src/logger.ts new file mode 100644 index 0000000000..fd777080c3 --- /dev/null +++ b/packages/start-vite-plugin/src/logger.ts @@ -0,0 +1,59 @@ +import chalk from 'chalk' +import { diffWords } from 'diff' + +export function logDiff(oldStr: string, newStr: string) { + const differences = diffWords(oldStr, newStr) + + let output = '' + let unchangedLines = '' + + function processUnchangedLines(lines: string): string { + const lineArray = lines.split('\n') + if (lineArray.length > 4) { + return [ + chalk.dim(lineArray[0]), + chalk.dim(lineArray[1]), + '', + chalk.dim.bold(`... (${lineArray.length - 4} lines) ...`), + '', + chalk.dim(lineArray[lineArray.length - 2]), + chalk.dim(lineArray[lineArray.length - 1]), + ].join('\n') + } + return chalk.dim(lines) + } + + differences.forEach((part, index) => { + const nextPart = differences[index + 1] + + if (part.added) { + if (unchangedLines) { + output += processUnchangedLines(unchangedLines) + unchangedLines = '' + } + output += chalk.green.bold(part.value) + if (nextPart?.removed) output += ' ' + } else if (part.removed) { + if (unchangedLines) { + output += processUnchangedLines(unchangedLines) + unchangedLines = '' + } + output += chalk.red.bold(part.value) + if (nextPart?.added) output += ' ' + } else { + unchangedLines += part.value + } + }) + + // Process any remaining unchanged lines at the end + if (unchangedLines) { + output += processUnchangedLines(unchangedLines) + } + + if (output) { + console.log('\nDiff:') + console.log(output) + } else { + console.log('No changes') + } +} diff --git a/packages/start/src/client/createServerFn.ts b/packages/start/src/client/createServerFn.ts index 191d3bd84b..4c0b2c4cde 100644 --- a/packages/start/src/client/createServerFn.ts +++ b/packages/start/src/client/createServerFn.ts @@ -1,6 +1,6 @@ -import invariant from 'tiny-invariant' import { defaultTransformer, + invariant, isNotFound, isRedirect, } from '@tanstack/react-router' @@ -229,10 +229,13 @@ export function createServerFn< serverFn, }) - invariant( - extractedFn.url, - `createServerFn must be called with a function that has a 'url' property. Are you using the @tanstack/start-vite-plugin properly?`, - ) + if (!extractedFn.url) { + console.info(extractedFn) + invariant( + false, + `createServerFn must be called with a function that has a 'url' property. Ensure that the @tanstack/start-vite-plugin is ordered **before** the @tanstack/server-functions-plugin.`, + ) + } const resolvedMiddleware = [ ...(resolvedOptions.middleware || []), diff --git a/packages/start/src/config/index.ts b/packages/start/src/config/index.ts index 6572f430f1..cdd4b67642 100644 --- a/packages/start/src/config/index.ts +++ b/packages/start/src/config/index.ts @@ -5,10 +5,7 @@ import { fileURLToPath } from 'node:url' import viteReact from '@vitejs/plugin-react' import { resolve } from 'import-meta-resolve' import { TanStackRouterVite } from '@tanstack/router-plugin/vite' -import { - TanStackStartViteDeadCodeElimination, - TanStackStartVitePlugin, -} from '@tanstack/start-vite-plugin' +import { TanStackStartVitePlugin } from '@tanstack/start-vite-plugin' import { getConfig } from '@tanstack/router-generator' import { createApp } from 'vinxi' import { config } from 'vinxi/plugins/config' @@ -28,10 +25,7 @@ import type { TanStackStartInputConfig, TanStackStartOutputConfig, } from './schema.js' -import type { - App as VinxiApp, - RouterSchemaInput as VinxiRouterSchemaInput, -} from 'vinxi' +import type { App as VinxiApp } from 'vinxi' import type { Manifest } from '@tanstack/react-router' import type * as vite from 'vite' @@ -40,8 +34,6 @@ export type { TanStackStartOutputConfig, } from './schema.js' -type RouterType = 'client' | 'server' | 'ssr' | 'api' - function setTsrDefaults(config: TanStackStartOutputConfig['tsr']) { // Normally these are `./src/___`, but we're using `./app/___` for Start stuff const appDirectory = config?.appDirectory ?? './app' @@ -97,10 +89,9 @@ export function defineConfig( configDeploymentPreset || 'node-server', ) const tsr = setTsrDefaults(opts.tsr) - const appDirectory = tsr.appDirectory - const tsrConfig = getConfig(tsr) + const appDirectory = tsr.appDirectory const publicDir = opts.routers?.public?.dir || './public' const publicBase = opts.routers?.public?.base || '/' @@ -120,6 +111,8 @@ export function defineConfig( const apiEntryExists = existsSync(apiEntry) + const viteConfig = getUserViteConfig(opts.vite) + const TanStackServerFnsPlugin = createTanStackServerFnPlugin() let vinxiApp = createApp({ @@ -138,21 +131,21 @@ export function defineConfig( dir: publicDir, base: publicBase, }, - withStartPlugins( - opts, - 'client', - )({ + { name: 'client', type: 'client', target: 'browser', handler: clientEntry, base: clientBase, + // @ts-expect-error build: { sourcemap: true, }, plugins: () => { - const viteConfig = getUserViteConfig(opts.vite) - const clientViteConfig = getUserViteConfig(opts.routers?.client?.vite) + const routerType = 'client' + const clientViteConfig = getUserViteConfig( + opts.routers?.[routerType]?.vite, + ) return [ config('tss-vite-config-client', { @@ -166,28 +159,55 @@ export function defineConfig( ...injectDefineEnv('TSS_SERVER_BASE', serverBase), ...injectDefineEnv('TSS_API_BASE', apiBase), }, + ssr: mergeSsrOptions([ + viteConfig.userConfig.ssr, + clientViteConfig.userConfig.ssr, + { noExternal: ['@tanstack/start', 'tsr:routes-manifest'] }, + ]), + optimizeDeps: { + entries: [], + ...(viteConfig.userConfig.optimizeDeps || {}), + ...(clientViteConfig.userConfig.optimizeDeps || {}), + // include: ['@tanstack/start/server-runtime'], + }, + }), + TanStackStartVitePlugin({ + env: 'client', + }), + TanStackServerFnsPlugin.client, + TanStackRouterVite({ + ...tsrConfig, + autoCodeSplitting: true, + experimental: { + ...tsrConfig.experimental, + }, }), ...(viteConfig.plugins || []), ...(clientViteConfig.plugins || []), - TanStackServerFnsPlugin.client, viteReact(opts.react), + // TanStackStartViteDeadCodeElimination({ + // env: router === 'client' ? 'client' : 'server', + // }), // TODO: RSCS - enable this // serverComponents.client(), ] }, - }), - withStartPlugins( - opts, - 'ssr', - )({ + }, + { name: 'ssr', type: 'http', target: 'server', handler: ssrEntry, middleware: ssrMiddleware, + // @ts-expect-error + link: { + client: 'client', + }, plugins: () => { - const viteConfig = getUserViteConfig(opts.vite) - const ssrViteConfig = getUserViteConfig(opts.routers?.ssr?.vite) + const routerType = 'ssr' + const ssrViteConfig = getUserViteConfig( + opts.routers?.[routerType]?.vite, + ) return [ config('tss-vite-config-ssr', { @@ -201,6 +221,31 @@ export function defineConfig( ...injectDefineEnv('TSS_SERVER_BASE', serverBase), ...injectDefineEnv('TSS_API_BASE', apiBase), }, + ssr: mergeSsrOptions([ + viteConfig.userConfig.ssr, + ssrViteConfig.userConfig.ssr, + { + noExternal: ['@tanstack/start', 'tsr:routes-manifest'], + external: ['@vinxi/react-server-dom/client'], + }, + ]), + optimizeDeps: { + entries: [], + ...(viteConfig.userConfig.optimizeDeps || {}), + ...(ssrViteConfig.userConfig.optimizeDeps || {}), + // include: ['@tanstack/start/server-runtime'], + }, + }), + TanStackStartVitePlugin({ + env: 'server', + }), + TanStackServerFnsPlugin.ssr, + TanStackRouterVite({ + ...tsrConfig, + autoCodeSplitting: true, + experimental: { + ...tsrConfig.experimental, + }, }), tsrRoutesManifest({ tsrConfig, @@ -208,22 +253,10 @@ export function defineConfig( }), ...(getUserViteConfig(opts.vite).plugins || []), ...(getUserViteConfig(opts.routers?.ssr?.vite).plugins || []), - TanStackServerFnsPlugin.ssr, - config('start-ssr', { - ssr: { - external: ['@vinxi/react-server-dom/client'], - }, - }), ] }, - link: { - client: 'client', - }, - }), - withStartPlugins( - opts, - 'server', - )({ + }, + { name: 'server', type: 'http', target: 'server', @@ -233,8 +266,10 @@ export function defineConfig( // worker: true, handler: importToProjectRelative('@tanstack/start/server-handler'), plugins: () => { - const viteConfig = getUserViteConfig(opts.vite) - const serverViteConfig = getUserViteConfig(opts.routers?.server?.vite) + const routerType = 'server' + const serverViteConfig = getUserViteConfig( + opts.routers?.[routerType]?.vite, + ) return [ config('tss-vite-config-ssr', { @@ -248,8 +283,29 @@ export function defineConfig( ...injectDefineEnv('TSS_SERVER_BASE', serverBase), ...injectDefineEnv('TSS_API_BASE', apiBase), }, + ssr: mergeSsrOptions([ + viteConfig.userConfig.ssr, + serverViteConfig.userConfig.ssr, + { noExternal: ['@tanstack/start', 'tsr:routes-manifest'] }, + ]), + optimizeDeps: { + entries: [], + ...(viteConfig.userConfig.optimizeDeps || {}), + ...(serverViteConfig.userConfig.optimizeDeps || {}), + // include: ['@tanstack/start/server-runtime'], + }, + }), + TanStackStartVitePlugin({ + env: 'server', }), TanStackServerFnsPlugin.server, + TanStackRouterVite({ + ...tsrConfig, + autoCodeSplitting: true, + experimental: { + ...tsrConfig.experimental, + }, + }), // TODO: RSCS - remove this // resolve: { // conditions: [], @@ -267,11 +323,14 @@ export function defineConfig( // runtime: '@vinxi/react-server-dom/runtime', // transpileDeps: ['react', 'react-dom', '@vinxi/react-server-dom'], // }), + // TanStackStartViteDeadCodeElimination({ + // env: router === 'client' ? 'client' : 'server', + // }), ...(viteConfig.plugins || []), ...(serverViteConfig.plugins || []), ] }, - }), + }, ], }) @@ -319,6 +378,9 @@ export function defineConfig( ...tsrConfig.experimental, }, }), + // TanStackStartViteDeadCodeElimination({ + // env: router === 'client' ? 'client' : 'server', + // }), ...(viteConfig.plugins || []), ...(apiViteConfig.plugins || []), ] @@ -329,78 +391,6 @@ export function defineConfig( return vinxiApp } -type TempRouter = Extract< - VinxiRouterSchemaInput, - { - type: 'client' | 'http' - } -> & { - base?: string - link?: { - client: string - } - runtime?: string - build?: { - sourcemap?: boolean - } -} - -function withPlugins(prePlugins: Array, postPlugins?: Array) { - return (router: TempRouter) => { - return { - ...router, - plugins: async () => [ - ...prePlugins, - ...((await router.plugins?.()) ?? []), - ...(postPlugins ?? []), - ], - } - } -} - -function withStartPlugins(opts: TanStackStartOutputConfig, router: RouterType) { - const tsrConfig = getConfig(setTsrDefaults(opts.tsr)) - const { userConfig } = getUserViteConfig(opts.vite) - const { userConfig: routerUserConfig } = getUserViteConfig( - opts.routers?.[router]?.vite, - ) - - return withPlugins( - [ - config('start-vite', { - ...userConfig, - ...routerUserConfig, - ssr: mergeSsrOptions([ - userConfig.ssr, - routerUserConfig.ssr, - { noExternal: ['@tanstack/start', 'tsr:routes-manifest'] }, - ]), - optimizeDeps: { - entries: [], - ...(userConfig.optimizeDeps || {}), - ...(routerUserConfig.optimizeDeps || {}), - // include: ['@tanstack/start/server-runtime'], - }, - }), - TanStackRouterVite({ - ...tsrConfig, - autoCodeSplitting: true, - experimental: { - ...tsrConfig.experimental, - }, - }), - TanStackStartVitePlugin({ - env: router === 'server' ? 'server' : 'client', - }), - ], - [ - TanStackStartViteDeadCodeElimination({ - env: router === 'client' ? 'client' : 'server', - }), - ], - ) -} - // function resolveRelativePath(p: string) { // return path.relative( // process.cwd(), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 11251e2a3b..ba751a3727 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3629,12 +3629,21 @@ importers: '@types/babel__traverse': specifier: ^7.20.6 version: 7.20.6 + '@types/diff': + specifier: ^6.0.0 + version: 6.0.0 babel-dead-code-elimination: specifier: ^1.0.8 version: 1.0.8 + chalk: + specifier: ^5.3.0 + version: 5.3.0 chokidar: specifier: ^3.6.0 version: 3.6.0 + diff: + specifier: ^7.0.0 + version: 7.0.0 unplugin: specifier: ^1.16.0 version: 1.16.0 @@ -3837,9 +3846,18 @@ importers: '@types/babel__traverse': specifier: ^7.20.6 version: 7.20.6 + '@types/diff': + specifier: ^6.0.0 + version: 6.0.0 babel-dead-code-elimination: specifier: ^1.0.8 version: 1.0.8 + chalk: + specifier: ^5.3.0 + version: 5.3.0 + diff: + specifier: ^7.0.0 + version: 7.0.0 tiny-invariant: specifier: ^1.3.3 version: 1.3.3 From 71037fb695997fcecb97a1ad732667ce906bbdc6 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Tue, 7 Jan 2025 16:51:53 -0700 Subject: [PATCH 35/38] fix: update test snapshots --- .../tests/code-splitter/snapshots/arrow-function.tsx | 4 ++-- .../snapshots/destructured-react-memo-imported-component.tsx | 4 ++-- .../tests/code-splitter/snapshots/function-declaration.tsx | 4 ++-- .../imported-default-component-destructured-loader.tsx | 4 ++-- .../code-splitter/snapshots/imported-default-component.tsx | 2 +- .../router-plugin/tests/code-splitter/snapshots/imported.tsx | 4 ++-- .../router-plugin/tests/code-splitter/snapshots/inline.tsx | 2 +- .../tests/code-splitter/snapshots/random-number.tsx | 4 ++-- .../tests/code-splitter/snapshots/react-memo-component.tsx | 4 ++-- .../code-splitter/snapshots/react-memo-imported-component.tsx | 4 ++-- .../tests/code-splitter/snapshots/retain-export-component.tsx | 2 +- .../tests/code-splitter/snapshots/retain-exports-loader.tsx | 2 +- .../tests/code-splitter/snapshots/useStateDestructure.tsx | 2 +- 13 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/router-plugin/tests/code-splitter/snapshots/arrow-function.tsx b/packages/router-plugin/tests/code-splitter/snapshots/arrow-function.tsx index e0859d59e7..12220c333f 100644 --- a/packages/router-plugin/tests/code-splitter/snapshots/arrow-function.tsx +++ b/packages/router-plugin/tests/code-splitter/snapshots/arrow-function.tsx @@ -1,6 +1,6 @@ -const $$splitComponentImporter = () => import('tsr-split:arrow-function.tsx?tsr-split'); +const $$splitComponentImporter = () => import('tsr-split:arrow-function.tsx?tsr-split='); import { lazyRouteComponent } from '@tanstack/react-router'; -const $$splitLoaderImporter = () => import('tsr-split:arrow-function.tsx?tsr-split'); +const $$splitLoaderImporter = () => import('tsr-split:arrow-function.tsx?tsr-split='); import { lazyFn } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/posts')({ diff --git a/packages/router-plugin/tests/code-splitter/snapshots/destructured-react-memo-imported-component.tsx b/packages/router-plugin/tests/code-splitter/snapshots/destructured-react-memo-imported-component.tsx index 44ed2c32f5..a2e254d85f 100644 --- a/packages/router-plugin/tests/code-splitter/snapshots/destructured-react-memo-imported-component.tsx +++ b/packages/router-plugin/tests/code-splitter/snapshots/destructured-react-memo-imported-component.tsx @@ -1,6 +1,6 @@ -const $$splitLoaderImporter = () => import('tsr-split:destructured-react-memo-imported-component.tsx?tsr-split'); +const $$splitLoaderImporter = () => import('tsr-split:destructured-react-memo-imported-component.tsx?tsr-split='); import { lazyFn } from '@tanstack/react-router'; -const $$splitComponentImporter = () => import('tsr-split:destructured-react-memo-imported-component.tsx?tsr-split'); +const $$splitComponentImporter = () => import('tsr-split:destructured-react-memo-imported-component.tsx?tsr-split='); import { lazyRouteComponent } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/')({ diff --git a/packages/router-plugin/tests/code-splitter/snapshots/function-declaration.tsx b/packages/router-plugin/tests/code-splitter/snapshots/function-declaration.tsx index bad40895fc..8b37f2652d 100644 --- a/packages/router-plugin/tests/code-splitter/snapshots/function-declaration.tsx +++ b/packages/router-plugin/tests/code-splitter/snapshots/function-declaration.tsx @@ -1,6 +1,6 @@ -const $$splitComponentImporter = () => import('tsr-split:function-declaration.tsx?tsr-split'); +const $$splitComponentImporter = () => import('tsr-split:function-declaration.tsx?tsr-split='); import { lazyRouteComponent } from '@tanstack/react-router'; -const $$splitLoaderImporter = () => import('tsr-split:function-declaration.tsx?tsr-split'); +const $$splitLoaderImporter = () => import('tsr-split:function-declaration.tsx?tsr-split='); import { lazyFn } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/posts')({ diff --git a/packages/router-plugin/tests/code-splitter/snapshots/imported-default-component-destructured-loader.tsx b/packages/router-plugin/tests/code-splitter/snapshots/imported-default-component-destructured-loader.tsx index 57b8ab4e04..48a372237e 100644 --- a/packages/router-plugin/tests/code-splitter/snapshots/imported-default-component-destructured-loader.tsx +++ b/packages/router-plugin/tests/code-splitter/snapshots/imported-default-component-destructured-loader.tsx @@ -1,6 +1,6 @@ -const $$splitLoaderImporter = () => import('tsr-split:imported-default-component-destructured-loader.tsx?tsr-split'); +const $$splitLoaderImporter = () => import('tsr-split:imported-default-component-destructured-loader.tsx?tsr-split='); import { lazyFn } from '@tanstack/react-router'; -const $$splitComponentImporter = () => import('tsr-split:imported-default-component-destructured-loader.tsx?tsr-split'); +const $$splitComponentImporter = () => import('tsr-split:imported-default-component-destructured-loader.tsx?tsr-split='); import { lazyRouteComponent } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/')({ diff --git a/packages/router-plugin/tests/code-splitter/snapshots/imported-default-component.tsx b/packages/router-plugin/tests/code-splitter/snapshots/imported-default-component.tsx index f37448ed3f..67aeb9895a 100644 --- a/packages/router-plugin/tests/code-splitter/snapshots/imported-default-component.tsx +++ b/packages/router-plugin/tests/code-splitter/snapshots/imported-default-component.tsx @@ -1,4 +1,4 @@ -const $$splitComponentImporter = () => import('tsr-split:imported-default-component.tsx?tsr-split'); +const $$splitComponentImporter = () => import('tsr-split:imported-default-component.tsx?tsr-split='); import { lazyRouteComponent } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/')({ diff --git a/packages/router-plugin/tests/code-splitter/snapshots/imported.tsx b/packages/router-plugin/tests/code-splitter/snapshots/imported.tsx index 693dac9743..090d0ed379 100644 --- a/packages/router-plugin/tests/code-splitter/snapshots/imported.tsx +++ b/packages/router-plugin/tests/code-splitter/snapshots/imported.tsx @@ -1,6 +1,6 @@ -const $$splitLoaderImporter = () => import('tsr-split:imported.tsx?tsr-split'); +const $$splitLoaderImporter = () => import('tsr-split:imported.tsx?tsr-split='); import { lazyFn } from '@tanstack/react-router'; -const $$splitComponentImporter = () => import('tsr-split:imported.tsx?tsr-split'); +const $$splitComponentImporter = () => import('tsr-split:imported.tsx?tsr-split='); import { lazyRouteComponent } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/')({ diff --git a/packages/router-plugin/tests/code-splitter/snapshots/inline.tsx b/packages/router-plugin/tests/code-splitter/snapshots/inline.tsx index 0ff78ef67b..e8e6358e48 100644 --- a/packages/router-plugin/tests/code-splitter/snapshots/inline.tsx +++ b/packages/router-plugin/tests/code-splitter/snapshots/inline.tsx @@ -1,4 +1,4 @@ -const $$splitComponentImporter = () => import('tsr-split:inline.tsx?tsr-split'); +const $$splitComponentImporter = () => import('tsr-split:inline.tsx?tsr-split='); import { lazyRouteComponent } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/')({ diff --git a/packages/router-plugin/tests/code-splitter/snapshots/random-number.tsx b/packages/router-plugin/tests/code-splitter/snapshots/random-number.tsx index dbe8c7d6fe..81c38bf456 100644 --- a/packages/router-plugin/tests/code-splitter/snapshots/random-number.tsx +++ b/packages/router-plugin/tests/code-splitter/snapshots/random-number.tsx @@ -1,6 +1,6 @@ -const $$splitComponentImporter = () => import('tsr-split:random-number.tsx?tsr-split'); +const $$splitComponentImporter = () => import('tsr-split:random-number.tsx?tsr-split='); import { lazyRouteComponent } from '@tanstack/react-router'; -const $$splitLoaderImporter = () => import('tsr-split:random-number.tsx?tsr-split'); +const $$splitLoaderImporter = () => import('tsr-split:random-number.tsx?tsr-split='); import { lazyFn } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router'; export const textColors = [`text-rose-500`, `text-yellow-500`, `text-teal-500`, `text-blue-500`]; diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react-memo-component.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react-memo-component.tsx index 58a2892393..acf7d2ce44 100644 --- a/packages/router-plugin/tests/code-splitter/snapshots/react-memo-component.tsx +++ b/packages/router-plugin/tests/code-splitter/snapshots/react-memo-component.tsx @@ -1,6 +1,6 @@ -const $$splitLoaderImporter = () => import('tsr-split:react-memo-component.tsx?tsr-split'); +const $$splitLoaderImporter = () => import('tsr-split:react-memo-component.tsx?tsr-split='); import { lazyFn } from '@tanstack/react-router'; -const $$splitComponentImporter = () => import('tsr-split:react-memo-component.tsx?tsr-split'); +const $$splitComponentImporter = () => import('tsr-split:react-memo-component.tsx?tsr-split='); import { lazyRouteComponent } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/')({ diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react-memo-imported-component.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react-memo-imported-component.tsx index 6ac9b1df7e..d44968d06d 100644 --- a/packages/router-plugin/tests/code-splitter/snapshots/react-memo-imported-component.tsx +++ b/packages/router-plugin/tests/code-splitter/snapshots/react-memo-imported-component.tsx @@ -1,6 +1,6 @@ -const $$splitLoaderImporter = () => import('tsr-split:react-memo-imported-component.tsx?tsr-split'); +const $$splitLoaderImporter = () => import('tsr-split:react-memo-imported-component.tsx?tsr-split='); import { lazyFn } from '@tanstack/react-router'; -const $$splitComponentImporter = () => import('tsr-split:react-memo-imported-component.tsx?tsr-split'); +const $$splitComponentImporter = () => import('tsr-split:react-memo-imported-component.tsx?tsr-split='); import { lazyRouteComponent } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/')({ diff --git a/packages/router-plugin/tests/code-splitter/snapshots/retain-export-component.tsx b/packages/router-plugin/tests/code-splitter/snapshots/retain-export-component.tsx index 886cad2399..d7634fbfc2 100644 --- a/packages/router-plugin/tests/code-splitter/snapshots/retain-export-component.tsx +++ b/packages/router-plugin/tests/code-splitter/snapshots/retain-export-component.tsx @@ -1,4 +1,4 @@ -const $$splitLoaderImporter = () => import('tsr-split:retain-export-component.tsx?tsr-split'); +const $$splitLoaderImporter = () => import('tsr-split:retain-export-component.tsx?tsr-split='); import { lazyFn } from '@tanstack/react-router'; import { createFileRoute, Outlet } from '@tanstack/react-router'; import { importedComponent as ImportedComponent } from '../shared/imported'; diff --git a/packages/router-plugin/tests/code-splitter/snapshots/retain-exports-loader.tsx b/packages/router-plugin/tests/code-splitter/snapshots/retain-exports-loader.tsx index fda27416a8..d1ab86e388 100644 --- a/packages/router-plugin/tests/code-splitter/snapshots/retain-exports-loader.tsx +++ b/packages/router-plugin/tests/code-splitter/snapshots/retain-exports-loader.tsx @@ -1,4 +1,4 @@ -const $$splitComponentImporter = () => import('tsr-split:retain-exports-loader.tsx?tsr-split'); +const $$splitComponentImporter = () => import('tsr-split:retain-exports-loader.tsx?tsr-split='); import { lazyRouteComponent } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router'; export function loaderFn() { diff --git a/packages/router-plugin/tests/code-splitter/snapshots/useStateDestructure.tsx b/packages/router-plugin/tests/code-splitter/snapshots/useStateDestructure.tsx index 7229cc62fb..963f6113c5 100644 --- a/packages/router-plugin/tests/code-splitter/snapshots/useStateDestructure.tsx +++ b/packages/router-plugin/tests/code-splitter/snapshots/useStateDestructure.tsx @@ -1,4 +1,4 @@ -const $$splitComponentImporter = () => import('tsr-split:useStateDestructure.tsx?tsr-split'); +const $$splitComponentImporter = () => import('tsr-split:useStateDestructure.tsx?tsr-split='); import { lazyRouteComponent } from '@tanstack/react-router'; import { startProject } from '~/projects/start'; import { createFileRoute } from '@tanstack/react-router'; From bbaf8fdaf9e09debd25abf4b3d61736c8d91292e Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Wed, 8 Jan 2025 15:38:26 -0700 Subject: [PATCH 36/38] fix: non-split directives, clean up compilers, logging, and --- .../src/compilers.ts | 59 ++-- .../directive-functions-plugin/src/index.ts | 3 +- .../tests/compiler.test.ts | 283 +++++------------- .../src/core/code-splitter/compilers.ts | 29 +- .../src/core/router-code-splitter-plugin.ts | 56 +--- .../snapshots/arrow-function.tsx | 4 +- ...ructured-react-memo-imported-component.tsx | 4 +- .../snapshots/function-declaration.tsx | 4 +- ...-default-component-destructured-loader.tsx | 4 +- .../snapshots/imported-default-component.tsx | 2 +- .../code-splitter/snapshots/imported.tsx | 4 +- .../tests/code-splitter/snapshots/inline.tsx | 2 +- .../code-splitter/snapshots/random-number.tsx | 4 +- .../snapshots/react-memo-component.tsx | 4 +- .../react-memo-imported-component.tsx | 4 +- .../snapshots/retain-export-component.tsx | 2 +- .../snapshots/retain-exports-loader.tsx | 2 +- .../snapshots/useStateDestructure.tsx | 2 +- packages/server-functions-plugin/src/index.ts | 6 +- packages/start-vite-plugin/src/compilers.ts | 34 +-- packages/start-vite-plugin/src/index.ts | 44 +-- packages/start/src/config/index.ts | 9 - packages/start/src/server-handler/index.tsx | 28 +- 23 files changed, 178 insertions(+), 415 deletions(-) diff --git a/packages/directive-functions-plugin/src/compilers.ts b/packages/directive-functions-plugin/src/compilers.ts index 01eaefb28a..fb3a176307 100644 --- a/packages/directive-functions-plugin/src/compilers.ts +++ b/packages/directive-functions-plugin/src/compilers.ts @@ -17,7 +17,7 @@ export interface DirectiveFn { functionName: string functionId: string referenceName: string - splitFilename: string + extractedFilename: string filename: string chunkName: string } @@ -32,7 +32,7 @@ export type ReplacerFn = (opts: { splitImportFn: string filename: string functionId: string - isSplitFn: boolean + isSourceFn: boolean }) => string // const debug = process.env.TSR_VITE_DEBUG === 'true' @@ -66,15 +66,15 @@ export function parseAst(opts: ParseAstOptions): ParseResult { } export function compileDirectives(opts: CompileDirectivesOpts) { - const [_, searchParamsStr] = opts.filename.split('?') - const searchParams = new URLSearchParams(searchParamsStr) - const directiveSplitParam = `tsr-directive-${opts.directive.replace(/[^a-zA-Z0-9]/g, '-')}-split` - const functionName = searchParams.get(directiveSplitParam) + const [_, ...searchParamsStr] = opts.filename.split('?') + const searchParams = new URLSearchParams(searchParamsStr.join('&')) + const directiveSplitParam = `tsr-directive-${opts.directive.replace(/[^a-zA-Z0-9]/g, '-')}` + const isDirectiveSplitParam = searchParams.has(directiveSplitParam) const ast = parseAst(opts) const directiveFnsById = findDirectives(ast, { ...opts, - splitFunctionName: functionName, + isSourceFile: isDirectiveSplitParam, directiveSplitParam, }) @@ -82,7 +82,6 @@ export function compileDirectives(opts: CompileDirectivesOpts) { Object.entries(directiveFnsById).map(([, fn]) => [fn.functionName, fn]), ) - // Add runtime code if there are directives // Add runtime code if there are directives if (Object.keys(directiveFnsById).length > 0) { // Add a vite import to the top of the file @@ -101,21 +100,23 @@ export function compileDirectives(opts: CompileDirectivesOpts) { } } - // If there is a functionName, we need to remove all exports - // then make sure that our function is exported under the + // 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 (functionName) { - const directiveFn = directiveFnsByFunctionName[functionName] - - if (!directiveFn) { - throw new Error(`${opts.directiveLabel} ${functionName} not found`) - } - + if (isDirectiveSplitParam) { safeRemoveExports(ast) + // Export a single object with all of the functions + // e.g. export { directiveFn1, directiveFn2 } ast.program.body.push( - babel.types.exportDefaultDeclaration( - babel.types.identifier(directiveFn.referenceName), + babel.types.exportNamedDeclaration( + undefined, + Object.values(directiveFnsByFunctionName).map((fn) => + babel.types.exportSpecifier( + babel.types.identifier(fn.referenceName), + babel.types.identifier(fn.referenceName), + ), + ), ), ) } @@ -245,9 +246,8 @@ export function findDirectives( directive: string directiveLabel: string replacer?: ReplacerFn - splitFunctionName?: string | null + isSourceFile?: boolean directiveSplitParam: string - // devSplitImporter: string }, ) { const directiveFnsById: Record = {} @@ -456,20 +456,19 @@ export function findDirectives( `${opts.filename}--${functionName}`.replace(opts.root, ''), ) - const [filename, searchParamsStr] = opts.filename.split('?') - const searchParams = new URLSearchParams(searchParamsStr) - searchParams.set(opts.directiveSplitParam, functionName) - const splitFilename = `${filename}?${searchParams.toString()}` + const [filename] = opts.filename.split('?') + // const extractedFilename = `${filename}?${opts.directiveSplitParam}=${functionName}` + const extractedFilename = `${filename}?${opts.directiveSplitParam}` // If a replacer is provided, replace the function with the replacer if (opts.replacer) { const replacer = opts.replacer({ fn: '$$fn$$', splitImportFn: '$$splitImportFn$$', - // splitFilename, + // extractedFilename, filename: filename!, functionId: functionId, - isSplitFn: functionName === opts.splitFunctionName, + isSourceFn: !!opts.isSourceFile, }) const replacement = babel.template.expression(replacer, { @@ -481,7 +480,7 @@ export function findDirectives( : {}), ...(replacer.includes('$$splitImportFn$$') ? { - $$splitImportFn$$: `(...args) => import(${JSON.stringify(splitFilename)}).then(module => module.default(...args))`, + $$splitImportFn$$: `(...args) => import(${JSON.stringify(extractedFilename)}).then(module => module.default(...args))`, } : {}), }) @@ -496,9 +495,9 @@ export function findDirectives( referenceName, functionName: functionName || '', functionId: functionId, - splitFilename, + extractedFilename, filename: opts.filename, - chunkName: fileNameToChunkName(opts.root, splitFilename), + chunkName: fileNameToChunkName(opts.root, extractedFilename), } } } diff --git a/packages/directive-functions-plugin/src/index.ts b/packages/directive-functions-plugin/src/index.ts index d1bce43f7d..f972cc8e2e 100644 --- a/packages/directive-functions-plugin/src/index.ts +++ b/packages/directive-functions-plugin/src/index.ts @@ -42,6 +42,8 @@ export function TanStackDirectiveFunctionsPlugin( return null } + if (debug) console.info('Compiling Directives: ', id) + const { compiledResult, directiveFnsById } = compileDirectives({ ...opts, code, @@ -53,7 +55,6 @@ export function TanStackDirectiveFunctionsPlugin( opts.onDirectiveFnsById?.(directiveFnsById) - if (debug) console.info('Directive Input/Output') if (debug) logDiff(code, compiledResult.code) return compiledResult diff --git a/packages/directive-functions-plugin/tests/compiler.test.ts b/packages/directive-functions-plugin/tests/compiler.test.ts index 1ee2f0ee7d..cb590abf65 100644 --- a/packages/directive-functions-plugin/tests/compiler.test.ts +++ b/packages/directive-functions-plugin/tests/compiler.test.ts @@ -45,7 +45,7 @@ const serverConfig: Omit = { // For any other server functions the split function may reference, // we use the splitImportFn which is a dynamic import of the split file. `createServerRpc({ - fn: ${opts.isSplitFn ? opts.fn : opts.splitImportFn}, + fn: ${opts.isSourceFn ? opts.fn : opts.splitImportFn}, filename: ${JSON.stringify(opts.filename)}, functionId: ${JSON.stringify(opts.functionId)}, })`, @@ -127,17 +127,12 @@ describe('server function compilation', () => { code, }) const ssr = compileDirectives({ ...ssrConfig, code }) - const splitFiles = Object.entries(ssr.directiveFnsById) - .map(([_fnId, directiveFn]) => { - return `// ${directiveFn.functionId}\n\n${ - compileDirectives({ - ...serverConfig, - code, - filename: directiveFn.splitFilename, - }).compiledResult.code - }` - }) - .join('\n\n\n') + + 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"; @@ -245,50 +240,30 @@ describe('server function compilation', () => { export { namedExport };" `) - expect(splitFiles).toMatchInlineSnapshot( + expect(server.compiledResult.code).toMatchInlineSnapshot( ` - "// test_ts--namedFunction_wrapper_namedFunction - - import { createServerRpc } from "my-rpc-lib-server"; + "import { createServerRpc } from "my-rpc-lib-server"; const namedFunction_wrapper_namedFunction = createServerRpc({ fn: function namedFunction() { return 'hello'; }, filename: "test.ts", - functionId: "test_ts_tsr-directive-use-server-split_namedFunction_wrapper_namedFunction--namedFunction_wrapper_namedFunction" + functionId: "test_ts_tsr-directive-use-server--namedFunction_wrapper_namedFunction" }); - export default namedFunction_wrapper_namedFunction; - - - // test_ts--arrowFunction_wrapper - - import { createServerRpc } from "my-rpc-lib-server"; const arrowFunction_wrapper = createServerRpc({ fn: () => { return 'hello'; }, filename: "test.ts", - functionId: "test_ts_tsr-directive-use-server-split_arrowFunction_wrapper--arrowFunction_wrapper" + functionId: "test_ts_tsr-directive-use-server--arrowFunction_wrapper" }); - export default arrowFunction_wrapper; - - - // test_ts--anonymousFunction_wrapper - - import { createServerRpc } from "my-rpc-lib-server"; const anonymousFunction_wrapper = createServerRpc({ fn: function () { return 'hello'; }, filename: "test.ts", - functionId: "test_ts_tsr-directive-use-server-split_anonymousFunction_wrapper--anonymousFunction_wrapper" + functionId: "test_ts_tsr-directive-use-server--anonymousFunction_wrapper" }); - export default anonymousFunction_wrapper; - - - // test_ts--multipleDirectives_multipleDirectives - - import { createServerRpc } from "my-rpc-lib-server"; const multipleDirectives_multipleDirectives = createServerRpc({ fn: function multipleDirectives() { 'use strict'; @@ -296,77 +271,47 @@ describe('server function compilation', () => { return 'hello'; }, filename: "test.ts", - functionId: "test_ts_tsr-directive-use-server-split_multipleDirectives_multipleDirectives--multipleDirectives_multipleDirectives" + functionId: "test_ts_tsr-directive-use-server--multipleDirectives_multipleDirectives" }); - export default multipleDirectives_multipleDirectives; - - - // test_ts--iife - - import { createServerRpc } from "my-rpc-lib-server"; const iife_1 = createServerRpc({ fn: function () { return 'hello'; }, filename: "test.ts", - functionId: "test_ts_tsr-directive-use-server-split_iife--iife" + functionId: "test_ts_tsr-directive-use-server--iife" }); - export default iife_1; - - - // test_ts--defaultExportFn - - import { createServerRpc } from "my-rpc-lib-server"; const defaultExportFn_1 = createServerRpc({ fn: function defaultExportFn() { return 'hello'; }, filename: "test.ts", - functionId: "test_ts_tsr-directive-use-server-split_defaultExportFn--defaultExportFn" + functionId: "test_ts_tsr-directive-use-server--defaultExportFn" }); - export default defaultExportFn_1; - - - // test_ts--namedExportFn - - import { createServerRpc } from "my-rpc-lib-server"; const namedExportFn_1 = createServerRpc({ fn: function namedExportFn() { return 'hello'; }, filename: "test.ts", - functionId: "test_ts_tsr-directive-use-server-split_namedExportFn--namedExportFn" + functionId: "test_ts_tsr-directive-use-server--namedExportFn" }); - export default namedExportFn_1; - - - // test_ts--exportedArrowFunction_wrapper - - import { createServerRpc } from "my-rpc-lib-server"; const exportedArrowFunction_wrapper = createServerRpc({ fn: () => { return 'hello'; }, filename: "test.ts", - functionId: "test_ts_tsr-directive-use-server-split_exportedArrowFunction_wrapper--exportedArrowFunction_wrapper" + functionId: "test_ts_tsr-directive-use-server--exportedArrowFunction_wrapper" }); - export default exportedArrowFunction_wrapper; - - - // test_ts--namedExportConst - - import { createServerRpc } from "my-rpc-lib-server"; const namedExportConst_1 = createServerRpc({ fn: () => { return usedFn(); }, filename: "test.ts", - functionId: "test_ts_tsr-directive-use-server-split_namedExportConst--namedExportConst" + functionId: "test_ts_tsr-directive-use-server--namedExportConst" }); function usedFn() { return 'hello'; } - export default namedExportConst_1;" + export { namedFunction_wrapper_namedFunction, arrowFunction_wrapper, anonymousFunction_wrapper, multipleDirectives_multipleDirectives, iife_1, defaultExportFn_1, namedExportFn_1, exportedArrowFunction_wrapper, namedExportConst_1 };" `, ) }) @@ -474,17 +419,14 @@ describe('server function compilation', () => { const client = compileDirectives({ ...clientConfig, code }) const ssr = compileDirectives({ ...ssrConfig, code }) - const splitFiles = Object.entries(ssr.directiveFnsById) - .map(([_fnId, directiveFn]) => { - return `// ${directiveFn.functionId}\n\n${ - compileDirectives({ - ...serverConfig, - code, - filename: directiveFn.splitFilename, - }).compiledResult.code - }` - }) - .join('\n\n\n') + + 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"; @@ -500,10 +442,8 @@ describe('server function compilation', () => { functionId: "test_ts--multiDirective" });" `) - expect(splitFiles).toMatchInlineSnapshot(` - "// test_ts--multiDirective - - import { createServerRpc } from "my-rpc-lib-server"; + expect(server.compiledResult.code).toMatchInlineSnapshot(` + "import { createServerRpc } from "my-rpc-lib-server"; createServerRpc({ fn: function multiDirective() { 'use strict'; @@ -511,9 +451,9 @@ describe('server function compilation', () => { return 'hello'; }, filename: "test.ts", - functionId: "test_ts_tsr-directive-use-server-split_multiDirective--multiDirective" + functionId: "test_ts_tsr-directive-use-server--multiDirective" }); - export default multiDirective_1;" + export { multiDirective_1 };" `) }) @@ -527,17 +467,14 @@ describe('server function compilation', () => { const client = compileDirectives({ ...clientConfig, code }) const ssr = compileDirectives({ ...ssrConfig, code }) - const splitFiles = Object.entries(ssr.directiveFnsById) - .map(([_fnId, directiveFn]) => { - return `// ${directiveFn.functionId}\n\n${ - compileDirectives({ - ...serverConfig, - code, - filename: directiveFn.splitFilename, - }).compiledResult.code - }` - }) - .join('\n\n\n') + + 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"; @@ -557,18 +494,16 @@ describe('server function compilation', () => { export const iife = iife_1();" `) - expect(splitFiles).toMatchInlineSnapshot(` - "// test_ts--iife - - import { createServerRpc } from "my-rpc-lib-server"; + expect(server.compiledResult.code).toMatchInlineSnapshot(` + "import { createServerRpc } from "my-rpc-lib-server"; const iife_1 = createServerRpc({ fn: function () { return 'hello'; }, filename: "test.ts", - functionId: "test_ts_tsr-directive-use-server-split_iife--iife" + functionId: "test_ts_tsr-directive-use-server--iife" }); - export default iife_1;" + export { iife_1 };" `) }) @@ -588,17 +523,13 @@ describe('server function compilation', () => { const client = compileDirectives({ ...clientConfig, code }) const ssr = compileDirectives({ ...ssrConfig, code }) - const splitFiles = Object.entries(ssr.directiveFnsById) - .map(([_fnId, directiveFn]) => { - return `// ${directiveFn.functionId}\n\n${ - compileDirectives({ - ...serverConfig, - code, - filename: directiveFn.splitFilename, - }).compiledResult.code - }` - }) - .join('\n\n\n') + 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"; @@ -628,34 +559,14 @@ describe('server function compilation', () => { outer(outer_useServer_1);" `) - expect(splitFiles).toMatchInlineSnapshot(` - "// test_ts--outer_useServer - - import { createServerRpc } from "my-rpc-lib-server"; + expect(server.compiledResult.code).toMatchInlineSnapshot(` + "import { createServerRpc } from "my-rpc-lib-server"; const outer_useServer = createServerRpc({ fn: function useServer() { return 'hello'; }, filename: "test.ts", - functionId: "test_ts_tsr-directive-use-server-split_outer_useServer--outer_useServer" - }); - outer(outer_useServer); - const outer_useServer_1 = createServerRpc({ - fn: (...args) => import("test.ts?tsr-directive-use-server-split=outer_useServer_1").then(module => module.default(...args)), - filename: "test.ts", - functionId: "test_ts_tsr-directive-use-server-split_outer_useServer--outer_useServer_1" - }); - outer(outer_useServer_1); - export default outer_useServer; - - - // test_ts--outer_useServer_1 - - import { createServerRpc } from "my-rpc-lib-server"; - const outer_useServer = createServerRpc({ - fn: (...args) => import("test.ts?tsr-directive-use-server-split=outer_useServer").then(module => module.default(...args)), - filename: "test.ts", - functionId: "test_ts_tsr-directive-use-server-split_outer_useServer_1--outer_useServer" + functionId: "test_ts_tsr-directive-use-server--outer_useServer" }); outer(outer_useServer); const outer_useServer_1 = createServerRpc({ @@ -663,10 +574,10 @@ describe('server function compilation', () => { return 'hello'; }, filename: "test.ts", - functionId: "test_ts_tsr-directive-use-server-split_outer_useServer_1--outer_useServer_1" + functionId: "test_ts_tsr-directive-use-server--outer_useServer_1" }); outer(outer_useServer_1); - export default outer_useServer_1;" + export { outer_useServer, outer_useServer_1 };" `) }) @@ -693,17 +604,13 @@ describe('server function compilation', () => { const client = compileDirectives({ ...clientConfig, code }) const ssr = compileDirectives({ ...ssrConfig, code }) - const splitFiles = Object.entries(ssr.directiveFnsById) - .map(([_fnId, directive]) => { - return `// ${directive.functionName}\n\n${ - compileDirectives({ - ...serverConfig, - code, - filename: directive.splitFilename, - }).compiledResult.code - }` - }) - .join('\n\n\n') + const server = compileDirectives({ + ...serverConfig, + code, + filename: + ssr.directiveFnsById[Object.keys(ssr.directiveFnsById)[0]!]! + .extractedFilename, + }) expect(client.compiledResult.code).toMatchInlineSnapshot(` "'use server'; @@ -735,10 +642,8 @@ describe('server function compilation', () => { }); export default defaultExport_1;" `) - expect(splitFiles).toMatchInlineSnapshot(` - "// useServer - - 'use server'; + expect(server.compiledResult.code).toMatchInlineSnapshot(` + "'use server'; import { createServerRpc } from "my-rpc-lib-server"; const useServer_1 = createServerRpc({ @@ -746,27 +651,19 @@ describe('server function compilation', () => { return usedInUseServer(); }, filename: "test.ts", - functionId: "test_ts_tsr-directive-use-server-split_useServer--useServer" + functionId: "test_ts_tsr-directive-use-server--useServer" }); function usedInUseServer() { return 'hello'; } - export default useServer_1; - - - // defaultExport - - 'use server'; - - import { createServerRpc } from "my-rpc-lib-server"; const defaultExport_1 = createServerRpc({ fn: function defaultExport() { return 'hello'; }, filename: "test.ts", - functionId: "test_ts_tsr-directive-use-server-split_defaultExport--defaultExport" + functionId: "test_ts_tsr-directive-use-server--defaultExport" }); - export default defaultExport_1;" + export { useServer_1, defaultExport_1 };" `) }) @@ -811,17 +708,13 @@ describe('server function compilation', () => { const client = compileDirectives({ ...clientConfig, code: clientOrSsrCode }) const ssr = compileDirectives({ ...ssrConfig, code: clientOrSsrCode }) - const splitFiles = Object.entries(ssr.directiveFnsById) - .map(([_fnId, directiveFn]) => { - return `// ${directiveFn.functionId}\n\n${ - compileDirectives({ - ...serverConfig, - code: serverCode, - filename: directiveFn.splitFilename, - }).compiledResult.code - }` - }) - .join('\n\n\n') + 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"; @@ -851,10 +744,8 @@ describe('server function compilation', () => { }); export const myServerFn2 = createServerFn().handler(myServerFn2_createServerFn_handler);" `) - expect(splitFiles).toMatchInlineSnapshot(` - "// test_ts--myServerFn_createServerFn_handler - - import { createServerRpc } from "my-rpc-lib-server"; + expect(server.compiledResult.code).toMatchInlineSnapshot(` + "import { createServerRpc } from "my-rpc-lib-server"; import { createServerFn } from '@tanstack/start'; const myFunc = () => { return 'hello from the server'; @@ -864,23 +755,7 @@ describe('server function compilation', () => { return myServerFn.__executeServer(opts); }, filename: "test.ts", - functionId: "test_ts_tsr-directive-use-server-split_myServerFn_createServerFn_handler--myServerFn_createServerFn_handler" - }); - const myServerFn = createServerFn().handler(myServerFn_createServerFn_handler, myFunc); - export default myServerFn_createServerFn_handler; - - - // test_ts--myServerFn2_createServerFn_handler - - import { createServerRpc } from "my-rpc-lib-server"; - import { createServerFn } from '@tanstack/start'; - const myFunc = () => { - return 'hello from the server'; - }; - const myServerFn_createServerFn_handler = createServerRpc({ - fn: (...args) => import("test.ts?tsr-directive-use-server-split=myServerFn_createServerFn_handler").then(module => module.default(...args)), - filename: "test.ts", - functionId: "test_ts_tsr-directive-use-server-split_myServerFn2_createServerFn_handler--myServerFn_createServerFn_handler" + functionId: "test_ts_tsr-directive-use-server--myServerFn_createServerFn_handler" }); const myFunc2 = () => { return myServerFn({ @@ -892,11 +767,11 @@ describe('server function compilation', () => { return myServerFn2.__executeServer(opts); }, filename: "test.ts", - functionId: "test_ts_tsr-directive-use-server-split_myServerFn2_createServerFn_handler--myServerFn2_createServerFn_handler" + functionId: "test_ts_tsr-directive-use-server--myServerFn2_createServerFn_handler" }); const myServerFn = createServerFn().handler(myServerFn_createServerFn_handler, myFunc); const myServerFn2 = createServerFn().handler(myServerFn2_createServerFn_handler, myFunc2); - export default myServerFn2_createServerFn_handler;" + export { myServerFn_createServerFn_handler, myServerFn2_createServerFn_handler };" `) }) }) diff --git a/packages/router-plugin/src/core/code-splitter/compilers.ts b/packages/router-plugin/src/core/code-splitter/compilers.ts index d05ce6e2cc..cff905e5d1 100644 --- a/packages/router-plugin/src/core/code-splitter/compilers.ts +++ b/packages/router-plugin/src/core/code-splitter/compilers.ts @@ -5,7 +5,6 @@ import * as template from '@babel/template' import { deadCodeElimination } from 'babel-dead-code-elimination' import { splitPrefix } from '../constants' -import { logDiff } from '../../logger' import { parseAst } from './ast' import type { ParseAstOptions } from './ast' @@ -39,10 +38,8 @@ interface State { } function addSplitSearchParamToFilename(filename: string) { - const [bareFilename, ...searchParams] = filename.split('?') - const filenameSearchParams = new URLSearchParams(searchParams.join('&')) - filenameSearchParams.set(splitPrefix, '') - return `${bareFilename}?${filenameSearchParams.toString()}` + const [bareFilename] = filename.split('?') + return `${bareFilename}?${splitPrefix}` } function removeSplitSearchParamFromFilename(filename: string) { @@ -65,9 +62,7 @@ export function compileCodeSplitReferenceRoute(opts: ParseAstOptions) { // We need to extract the existing search params from the filename, if any // and add the splitPrefix to them, then write them back to the filename - const splitUrl = `${splitPrefix}:${addSplitSearchParamToFilename( - opts.filename, - )}` + const splitUrl = addSplitSearchParamToFilename(opts.filename) /** * If the component for the route is being imported from @@ -273,17 +268,8 @@ export function compileCodeSplitReferenceRoute(opts: ParseAstOptions) { }, }) - const beforeDCE = debug ? generate(ast, { sourceMaps: true }).code : '' - deadCodeElimination(ast) - const afterDCE = debug ? generate(ast, { sourceMaps: true }).code : '' - - if (debug) { - console.info('Code Splitting DCE Input/Output') - logDiff(beforeDCE, afterDCE) - } - return generate(ast, { sourceMaps: true, sourceFileName: opts.filename, @@ -520,17 +506,8 @@ export function compileCodeSplitVirtualRoute(opts: ParseAstOptions) { }, }) - const beforeDCE = debug ? generate(ast, { sourceMaps: true }).code : '' - deadCodeElimination(ast) - const afterDCE = debug ? generate(ast, { sourceMaps: true }).code : '' - - if (debug) { - console.info('Code Splitting DCE Input/Output') - logDiff(beforeDCE, afterDCE) - } - // if there are exported identifiers, then we need to add a warning // to the file to let the user know that the exported identifiers // will not in the split file but in the original file, therefore diff --git a/packages/router-plugin/src/core/router-code-splitter-plugin.ts b/packages/router-plugin/src/core/router-code-splitter-plugin.ts index 1727386d5a..fe07f7ea9d 100644 --- a/packages/router-plugin/src/core/router-code-splitter-plugin.ts +++ b/packages/router-plugin/src/core/router-code-splitter-plugin.ts @@ -58,7 +58,6 @@ plugins: [ } const PLUGIN_NAME = 'unplugin:router-code-splitter' -const JoinedSplitPrefix = splitPrefix + ':' export const unpluginRouterCodeSplitterFactory: UnpluginFactory< Partial | undefined @@ -69,7 +68,7 @@ export const unpluginRouterCodeSplitterFactory: UnpluginFactory< let userConfig = options as Config const handleSplittingFile = (code: string, id: string) => { - if (debug) console.info('Splitting route: ', id) + if (debug) console.info('Splitting Route: ', id) const compiledVirtualRoute = compileCodeSplitVirtualRoute({ code, @@ -78,7 +77,6 @@ export const unpluginRouterCodeSplitterFactory: UnpluginFactory< }) if (debug) { - console.info('Code Splitting Input/Output: ', id) logDiff(code, compiledVirtualRoute.code) } @@ -86,7 +84,7 @@ export const unpluginRouterCodeSplitterFactory: UnpluginFactory< } const handleCompilingFile = (code: string, id: string) => { - if (debug) console.info('Handling createRoute: ', id) + if (debug) console.info('Compiling Route: ', id) const compiledReferenceRoute = compileCodeSplitReferenceRoute({ code, @@ -95,7 +93,6 @@ export const unpluginRouterCodeSplitterFactory: UnpluginFactory< }) if (debug) { - console.info('Router Compiler Input/Output: ', id) logDiff(code, compiledReferenceRoute.code) } @@ -106,17 +103,6 @@ export const unpluginRouterCodeSplitterFactory: UnpluginFactory< name: 'router-code-splitter-plugin', enforce: 'pre', - resolveId(source) { - if (!userConfig.autoCodeSplitting) { - return null - } - - if (source.startsWith(splitPrefix + ':')) { - return source.replace(splitPrefix + ':', '') - } - return null - }, - transform(code, id) { if (!userConfig.autoCodeSplitting) { return null @@ -148,17 +134,11 @@ export const unpluginRouterCodeSplitterFactory: UnpluginFactory< return null }, - transformInclude(transformId) { + transformInclude(id) { if (!userConfig.autoCodeSplitting) { return undefined } - let id = transformId - - if (id.startsWith(JoinedSplitPrefix)) { - id = id.replace(JoinedSplitPrefix, '') - } - if ( fileIsInRoutesDirectory(id, userConfig.routesDirectory) || id.includes(splitPrefix) @@ -178,41 +158,11 @@ export const unpluginRouterCodeSplitterFactory: UnpluginFactory< rspack(compiler) { ROOT = process.cwd() - - compiler.hooks.beforeCompile.tap(PLUGIN_NAME, (self) => { - self.normalModuleFactory.hooks.beforeResolve.tap( - PLUGIN_NAME, - (resolveData: { request: string }) => { - if (resolveData.request.includes(JoinedSplitPrefix)) { - resolveData.request = resolveData.request.replace( - JoinedSplitPrefix, - '', - ) - } - }, - ) - }) - userConfig = getConfig(options, ROOT) }, webpack(compiler) { ROOT = process.cwd() - - compiler.hooks.beforeCompile.tap(PLUGIN_NAME, (self) => { - self.normalModuleFactory.hooks.beforeResolve.tap( - PLUGIN_NAME, - (resolveData: { request: string }) => { - if (resolveData.request.includes(JoinedSplitPrefix)) { - resolveData.request = resolveData.request.replace( - JoinedSplitPrefix, - '', - ) - } - }, - ) - }) - userConfig = getConfig(options, ROOT) if ( diff --git a/packages/router-plugin/tests/code-splitter/snapshots/arrow-function.tsx b/packages/router-plugin/tests/code-splitter/snapshots/arrow-function.tsx index 12220c333f..1b49b7ff9a 100644 --- a/packages/router-plugin/tests/code-splitter/snapshots/arrow-function.tsx +++ b/packages/router-plugin/tests/code-splitter/snapshots/arrow-function.tsx @@ -1,6 +1,6 @@ -const $$splitComponentImporter = () => import('tsr-split:arrow-function.tsx?tsr-split='); +const $$splitComponentImporter = () => import('arrow-function.tsx?tsr-split'); import { lazyRouteComponent } from '@tanstack/react-router'; -const $$splitLoaderImporter = () => import('tsr-split:arrow-function.tsx?tsr-split='); +const $$splitLoaderImporter = () => import('arrow-function.tsx?tsr-split'); import { lazyFn } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/posts')({ diff --git a/packages/router-plugin/tests/code-splitter/snapshots/destructured-react-memo-imported-component.tsx b/packages/router-plugin/tests/code-splitter/snapshots/destructured-react-memo-imported-component.tsx index a2e254d85f..63033a97e2 100644 --- a/packages/router-plugin/tests/code-splitter/snapshots/destructured-react-memo-imported-component.tsx +++ b/packages/router-plugin/tests/code-splitter/snapshots/destructured-react-memo-imported-component.tsx @@ -1,6 +1,6 @@ -const $$splitLoaderImporter = () => import('tsr-split:destructured-react-memo-imported-component.tsx?tsr-split='); +const $$splitLoaderImporter = () => import('destructured-react-memo-imported-component.tsx?tsr-split'); import { lazyFn } from '@tanstack/react-router'; -const $$splitComponentImporter = () => import('tsr-split:destructured-react-memo-imported-component.tsx?tsr-split='); +const $$splitComponentImporter = () => import('destructured-react-memo-imported-component.tsx?tsr-split'); import { lazyRouteComponent } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/')({ diff --git a/packages/router-plugin/tests/code-splitter/snapshots/function-declaration.tsx b/packages/router-plugin/tests/code-splitter/snapshots/function-declaration.tsx index 8b37f2652d..fa3fa3f6b3 100644 --- a/packages/router-plugin/tests/code-splitter/snapshots/function-declaration.tsx +++ b/packages/router-plugin/tests/code-splitter/snapshots/function-declaration.tsx @@ -1,6 +1,6 @@ -const $$splitComponentImporter = () => import('tsr-split:function-declaration.tsx?tsr-split='); +const $$splitComponentImporter = () => import('function-declaration.tsx?tsr-split'); import { lazyRouteComponent } from '@tanstack/react-router'; -const $$splitLoaderImporter = () => import('tsr-split:function-declaration.tsx?tsr-split='); +const $$splitLoaderImporter = () => import('function-declaration.tsx?tsr-split'); import { lazyFn } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/posts')({ diff --git a/packages/router-plugin/tests/code-splitter/snapshots/imported-default-component-destructured-loader.tsx b/packages/router-plugin/tests/code-splitter/snapshots/imported-default-component-destructured-loader.tsx index 48a372237e..b4f926eb1f 100644 --- a/packages/router-plugin/tests/code-splitter/snapshots/imported-default-component-destructured-loader.tsx +++ b/packages/router-plugin/tests/code-splitter/snapshots/imported-default-component-destructured-loader.tsx @@ -1,6 +1,6 @@ -const $$splitLoaderImporter = () => import('tsr-split:imported-default-component-destructured-loader.tsx?tsr-split='); +const $$splitLoaderImporter = () => import('imported-default-component-destructured-loader.tsx?tsr-split'); import { lazyFn } from '@tanstack/react-router'; -const $$splitComponentImporter = () => import('tsr-split:imported-default-component-destructured-loader.tsx?tsr-split='); +const $$splitComponentImporter = () => import('imported-default-component-destructured-loader.tsx?tsr-split'); import { lazyRouteComponent } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/')({ diff --git a/packages/router-plugin/tests/code-splitter/snapshots/imported-default-component.tsx b/packages/router-plugin/tests/code-splitter/snapshots/imported-default-component.tsx index 67aeb9895a..d8ca2602d0 100644 --- a/packages/router-plugin/tests/code-splitter/snapshots/imported-default-component.tsx +++ b/packages/router-plugin/tests/code-splitter/snapshots/imported-default-component.tsx @@ -1,4 +1,4 @@ -const $$splitComponentImporter = () => import('tsr-split:imported-default-component.tsx?tsr-split='); +const $$splitComponentImporter = () => import('imported-default-component.tsx?tsr-split'); import { lazyRouteComponent } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/')({ diff --git a/packages/router-plugin/tests/code-splitter/snapshots/imported.tsx b/packages/router-plugin/tests/code-splitter/snapshots/imported.tsx index 090d0ed379..e3484305f6 100644 --- a/packages/router-plugin/tests/code-splitter/snapshots/imported.tsx +++ b/packages/router-plugin/tests/code-splitter/snapshots/imported.tsx @@ -1,6 +1,6 @@ -const $$splitLoaderImporter = () => import('tsr-split:imported.tsx?tsr-split='); +const $$splitLoaderImporter = () => import('imported.tsx?tsr-split'); import { lazyFn } from '@tanstack/react-router'; -const $$splitComponentImporter = () => import('tsr-split:imported.tsx?tsr-split='); +const $$splitComponentImporter = () => import('imported.tsx?tsr-split'); import { lazyRouteComponent } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/')({ diff --git a/packages/router-plugin/tests/code-splitter/snapshots/inline.tsx b/packages/router-plugin/tests/code-splitter/snapshots/inline.tsx index e8e6358e48..4f94f8bdd2 100644 --- a/packages/router-plugin/tests/code-splitter/snapshots/inline.tsx +++ b/packages/router-plugin/tests/code-splitter/snapshots/inline.tsx @@ -1,4 +1,4 @@ -const $$splitComponentImporter = () => import('tsr-split:inline.tsx?tsr-split='); +const $$splitComponentImporter = () => import('inline.tsx?tsr-split'); import { lazyRouteComponent } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/')({ diff --git a/packages/router-plugin/tests/code-splitter/snapshots/random-number.tsx b/packages/router-plugin/tests/code-splitter/snapshots/random-number.tsx index 81c38bf456..f194be898d 100644 --- a/packages/router-plugin/tests/code-splitter/snapshots/random-number.tsx +++ b/packages/router-plugin/tests/code-splitter/snapshots/random-number.tsx @@ -1,6 +1,6 @@ -const $$splitComponentImporter = () => import('tsr-split:random-number.tsx?tsr-split='); +const $$splitComponentImporter = () => import('random-number.tsx?tsr-split'); import { lazyRouteComponent } from '@tanstack/react-router'; -const $$splitLoaderImporter = () => import('tsr-split:random-number.tsx?tsr-split='); +const $$splitLoaderImporter = () => import('random-number.tsx?tsr-split'); import { lazyFn } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router'; export const textColors = [`text-rose-500`, `text-yellow-500`, `text-teal-500`, `text-blue-500`]; diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react-memo-component.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react-memo-component.tsx index acf7d2ce44..c80202c1af 100644 --- a/packages/router-plugin/tests/code-splitter/snapshots/react-memo-component.tsx +++ b/packages/router-plugin/tests/code-splitter/snapshots/react-memo-component.tsx @@ -1,6 +1,6 @@ -const $$splitLoaderImporter = () => import('tsr-split:react-memo-component.tsx?tsr-split='); +const $$splitLoaderImporter = () => import('react-memo-component.tsx?tsr-split'); import { lazyFn } from '@tanstack/react-router'; -const $$splitComponentImporter = () => import('tsr-split:react-memo-component.tsx?tsr-split='); +const $$splitComponentImporter = () => import('react-memo-component.tsx?tsr-split'); import { lazyRouteComponent } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/')({ diff --git a/packages/router-plugin/tests/code-splitter/snapshots/react-memo-imported-component.tsx b/packages/router-plugin/tests/code-splitter/snapshots/react-memo-imported-component.tsx index d44968d06d..fd823829bc 100644 --- a/packages/router-plugin/tests/code-splitter/snapshots/react-memo-imported-component.tsx +++ b/packages/router-plugin/tests/code-splitter/snapshots/react-memo-imported-component.tsx @@ -1,6 +1,6 @@ -const $$splitLoaderImporter = () => import('tsr-split:react-memo-imported-component.tsx?tsr-split='); +const $$splitLoaderImporter = () => import('react-memo-imported-component.tsx?tsr-split'); import { lazyFn } from '@tanstack/react-router'; -const $$splitComponentImporter = () => import('tsr-split:react-memo-imported-component.tsx?tsr-split='); +const $$splitComponentImporter = () => import('react-memo-imported-component.tsx?tsr-split'); import { lazyRouteComponent } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/')({ diff --git a/packages/router-plugin/tests/code-splitter/snapshots/retain-export-component.tsx b/packages/router-plugin/tests/code-splitter/snapshots/retain-export-component.tsx index d7634fbfc2..5e41828800 100644 --- a/packages/router-plugin/tests/code-splitter/snapshots/retain-export-component.tsx +++ b/packages/router-plugin/tests/code-splitter/snapshots/retain-export-component.tsx @@ -1,4 +1,4 @@ -const $$splitLoaderImporter = () => import('tsr-split:retain-export-component.tsx?tsr-split='); +const $$splitLoaderImporter = () => import('retain-export-component.tsx?tsr-split'); import { lazyFn } from '@tanstack/react-router'; import { createFileRoute, Outlet } from '@tanstack/react-router'; import { importedComponent as ImportedComponent } from '../shared/imported'; diff --git a/packages/router-plugin/tests/code-splitter/snapshots/retain-exports-loader.tsx b/packages/router-plugin/tests/code-splitter/snapshots/retain-exports-loader.tsx index d1ab86e388..4ed9513983 100644 --- a/packages/router-plugin/tests/code-splitter/snapshots/retain-exports-loader.tsx +++ b/packages/router-plugin/tests/code-splitter/snapshots/retain-exports-loader.tsx @@ -1,4 +1,4 @@ -const $$splitComponentImporter = () => import('tsr-split:retain-exports-loader.tsx?tsr-split='); +const $$splitComponentImporter = () => import('retain-exports-loader.tsx?tsr-split'); import { lazyRouteComponent } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router'; export function loaderFn() { diff --git a/packages/router-plugin/tests/code-splitter/snapshots/useStateDestructure.tsx b/packages/router-plugin/tests/code-splitter/snapshots/useStateDestructure.tsx index 963f6113c5..f847174b9e 100644 --- a/packages/router-plugin/tests/code-splitter/snapshots/useStateDestructure.tsx +++ b/packages/router-plugin/tests/code-splitter/snapshots/useStateDestructure.tsx @@ -1,4 +1,4 @@ -const $$splitComponentImporter = () => import('tsr-split:useStateDestructure.tsx?tsr-split='); +const $$splitComponentImporter = () => import('useStateDestructure.tsx?tsr-split'); import { lazyRouteComponent } from '@tanstack/react-router'; import { startProject } from '~/projects/start'; import { createFileRoute } from '@tanstack/react-router'; diff --git a/packages/server-functions-plugin/src/index.ts b/packages/server-functions-plugin/src/index.ts index b5f9e36926..d5f498363b 100644 --- a/packages/server-functions-plugin/src/index.ts +++ b/packages/server-functions-plugin/src/index.ts @@ -42,7 +42,7 @@ export function createTanStackServerFnPlugin(_opts?: {}): { { functionName: fn.functionName, referenceName: fn.referenceName, - splitFilename: fn.splitFilename, + extractedFilename: fn.extractedFilename, filename: fn.filename, chunkName: fn.chunkName, }, @@ -140,7 +140,7 @@ export function createTanStackServerFnPlugin(_opts?: {}): { // By using the provided splitImportFn, we can both trigger vite // to create a new chunk/entry for the server function and also // replace other function references to it with the import statement - `createServerRpc(${JSON.stringify(opts.functionId)}, ${opts.isSplitFn ? opts.fn : opts.splitImportFn})`, + `createServerRpc(${JSON.stringify(opts.functionId)}, ${opts.isSourceFn ? opts.fn : opts.splitImportFn})`, onDirectiveFnsById, // devSplitImporter: `(globalThis.app.getRouter('server').internals.devServer.ssrLoadModule)`, }), @@ -174,7 +174,7 @@ export function createTanStackServerFnPlugin(_opts?: {}): { configResolved(config) { const serverFnEntries = Object.fromEntries( Object.entries(serverFunctionsManifest).map(([id, fn]) => { - return [fn.chunkName, fn.splitFilename] + return [fn.chunkName, fn.extractedFilename] }), ) diff --git a/packages/start-vite-plugin/src/compilers.ts b/packages/start-vite-plugin/src/compilers.ts index 9024436413..e5f769429b 100644 --- a/packages/start-vite-plugin/src/compilers.ts +++ b/packages/start-vite-plugin/src/compilers.ts @@ -233,11 +233,11 @@ function handleCreateServerFnCallExpression( const rootCallExpression = getRootCallExpression(path) - if (debug) - console.info( - 'Handling createServerFn call expression:', - rootCallExpression.toString(), - ) + // if (debug) + // console.info( + // 'Handling createServerFn call expression:', + // rootCallExpression.toString(), + // ) // Check if the call is assigned to a variable if (!rootCallExpression.parentPath.isVariableDeclarator()) { @@ -365,11 +365,11 @@ function handleCreateMiddlewareCallExpression( ) { const rootCallExpression = getRootCallExpression(path) - if (debug) - console.info( - 'Handling createMiddleware call expression:', - rootCallExpression.toString(), - ) + // if (debug) + // console.info( + // 'Handling createMiddleware call expression:', + // rootCallExpression.toString(), + // ) const callExpressionPaths = { middleware: null as babel.NodePath | null, @@ -439,8 +439,8 @@ function buildEnvOnlyCallExpressionHandler(env: 'client' | 'server') { path: babel.NodePath, opts: ParseAstOptions, ) { - if (debug) - console.info(`Handling ${env}Only call expression:`, path.toString()) + // if (debug) + // console.info(`Handling ${env}Only call expression:`, path.toString()) const isEnvMatch = env === 'client' @@ -486,11 +486,11 @@ function handleCreateIsomorphicFnCallExpression( ) { const rootCallExpression = getRootCallExpression(path) - if (debug) - console.info( - 'Handling createIsomorphicFn call expression:', - rootCallExpression.toString(), - ) + // if (debug) + // console.info( + // 'Handling createIsomorphicFn call expression:', + // rootCallExpression.toString(), + // ) const callExpressionPaths = { client: null as babel.NodePath | null, diff --git a/packages/start-vite-plugin/src/index.ts b/packages/start-vite-plugin/src/index.ts index 1c60d20997..8a5700c81e 100644 --- a/packages/start-vite-plugin/src/index.ts +++ b/packages/start-vite-plugin/src/index.ts @@ -1,6 +1,6 @@ import { fileURLToPath, pathToFileURL } from 'node:url' -import { compileEliminateDeadCode, compileStartOutput } from './compilers' +import { compileStartOutput } from './compilers' import { logDiff } from './logger' import type { Plugin } from 'vite' @@ -63,6 +63,8 @@ plugins: [ ) } + if (debug) console.info('Compiling Start: ', id) + const compiled = compileStartOutput({ code, root: ROOT, @@ -71,7 +73,6 @@ plugins: [ }) if (debug) { - console.info('Start Output Input/Output: ', id) logDiff(code, compiled.code) } @@ -79,42 +80,3 @@ plugins: [ }, } } - -export function TanStackStartViteDeadCodeElimination( - opts: TanStackStartViteOptions, -): Plugin { - let ROOT: string = process.cwd() - - return { - name: 'vite-plugin-tanstack-start-dead-code-elimination', - enforce: 'post', - configResolved: (config) => { - ROOT = config.root - }, - transform(code, id) { - const url = pathToFileURL(id) - url.searchParams.delete('v') - id = fileURLToPath(url).replace(/\\/g, '/') - - if (transformFuncs.some((fn) => code.includes(fn))) { - if (debug) console.info('Handling dead code elimination: ', id) - - const compiled = compileEliminateDeadCode({ - code, - root: ROOT, - filename: id, - env: opts.env, - }) - - if (debug) { - console.info('Start DCE Input/Output: ', id) - logDiff(code, compiled.code) - } - - return compiled - } - - return null - }, - } -} diff --git a/packages/start/src/config/index.ts b/packages/start/src/config/index.ts index cdd4b67642..5c736eaf13 100644 --- a/packages/start/src/config/index.ts +++ b/packages/start/src/config/index.ts @@ -185,9 +185,6 @@ export function defineConfig( ...(viteConfig.plugins || []), ...(clientViteConfig.plugins || []), viteReact(opts.react), - // TanStackStartViteDeadCodeElimination({ - // env: router === 'client' ? 'client' : 'server', - // }), // TODO: RSCS - enable this // serverComponents.client(), ] @@ -323,9 +320,6 @@ export function defineConfig( // runtime: '@vinxi/react-server-dom/runtime', // transpileDeps: ['react', 'react-dom', '@vinxi/react-server-dom'], // }), - // TanStackStartViteDeadCodeElimination({ - // env: router === 'client' ? 'client' : 'server', - // }), ...(viteConfig.plugins || []), ...(serverViteConfig.plugins || []), ] @@ -378,9 +372,6 @@ export function defineConfig( ...tsrConfig.experimental, }, }), - // TanStackStartViteDeadCodeElimination({ - // env: router === 'client' ? 'client' : 'server', - // }), ...(viteConfig.plugins || []), ...(apiViteConfig.plugins || []), ] diff --git a/packages/start/src/server-handler/index.tsx b/packages/start/src/server-handler/index.tsx index 7555b1d28f..ea7ddc36f0 100644 --- a/packages/start/src/server-handler/index.tsx +++ b/packages/start/src/server-handler/index.tsx @@ -16,6 +16,7 @@ import { } from 'vinxi/http' // @ts-expect-error import serverFnManifest from 'tsr:server-fn-manifest' +import type { DirectiveFn } from '../../../directive-functions-plugin/dist/esm/compilers' import type { H3Event } from 'vinxi/server' export default eventHandler(handleServerAction) @@ -54,7 +55,7 @@ export async function handleServerRequest(request: Request, _event?: H3Event) { throw new Error('Invalid server action param for serverFnId: ' + serverFnId) } - const serverFnInfo = serverFnManifest[serverFnId] + const serverFnInfo = serverFnManifest[serverFnId] as DirectiveFn | undefined if (!serverFnInfo) { console.log('serverFnManifest', serverFnManifest) @@ -64,14 +65,14 @@ export async function handleServerRequest(request: Request, _event?: H3Event) { if (process.env.NODE_ENV === 'development') console.info(`\nServerFn Request: ${serverFnId}`) - let action: Function | undefined + let fnModule: undefined | { [key: string]: any } + let moduleUrl = serverFnInfo.extractedFilename // In dev, we (for now) use Vinxi to get the "server" server-side router // Then we use that router's devServer.ssrLoadModule to get the serverFn if (process.env.NODE_ENV === 'development') { - action = await (globalThis as any).app + fnModule = await (globalThis as any).app .getRouter('server') - .internals.devServer.ssrLoadModule(serverFnInfo.splitFilename) - .then((d: any) => d.default) + .internals.devServer.ssrLoadModule(serverFnInfo.extractedFilename) } else { // In prod, we use the serverFn's chunkName to get the serverFn const router = (globalThis as any).app.getRouter('server') @@ -80,15 +81,22 @@ export async function handleServerRequest(request: Request, _event?: H3Event) { router.base, serverFnInfo.chunkName + '.mjs', ) - const url = pathToFileURL(filePath).toString() - action = (await import(/* @vite-ignore */ url).then( - (d) => d.default, - )) as Function + moduleUrl = pathToFileURL(filePath).toString() + fnModule = await import(/* @vite-ignore */ moduleUrl) } + if (!fnModule) { + console.log('serverFnManifest', serverFnManifest) + throw new Error('Server function module not resolved for ' + serverFnId) + } + + const action = fnModule[serverFnInfo.referenceName] + if (!action) { console.log('serverFnManifest', serverFnManifest) - throw new Error('Server function fn not resolved for ' + serverFnId) + throw new Error( + `Server function module export not resolved module: ${moduleUrl} for serverFn ID: ${serverFnId}`, + ) } const response = await (async () => { From 6eab24e5636bce24776fbda057355f7cde4e2572 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Wed, 8 Jan 2025 16:38:36 -0700 Subject: [PATCH 37/38] fix: production server fn builds --- packages/directive-functions-plugin/src/compilers.ts | 4 ++-- packages/server-functions-plugin/src/index.ts | 7 ++++--- packages/start/src/config/index.ts | 4 +++- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/directive-functions-plugin/src/compilers.ts b/packages/directive-functions-plugin/src/compilers.ts index fb3a176307..7f7ab163a3 100644 --- a/packages/directive-functions-plugin/src/compilers.ts +++ b/packages/directive-functions-plugin/src/compilers.ts @@ -467,7 +467,7 @@ export function findDirectives( splitImportFn: '$$splitImportFn$$', // extractedFilename, filename: filename!, - functionId: functionId, + functionId, isSourceFn: !!opts.isSourceFile, }) @@ -494,7 +494,7 @@ export function findDirectives( nodePath: directiveFn, referenceName, functionName: functionName || '', - functionId: functionId, + functionId, extractedFilename, filename: opts.filename, chunkName: fileNameToChunkName(opts.root, extractedFilename), diff --git a/packages/server-functions-plugin/src/index.ts b/packages/server-functions-plugin/src/index.ts index d5f498363b..7471fb9acd 100644 --- a/packages/server-functions-plugin/src/index.ts +++ b/packages/server-functions-plugin/src/index.ts @@ -62,9 +62,10 @@ export function createTanStackServerFnPlugin(_opts?: {}): { load(id) { if (id === 'tsr:server-fn-manifest') { if (process.env.NODE_ENV === 'production') { - return `export default ${JSON.stringify( - directiveFnsByIdToManifest(globalThis.TSR_directiveFnsById), - )}` + const manifest = JSON.parse( + readFileSync(path.join(ROOT, manifestFilename), 'utf-8'), + ) + return `export default ${JSON.stringify(manifest)}` } return `export default globalThis.TSR_directiveFnsById` diff --git a/packages/start/src/config/index.ts b/packages/start/src/config/index.ts index 5c736eaf13..38618b8555 100644 --- a/packages/start/src/config/index.ts +++ b/packages/start/src/config/index.ts @@ -553,7 +553,9 @@ function tsrRoutesManifest(opts: { } if (process.env.TSR_VITE_DEBUG) { - console.info(JSON.stringify(routesManifest, null, 2)) + console.info( + 'Routes Manifest: \n' + JSON.stringify(routesManifest, null, 2), + ) } return `export default () => (${JSON.stringify(routesManifest)})` From dfb52482c90b387f620c5e889df473d857809992 Mon Sep 17 00:00:00 2001 From: SeanCassiere <33615041+SeanCassiere@users.noreply.github.com> Date: Sun, 12 Jan 2025 12:58:49 +1300 Subject: [PATCH 38/38] feat(start): explicitly enable and disable route generation --- packages/start/src/config/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/start/src/config/index.ts b/packages/start/src/config/index.ts index 38618b8555..8e7b7e2600 100644 --- a/packages/start/src/config/index.ts +++ b/packages/start/src/config/index.ts @@ -177,6 +177,7 @@ export function defineConfig( TanStackServerFnsPlugin.client, TanStackRouterVite({ ...tsrConfig, + enableRouteGeneration: true, autoCodeSplitting: true, experimental: { ...tsrConfig.experimental, @@ -239,6 +240,7 @@ export function defineConfig( TanStackServerFnsPlugin.ssr, TanStackRouterVite({ ...tsrConfig, + enableRouteGeneration: false, autoCodeSplitting: true, experimental: { ...tsrConfig.experimental, @@ -269,7 +271,7 @@ export function defineConfig( ) return [ - config('tss-vite-config-ssr', { + config('tss-vite-config-server', { ...viteConfig.userConfig, ...serverViteConfig.userConfig, define: { @@ -298,6 +300,7 @@ export function defineConfig( TanStackServerFnsPlugin.server, TanStackRouterVite({ ...tsrConfig, + enableRouteGeneration: false, autoCodeSplitting: true, experimental: { ...tsrConfig.experimental, @@ -367,6 +370,7 @@ export function defineConfig( }), TanStackRouterVite({ ...tsrConfig, + enableRouteGeneration: false, autoCodeSplitting: true, experimental: { ...tsrConfig.experimental,