From d11d7e02b7efceaa748245113a121042f270a32c Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Sat, 22 Feb 2025 12:45:28 +0100 Subject: [PATCH 001/124] solid-start-client --- packages/solid-start-client/README.md | 33 + packages/solid-start-client/eslint.config.js | 27 + packages/solid-start-client/package.json | 82 ++ packages/solid-start-client/src/Meta.tsx | 10 + packages/solid-start-client/src/Scripts.tsx | 8 + .../solid-start-client/src/StartClient.tsx | 21 + .../src/createIsomorphicFn.ts | 36 + .../src/createMiddleware.ts | 517 ++++++++++++ .../solid-start-client/src/createServerFn.ts | 793 ++++++++++++++++++ packages/solid-start-client/src/envOnly.ts | 9 + packages/solid-start-client/src/headers.ts | 50 ++ packages/solid-start-client/src/index.tsx | 74 ++ packages/solid-start-client/src/json.ts | 15 + .../src/registerGlobalMiddleware.ts | 9 + packages/solid-start-client/src/renderRSC.tsx | 91 ++ .../solid-start-client/src/routesManifest.ts | 0 packages/solid-start-client/src/serializer.ts | 177 ++++ .../solid-start-client/src/ssr-client.tsx | 221 +++++ .../src/tests/createIsomorphicFn.test-d.ts | 72 ++ .../src/tests/createServerFn.test-d.tsx | 390 +++++++++ .../tests/createServerMiddleware.test-d.ts | 611 ++++++++++++++ .../src/tests/envOnly.test-d.ts | 34 + .../solid-start-client/src/tests/json.test.ts | 37 + .../src/tests/transformer.test.tsx | 147 ++++ .../solid-start-client/src/useServerFn.ts | 30 + packages/solid-start-client/tsconfig.json | 9 + packages/solid-start-client/vite.config.ts | 22 + 27 files changed, 3525 insertions(+) create mode 100644 packages/solid-start-client/README.md create mode 100644 packages/solid-start-client/eslint.config.js create mode 100644 packages/solid-start-client/package.json create mode 100644 packages/solid-start-client/src/Meta.tsx create mode 100644 packages/solid-start-client/src/Scripts.tsx create mode 100644 packages/solid-start-client/src/StartClient.tsx create mode 100644 packages/solid-start-client/src/createIsomorphicFn.ts create mode 100644 packages/solid-start-client/src/createMiddleware.ts create mode 100644 packages/solid-start-client/src/createServerFn.ts create mode 100644 packages/solid-start-client/src/envOnly.ts create mode 100644 packages/solid-start-client/src/headers.ts create mode 100644 packages/solid-start-client/src/index.tsx create mode 100644 packages/solid-start-client/src/json.ts create mode 100644 packages/solid-start-client/src/registerGlobalMiddleware.ts create mode 100644 packages/solid-start-client/src/renderRSC.tsx create mode 100644 packages/solid-start-client/src/routesManifest.ts create mode 100644 packages/solid-start-client/src/serializer.ts create mode 100644 packages/solid-start-client/src/ssr-client.tsx create mode 100644 packages/solid-start-client/src/tests/createIsomorphicFn.test-d.ts create mode 100644 packages/solid-start-client/src/tests/createServerFn.test-d.tsx create mode 100644 packages/solid-start-client/src/tests/createServerMiddleware.test-d.ts create mode 100644 packages/solid-start-client/src/tests/envOnly.test-d.ts create mode 100644 packages/solid-start-client/src/tests/json.test.ts create mode 100644 packages/solid-start-client/src/tests/transformer.test.tsx create mode 100644 packages/solid-start-client/src/useServerFn.ts create mode 100644 packages/solid-start-client/tsconfig.json create mode 100644 packages/solid-start-client/vite.config.ts diff --git a/packages/solid-start-client/README.md b/packages/solid-start-client/README.md new file mode 100644 index 0000000000..fd6e98ac6d --- /dev/null +++ b/packages/solid-start-client/README.md @@ -0,0 +1,33 @@ +> 🤫 we're cooking up something special! + + + +# TanStack Start + +![TanStack Router Header](https://github.com/tanstack/router/raw/main/media/header.png) + +🤖 Type-safe router w/ built-in caching & URL state management for React! + + + #TanStack + + + + + + + + semantic-release + + Join the discussion on Github +Best of JS + + + + + + + +Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [React Query](https://github.com/tannerlinsley/react-query), [React Table](https://github.com/tanstack/react-table), [React Charts](https://github.com/tannerlinsley/react-charts), [React Virtual](https://github.com/tannerlinsley/react-virtual) + +## Visit [tanstack.com/router](https://tanstack.com/router) for docs, guides, API and more! diff --git a/packages/solid-start-client/eslint.config.js b/packages/solid-start-client/eslint.config.js new file mode 100644 index 0000000000..7699a9bcc0 --- /dev/null +++ b/packages/solid-start-client/eslint.config.js @@ -0,0 +1,27 @@ +// @ts-check + +import pluginReact from '@eslint-react/eslint-plugin' +import pluginReactHooks from 'eslint-plugin-react-hooks' +import rootConfig from '../../eslint.config.js' + +export default [ + ...rootConfig, + { + ...pluginReact.configs.recommended, + files: ['**/*.{ts,tsx}'], + }, + { + plugins: { + 'react-hooks': pluginReactHooks, + }, + rules: { + + }, + }, + { + files: ['**/__tests__/**'], + rules: { + '@typescript-eslint/no-unnecessary-condition': 'off', + }, + }, +] diff --git a/packages/solid-start-client/package.json b/packages/solid-start-client/package.json new file mode 100644 index 0000000000..f307bf808b --- /dev/null +++ b/packages/solid-start-client/package.json @@ -0,0 +1,82 @@ +{ + "name": "@tanstack/solid-start-client", + "version": "1.109.2", + "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/solid-start-client" + }, + "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:deps && 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", + "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/solid-cross-context": "workspace:^", + "@tanstack/solid-router": "workspace:^", + "cookie-es": "^1.2.2", + "jsesc": "^3.1.0", + "tiny-invariant": "^1.3.3", + "vinxi": "^0.5.3" + }, + "devDependencies": { + "@solidjs/testing-library": "^0.8.10", + "@testing-library/jest-dom": "^6.6.3", + "@types/jsesc": "^3.0.3", + "vite-plugin-solid": "^2.11.2" + }, + "peerDependencies": { + "solid-js": ">=1.0.0" + } +} diff --git a/packages/solid-start-client/src/Meta.tsx b/packages/solid-start-client/src/Meta.tsx new file mode 100644 index 0000000000..5da9f9268b --- /dev/null +++ b/packages/solid-start-client/src/Meta.tsx @@ -0,0 +1,10 @@ +import { HeadContent } from '@tanstack/solid-router' + +export const Meta = () => { + if (process.env.NODE_ENV === 'development') { + console.warn( + 'The Meta component is deprecated. Use `HeadContent` from `@tanstack/solid-router` instead.', + ) + } + return +} diff --git a/packages/solid-start-client/src/Scripts.tsx b/packages/solid-start-client/src/Scripts.tsx new file mode 100644 index 0000000000..3f782d7d54 --- /dev/null +++ b/packages/solid-start-client/src/Scripts.tsx @@ -0,0 +1,8 @@ +import { Scripts as RouterScripts } from '@tanstack/solid-router' + +export const Scripts = () => { + if (process.env.NODE_ENV === 'development') { + console.warn('The Scripts component was moved to `@tanstack/solid-router`') + } + return +} diff --git a/packages/solid-start-client/src/StartClient.tsx b/packages/solid-start-client/src/StartClient.tsx new file mode 100644 index 0000000000..7bb49c2e8b --- /dev/null +++ b/packages/solid-start-client/src/StartClient.tsx @@ -0,0 +1,21 @@ +import { Await, RouterProvider } from '@tanstack/solid-router' +import { hydrate } from './ssr-client' +import type { AnyRouter } from '@tanstack/solid-router' + +let hydrationPromise: Promise>> | undefined + +export function StartClient(props: { router: AnyRouter }) { + if (!hydrationPromise) { + if (!props.router.state.matches.length) { + hydrationPromise = hydrate(props.router) + } else { + hydrationPromise = Promise.resolve() + } + } + return ( + } + /> + ) +} diff --git a/packages/solid-start-client/src/createIsomorphicFn.ts b/packages/solid-start-client/src/createIsomorphicFn.ts new file mode 100644 index 0000000000..8703bd465b --- /dev/null +++ b/packages/solid-start-client/src/createIsomorphicFn.ts @@ -0,0 +1,36 @@ +// a function that can have different implementations on the client and server. +// implementations not provided will default to a no-op function. + +export type IsomorphicFn< + TArgs extends Array = [], + TServer = undefined, + TClient = undefined, +> = (...args: TArgs) => TServer | TClient + +export interface ServerOnlyFn, TServer> + extends IsomorphicFn { + client: ( + clientImpl: (...args: TArgs) => TClient, + ) => IsomorphicFn +} + +export interface ClientOnlyFn, TClient> + extends IsomorphicFn { + server: ( + serverImpl: (...args: TArgs) => TServer, + ) => IsomorphicFn +} + +export interface IsomorphicFnBase extends IsomorphicFn { + server: , TServer>( + serverImpl: (...args: TArgs) => TServer, + ) => ServerOnlyFn + client: , TClient>( + clientImpl: (...args: TArgs) => TClient, + ) => ClientOnlyFn +} + +// this is a dummy function, it will be replaced by the transformer +export function createIsomorphicFn(): IsomorphicFnBase { + return null! +} diff --git a/packages/solid-start-client/src/createMiddleware.ts b/packages/solid-start-client/src/createMiddleware.ts new file mode 100644 index 0000000000..0a09412dc1 --- /dev/null +++ b/packages/solid-start-client/src/createMiddleware.ts @@ -0,0 +1,517 @@ +import type { + ConstrainValidator, + Method, + ServerFnTypeOrTypeFn, +} from './createServerFn' +import type { + Assign, + Constrain, + Expand, + IntersectAssign, + ResolveValidatorInput, + ResolveValidatorOutput, + SerializerStringify, +} from '@tanstack/solid-router' + +export type AssignAllMiddleware< + TMiddlewares, + TType extends keyof AnyMiddleware['_types'], + TAcc = undefined, +> = TMiddlewares extends readonly [ + infer TMiddleware extends AnyMiddleware, + ...infer TRest, +] + ? AssignAllMiddleware< + TRest, + TType, + Assign + > + : TAcc + +/** + * Recursively resolve the client context type produced by a sequence of middleware + */ +export type AssignAllClientContextBeforeNext< + TMiddlewares, + TClientContext = undefined, +> = unknown extends TClientContext + ? TClientContext + : Assign< + AssignAllMiddleware, + TClientContext + > + +export type AssignAllClientSendContext< + TMiddlewares, + TSendContext = undefined, +> = unknown extends TSendContext + ? TSendContext + : Assign< + AssignAllMiddleware, + TSendContext + > + +export type AssignAllClientContextAfterNext< + TMiddlewares, + TClientContext = undefined, + TSendContext = undefined, +> = unknown extends TClientContext + ? Assign + : Assign< + AssignAllMiddleware, + Assign + > + +/** + * Recursively resolve the server context type produced by a sequence of middleware + */ +export type AssignAllServerContext< + TMiddlewares, + TSendContext = undefined, + TServerContext = undefined, +> = unknown extends TSendContext + ? Assign + : Assign< + AssignAllMiddleware, + Assign + > + +export type AssignAllServerSendContext< + TMiddlewares, + TSendContext = undefined, +> = unknown extends TSendContext + ? TSendContext + : Assign< + AssignAllMiddleware, + TSendContext + > + +export type IntersectAllMiddleware< + TMiddlewares, + TType extends keyof AnyMiddleware['_types'], + TAcc = undefined, +> = TMiddlewares extends readonly [ + infer TMiddleware extends AnyMiddleware, + ...infer TRest, +] + ? IntersectAllMiddleware< + TRest, + TType, + IntersectAssign + > + : TAcc + +/** + * Recursively resolve the input type produced by a sequence of middleware + */ +export type IntersectAllValidatorInputs = + unknown extends TValidator + ? TValidator + : IntersectAssign< + IntersectAllMiddleware, + TValidator extends undefined + ? undefined + : ResolveValidatorInput + > +/** + * Recursively merge the output type produced by a sequence of middleware + */ +export type IntersectAllValidatorOutputs = + unknown extends TValidator + ? TValidator + : IntersectAssign< + IntersectAllMiddleware, + TValidator extends undefined + ? undefined + : ResolveValidatorOutput + > + +export interface MiddlewareOptions< + in out TMiddlewares, + in out TValidator, + in out TServerContext, + in out TClientContext, +> { + validateClient?: boolean + middleware?: TMiddlewares + validator?: ConstrainValidator + client?: MiddlewareClientFn< + TMiddlewares, + TValidator, + TServerContext, + TClientContext + > + server?: MiddlewareServerFn< + TMiddlewares, + TValidator, + TServerContext, + unknown, + unknown + > +} + +export type MiddlewareServerNextFn = < + TNewServerContext = undefined, + TSendContext = undefined, +>(ctx?: { + context?: TNewServerContext + sendContext?: SerializerStringify +}) => Promise< + ServerResultWithContext< + TMiddlewares, + TServerSendContext, + TNewServerContext, + TSendContext + > +> + +export interface MiddlewareServerFnOptions< + in out TMiddlewares, + in out TValidator, + in out TServerSendContext, +> { + data: Expand> + context: Expand> + next: MiddlewareServerNextFn + method: Method + filename: string + functionId: string + signal: AbortSignal +} + +export type MiddlewareServerFn< + TMiddlewares, + TValidator, + TServerSendContext, + TNewServerContext, + TSendContext, +> = ( + options: MiddlewareServerFnOptions< + TMiddlewares, + TValidator, + TServerSendContext + >, +) => MiddlewareServerFnResult< + TMiddlewares, + TServerSendContext, + TNewServerContext, + TSendContext +> + +export type MiddlewareServerFnResult< + TMiddlewares, + TServerSendContext, + TServerContext, + TSendContext, +> = + | Promise< + ServerResultWithContext< + TMiddlewares, + TServerSendContext, + TServerContext, + TSendContext + > + > + | ServerResultWithContext< + TMiddlewares, + TServerSendContext, + TServerContext, + TSendContext + > + +export type MiddlewareClientNextFn = < + TSendContext = undefined, + TNewClientContext = undefined, +>(ctx?: { + context?: TNewClientContext + sendContext?: SerializerStringify + headers?: HeadersInit +}) => Promise< + ClientResultWithContext +> + +export interface MiddlewareClientFnOptions< + in out TMiddlewares, + in out TValidator, +> { + data: Expand> + context: Expand> + sendContext: Expand> + method: Method + signal: AbortSignal + next: MiddlewareClientNextFn + filename: string + functionId: string + type: ServerFnTypeOrTypeFn +} + +export type MiddlewareClientFn< + TMiddlewares, + TValidator, + TSendContext, + TClientContext, +> = ( + options: MiddlewareClientFnOptions, +) => MiddlewareClientFnResult + +export type MiddlewareClientFnResult< + TMiddlewares, + TSendContext, + TClientContext, +> = + | Promise> + | ClientResultWithContext + +export type ServerResultWithContext< + in out TMiddlewares, + in out TServerSendContext, + in out TServerContext, + in out TSendContext, +> = { + 'use functions must return the result of next()': true + _types: { + context: TServerContext + sendContext: TSendContext + } + context: Expand< + AssignAllServerContext + > + sendContext: Expand> +} + +export type ClientResultWithContext< + in out TMiddlewares, + in out TSendContext, + in out TClientContext, +> = { + 'use functions must return the result of next()': true + context: Expand> + sendContext: Expand> + headers: HeadersInit +} + +export type AnyMiddleware = MiddlewareWithTypes + +export interface MiddlewareTypes< + in out TMiddlewares, + in out TValidator, + in out TServerContext, + in out TServerSendContext, + in out TClientContext, + in out TClientSendContext, +> { + middlewares: TMiddlewares + input: ResolveValidatorInput + allInput: IntersectAllValidatorInputs + output: ResolveValidatorOutput + allOutput: IntersectAllValidatorOutputs + clientContext: TClientContext + allClientContextBeforeNext: AssignAllClientContextBeforeNext< + TMiddlewares, + TClientContext + > + allClientContextAfterNext: AssignAllClientContextAfterNext< + TMiddlewares, + TClientContext, + TClientSendContext + > + serverContext: TServerContext + serverSendContext: TServerSendContext + allServerSendContext: AssignAllServerSendContext< + TMiddlewares, + TServerSendContext + > + allServerContext: AssignAllServerContext< + TMiddlewares, + TServerSendContext, + TServerContext + > + clientSendContext: TClientSendContext + allClientSendContext: AssignAllClientSendContext< + TMiddlewares, + TClientSendContext + > + validator: TValidator +} + +export interface MiddlewareWithTypes< + TMiddlewares, + TValidator, + TServerContext, + TServerSendContext, + TClientContext, + TClientSendContext, +> { + _types: MiddlewareTypes< + TMiddlewares, + TValidator, + TServerContext, + TServerSendContext, + TClientContext, + TClientSendContext + > + options: MiddlewareOptions< + TMiddlewares, + TValidator, + TServerContext, + TClientContext + > +} + +export interface MiddlewareAfterValidator + extends MiddlewareWithTypes< + TMiddlewares, + TValidator, + undefined, + undefined, + undefined, + undefined + >, + MiddlewareServer, + MiddlewareClient {} + +export interface MiddlewareValidator { + validator: ( + input: ConstrainValidator, + ) => MiddlewareAfterValidator +} + +export interface MiddlewareAfterServer< + TMiddlewares, + TValidator, + TServerContext, + TServerSendContext, + TClientContext, + TClientSendContext, +> extends MiddlewareWithTypes< + TMiddlewares, + TValidator, + TServerContext, + TServerSendContext, + TClientContext, + TClientSendContext + > {} + +export interface MiddlewareServer< + TMiddlewares, + TValidator, + TServerSendContext, + TClientContext, +> { + server: ( + server: MiddlewareServerFn< + TMiddlewares, + TValidator, + TServerSendContext, + TNewServerContext, + TSendContext + >, + ) => MiddlewareAfterServer< + TMiddlewares, + TValidator, + TNewServerContext, + TServerSendContext, + TClientContext, + TSendContext + > +} + +export interface MiddlewareAfterClient< + TMiddlewares, + TValidator, + TServerSendContext, + TClientContext, +> extends MiddlewareWithTypes< + TMiddlewares, + TValidator, + undefined, + TServerSendContext, + TClientContext, + undefined + >, + MiddlewareServer< + TMiddlewares, + TValidator, + TServerSendContext, + TClientContext + > {} + +export interface MiddlewareClient { + client: ( + client: MiddlewareClientFn< + TMiddlewares, + TValidator, + TSendServerContext, + TNewClientContext + >, + ) => MiddlewareAfterClient< + TMiddlewares, + TValidator, + TSendServerContext, + TNewClientContext + > +} + +export interface MiddlewareAfterMiddleware + extends MiddlewareWithTypes< + TMiddlewares, + undefined, + undefined, + undefined, + undefined, + undefined + >, + MiddlewareServer, + MiddlewareClient, + MiddlewareValidator {} + +export interface Middleware extends MiddlewareAfterMiddleware { + middleware: ( + middlewares: Constrain>, + ) => MiddlewareAfterMiddleware +} + +export function createMiddleware( + options?: { + validateClient?: boolean + }, + __opts?: MiddlewareOptions, +): Middleware { + // const resolvedOptions = (__opts || options) as MiddlewareOptions< + const resolvedOptions = + __opts || + ((options || {}) as MiddlewareOptions< + unknown, + undefined, + undefined, + undefined + >) + + return { + options: resolvedOptions as any, + middleware: (middleware: any) => { + return createMiddleware( + undefined, + Object.assign(resolvedOptions, { middleware }), + ) as any + }, + validator: (validator: any) => { + return createMiddleware( + undefined, + Object.assign(resolvedOptions, { validator }), + ) as any + }, + client: (client: any) => { + return createMiddleware( + undefined, + Object.assign(resolvedOptions, { client }), + ) as any + }, + server: (server: any) => { + return createMiddleware( + undefined, + Object.assign(resolvedOptions, { server }), + ) as any + }, + } as unknown as Middleware +} diff --git a/packages/solid-start-client/src/createServerFn.ts b/packages/solid-start-client/src/createServerFn.ts new file mode 100644 index 0000000000..add2f22bb2 --- /dev/null +++ b/packages/solid-start-client/src/createServerFn.ts @@ -0,0 +1,793 @@ +import { + invariant, + isNotFound, + isRedirect, + warning, +} from '@tanstack/solid-router' +import { mergeHeaders } from './headers' +import { globalMiddleware } from './registerGlobalMiddleware' +import { startSerializer } from './serializer' +import type { + AnyValidator, + Constrain, + Expand, + ResolveValidatorInput, + SerializerParse, + SerializerStringify, + SerializerStringifyBy, + Validator, +} from '@tanstack/solid-router' +import type { + AnyMiddleware, + AssignAllClientSendContext, + AssignAllServerContext, + IntersectAllValidatorInputs, + IntersectAllValidatorOutputs, + MiddlewareClientFnResult, + MiddlewareServerFnResult, +} from './createMiddleware' + +export interface JsonResponse extends Response { + json: () => Promise +} + +export type CompiledFetcherFnOptions = { + method: Method + data: unknown + headers?: HeadersInit + signal?: AbortSignal + context?: any +} + +export type Fetcher = + undefined extends IntersectAllValidatorInputs + ? OptionalFetcher + : RequiredFetcher + +export interface FetcherBase { + url: string + __executeServer: (opts: { + method: Method + data: unknown + headers?: HeadersInit + context?: any + signal: AbortSignal + }) => Promise +} + +export type FetchResult< + TMiddlewares, + TResponse, + TFullResponse extends boolean, +> = false extends TFullResponse + ? Promise> + : Promise> + +export interface OptionalFetcher + extends FetcherBase { + ( + options?: OptionalFetcherDataOptions< + TMiddlewares, + TValidator, + TFullResponse + >, + ): FetchResult +} + +export interface RequiredFetcher + extends FetcherBase { + ( + opts: RequiredFetcherDataOptions, + ): FetchResult +} + +export type FetcherBaseOptions = { + headers?: HeadersInit + type?: ServerFnType + signal?: AbortSignal + fullResponse?: TFullResponse +} + +export type ServerFnType = 'static' | 'dynamic' + +export interface OptionalFetcherDataOptions< + TMiddlewares, + TValidator, + TFullResponse extends boolean, +> extends FetcherBaseOptions { + data?: Expand> +} + +export interface RequiredFetcherDataOptions< + TMiddlewares, + TValidator, + TFullResponse extends boolean, +> extends FetcherBaseOptions { + data: Expand> +} + +export interface FullFetcherData { + error: unknown + result: FetcherData + context: AssignAllClientSendContext +} + +export type FetcherData = + TResponse extends JsonResponse + ? SerializerParse> + : SerializerParse + +export type RscStream = { + __cacheState: T +} + +export type Method = 'GET' | 'POST' + +export type ServerFn = ( + ctx: ServerFnCtx, +) => Promise> | SerializerStringify + +export interface ServerFnCtx { + method: TMethod + data: Expand> + context: Expand> + signal: AbortSignal +} + +export type CompiledFetcherFn = { + ( + opts: CompiledFetcherFnOptions & ServerFnBaseOptions, + ): Promise + url: string +} + +type ServerFnBaseOptions< + TMethod extends Method = 'GET', + TResponse = unknown, + TMiddlewares = unknown, + TInput = unknown, +> = { + method: TMethod + validateClient?: boolean + middleware?: Constrain> + validator?: ConstrainValidator + extractedFn?: CompiledFetcherFn + serverFn?: ServerFn + functionId: string + type: ServerFnTypeOrTypeFn +} + +export type ValidatorSerializerStringify = Validator< + SerializerStringifyBy< + ResolveValidatorInput, + Date | undefined | FormData + >, + any +> + +export type ConstrainValidator = unknown extends TValidator + ? TValidator + : Constrain> + +export interface ServerFnMiddleware { + middleware: ( + middlewares: Constrain>, + ) => ServerFnAfterMiddleware +} + +export interface ServerFnAfterMiddleware< + TMethod extends Method, + TMiddlewares, + TValidator, +> extends ServerFnValidator, + ServerFnTyper, + ServerFnHandler {} + +export type ValidatorFn = ( + validator: ConstrainValidator, +) => ServerFnAfterValidator + +export interface ServerFnValidator { + validator: ValidatorFn +} + +export interface ServerFnAfterValidator< + TMethod extends Method, + TMiddlewares, + TValidator, +> extends ServerFnMiddleware, + ServerFnTyper, + ServerFnHandler {} + +// Typer +export interface ServerFnTyper< + TMethod extends Method, + TMiddlewares, + TValidator, +> { + type: ( + typer: ServerFnTypeOrTypeFn, + ) => ServerFnAfterTyper +} + +export type ServerFnTypeOrTypeFn< + TMethod extends Method, + TMiddlewares, + TValidator, +> = + | ServerFnType + | ((ctx: ServerFnCtx) => ServerFnType) + +export interface ServerFnAfterTyper< + TMethod extends Method, + TMiddlewares, + TValidator, +> extends ServerFnHandler {} + +// Handler +export interface ServerFnHandler< + TMethod extends Method, + TMiddlewares, + TValidator, +> { + handler: ( + fn?: ServerFn, + ) => Fetcher +} + +export interface ServerFnBuilder + extends ServerFnMiddleware, + ServerFnValidator, + ServerFnTyper, + ServerFnHandler { + options: ServerFnBaseOptions +} + +type StaticCachedResult = { + ctx?: { + result: any + context: any + } + error?: any +} + +export type ServerFnStaticCache = { + getItem: ( + ctx: MiddlewareResult, + ) => StaticCachedResult | Promise + setItem: ( + ctx: MiddlewareResult, + response: StaticCachedResult, + ) => Promise + fetchItem: ( + ctx: MiddlewareResult, + ) => StaticCachedResult | Promise +} + +let serverFnStaticCache: ServerFnStaticCache | undefined + +export function setServerFnStaticCache( + cache?: ServerFnStaticCache | (() => ServerFnStaticCache | undefined), +) { + const previousCache = serverFnStaticCache + serverFnStaticCache = typeof cache === 'function' ? cache() : cache + + return () => { + serverFnStaticCache = previousCache + } +} + +export function createServerFnStaticCache( + serverFnStaticCache: ServerFnStaticCache, +) { + return serverFnStaticCache +} + +setServerFnStaticCache(() => { + const getStaticCacheUrl = (options: MiddlewareResult, hash: string) => { + return `/__tsr/staticServerFnCache/${options.functionId}__${hash}.json` + } + + const jsonToFilenameSafeString = (json: any) => { + // Custom replacer to sort keys + const sortedKeysReplacer = (key: string, value: any) => + value && typeof value === 'object' && !Array.isArray(value) + ? Object.keys(value) + .sort() + .reduce((acc: any, curr: string) => { + acc[curr] = value[curr] + return acc + }, {}) + : value + + // Convert JSON to string with sorted keys + const jsonString = JSON.stringify(json ?? '', sortedKeysReplacer) + + // Replace characters invalid in filenames + return jsonString + .replace(/[/\\?%*:|"<>]/g, '-') // Replace invalid characters with a dash + .replace(/\s+/g, '_') // Optionally replace whitespace with underscores + } + + const staticClientCache = + typeof document !== 'undefined' ? new Map() : null + + return createServerFnStaticCache({ + getItem: async (ctx) => { + if (typeof document === 'undefined') { + const hash = jsonToFilenameSafeString(ctx.data) + const url = getStaticCacheUrl(ctx, hash) + const publicUrl = process.env.TSS_OUTPUT_PUBLIC_DIR! + + // Use fs instead of fetch to read from filesystem + const { promises: fs } = await import('node:fs') + const path = await import('node:path') + const filePath = path.join(publicUrl, url) + + const [cachedResult, readError] = await fs + .readFile(filePath, 'utf-8') + .then((c) => [ + startSerializer.parse(c) as { + ctx: unknown + error: any + }, + null, + ]) + .catch((e) => [null, e]) + + if (readError && readError.code !== 'ENOENT') { + throw readError + } + + return cachedResult as StaticCachedResult + } + + return undefined + }, + setItem: async (ctx, response) => { + const { promises: fs } = await import('node:fs') + const path = await import('node:path') + + const hash = jsonToFilenameSafeString(ctx.data) + const url = getStaticCacheUrl(ctx, hash) + const publicUrl = process.env.TSS_OUTPUT_PUBLIC_DIR! + const filePath = path.join(publicUrl, url) + + // Ensure the directory exists + await fs.mkdir(path.dirname(filePath), { recursive: true }) + + // Store the result with fs + await fs.writeFile(filePath, startSerializer.stringify(response)) + }, + fetchItem: async (ctx) => { + const hash = jsonToFilenameSafeString(ctx.data) + const url = getStaticCacheUrl(ctx, hash) + + let result: any = staticClientCache?.get(url) + + if (!result) { + result = await fetch(url, { + method: 'GET', + }) + .then((r) => r.text()) + .then((d) => startSerializer.parse(d)) + + staticClientCache?.set(url, result) + } + + return result + }, + }) +}) + +export function createServerFn< + TMethod extends Method, + TResponse = unknown, + TMiddlewares = undefined, + TValidator = undefined, +>( + options?: { + method?: TMethod + type?: ServerFnType + }, + __opts?: ServerFnBaseOptions, +): ServerFnBuilder { + const resolvedOptions = (__opts || options || {}) as ServerFnBaseOptions< + TMethod, + TResponse, + TMiddlewares, + TValidator + > + + if (typeof resolvedOptions.method === 'undefined') { + resolvedOptions.method = 'GET' as TMethod + } + + return { + options: resolvedOptions as any, + middleware: (middleware) => { + return createServerFn( + undefined, + Object.assign(resolvedOptions, { middleware }), + ) as any + }, + validator: (validator) => { + return createServerFn( + undefined, + Object.assign(resolvedOptions, { validator }), + ) as any + }, + type: (type) => { + return createServerFn( + undefined, + Object.assign(resolvedOptions, { type }), + ) as any + }, + handler: (...args) => { + // This function signature changes due to AST transformations + // in the babel plugin. We need to cast it to the correct + // function signature post-transformation + const [extractedFn, serverFn] = args as unknown as [ + CompiledFetcherFn, + ServerFn, + ] + + // Keep the original function around so we can use it + // in the server environment + Object.assign(resolvedOptions, { + ...extractedFn, + extractedFn, + serverFn, + }) + + const resolvedMiddleware = [ + ...(resolvedOptions.middleware || []), + serverFnBaseToMiddleware(resolvedOptions), + ] + + // We want to make sure the new function has the same + // properties as the original function + return Object.assign( + async (opts?: CompiledFetcherFnOptions) => { + // Start by executing the client-side middleware chain + return executeMiddleware(resolvedMiddleware, 'client', { + ...extractedFn, + ...resolvedOptions, + data: opts?.data as any, + headers: opts?.headers, + signal: opts?.signal, + context: {}, + }).then((d) => { + if (d.error) throw d.error + return d.result + }) + }, + { + // This copies over the URL, function ID + ...extractedFn, + // The extracted function on the server-side calls + // this function + __executeServer: async (opts_: any, signal: AbortSignal) => { + const opts = + opts_ instanceof FormData ? extractFormDataContext(opts_) : opts_ + + opts.type = + typeof resolvedOptions.type === 'function' + ? resolvedOptions.type(opts) + : resolvedOptions.type + + const ctx = { + ...extractedFn, + ...opts, + signal, + } + + const run = () => + executeMiddleware(resolvedMiddleware, 'server', ctx).then( + (d) => ({ + // Only send the result and sendContext back to the client + result: d.result, + error: d.error, + context: d.sendContext, + }), + ) + + if (ctx.type === 'static') { + let response: StaticCachedResult | undefined + + // If we can get the cached item, try to get it + if (serverFnStaticCache?.getItem) { + // If this throws, it's okay to let it bubble up + response = await serverFnStaticCache.getItem(ctx) + } + + if (!response) { + // If there's no cached item, execute the server function + response = await run() + .then((d) => { + return { + ctx: d, + error: null, + } + }) + .catch((e) => { + return { + ctx: undefined, + error: e, + } + }) + + if (serverFnStaticCache?.setItem) { + await serverFnStaticCache.setItem(ctx, response) + } + } + + invariant( + response, + 'No response from both server and static cache!', + ) + + if (response.error) { + throw response.error + } + + return response.ctx + } + + return run() + }, + }, + ) as any + }, + } +} + +function extractFormDataContext(formData: FormData) { + const serializedContext = formData.get('__TSR_CONTEXT') + formData.delete('__TSR_CONTEXT') + + if (typeof serializedContext !== 'string') { + return { + context: {}, + data: formData, + } + } + + try { + const context = startSerializer.parse(serializedContext) + return { + context, + data: formData, + } + } catch { + return { + data: formData, + } + } +} + +function flattenMiddlewares( + middlewares: Array, +): Array { + const seen = new Set() + const flattened: Array = [] + + const recurse = (middleware: Array) => { + middleware.forEach((m) => { + if (m.options.middleware) { + recurse(m.options.middleware) + } + + if (!seen.has(m)) { + seen.add(m) + flattened.push(m) + } + }) + } + + recurse(middlewares) + + return flattened +} + +export type MiddlewareOptions = { + method: Method + data: any + headers?: HeadersInit + signal?: AbortSignal + sendContext?: any + context?: any + type: ServerFnTypeOrTypeFn + functionId: string +} + +export type MiddlewareResult = MiddlewareOptions & { + result?: unknown + error?: unknown + type: ServerFnTypeOrTypeFn +} + +export type NextFn = (ctx: MiddlewareResult) => Promise + +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({ + ...ctx, + ...userCtx, + context: { + ...ctx.context, + ...userCtx.context, + }, + sendContext: { + ...ctx.sendContext, + ...(userCtx.sendContext ?? {}), + }, + headers: mergeHeaders(ctx.headers, userCtx.headers), + result: + userCtx.result !== undefined ? userCtx.result : (ctx as any).result, + error: userCtx.error ?? (ctx as any).error, + }) + }) as any, + } as any) +} + +function execValidator(validator: AnyValidator, input: unknown): unknown { + if (validator == null) return {} + + if ('~standard' in validator) { + const result = validator['~standard'].validate(input) + + if (result instanceof Promise) + throw new Error('Async validation not supported') + + if (result.issues) + throw new Error(JSON.stringify(result.issues, undefined, 2)) + + return result.value + } + + if ('parse' in validator) { + return validator.parse(input) + } + + if (typeof validator === 'function') { + return validator(input) + } + + throw new Error('Invalid validator type!') +} + +async function executeMiddleware( + middlewares: Array, + env: 'client' | 'server', + opts: MiddlewareOptions, +): Promise { + const flattenedMiddlewares = flattenMiddlewares([ + ...globalMiddleware, + ...middlewares, + ]) + + 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 + } + + if ( + nextMiddleware.options.validator && + (env === 'client' ? nextMiddleware.options.validateClient : true) + ) { + // Execute the middleware's input function + ctx.data = await execValidator(nextMiddleware.options.validator, ctx.data) + } + + const middlewareFn = ( + env === 'client' + ? nextMiddleware.options.client + : nextMiddleware.options.server + ) as MiddlewareFn | undefined + + if (middlewareFn) { + // Execute the middleware + return applyMiddleware(middlewareFn, ctx, async (newCtx) => { + return next(newCtx).catch((error) => { + if (isRedirect(error) || isNotFound(error)) { + return { + ...newCtx, + error, + } + } + + throw error + }) + }) + } + + return next(ctx) + } + + // Start the middleware chain + return next({ + ...opts, + headers: opts.headers || {}, + sendContext: opts.sendContext || {}, + context: opts.context || {}, + }) +} + +function serverFnBaseToMiddleware( + options: ServerFnBaseOptions, +): AnyMiddleware { + return { + _types: undefined!, + options: { + validator: options.validator, + validateClient: options.validateClient, + client: async ({ next, sendContext, ...ctx }) => { + const payload = { + ...ctx, + // switch the sendContext over to context + context: sendContext, + type: typeof ctx.type === 'function' ? ctx.type(ctx) : ctx.type, + } as any + + if ( + ctx.type === 'static' && + process.env.NODE_ENV === 'production' && + typeof document !== 'undefined' + ) { + invariant( + serverFnStaticCache, + 'serverFnStaticCache.fetchItem is not available!', + ) + + const result = await serverFnStaticCache.fetchItem(payload) + + if (result) { + if (result.error) { + throw result.error + } + + return next(result.ctx) + } + + warning( + result, + `No static cache item found for ${payload.functionId}__${JSON.stringify(payload.data)}, falling back to server function...`, + ) + } + + // Execute the extracted function + // but not before serializing the context + const res = await options.extractedFn?.(payload) + + return next(res) as unknown as MiddlewareClientFnResult + }, + server: async ({ next, ...ctx }) => { + // Execute the server function + const result = await options.serverFn?.(ctx) + + return next({ + ...ctx, + result, + } as any) as unknown as MiddlewareServerFnResult + }, + }, + } +} diff --git a/packages/solid-start-client/src/envOnly.ts b/packages/solid-start-client/src/envOnly.ts new file mode 100644 index 0000000000..2b444578c2 --- /dev/null +++ b/packages/solid-start-client/src/envOnly.ts @@ -0,0 +1,9 @@ +type EnvOnlyFn = ) => any>(fn: TFn) => TFn + +// A function that will only be available in the server build +// If called on the client, it will throw an error +export const serverOnly: EnvOnlyFn = (fn) => fn + +// A function that will only be available in the client build +// If called on the server, it will throw an error +export const clientOnly: EnvOnlyFn = (fn) => fn diff --git a/packages/solid-start-client/src/headers.ts b/packages/solid-start-client/src/headers.ts new file mode 100644 index 0000000000..b9b1537452 --- /dev/null +++ b/packages/solid-start-client/src/headers.ts @@ -0,0 +1,50 @@ +import { splitSetCookieString } from 'cookie-es' +import type { OutgoingHttpHeaders } from 'node:http2' +// A utility function to turn HeadersInit into an object +export function headersInitToObject( + headers: HeadersInit, +): Record { + const obj: Record = {} + const headersInstance = new Headers(headers) + for (const [key, value] of headersInstance.entries()) { + obj[key] = value + } + return obj +} + +type AnyHeaders = + | Headers + | HeadersInit + | Record + | Array<[string, string]> + | OutgoingHttpHeaders + | undefined + +// Helper function to convert various HeaderInit types to a Headers instance +function toHeadersInstance(init: AnyHeaders) { + if (init instanceof Headers) { + return new Headers(init) + } else if (Array.isArray(init)) { + return new Headers(init) + } else if (typeof init === 'object') { + return new Headers(init as HeadersInit) + } else { + return new Headers() + } +} + +// Function to merge headers with proper overrides +export function mergeHeaders(...headers: Array) { + return headers.reduce((acc: Headers, header) => { + const headersInstance = toHeadersInstance(header) + for (const [key, value] of headersInstance.entries()) { + if (key === 'set-cookie') { + const splitCookies = splitSetCookieString(value) + splitCookies.forEach((cookie) => acc.append('set-cookie', cookie)) + } else { + acc.set(key, value) + } + } + return acc + }, new Headers()) +} diff --git a/packages/solid-start-client/src/index.tsx b/packages/solid-start-client/src/index.tsx new file mode 100644 index 0000000000..28b3ff5dd6 --- /dev/null +++ b/packages/solid-start-client/src/index.tsx @@ -0,0 +1,74 @@ +/// +export { + createIsomorphicFn, + type IsomorphicFn, + type ServerOnlyFn, + type ClientOnlyFn, + type IsomorphicFnBase, +} from './createIsomorphicFn' +export { + createServerFn, + type JsonResponse, + type ServerFn as FetchFn, + type ServerFnCtx as FetchFnCtx, + type CompiledFetcherFnOptions, + type CompiledFetcherFn, + type Fetcher, + type RscStream, + type FetcherData, + type FetcherBaseOptions, + type ServerFn, + type ServerFnCtx, +} from './createServerFn' +export { + createMiddleware, + type IntersectAllValidatorInputs, + type IntersectAllValidatorOutputs, + type MiddlewareServerFn, + type AnyMiddleware, + type MiddlewareOptions, + type MiddlewareWithTypes, + type MiddlewareValidator, + type MiddlewareServer, + type MiddlewareAfterClient, + type MiddlewareAfterMiddleware, + type MiddlewareAfterServer, + type Middleware, + type MiddlewareClientFnOptions, + type MiddlewareClientFnResult, + type MiddlewareClientNextFn, + type ClientResultWithContext, + type AssignAllClientContextBeforeNext, + type AssignAllMiddleware, + type AssignAllServerContext, + type MiddlewareAfterValidator, + type MiddlewareClientFn, + type MiddlewareServerFnResult, + type MiddlewareClient, + type MiddlewareServerFnOptions, + type MiddlewareServerNextFn, + type ServerResultWithContext, +} from './createMiddleware' +export { + registerGlobalMiddleware, + globalMiddleware, +} from './registerGlobalMiddleware' +export { serverOnly, clientOnly } from './envOnly' +export { json } from './json' +export { Meta } from './Meta' +export { Scripts } from './Scripts' +export { StartClient } from './StartClient' +export { mergeHeaders } from './headers' +export { renderRsc } from './renderRSC' +export { useServerFn } from './useServerFn' +export { + type DehydratedRouter, + type ClientExtractedBaseEntry, + type StartSsrGlobal, + type ClientExtractedEntry, + type SsrMatch, + type ClientExtractedPromise, + type ClientExtractedStream, + type ResolvePromiseState, +} from './ssr-client' +export { startSerializer } from './serializer' diff --git a/packages/solid-start-client/src/json.ts b/packages/solid-start-client/src/json.ts new file mode 100644 index 0000000000..ba716bc922 --- /dev/null +++ b/packages/solid-start-client/src/json.ts @@ -0,0 +1,15 @@ +import { mergeHeaders } from './headers' +import type { JsonResponse } from './createServerFn' + +export function json( + payload: TData, + init?: ResponseInit, +): JsonResponse { + return new Response(JSON.stringify(payload), { + ...init, + headers: mergeHeaders( + { 'content-type': 'application/json' }, + init?.headers, + ), + }) +} diff --git a/packages/solid-start-client/src/registerGlobalMiddleware.ts b/packages/solid-start-client/src/registerGlobalMiddleware.ts new file mode 100644 index 0000000000..3d7effc85b --- /dev/null +++ b/packages/solid-start-client/src/registerGlobalMiddleware.ts @@ -0,0 +1,9 @@ +import type { AnyMiddleware } from './createMiddleware' + +export const globalMiddleware: Array = [] + +export function registerGlobalMiddleware(options: { + middleware: Array +}) { + globalMiddleware.push(...options.middleware) +} diff --git a/packages/solid-start-client/src/renderRSC.tsx b/packages/solid-start-client/src/renderRSC.tsx new file mode 100644 index 0000000000..6201bfa476 --- /dev/null +++ b/packages/solid-start-client/src/renderRSC.tsx @@ -0,0 +1,91 @@ +// TODO: RSCs +// // @ts-expect-error +// import * as reactDom from '@vinxi/react-server-dom/client' +import { isValidElement } from 'react' +import invariant from 'tiny-invariant' +import type React from 'react' + +export function renderRsc(input: any): React.JSX.Element { + if (isValidElement(input)) { + return input + } + + if (typeof input === 'object' && !input.state) { + input.state = { + status: 'pending', + promise: Promise.resolve() + .then(() => { + let element + + // We're in node + // TODO: RSCs + // if (reactDom.createFromNodeStream) { + // const stream = await import('node:stream') + + // let body: any = input + + // // Unwrap the response + // if (input instanceof Response) { + // body = input.body + // } + + // // Convert ReadableStream to NodeJS stream.Readable + // if (body instanceof ReadableStream) { + // body = stream.Readable.fromWeb(body as any) + // } + + // if (stream.Readable.isReadable(body)) { + // // body = copyStreamToRaw(body) + // } else if (input.text) { + // // create a readable stream by awaiting the text method + // body = new stream.Readable({ + // async read() { + // input.text().then((value: any) => { + // this.push(value) + // this.push(null) + // }) + // }, + // }) + // } else { + // console.error('input', input) + // throw new Error('Unexpected rsc input type 👆') + // } + + // element = await reactDom.createFromNodeStream(body) + // } else { + // // We're in the browser + // if (input.body instanceof ReadableStream) { + // input = input.body + // } + + // if (input instanceof ReadableStream) { + // element = await reactDom.createFromReadableStream(input) + // } + + // if (input instanceof Response) { + // // copy to the response body to cache the raw data + // element = await reactDom.createFromFetch(input) + // } + // } + + // return element + + invariant(false, 'renderRSC() is coming soon!') + }) + .then((element) => { + input.state.value = element + input.state.status = 'success' + }) + .catch((err) => { + input.state.status = 'error' + input.state.error = err + }), + } + } + + if (input.state.status === 'pending') { + throw input.state.promise + } + + return input.state.value +} diff --git a/packages/solid-start-client/src/routesManifest.ts b/packages/solid-start-client/src/routesManifest.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/solid-start-client/src/serializer.ts b/packages/solid-start-client/src/serializer.ts new file mode 100644 index 0000000000..24a9b315c9 --- /dev/null +++ b/packages/solid-start-client/src/serializer.ts @@ -0,0 +1,177 @@ +import { isPlainObject } from '@tanstack/solid-router' +import type { StartSerializer } from '@tanstack/solid-router' + +export const startSerializer: StartSerializer = { + stringify: (value: any) => + JSON.stringify(value, function replacer(key, val) { + const ogVal = this[key] + const serializer = serializers.find((t) => t.stringifyCondition(ogVal)) + + if (serializer) { + return serializer.stringify(ogVal) + } + + return val + }), + parse: (value: string) => + JSON.parse(value, function parser(key, val) { + const ogVal = this[key] + if (isPlainObject(ogVal)) { + const serializer = serializers.find((t) => t.parseCondition(ogVal)) + + if (serializer) { + return serializer.parse(ogVal) + } + } + + return val + }), + encode: (value: any) => { + // When encoding, dive first + if (Array.isArray(value)) { + return value.map((v) => startSerializer.encode(v)) + } + + if (isPlainObject(value)) { + return Object.fromEntries( + Object.entries(value).map(([key, v]) => [ + key, + startSerializer.encode(v), + ]), + ) + } + + const serializer = serializers.find((t) => t.stringifyCondition(value)) + if (serializer) { + return serializer.stringify(value) + } + + return value + }, + decode: (value: any) => { + // Attempt transform first + if (isPlainObject(value)) { + const serializer = serializers.find((t) => t.parseCondition(value)) + if (serializer) { + return serializer.parse(value) + } + } + + if (Array.isArray(value)) { + return value.map((v) => startSerializer.decode(v)) + } + + if (isPlainObject(value)) { + return Object.fromEntries( + Object.entries(value).map(([key, v]) => [ + key, + startSerializer.decode(v), + ]), + ) + } + + return value + }, +} + +const createSerializer = ( + key: TKey, + check: (value: any) => value is TInput, + toValue: (value: TInput) => TSerialized, + fromValue: (value: TSerialized) => TInput, +) => ({ + 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 +// Make sure to keep DefaultSerializable in sync with these serializers +// Also, make sure that they are unit tested in serializer.test.tsx +const serializers = [ + createSerializer( + // Key + 'undefined', + // Check + (v): v is undefined => v === undefined, + // To + () => 0, + // From + () => undefined, + ), + createSerializer( + // Key + 'date', + // Check + (v): v is Date => v instanceof Date, + // To + (v) => v.toISOString(), + // From + (v) => new Date(v), + ), + createSerializer( + // Key + 'error', + // Check + (v): v is Error => v instanceof Error, + // To + (v) => ({ + ...v, + message: v.message, + stack: process.env.NODE_ENV === 'development' ? v.stack : undefined, + cause: v.cause, + }), + // From + (v) => Object.assign(new Error(v.message), v), + ), + createSerializer( + // Key + 'formData', + // Check + (v): v is FormData => v instanceof FormData, + // To + (v) => { + const entries: Record< + string, + Array | FormDataEntryValue + > = {} + v.forEach((value, key) => { + const entry = entries[key] + if (entry !== undefined) { + if (Array.isArray(entry)) { + entry.push(value) + } else { + entries[key] = [entry, value] + } + } else { + entries[key] = value + } + }) + return entries + }, + // From + (v) => { + const formData = new FormData() + Object.entries(v).forEach(([key, value]) => { + if (Array.isArray(value)) { + value.forEach((val) => formData.append(key, val)) + } else { + formData.append(key, value) + } + }) + return formData + }, + ), + createSerializer( + // Key + 'bigint', + // Check + (v): v is bigint => typeof v === 'bigint', + // To + (v) => v.toString(), + // From + (v) => BigInt(v), + ), +] as const diff --git a/packages/solid-start-client/src/ssr-client.tsx b/packages/solid-start-client/src/ssr-client.tsx new file mode 100644 index 0000000000..2cb95914d0 --- /dev/null +++ b/packages/solid-start-client/src/ssr-client.tsx @@ -0,0 +1,221 @@ +import { isPlainObject } from '@tanstack/solid-router' + +import invariant from 'tiny-invariant' + +import { startSerializer } from './serializer' +import type { + AnyRouter, + ControllablePromise, + DeferredPromiseState, + MakeRouteMatch, + Manifest, +} from '@tanstack/solid-router' + +declare global { + interface Window { + __TSR_SSR__?: StartSsrGlobal + } +} + +export interface StartSsrGlobal { + matches: Array + streamedValues: Record< + string, + { + value: any + parsed: any + } + > + cleanScripts: () => void + dehydrated?: any + initMatch: (match: SsrMatch) => void + resolvePromise: (opts: { + matchId: string + id: number + promiseState: DeferredPromiseState + }) => void + injectChunk: (opts: { matchId: string; id: number; chunk: string }) => void + closeStream: (opts: { matchId: string; id: number }) => void +} + +export interface SsrMatch { + id: string + __beforeLoadContext: string + loaderData?: string + error?: string + extracted?: Array + updatedAt: MakeRouteMatch['updatedAt'] + status: MakeRouteMatch['status'] +} + +export type ClientExtractedEntry = + | ClientExtractedStream + | ClientExtractedPromise + +export interface ClientExtractedPromise extends ClientExtractedBaseEntry { + type: 'promise' + value?: ControllablePromise +} + +export interface ClientExtractedStream extends ClientExtractedBaseEntry { + type: 'stream' + value?: ReadableStream & { controller?: ReadableStreamDefaultController } +} + +export interface ClientExtractedBaseEntry { + type: string + path: Array +} + +export interface ResolvePromiseState { + matchId: string + id: number + promiseState: DeferredPromiseState +} + +export interface DehydratedRouter { + manifest: Manifest | undefined + dehydratedData: any +} + +export function hydrate(router: AnyRouter) { + invariant( + window.__TSR_SSR__?.dehydrated, + 'Expected to find a dehydrated data on window.__TSR_SSR__.dehydrated... but we did not. Please file an issue!', + ) + + const { manifest, dehydratedData } = startSerializer.parse( + window.__TSR_SSR__.dehydrated, + ) as DehydratedRouter + + router.ssr = { + manifest, + serializer: startSerializer, + } + + router.clientSsr = { + getStreamedValue: (key: string): T | undefined => { + if (router.isServer) { + return undefined + } + + const streamedValue = window.__TSR_SSR__?.streamedValues[key] + + if (!streamedValue) { + return + } + + if (!streamedValue.parsed) { + streamedValue.parsed = router.ssr!.serializer.parse(streamedValue.value) + } + + return streamedValue.parsed + }, + } + + // Hydrate the router state + const matches = router.matchRoutes(router.state.location) + // kick off loading the route chunks + const routeChunkPromise = Promise.all( + matches.map((match) => { + const route = router.looseRoutesById[match.routeId]! + return router.loadRouteChunk(route) + }), + ) + matches.forEach((match) => { + const route = router.looseRoutesById[match.routeId]! + + // Right after hydration and before the first render, we need to rehydrate each match + // This includes rehydrating the loaderData and also using the beforeLoadContext + // to reconstruct any context that was serialized on the server + + const dehydratedMatch = window.__TSR_SSR__!.matches.find( + (d) => d.id === match.id, + ) + + if (dehydratedMatch) { + Object.assign(match, dehydratedMatch) + + const parentMatch = matches[match.index - 1] + const parentContext = parentMatch?.context ?? router.options.context ?? {} + + // Handle beforeLoadContext + if (dehydratedMatch.__beforeLoadContext) { + match.__beforeLoadContext = router.ssr!.serializer.parse( + dehydratedMatch.__beforeLoadContext, + ) as any + + match.context = { + ...parentContext, + ...match.__routeContext, + ...match.__beforeLoadContext, + } + } + + // Handle loaderData + if (dehydratedMatch.loaderData) { + match.loaderData = router.ssr!.serializer.parse( + dehydratedMatch.loaderData, + ) + } + + // Handle error + if (dehydratedMatch.error) { + match.error = router.ssr!.serializer.parse(dehydratedMatch.error) + } + + // Handle extracted + ;(match as unknown as SsrMatch).extracted?.forEach((ex) => { + deepMutableSetByPath(match, ['loaderData', ...ex.path], ex.value) + }) + } else { + Object.assign(match, { + status: 'success', + updatedAt: Date.now(), + }) + } + + const assetContext = { + matches: router.state.matches, + match, + params: match.params, + loaderData: match.loaderData, + } + const headFnContent = route.options.head?.(assetContext) + + const scripts = route.options.scripts?.(assetContext) + + match.meta = headFnContent?.meta + match.links = headFnContent?.links + match.headScripts = headFnContent?.scripts + match.scripts = scripts + + return match + }) + + router.__store.setState((s) => { + return { + ...s, + matches, + } + }) + + // Allow the user to handle custom hydration data + router.options.hydrate?.(dehydratedData) + return routeChunkPromise +} + +function deepMutableSetByPath(obj: T, path: Array, value: any) { + // mutable set by path retaining array and object references + if (path.length === 1) { + ;(obj as any)[path[0]!] = value + } + + const [key, ...rest] = path + + if (Array.isArray(obj)) { + deepMutableSetByPath(obj[Number(key)], rest, value) + } else if (isPlainObject(obj)) { + deepMutableSetByPath((obj as any)[key!], rest, value) + } +} diff --git a/packages/solid-start-client/src/tests/createIsomorphicFn.test-d.ts b/packages/solid-start-client/src/tests/createIsomorphicFn.test-d.ts new file mode 100644 index 0000000000..89f427d8c6 --- /dev/null +++ b/packages/solid-start-client/src/tests/createIsomorphicFn.test-d.ts @@ -0,0 +1,72 @@ +import { expectTypeOf, test } from 'vitest' +import { createIsomorphicFn } from '../createIsomorphicFn' + +test('createIsomorphicFn with no implementations', () => { + const fn = createIsomorphicFn() + + expectTypeOf(fn).toBeCallableWith() + expectTypeOf(fn).returns.toBeUndefined() + + expectTypeOf(fn).toHaveProperty('server') + expectTypeOf(fn).toHaveProperty('client') +}) + +test('createIsomorphicFn with server implementation', () => { + const fn = createIsomorphicFn().server(() => 'data') + + expectTypeOf(fn).toBeCallableWith() + expectTypeOf(fn).returns.toEqualTypeOf() + + expectTypeOf(fn).toHaveProperty('client') + expectTypeOf(fn).not.toHaveProperty('server') +}) + +test('createIsomorphicFn with client implementation', () => { + const fn = createIsomorphicFn().client(() => 'data') + + expectTypeOf(fn).toBeCallableWith() + expectTypeOf(fn).returns.toEqualTypeOf() + + expectTypeOf(fn).toHaveProperty('server') + expectTypeOf(fn).not.toHaveProperty('client') +}) + +test('createIsomorphicFn with server and client implementation', () => { + const fn = createIsomorphicFn() + .server(() => 'data') + .client(() => 'data') + + expectTypeOf(fn).toBeCallableWith() + expectTypeOf(fn).returns.toEqualTypeOf() + + expectTypeOf(fn).not.toHaveProperty('server') + expectTypeOf(fn).not.toHaveProperty('client') +}) + +test('createIsomorphicFn with varying returns', () => { + const fn = createIsomorphicFn() + .server(() => 'data') + .client(() => 1) + expectTypeOf(fn).toBeCallableWith() + expectTypeOf(fn).returns.toEqualTypeOf() +}) + +test('createIsomorphicFn with arguments', () => { + const fn = createIsomorphicFn() + .server((a: number, b: string) => 'data') + .client((...args) => { + expectTypeOf(args).toEqualTypeOf<[number, string]>() + return 1 + }) + expectTypeOf(fn).toBeCallableWith(1, 'a') + expectTypeOf(fn).returns.toEqualTypeOf() + + const fn2 = createIsomorphicFn() + .client((a: number, b: string) => 'data') + .server((...args) => { + expectTypeOf(args).toEqualTypeOf<[number, string]>() + return 1 + }) + expectTypeOf(fn2).toBeCallableWith(1, 'a') + expectTypeOf(fn2).returns.toEqualTypeOf() +}) diff --git a/packages/solid-start-client/src/tests/createServerFn.test-d.tsx b/packages/solid-start-client/src/tests/createServerFn.test-d.tsx new file mode 100644 index 0000000000..4ae3396ab6 --- /dev/null +++ b/packages/solid-start-client/src/tests/createServerFn.test-d.tsx @@ -0,0 +1,390 @@ +import { expectTypeOf, test } from 'vitest' +import { createServerFn } from '../createServerFn' +import { createMiddleware } from '../createMiddleware' +import type { Constrain, Validator } from '@tanstack/solid-router' + +test('createServerFn method with autocomplete', () => { + createServerFn().handler((options) => { + expectTypeOf(options.method).toEqualTypeOf<'GET' | 'POST'>() + }) +}) + +test('createServerFn without middleware', () => { + expectTypeOf(createServerFn()).toHaveProperty('handler') + expectTypeOf(createServerFn()).toHaveProperty('middleware') + expectTypeOf(createServerFn()).toHaveProperty('validator') + + createServerFn({ method: 'GET' }).handler((options) => { + expectTypeOf(options).toEqualTypeOf<{ + method: 'GET' + context: undefined + data: undefined + signal: AbortSignal + }>() + }) +}) + +test('createServerFn with validator', () => { + const fnAfterValidator = createServerFn({ method: 'GET' }).validator( + (input: { input: string }) => ({ + a: input.input, + }), + ) + + expectTypeOf(fnAfterValidator).toHaveProperty('handler') + expectTypeOf(fnAfterValidator).toHaveProperty('middleware') + expectTypeOf(fnAfterValidator).not.toHaveProperty('validator') + + const fn = fnAfterValidator.handler((options) => { + expectTypeOf(options).toEqualTypeOf<{ + method: 'GET' + context: undefined + data: { + a: string + } + signal: AbortSignal + }>() + }) + + expectTypeOf(fn).parameter(0).toEqualTypeOf<{ + data: { input: string } + headers?: HeadersInit + type?: 'static' | 'dynamic' + fullResponse?: boolean + signal?: AbortSignal + }>() + + expectTypeOf(fn).returns.resolves.toEqualTypeOf() +}) + +test('createServerFn with middleware and context', () => { + const middleware1 = createMiddleware().server(({ next }) => { + return next({ context: { a: 'a' } as const }) + }) + + const middleware2 = createMiddleware().server(({ next }) => { + return next({ context: { b: 'b' } as const }) + }) + + const middleware3 = createMiddleware() + .middleware([middleware1, middleware2]) + .client(({ next }) => { + return next({ context: { c: 'c' } as const }) + }) + + const middleware4 = createMiddleware() + .middleware([middleware3]) + .client(({ context, next }) => { + return next({ sendContext: context }) + }) + .server(({ context, next }) => { + expectTypeOf(context).toEqualTypeOf<{ + readonly a: 'a' + readonly b: 'b' + readonly c: 'c' + }>() + return next({ context: { d: 'd' } as const }) + }) + + const fnWithMiddleware = createServerFn({ method: 'GET' }).middleware([ + middleware4, + ]) + + expectTypeOf(fnWithMiddleware).toHaveProperty('handler') + expectTypeOf(fnWithMiddleware).toHaveProperty('validator') + expectTypeOf(fnWithMiddleware).not.toHaveProperty('middleware') + + fnWithMiddleware.handler((options) => { + expectTypeOf(options).toEqualTypeOf<{ + method: 'GET' + context: { + readonly a: 'a' + readonly b: 'b' + readonly c: 'c' + readonly d: 'd' + } + data: undefined + signal: AbortSignal + }>() + }) +}) + +test('createServerFn with middleware and validator', () => { + const middleware1 = createMiddleware().validator( + (input: { readonly inputA: 'inputA' }) => + ({ + outputA: 'outputA', + }) as const, + ) + + const middleware2 = createMiddleware().validator( + (input: { readonly inputB: 'inputB' }) => + ({ + outputB: 'outputB', + }) as const, + ) + + const middleware3 = createMiddleware().middleware([middleware1, middleware2]) + + const fn = createServerFn({ method: 'GET' }) + .middleware([middleware3]) + .validator( + (input: { readonly inputC: 'inputC' }) => + ({ + outputC: 'outputC', + }) as const, + ) + .handler((options) => { + expectTypeOf(options).toEqualTypeOf<{ + method: 'GET' + context: undefined + data: { + readonly outputA: 'outputA' + readonly outputB: 'outputB' + readonly outputC: 'outputC' + } + signal: AbortSignal + }>() + + return 'data' as const + }) + + expectTypeOf(fn).parameter(0).toEqualTypeOf<{ + data: { + readonly inputA: 'inputA' + readonly inputB: 'inputB' + readonly inputC: 'inputC' + } + headers?: HeadersInit + type?: 'static' | 'dynamic' + fullResponse?: boolean + signal?: AbortSignal + }>() + + expectTypeOf(fn).returns.resolves.toEqualTypeOf<'data'>() + + expectTypeOf(() => + fn({ + fullResponse: false, + data: { inputA: 'inputA', inputB: 'inputB', inputC: 'inputC' }, + }), + ).returns.resolves.toEqualTypeOf<'data'>() + + expectTypeOf(() => + fn({ + fullResponse: true, + data: { inputA: 'inputA', inputB: 'inputB', inputC: 'inputC' }, + }), + ).returns.resolves.toEqualTypeOf<{ + result: 'data' + context: undefined + error: unknown + }>() +}) + +test('createServerFn overrides properties', () => { + const middleware1 = createMiddleware() + .validator( + () => + ({ + input: 'a' as 'a' | 'b' | 'c', + }) as const, + ) + .client(({ context, next }) => { + expectTypeOf(context).toEqualTypeOf() + + const newContext = { context: 'a' } as const + return next({ sendContext: newContext, context: newContext }) + }) + .server(({ data, context, next }) => { + expectTypeOf(data).toEqualTypeOf<{ readonly input: 'a' | 'b' | 'c' }>() + + expectTypeOf(context).toEqualTypeOf<{ + readonly context: 'a' + }>() + + const newContext = { context: 'b' } as const + return next({ sendContext: newContext, context: newContext }) + }) + + const middleware2 = createMiddleware() + .middleware([middleware1]) + .validator( + () => + ({ + input: 'b' as 'b' | 'c', + }) as const, + ) + .client(({ context, next }) => { + expectTypeOf(context).toEqualTypeOf<{ readonly context: 'a' }>() + + const newContext = { context: 'aa' } as const + + return next({ sendContext: newContext, context: newContext }) + }) + .server(({ context, next }) => { + expectTypeOf(context).toEqualTypeOf<{ readonly context: 'aa' }>() + + const newContext = { context: 'bb' } as const + + return next({ sendContext: newContext, context: newContext }) + }) + + createServerFn() + .middleware([middleware2]) + .validator( + () => + ({ + input: 'c', + }) as const, + ) + .handler(({ data, context }) => { + expectTypeOf(data).toEqualTypeOf<{ + readonly input: 'c' + }>() + expectTypeOf(context).toEqualTypeOf<{ readonly context: 'bb' }>() + }) +}) + +test('createServerFn where validator is a primitive', () => { + createServerFn({ method: 'GET' }) + .validator(() => 'c' as const) + .handler((options) => { + expectTypeOf(options).toEqualTypeOf<{ + method: 'GET' + context: undefined + data: 'c' + signal: AbortSignal + }>() + }) +}) + +test('createServerFn where validator is optional if object is optional', () => { + const fn = createServerFn({ method: 'GET' }) + .validator((input: 'c' | undefined) => input) + .handler((options) => { + expectTypeOf(options).toEqualTypeOf<{ + method: 'GET' + context: undefined + data: 'c' | undefined + signal: AbortSignal + }>() + }) + + expectTypeOf(fn).parameter(0).toEqualTypeOf< + | { + data?: 'c' | undefined + headers?: HeadersInit + type?: 'static' | 'dynamic' + fullResponse?: boolean + signal?: AbortSignal + } + | undefined + >() + + expectTypeOf(fn).returns.resolves.toEqualTypeOf() +}) + +test('createServerFn where data is optional if there is no validator', () => { + const fn = createServerFn({ method: 'GET' }).handler((options) => { + expectTypeOf(options).toEqualTypeOf<{ + method: 'GET' + context: undefined + data: undefined + signal: AbortSignal + }>() + }) + + expectTypeOf(fn).parameter(0).toEqualTypeOf< + | { + data?: undefined + headers?: HeadersInit + type?: 'static' | 'dynamic' + fullResponse?: boolean + signal?: AbortSignal + } + | undefined + >() + + expectTypeOf(fn).returns.resolves.toEqualTypeOf() +}) + +test('createServerFn returns Date', () => { + const fn = createServerFn().handler(() => ({ + dates: [new Date(), new Date()] as const, + })) + + expectTypeOf(fn()).toEqualTypeOf>() +}) + +test('createServerFn returns RSC', () => { + const fn = createServerFn().handler(() => ({ + rscs: [ +
I'm an RSC
, +
I'm an RSC
, + ] as const, + })) + + expectTypeOf(fn()).toEqualTypeOf< + Promise<{ rscs: readonly [ReadableStream, ReadableStream] }> + >() +}) + +test('createServerFn returns undefined', () => { + const fn = createServerFn().handler(() => ({ + nothing: undefined, + })) + + expectTypeOf(fn()).toEqualTypeOf>() +}) + +test('createServerFn cannot return function', () => { + expectTypeOf(createServerFn().handler<{ func: () => 'func' }>) + .parameter(0) + .returns.toEqualTypeOf< + | { func: 'Function is not serializable' } + | Promise<{ func: 'Function is not serializable' }> + >() +}) + +test('createServerFn cannot validate function', () => { + const validator = createServerFn().validator< + (input: { func: () => 'string' }) => { output: 'string' } + > + + expectTypeOf(validator) + .parameter(0) + .toEqualTypeOf< + Constrain< + (input: { func: () => 'string' }) => { output: 'string' }, + Validator<{ func: 'Function is not serializable' }, any> + > + >() +}) + +test('createServerFn can validate Date', () => { + const validator = createServerFn().validator< + (input: Date) => { output: 'string' } + > + + expectTypeOf(validator) + .parameter(0) + .toEqualTypeOf< + Constrain<(input: Date) => { output: 'string' }, Validator> + >() +}) + +test('createServerFn can validate FormData', () => { + const validator = createServerFn().validator< + (input: FormData) => { output: 'string' } + > + + expectTypeOf(validator) + .parameter(0) + .toEqualTypeOf< + Constrain< + (input: FormData) => { output: 'string' }, + Validator + > + >() +}) diff --git a/packages/solid-start-client/src/tests/createServerMiddleware.test-d.ts b/packages/solid-start-client/src/tests/createServerMiddleware.test-d.ts new file mode 100644 index 0000000000..f19e92c722 --- /dev/null +++ b/packages/solid-start-client/src/tests/createServerMiddleware.test-d.ts @@ -0,0 +1,611 @@ +import { expectTypeOf, test } from 'vitest' +import { createMiddleware } from '../createMiddleware' +import type { Constrain, Validator } from '@tanstack/solid-router' + +test('createServeMiddleware removes middleware after middleware,', () => { + const middleware = createMiddleware() + + expectTypeOf(middleware).toHaveProperty('middleware') + expectTypeOf(middleware).toHaveProperty('server') + expectTypeOf(middleware).toHaveProperty('validator') + + const middlewareAfterMiddleware = middleware.middleware([]) + + expectTypeOf(middlewareAfterMiddleware).toHaveProperty('validator') + expectTypeOf(middlewareAfterMiddleware).toHaveProperty('server') + expectTypeOf(middlewareAfterMiddleware).not.toHaveProperty('middleware') + + const middlewareAfterInput = middleware.validator(() => {}) + + expectTypeOf(middlewareAfterInput).toHaveProperty('server') + expectTypeOf(middlewareAfterInput).not.toHaveProperty('middleware') + + const middlewareAfterServer = middleware.server(async (options) => { + expectTypeOf(options.context).toEqualTypeOf() + expectTypeOf(options.data).toEqualTypeOf() + expectTypeOf(options.method).toEqualTypeOf<'GET' | 'POST'>() + + const result = await options.next({ + context: { + a: 'a', + }, + }) + + expectTypeOf(result.context).toEqualTypeOf<{ a: string }>() + + expectTypeOf(result.sendContext).toEqualTypeOf() + + return result + }) + + expectTypeOf(middlewareAfterServer).not.toHaveProperty('server') + expectTypeOf(middlewareAfterServer).not.toHaveProperty('input') + expectTypeOf(middlewareAfterServer).not.toHaveProperty('middleware') +}) + +test('createMiddleware merges server context', () => { + const middleware1 = createMiddleware().server(async (options) => { + expectTypeOf(options.context).toEqualTypeOf() + expectTypeOf(options.data).toEqualTypeOf() + expectTypeOf(options.method).toEqualTypeOf<'GET' | 'POST'>() + + const result = await options.next({ context: { a: true } }) + + expectTypeOf(result).toEqualTypeOf<{ + 'use functions must return the result of next()': true + _types: { + context: { + a: boolean + } + sendContext: undefined + } + context: { a: boolean } + sendContext: undefined + }>() + + return result + }) + + const middleware2 = createMiddleware().server(async (options) => { + expectTypeOf(options.context).toEqualTypeOf() + expectTypeOf(options.data).toEqualTypeOf() + expectTypeOf(options.method).toEqualTypeOf<'GET' | 'POST'>() + + const result = await options.next({ context: { b: 'test' } }) + + expectTypeOf(result).toEqualTypeOf<{ + 'use functions must return the result of next()': true + _types: { + context: { + b: string + } + sendContext: undefined + } + context: { b: string } + sendContext: undefined + }>() + + return result + }) + + const middleware3 = createMiddleware() + .middleware([middleware1, middleware2]) + .server(async (options) => { + expectTypeOf(options.context).toEqualTypeOf<{ a: boolean; b: string }>() + + const result = await options.next({ context: { c: 0 } }) + + expectTypeOf(result).toEqualTypeOf<{ + 'use functions must return the result of next()': true + _types: { + context: { + c: number + } + sendContext: undefined + } + context: { a: boolean; b: string; c: number } + sendContext: undefined + }>() + + return result + }) + + createMiddleware() + .middleware([middleware3]) + .server(async (options) => { + expectTypeOf(options.context).toEqualTypeOf<{ + a: boolean + b: string + c: number + }>() + + const result = await options.next({ context: { d: 5 } }) + + expectTypeOf(result).toEqualTypeOf<{ + 'use functions must return the result of next()': true + _types: { + context: { + d: number + } + sendContext: undefined + } + context: { a: boolean; b: string; c: number; d: number } + sendContext: undefined + }>() + + return result + }) +}) + +test('createMiddleware merges client context and sends to the server', () => { + const middleware1 = createMiddleware().client(async (options) => { + expectTypeOf(options.context).toEqualTypeOf() + + const result = await options.next({ context: { a: true } }) + + expectTypeOf(result).toEqualTypeOf<{ + 'use functions must return the result of next()': true + context: { a: boolean } + sendContext: undefined + headers: HeadersInit + }>() + + return result + }) + + const middleware2 = createMiddleware().client(async (options) => { + expectTypeOf(options.context).toEqualTypeOf() + + const result = await options.next({ context: { b: 'test' } }) + + expectTypeOf(result).toEqualTypeOf<{ + 'use functions must return the result of next()': true + context: { b: string } + sendContext: undefined + headers: HeadersInit + }>() + + return result + }) + + const middleware3 = createMiddleware() + .middleware([middleware1, middleware2]) + .client(async (options) => { + expectTypeOf(options.context).toEqualTypeOf<{ a: boolean; b: string }>() + + const result = await options.next({ context: { c: 0 } }) + + expectTypeOf(result).toEqualTypeOf<{ + 'use functions must return the result of next()': true + context: { a: boolean; b: string; c: number } + sendContext: undefined + headers: HeadersInit + }>() + + return result + }) + + const middleware4 = createMiddleware() + .middleware([middleware3]) + .client(async (options) => { + expectTypeOf(options.context).toEqualTypeOf<{ + a: boolean + b: string + c: number + }>() + + const result = await options.next({ + sendContext: { ...options.context, d: 5 }, + }) + + expectTypeOf(result).toEqualTypeOf<{ + 'use functions must return the result of next()': true + context: { a: boolean; b: string; c: number } + sendContext: { a: boolean; b: string; c: number; d: number } + headers: HeadersInit + }>() + + return result + }) + + createMiddleware() + .middleware([middleware4]) + .server(async (options) => { + expectTypeOf(options.context).toEqualTypeOf<{ + a: boolean + b: string + c: number + d: number + }>() + + const result = await options.next({ + context: { + e: 'e', + }, + }) + + expectTypeOf(result).toEqualTypeOf<{ + 'use functions must return the result of next()': true + _types: { + context: { + e: string + } + sendContext: undefined + } + context: { a: boolean; b: string; c: number; d: number; e: string } + sendContext: undefined + }>() + + return result + }) +}) + +test('createMiddleware merges input', () => { + const middleware1 = createMiddleware() + .validator(() => { + return { + a: 'a', + } as const + }) + .server(({ data, next }) => { + expectTypeOf(data).toEqualTypeOf<{ readonly a: 'a' }>() + return next() + }) + + const middleware2 = createMiddleware() + .middleware([middleware1]) + .validator(() => { + return { + b: 'b', + } as const + }) + .server(({ data, next }) => { + expectTypeOf(data).toEqualTypeOf<{ readonly a: 'a'; readonly b: 'b' }> + return next() + }) + + createMiddleware() + .middleware([middleware2]) + .validator(() => ({ c: 'c' }) as const) + .server(({ next, data }) => { + expectTypeOf(data).toEqualTypeOf<{ + readonly a: 'a' + readonly b: 'b' + readonly c: 'c' + }> + return next() + }) +}) + +test('createMiddleware merges server context and client context, sends server context to the client and merges ', () => { + const middleware1 = createMiddleware() + .client(async (options) => { + expectTypeOf(options.context).toEqualTypeOf() + + const result = await options.next({ + context: { fromClient1: 'fromClient1' }, + }) + + expectTypeOf(result).toEqualTypeOf<{ + 'use functions must return the result of next()': true + context: { fromClient1: string } + sendContext: undefined + headers: HeadersInit + }>() + + return result + }) + .server(async (options) => { + expectTypeOf(options.context).toEqualTypeOf() + + const result = await options.next({ + context: { fromServer1: 'fromServer1' }, + }) + + expectTypeOf(result).toEqualTypeOf<{ + 'use functions must return the result of next()': true + _types: { + context: { + fromServer1: string + } + sendContext: undefined + } + context: { fromServer1: string } + sendContext: undefined + }>() + + return result + }) + + const middleware2 = createMiddleware() + .client(async (options) => { + expectTypeOf(options.context).toEqualTypeOf() + + const result = await options.next({ + context: { fromClient2: 'fromClient2' }, + }) + + expectTypeOf(result).toEqualTypeOf<{ + 'use functions must return the result of next()': true + context: { fromClient2: string } + sendContext: undefined + headers: HeadersInit + }>() + + return result + }) + .server(async (options) => { + expectTypeOf(options.context).toEqualTypeOf() + + const result = await options.next({ + context: { fromServer2: 'fromServer2' }, + }) + + expectTypeOf(result).toEqualTypeOf<{ + 'use functions must return the result of next()': true + _types: { + context: { + fromServer2: string + } + sendContext: undefined + } + context: { fromServer2: string } + sendContext: undefined + }>() + + return result + }) + + const middleware3 = createMiddleware() + .middleware([middleware1, middleware2]) + .client(async (options) => { + expectTypeOf(options.context).toEqualTypeOf<{ + fromClient1: string + fromClient2: string + }>() + + const result = await options.next({ + context: { fromClient3: 'fromClient3' }, + }) + + expectTypeOf(result).toEqualTypeOf<{ + 'use functions must return the result of next()': true + context: { + fromClient1: string + fromClient2: string + fromClient3: string + } + sendContext: undefined + headers: HeadersInit + }>() + + return result + }) + .server(async (options) => { + expectTypeOf(options.context).toEqualTypeOf<{ + fromServer1: string + fromServer2: string + }>() + + const result = await options.next({ + context: { fromServer3: 'fromServer3' }, + }) + + expectTypeOf(result).toEqualTypeOf<{ + 'use functions must return the result of next()': true + _types: { + context: { + fromServer3: string + } + sendContext: undefined + } + context: { + fromServer1: string + fromServer2: string + fromServer3: string + } + sendContext: undefined + }>() + + return result + }) + + const middleware4 = createMiddleware() + .middleware([middleware3]) + .client(async (options) => { + expectTypeOf(options.context).toEqualTypeOf<{ + fromClient1: string + fromClient2: string + fromClient3: string + }>() + + const result = await options.next({ + context: { fromClient4: 'fromClient4' }, + sendContext: { toServer1: 'toServer1' }, + }) + + expectTypeOf(result).toEqualTypeOf<{ + 'use functions must return the result of next()': true + context: { + fromClient1: string + fromClient2: string + fromClient3: string + fromClient4: string + } + sendContext: { toServer1: 'toServer1' } + headers: HeadersInit + }>() + + return result + }) + .server(async (options) => { + expectTypeOf(options.context).toEqualTypeOf<{ + fromServer1: string + fromServer2: string + fromServer3: string + toServer1: 'toServer1' + }>() + + const result = await options.next({ + context: { fromServer4: 'fromServer4' }, + sendContext: { toClient1: 'toClient1' }, + }) + + expectTypeOf(result).toEqualTypeOf<{ + 'use functions must return the result of next()': true + _types: { + context: { + fromServer4: string + } + sendContext: { + toClient1: 'toClient1' + } + } + context: { + fromServer1: string + fromServer2: string + fromServer3: string + fromServer4: string + toServer1: 'toServer1' + } + sendContext: { toClient1: 'toClient1' } + }>() + + return result + }) + + createMiddleware() + .middleware([middleware4]) + .client(async (options) => { + expectTypeOf(options.context).toEqualTypeOf<{ + fromClient1: string + fromClient2: string + fromClient3: string + fromClient4: string + }>() + + const result = await options.next({ + context: { fromClient5: 'fromClient5' }, + sendContext: { toServer2: 'toServer2' }, + }) + + expectTypeOf(result).toEqualTypeOf<{ + 'use functions must return the result of next()': true + context: { + fromClient1: string + fromClient2: string + fromClient3: string + fromClient4: string + fromClient5: string + toClient1: 'toClient1' + } + sendContext: { toServer1: 'toServer1'; toServer2: 'toServer2' } + headers: HeadersInit + }>() + + return result + }) + .server(async (options) => { + expectTypeOf(options.context).toEqualTypeOf<{ + fromServer1: string + fromServer2: string + fromServer3: string + fromServer4: string + toServer1: 'toServer1' + toServer2: 'toServer2' + }>() + + const result = await options.next({ + context: { fromServer5: 'fromServer5' }, + sendContext: { toClient2: 'toClient2' }, + }) + + expectTypeOf(result).toEqualTypeOf<{ + 'use functions must return the result of next()': true + _types: { + context: { + fromServer5: string + } + sendContext: { + toClient2: 'toClient2' + } + } + context: { + fromServer1: string + fromServer2: string + fromServer3: string + fromServer4: string + fromServer5: string + toServer1: 'toServer1' + toServer2: 'toServer2' + } + sendContext: { toClient1: 'toClient1'; toClient2: 'toClient2' } + }>() + + return result + }) +}) + +test('createMiddleware sendContext cannot send a function', () => { + createMiddleware() + .client(({ next }) => { + expectTypeOf(next<{ func: () => 'func' }>) + .parameter(0) + .exclude() + .toHaveProperty('sendContext') + .toEqualTypeOf<{ func: 'Function is not serializable' } | undefined>() + + return next() + }) + .server(({ next }) => { + expectTypeOf(next 'func' }>) + .parameter(0) + .exclude() + .toHaveProperty('sendContext') + .toEqualTypeOf<{ func: 'Function is not serializable' } | undefined>() + + return next() + }) +}) + +test('createMiddleware cannot validate function', () => { + const validator = createMiddleware().validator< + (input: { func: () => 'string' }) => { output: 'string' } + > + + expectTypeOf(validator) + .parameter(0) + .toEqualTypeOf< + Constrain< + (input: { func: () => 'string' }) => { output: 'string' }, + Validator<{ func: 'Function is not serializable' }, any> + > + >() +}) + +test('createMiddleware can validate Date', () => { + const validator = createMiddleware().validator< + (input: Date) => { output: 'string' } + > + + expectTypeOf(validator) + .parameter(0) + .toEqualTypeOf< + Constrain<(input: Date) => { output: 'string' }, Validator> + >() +}) + +test('createMiddleware can validate FormData', () => { + const validator = createMiddleware().validator< + (input: FormData) => { output: 'string' } + > + + expectTypeOf(validator) + .parameter(0) + .toEqualTypeOf< + Constrain< + (input: FormData) => { output: 'string' }, + Validator + > + >() +}) diff --git a/packages/solid-start-client/src/tests/envOnly.test-d.ts b/packages/solid-start-client/src/tests/envOnly.test-d.ts new file mode 100644 index 0000000000..ce8ac72c78 --- /dev/null +++ b/packages/solid-start-client/src/tests/envOnly.test-d.ts @@ -0,0 +1,34 @@ +import { expectTypeOf, test } from 'vitest' +import { clientOnly, serverOnly } from '../envOnly' + +const inputFn = () => 'output' + +const genericInputFn = (input: T) => input + +function overloadedFn(input: string): string +function overloadedFn(input: number): number +function overloadedFn(input: any) { + return input +} + +test("clientOnly returns the function it's given", () => { + const outputFn = clientOnly(inputFn) + expectTypeOf(outputFn).toEqualTypeOf() + + const genericOutputFn = clientOnly(genericInputFn) + expectTypeOf(genericOutputFn).toEqualTypeOf() + + const overloadedOutputFn = clientOnly(overloadedFn) + expectTypeOf(overloadedOutputFn).toEqualTypeOf() +}) + +test("serverOnly returns the function it's given", () => { + const outputFn = serverOnly(inputFn) + expectTypeOf(outputFn).toEqualTypeOf() + + const genericOutputFn = serverOnly(genericInputFn) + expectTypeOf(genericOutputFn).toEqualTypeOf() + + const overloadedOutputFn = serverOnly(overloadedFn) + expectTypeOf(overloadedOutputFn).toEqualTypeOf() +}) diff --git a/packages/solid-start-client/src/tests/json.test.ts b/packages/solid-start-client/src/tests/json.test.ts new file mode 100644 index 0000000000..fb9d8ac8ff --- /dev/null +++ b/packages/solid-start-client/src/tests/json.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest' +import { json } from '../json' + +describe('json', () => { + it('sets the content type to application/json and stringifies the data', async () => { + const data = { foo: 'bar' } + const response = json(data) + + expect(response.headers.get('Content-Type')).toBe('application/json') + + const responseClone = response.clone() + await expect(responseClone.text()).resolves.toEqual(JSON.stringify(data)) + + await expect(response.json()).resolves.toEqual(data) + }) + it("doesn't override the content type if it's already set", () => { + const response = json(null, { headers: { 'Content-Type': 'text/plain' } }) + + expect(response.headers.get('Content-Type')).toBe('text/plain') + }) + it('reflects passed status and statusText', () => { + const response = json(null, { status: 404, statusText: 'Not Found' }) + + expect(response.status).toBe(404) + expect(response.statusText).toBe('Not Found') + }) + it.each<[string, HeadersInit]>([ + ['plain object', { 'X-TYPE': 'example' }], + ['array', [['X-TYPE', 'example']]], + ['Headers', new Headers({ 'X-TYPE': 'example' })], + ])('merges headers from %s', (_, headers) => { + const response = json(null, { headers }) + + expect(response.headers.get('X-TYPE')).toBe('example') + expect(response.headers.get('Content-Type')).toBe('application/json') + }) +}) diff --git a/packages/solid-start-client/src/tests/transformer.test.tsx b/packages/solid-start-client/src/tests/transformer.test.tsx new file mode 100644 index 0000000000..32189c5849 --- /dev/null +++ b/packages/solid-start-client/src/tests/transformer.test.tsx @@ -0,0 +1,147 @@ +import { describe, expect, it } from 'vitest' + +import { startSerializer as tf } from '../serializer' + +describe('transformer.stringify', () => { + it('should stringify dates', () => { + const date = new Date('2021-08-19T20:00:00.000Z') + expect(tf.stringify(date)).toMatchInlineSnapshot(` + "{"$date":"2021-08-19T20:00:00.000Z"}" + `) + }) + + it('should stringify undefined', () => { + expect(tf.stringify(undefined)).toMatchInlineSnapshot(`"{"$undefined":0}"`) + }) + + it('should stringify object foo="bar"', () => { + expect(tf.stringify({ foo: 'bar' })).toMatchInlineSnapshot(` + "{"foo":"bar"}" + `) + }) + + it('should stringify object foo=undefined', () => { + expect(tf.stringify({ foo: undefined })).toMatchInlineSnapshot( + `"{"foo":{"$undefined":0}}"`, + ) + }) + + it('should stringify object foo=Date', () => { + const date = new Date('2021-08-19T20:00:00.000Z') + expect(tf.stringify({ foo: date })).toMatchInlineSnapshot(` + "{"foo":{"$date":"2021-08-19T20:00:00.000Z"}}" + `) + }) + + it('should stringify empty FormData', () => { + const formData = new FormData() + expect(tf.stringify(formData)).toMatchInlineSnapshot(`"{"$formData":{}}"`) + }) + + it('should stringify FormData with key-value pairs of foo="bar",name="Sean"', () => { + const formData = new FormData() + formData.append('foo', 'bar') + formData.append('name', 'Sean') + expect(tf.stringify(formData)).toMatchInlineSnapshot( + `"{"$formData":{"foo":"bar","name":"Sean"}}"`, + ) + }) + it('should stringify FormData with multiple values for the same key', () => { + const formData = new FormData() + formData.append('foo', 'bar') + formData.append('foo', 'baz') + expect(tf.stringify(formData)).toMatchInlineSnapshot( + `"{"$formData":{"foo":["bar","baz"]}}"`, + ) + }) + + it('should stringify bigint', () => { + const bigint = BigInt('9007199254740992') + expect(tf.stringify(bigint)).toMatchInlineSnapshot( + `"{"$bigint":"9007199254740992"}"`, + ) + }) + it('should stringify object foo=bigint', () => { + const bigint = BigInt('9007199254740992') + expect(tf.stringify({ foo: bigint })).toMatchInlineSnapshot( + `"{"foo":{"$bigint":"9007199254740992"}}"`, + ) + }) +}) + +describe('transformer.parse', () => { + it('should parse dates', () => { + const date = new Date('2021-08-19T20:00:00.000Z') + const str = tf.stringify(date) + expect(tf.parse(str)).toEqual(date) + }) + + it('should parse undefined', () => { + const str = tf.stringify(undefined) + expect(tf.parse(str)).toBeUndefined() + }) + + it('should parse object foo="bar"', () => { + const obj = { foo: 'bar' } + const str = tf.stringify(obj) + expect(tf.parse(str)).toEqual(obj) + }) + + it('should parse object foo=undefined', () => { + const obj = { foo: undefined } + const str = tf.stringify(obj) + expect(tf.parse(str)).toEqual(obj) + }) + + it('should parse object foo=Date', () => { + const date = new Date('2021-08-19T20:00:00.000Z') + const obj = { foo: date } + const str = tf.stringify(obj) + expect(tf.parse(str)).toEqual(obj) + }) + + it('should parse empty FormData', () => { + const formData = new FormData() + const str = tf.stringify(formData) + const parsed = tf.parse(str) as FormData + expect(parsed).toBeInstanceOf(FormData) + expect([...parsed.entries()]).toEqual([]) + }) + + it('should parse FormData with key-value pairs of foo="bar",name="Sean"', () => { + const formData = new FormData() + formData.append('foo', 'bar') + formData.append('name', 'Sean') + const str = tf.stringify(formData) + const parsed = tf.parse(str) as FormData + expect(parsed).toBeInstanceOf(FormData) + expect([...parsed.entries()]).toEqual([ + ['foo', 'bar'], + ['name', 'Sean'], + ]) + }) + it('should parse FormData with multiple values for the same key', () => { + const formData = new FormData() + formData.append('foo', 'bar') + formData.append('foo', 'baz') + const str = tf.stringify(formData) + const parsed = tf.parse(str) as FormData + expect(parsed).toBeInstanceOf(FormData) + expect([...parsed.entries()]).toEqual([ + ['foo', 'bar'], + ['foo', 'baz'], + ]) + }) + + it('should parse bigint', () => { + const bigint = BigInt('9007199254740992') + const str = tf.stringify(bigint) + expect(tf.parse(str)).toEqual(bigint) + }) + it('should parse object foo=bigint', () => { + const bigint = BigInt('9007199254740992') + const obj = { foo: bigint } + const str = tf.stringify(obj) + expect(tf.parse(str)).toEqual(obj) + }) +}) diff --git a/packages/solid-start-client/src/useServerFn.ts b/packages/solid-start-client/src/useServerFn.ts new file mode 100644 index 0000000000..a2b91060a9 --- /dev/null +++ b/packages/solid-start-client/src/useServerFn.ts @@ -0,0 +1,30 @@ +import { isRedirect, useRouter } from '@tanstack/solid-router' + +export function useServerFn) => Promise>( + serverFn: T, +): (...args: Parameters) => ReturnType { + const router = useRouter() + + return (async (...args: Array) => { + try { + const res = await serverFn(...args) + + if (isRedirect(res)) { + throw res + } + + return res + } catch (err) { + if (isRedirect(err)) { + const resolvedRedirect = router.resolveRedirect({ + ...err, + _fromLocation: router.state.location, + }) + + return router.navigate(resolvedRedirect) + } + + throw err + } + }) as any +} diff --git a/packages/solid-start-client/tsconfig.json b/packages/solid-start-client/tsconfig.json new file mode 100644 index 0000000000..fdda84d59f --- /dev/null +++ b/packages/solid-start-client/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "module": "esnext" + }, + "include": ["src", "vite.config.ts", "src/tsrScript.ts"] +} diff --git a/packages/solid-start-client/vite.config.ts b/packages/solid-start-client/vite.config.ts new file mode 100644 index 0000000000..231533c36b --- /dev/null +++ b/packages/solid-start-client/vite.config.ts @@ -0,0 +1,22 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import { tanstackViteConfig } from '@tanstack/config/vite' +import solid from 'vite-plugin-solid' +import packageJson from './package.json' +import type { ViteUserConfig } from 'vitest/config' + +const config = defineConfig({ + plugins: [solid()] as ViteUserConfig['plugins'], + test: { + name: packageJson.name, + watch: false, + environment: 'jsdom', + }, +}) + +export default mergeConfig( + config, + tanstackViteConfig({ + srcDir: './src', + entry: './src/index.tsx', + }), +) From 0d96c2f3ad97fbdc327c3491569ab2ae8fe10562 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Sat, 22 Feb 2025 12:46:48 +0100 Subject: [PATCH 002/124] solid-start-server --- packages/solid-start-server/README.md | 33 ++ packages/solid-start-server/eslint.config.js | 31 ++ packages/solid-start-server/package.json | 83 +++ .../solid-start-server/src/StartServer.tsx | 8 + .../src/createRequestHandler.ts | 75 +++ .../src/createStartHandler.ts | 82 +++ .../src/defaultRenderHandler.tsx | 25 + .../src/defaultStreamHandler.tsx | 70 +++ .../solid-start-server/src/handlerCallback.ts | 22 + packages/solid-start-server/src/index.tsx | 508 ++++++++++++++++++ packages/solid-start-server/src/ssr-server.ts | 350 ++++++++++++ .../src/transformStreamWithRouter.ts | 258 +++++++++ packages/solid-start-server/src/tsrScript.ts | 91 ++++ packages/solid-start-server/src/vite-env.d.ts | 4 + .../tests/h3-wrappers.test-d.ts | 350 ++++++++++++ packages/solid-start-server/tsconfig.json | 14 + .../solid-start-server/vite-minify-plugin.ts | 25 + packages/solid-start-server/vite.config.ts | 23 + 18 files changed, 2052 insertions(+) create mode 100644 packages/solid-start-server/README.md create mode 100644 packages/solid-start-server/eslint.config.js create mode 100644 packages/solid-start-server/package.json create mode 100644 packages/solid-start-server/src/StartServer.tsx create mode 100644 packages/solid-start-server/src/createRequestHandler.ts create mode 100644 packages/solid-start-server/src/createStartHandler.ts create mode 100644 packages/solid-start-server/src/defaultRenderHandler.tsx create mode 100644 packages/solid-start-server/src/defaultStreamHandler.tsx create mode 100644 packages/solid-start-server/src/handlerCallback.ts create mode 100644 packages/solid-start-server/src/index.tsx create mode 100644 packages/solid-start-server/src/ssr-server.ts create mode 100644 packages/solid-start-server/src/transformStreamWithRouter.ts create mode 100644 packages/solid-start-server/src/tsrScript.ts create mode 100644 packages/solid-start-server/src/vite-env.d.ts create mode 100644 packages/solid-start-server/tests/h3-wrappers.test-d.ts create mode 100644 packages/solid-start-server/tsconfig.json create mode 100644 packages/solid-start-server/vite-minify-plugin.ts create mode 100644 packages/solid-start-server/vite.config.ts diff --git a/packages/solid-start-server/README.md b/packages/solid-start-server/README.md new file mode 100644 index 0000000000..fd6e98ac6d --- /dev/null +++ b/packages/solid-start-server/README.md @@ -0,0 +1,33 @@ +> 🤫 we're cooking up something special! + + + +# TanStack Start + +![TanStack Router Header](https://github.com/tanstack/router/raw/main/media/header.png) + +🤖 Type-safe router w/ built-in caching & URL state management for React! + + + #TanStack + + + + + + + + semantic-release + + Join the discussion on Github +Best of JS + + + + + + + +Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [React Query](https://github.com/tannerlinsley/react-query), [React Table](https://github.com/tanstack/react-table), [React Charts](https://github.com/tannerlinsley/react-charts), [React Virtual](https://github.com/tannerlinsley/react-virtual) + +## Visit [tanstack.com/router](https://tanstack.com/router) for docs, guides, API and more! diff --git a/packages/solid-start-server/eslint.config.js b/packages/solid-start-server/eslint.config.js new file mode 100644 index 0000000000..931f0ec774 --- /dev/null +++ b/packages/solid-start-server/eslint.config.js @@ -0,0 +1,31 @@ +// @ts-check + +import pluginReact from '@eslint-react/eslint-plugin' +import pluginReactHooks from 'eslint-plugin-react-hooks' +import rootConfig from '../../eslint.config.js' + +export default [ + ...rootConfig, + { + ...pluginReact.configs.recommended, + files: ['**/*.{ts,tsx}'], + }, + { + plugins: { + 'react-hooks': pluginReactHooks, + }, + rules: { + '@eslint-react/no-unstable-context-value': 'off', + '@eslint-react/no-unstable-default-props': 'off', + '@eslint-react/dom/no-missing-button-type': 'off', + 'react-hooks/exhaustive-deps': 'error', + 'react-hooks/rules-of-hooks': 'error', + }, + }, + { + files: ['**/__tests__/**'], + rules: { + '@typescript-eslint/no-unnecessary-condition': 'off', + }, + }, +] diff --git a/packages/solid-start-server/package.json b/packages/solid-start-server/package.json new file mode 100644 index 0000000000..67cea9887b --- /dev/null +++ b/packages/solid-start-server/package.json @@ -0,0 +1,83 @@ +{ + "name": "@tanstack/solid-start-server", + "version": "1.109.2", + "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/solid-start-server" + }, + "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": "exit 0; 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", + "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/solid-cross-context": "workspace:^", + "@tanstack/solid-router": "workspace:^", + "@tanstack/start-client": "workspace:^", + "h3": "1.13.0", + "isbot": "^5.1.22", + "jsesc": "^3.1.0", + "unctx": "^2.4.1" + }, + "devDependencies": { + "vite-plugin-solid": "^2.11.2", + "@types/jsesc": "^3.0.3", + "esbuild": "^0.25.0", + "solid-js": "^1.0.0", + "typescript": "^5.7.2" + }, + "peerDependencies": { + "solid-js": "^1.0.0" + } +} diff --git a/packages/solid-start-server/src/StartServer.tsx b/packages/solid-start-server/src/StartServer.tsx new file mode 100644 index 0000000000..8f1d54b6b7 --- /dev/null +++ b/packages/solid-start-server/src/StartServer.tsx @@ -0,0 +1,8 @@ +import { RouterProvider } from '@tanstack/solid-router' +import type { AnyRouter } from '@tanstack/solid-router' + +export function StartServer(props: { + router: TRouter +}) { + return +} diff --git a/packages/solid-start-server/src/createRequestHandler.ts b/packages/solid-start-server/src/createRequestHandler.ts new file mode 100644 index 0000000000..c5156296b5 --- /dev/null +++ b/packages/solid-start-server/src/createRequestHandler.ts @@ -0,0 +1,75 @@ +import { createMemoryHistory } from '@tanstack/solid-router' +import { mergeHeaders } from '@tanstack/start-client' +import { attachRouterServerSsrUtils, dehydrateRouter } from './ssr-server' +import type { HandlerCallback } from './handlerCallback' +import type { AnyRouter, Manifest } from '@tanstack/solid-router' + +export type RequestHandler = ( + cb: HandlerCallback, +) => Promise + +export function createRequestHandler({ + createRouter, + request, + getRouterManifest, +}: { + createRouter: () => TRouter + request: Request + getRouterManifest?: () => Manifest +}): RequestHandler { + return async (cb) => { + const router = createRouter() + + attachRouterServerSsrUtils(router, getRouterManifest?.()) + + const url = new URL(request.url, 'http://localhost') + + const href = url.href.replace(url.origin, '') + + // Create a history for the router + const history = createMemoryHistory({ + initialEntries: [href], + }) + + // Update the router with the history and context + router.update({ + history, + }) + + await router.load() + + dehydrateRouter(router) + + const responseHeaders = getRequestHeaders({ + router, + }) + + return cb({ + request, + router, + responseHeaders, + } as any) + } +} + +function getRequestHeaders(opts: { router: AnyRouter }): Headers { + let headers = mergeHeaders( + { + 'Content-Type': 'text/html; charset=UTF-8', + }, + ...opts.router.state.matches.map((match) => { + return match.headers + }), + ) + + // Handle Redirects + const { redirect } = opts.router.state + + if (redirect) { + headers = mergeHeaders(headers, redirect.headers, { + Location: redirect.href, + }) + } + + return headers +} diff --git a/packages/solid-start-server/src/createStartHandler.ts b/packages/solid-start-server/src/createStartHandler.ts new file mode 100644 index 0000000000..135dc0f80a --- /dev/null +++ b/packages/solid-start-server/src/createStartHandler.ts @@ -0,0 +1,82 @@ +import { createMemoryHistory } from '@tanstack/solid-router' +import { mergeHeaders } from '@tanstack/start-client' +import { eventHandler, getResponseHeaders, toWebRequest } from 'h3' +import { attachRouterServerSsrUtils, dehydrateRouter } from './ssr-server' +import type { HandlerCallback } from './handlerCallback' +import type { EventHandlerResponse, H3Event } from 'h3' +import type { AnyRouter, Manifest } from '@tanstack/solid-router' + +export type CustomizeStartHandler< + TRouter extends AnyRouter, + TResponse extends EventHandlerResponse = EventHandlerResponse, +> = (cb: HandlerCallback) => ReturnType + +export function createStartHandler< + TRouter extends AnyRouter, + TResponse extends EventHandlerResponse = EventHandlerResponse, +>({ + createRouter, + getRouterManifest, +}: { + createRouter: () => TRouter + getRouterManifest?: () => Manifest +}): CustomizeStartHandler { + return (cb) => { + return eventHandler(async (event) => { + const request = toWebRequest(event) + + const url = new URL(request.url) + const href = url.href.replace(url.origin, '') + + // Create a history for the router + const history = createMemoryHistory({ + initialEntries: [href], + }) + + const router = createRouter() + + attachRouterServerSsrUtils(router, getRouterManifest?.()) + + // Update the router with the history and context + router.update({ + history, + }) + + await router.load() + + dehydrateRouter(router) + + const responseHeaders = getStartResponseHeaders({ event, router }) + const response = await cb({ + request, + router, + responseHeaders, + }) + + return response + }) + } +} + +function getStartResponseHeaders(opts: { event: H3Event; router: AnyRouter }) { + let headers = mergeHeaders( + getResponseHeaders(opts.event), + (opts.event as any).___ssrRpcResponseHeaders, + { + 'Content-Type': 'text/html; charset=UTF-8', + }, + ...opts.router.state.matches.map((match) => { + return match.headers + }), + ) + + // Handle Redirects + const { redirect } = opts.router.state + + if (redirect) { + headers = mergeHeaders(headers, redirect.headers, { + Location: redirect.href, + }) + } + return headers +} diff --git a/packages/solid-start-server/src/defaultRenderHandler.tsx b/packages/solid-start-server/src/defaultRenderHandler.tsx new file mode 100644 index 0000000000..afc30aedb6 --- /dev/null +++ b/packages/solid-start-server/src/defaultRenderHandler.tsx @@ -0,0 +1,25 @@ +import * as Solid from 'solid-js/web' +import { StartServer } from './StartServer' +import { defineHandlerCallback } from './handlerCallback' + +export const defaultRenderHandler = defineHandlerCallback( + async ({ router, responseHeaders }) => { + try { + let html = Solid.renderToString(()=>) + const injectedHtml = await Promise.all( + router.serverSsr!.injectedHtml, + ).then((htmls) => htmls.join('')) + html = html.replace(``, `${injectedHtml}`) + return new Response(`${html}`, { + status: router.state.statusCode, + headers: responseHeaders, + }) + } catch (error) { + console.error('Render to string error:', error) + return new Response('Internal Server Error', { + status: 500, + headers: responseHeaders, + }) + } + }, +) diff --git a/packages/solid-start-server/src/defaultStreamHandler.tsx b/packages/solid-start-server/src/defaultStreamHandler.tsx new file mode 100644 index 0000000000..cefeaa78d5 --- /dev/null +++ b/packages/solid-start-server/src/defaultStreamHandler.tsx @@ -0,0 +1,70 @@ +import { PassThrough } from 'node:stream' +import { isbot } from 'isbot' +import * as Solid from 'solid-js/web' + +import { StartServer } from './StartServer' + +import { + transformPipeableStreamWithRouter, + transformReadableStreamWithRouter, +} from './transformStreamWithRouter' + +import { defineHandlerCallback } from './handlerCallback' +import type { ReadableStream } from 'node:stream/web' + +export const defaultStreamHandler = defineHandlerCallback( + async ({ request, router, responseHeaders }) => { + if (typeof Solid.renderToStream === 'function') { + const stream = await Solid.renderToStream(()=> + + ) + + const responseStream = transformReadableStreamWithRouter( + router, + stream as unknown as ReadableStream, + ) + return new Response(responseStream as any, { + status: router.state.statusCode, + headers: responseHeaders, + }) + } + + if (typeof Solid.renderToStream === 'function') { + const reactAppPassthrough = new PassThrough() + + try { + const pipeable = Solid.renderToStream( + ()=>, + { + ...(isbot(request.headers.get('User-Agent')) + ? { + onCompleteAll() { + pipeable.pipe(reactAppPassthrough) + }, + } + : { + onCompleteShell() { + pipeable.pipe(reactAppPassthrough) + }, + }), + }, + ) + } catch (e) { + console.error('Error in renderToPipeableStream:', e) + } + + const responseStream = transformPipeableStreamWithRouter( + router, + reactAppPassthrough, + ) + return new Response(responseStream as any, { + status: router.state.statusCode, + headers: responseHeaders, + }) + } + + throw new Error( + 'No renderToReadableStream or renderToPipeableStream found in react-dom/server. Ensure you are using a version of react-dom that supports streaming.', + ) + }, +) diff --git a/packages/solid-start-server/src/handlerCallback.ts b/packages/solid-start-server/src/handlerCallback.ts new file mode 100644 index 0000000000..23583fe03a --- /dev/null +++ b/packages/solid-start-server/src/handlerCallback.ts @@ -0,0 +1,22 @@ +import type { EventHandlerResponse } from 'h3' +import type { AnyRouter } from '@tanstack/solid-router' + +export interface HandlerCallback< + TRouter extends AnyRouter, + TResponse extends EventHandlerResponse = EventHandlerResponse, +> { + (ctx: { + request: Request + router: TRouter + responseHeaders: Headers + }): TResponse +} + +export function defineHandlerCallback< + TRouter extends AnyRouter, + TResponse = EventHandlerResponse, +>( + handler: HandlerCallback, +): HandlerCallback { + return handler +} diff --git a/packages/solid-start-server/src/index.tsx b/packages/solid-start-server/src/index.tsx new file mode 100644 index 0000000000..d23e477dd7 --- /dev/null +++ b/packages/solid-start-server/src/index.tsx @@ -0,0 +1,508 @@ +import { AsyncLocalStorage } from 'node:async_hooks' +import { + H3Event, + appendCorsHeaders as _appendCorsHeaders, + appendCorsPreflightHeaders as _appendCorsPreflightHeaders, + appendHeader as _appendHeader, + appendHeaders as _appendHeaders, + appendResponseHeader as _appendResponseHeader, + appendResponseHeaders as _appendResponseHeaders, + assertMethod as _assertMethod, + clearResponseHeaders as _clearResponseHeaders, + clearSession as _clearSession, + defaultContentType as _defaultContentType, + deleteCookie as _deleteCookie, + fetchWithEvent as _fetchWithEvent, + getCookie as _getCookie, + getHeader as _getHeader, + getHeaders as _getHeaders, + getProxyRequestHeaders as _getProxyRequestHeaders, + getQuery as _getQuery, + getRequestFingerprint as _getRequestFingerprint, + getRequestHeader as _getRequestHeader, + getRequestHeaders as _getRequestHeaders, + getRequestHost as _getRequestHost, + getRequestIP as _getRequestIP, + getRequestProtocol as _getRequestProtocol, + getRequestURL as _getRequestURL, + getRequestWebStream as _getRequestWebStream, + getResponseHeader as _getResponseHeader, + getResponseHeaders as _getResponseHeaders, + getResponseStatus as _getResponseStatus, + getResponseStatusText as _getResponseStatusText, + getRouterParam as _getRouterParam, + getRouterParams as _getRouterParams, + getSession as _getSession, + getValidatedQuery as _getValidatedQuery, + getValidatedRouterParams as _getValidatedRouterParams, + handleCacheHeaders as _handleCacheHeaders, + handleCors as _handleCors, + isMethod as _isMethod, + isPreflightRequest as _isPreflightRequest, + parseCookies as _parseCookies, + proxyRequest as _proxyRequest, + readBody as _readBody, + readFormData as _readFormData, + readMultipartFormData as _readMultipartFormData, + readRawBody as _readRawBody, + readValidatedBody as _readValidatedBody, + removeResponseHeader as _removeResponseHeader, + sealSession as _sealSession, + send as _send, + sendError as _sendError, + sendNoContent as _sendNoContent, + sendProxy as _sendProxy, + sendRedirect as _sendRedirect, + sendStream as _sendStream, + sendWebResponse as _sendWebResponse, + setCookie as _setCookie, + setHeader as _setHeader, + setHeaders as _setHeaders, + setResponseHeader as _setResponseHeader, + setResponseHeaders as _setResponseHeaders, + setResponseStatus as _setResponseStatus, + unsealSession as _unsealSession, + updateSession as _updateSession, + useSession as _useSession, + writeEarlyHints as _writeEarlyHints, +} from 'h3' +import { getContext as getUnctxContext } from 'unctx' +import type { + Encoding, + HTTPHeaderName, + InferEventInput, + _RequestMiddleware, + _ResponseMiddleware, +} from 'h3' + +export { StartServer } from './StartServer' +export { createStartHandler } from './createStartHandler' +export { createRequestHandler } from './createRequestHandler' +export { defaultStreamHandler } from './defaultStreamHandler' +export { defaultRenderHandler } from './defaultRenderHandler' + +function _setContext(event: H3Event, key: string, value: any) { + event.context[key] = value +} + +function _getContext(event: H3Event, key: string) { + return event.context[key] +} + +export function defineMiddleware(options: { + onRequest?: _RequestMiddleware | Array<_RequestMiddleware> + onBeforeResponse?: _ResponseMiddleware | Array<_ResponseMiddleware> +}) { + return options +} + +function toWebRequestH3(event: H3Event) { + /** + * @type {ReadableStream | undefined} + */ + let readableStream: ReadableStream | undefined + + const url = getRequestURL(event) + const base = { + // @ts-ignore Undici option + duplex: 'half', + method: event.method, + headers: event.headers, + } + + if ((event.node.req as any).body instanceof ArrayBuffer) { + return new Request(url, { + ...base, + body: (event.node.req as any).body, + }) + } + + return new Request(url, { + ...base, + get body() { + if (readableStream) { + return readableStream + } + readableStream = getRequestWebStream(event) + return readableStream + }, + }) +} + +export function toWebRequest(event: H3Event) { + event.web ??= { + request: toWebRequestH3(event), + url: getRequestURL(event), + } + return event.web.request +} + +export { + H3Error, + H3Event, + MIMES, + callNodeListener, + createApp, + createAppEventHandler, + createEvent, + createRouter, + defineEventHandler, + defineLazyEventHandler, + defineNodeListener, + defineNodeMiddleware, + defineRequestMiddleware, + defineResponseMiddleware, + dynamicEventHandler, + defineWebSocket, + eventHandler, + splitCookiesString, + fromNodeMiddleware, + fromPlainHandler, + fromWebHandler, + isError, + isEventHandler, + isWebResponse, + lazyEventHandler, + promisifyNodeListener, + serveStatic, + toEventHandler, + toNodeListener, + toPlainHandler, + toWebHandler, + isCorsOriginAllowed, + isStream, + createError, + sanitizeStatusCode, + sanitizeStatusMessage, + useBase, + type AddRouteShortcuts, + type App, + type AppOptions, + type AppUse, + type CacheConditions, + type CreateRouterOptions, + type Duplex, + type DynamicEventHandler, + type Encoding, + type EventHandler, + type EventHandlerObject, + type EventHandlerRequest, + type EventHandlerResponse, + type H3CorsOptions, + type H3EventContext, + type HTTPHeaderName, + type HTTPMethod, + type InferEventInput, + type InputLayer, + type InputStack, + type Layer, + type LazyEventHandler, + type Matcher, + type MultiPartData, + type NodeEventContext, + type NodeListener, + type NodeMiddleware, + type NodePromisifiedHandler, + type PlainHandler, + type PlainRequest, + type PlainResponse, + type ProxyOptions, + type RequestFingerprintOptions, + type RequestHeaders, + type RouteNode, + type Router, + type RouterMethod, + type RouterUse, + type ServeStaticOptions, + type Session, + type SessionConfig, + type SessionData, + type Stack, + type StaticAssetMeta, + type ValidateFunction, + type ValidateResult, + type WebEventContext, + type WebHandler, + type _RequestMiddleware, + type _ResponseMiddleware, +} from 'h3' + +function getHTTPEvent() { + return getEvent() +} + +export const HTTPEventSymbol = Symbol('$HTTPEvent') + +export function isEvent( + obj: any, +): obj is H3Event | { [HTTPEventSymbol]: H3Event } { + return ( + typeof obj === 'object' && + (obj instanceof H3Event || + obj?.[HTTPEventSymbol] instanceof H3Event || + obj?.__is_event__ === true) + ) + // Implement logic to check if obj is an H3Event +} + +type Tail = T extends [any, ...infer U] ? U : never + +type PrependOverload< + TOriginal extends (...args: Array) => any, + TOverload extends (...args: Array) => any, +> = TOverload & TOriginal + +// add an overload to the function without the event argument +type WrapFunction) => any> = PrependOverload< + TFn, + ( + ...args: Parameters extends [H3Event, ...infer TArgs] + ? TArgs + : Parameters + ) => ReturnType +> + +function createWrapperFunction) => any>( + h3Function: TFn, +): WrapFunction { + return function (...args: Array) { + const event = args[0] + if (!isEvent(event)) { + if (!(globalThis as any).app.config.server.experimental?.asyncContext) { + throw new Error( + 'AsyncLocalStorage was not enabled. Use the `server.experimental.asyncContext: true` option in your app configuration to enable it. Or, pass the instance of HTTPEvent that you have as the first argument to the function.', + ) + } + args.unshift(getHTTPEvent()) + } else { + args[0] = + event instanceof H3Event || (event as any).__is_event__ + ? event + : event[HTTPEventSymbol] + } + + return (h3Function as any)(...args) + } as any +} + +// Creating wrappers for each utility and exporting them with their original names +type WrappedReadRawBody = ( + ...args: Tail>> +) => ReturnType> +export const readRawBody: PrependOverload< + typeof _readRawBody, + WrappedReadRawBody +> = createWrapperFunction(_readRawBody) +type WrappedReadBody = >( + ...args: Tail>> +) => ReturnType> +export const readBody: PrependOverload = + createWrapperFunction(_readBody) +type WrappedGetQuery = < + T, + TEventInput = Exclude, undefined>, +>( + ...args: Tail>> +) => ReturnType> +export const getQuery: PrependOverload = + createWrapperFunction(_getQuery) +export const isMethod = createWrapperFunction(_isMethod) +export const isPreflightRequest = createWrapperFunction(_isPreflightRequest) +type WrappedGetValidatedQuery = < + T, + TEventInput = InferEventInput<'query', H3Event, T>, +>( + ...args: Tail>> +) => ReturnType> +export const getValidatedQuery: PrependOverload< + typeof _getValidatedQuery, + WrappedGetValidatedQuery +> = createWrapperFunction(_getValidatedQuery) +export const getRouterParams = createWrapperFunction(_getRouterParams) +export const getRouterParam = createWrapperFunction(_getRouterParam) +type WrappedGetValidatedRouterParams = < + T, + TEventInput = InferEventInput<'routerParams', H3Event, T>, +>( + ...args: Tail< + Parameters> + > +) => ReturnType> +export const getValidatedRouterParams: PrependOverload< + typeof _getValidatedRouterParams, + WrappedGetValidatedRouterParams +> = createWrapperFunction(_getValidatedRouterParams) +export const assertMethod = createWrapperFunction(_assertMethod) +export const getRequestHeaders = createWrapperFunction(_getRequestHeaders) +export const getRequestHeader = createWrapperFunction(_getRequestHeader) +export const getRequestURL = createWrapperFunction(_getRequestURL) +export const getRequestHost = createWrapperFunction(_getRequestHost) +export const getRequestProtocol = createWrapperFunction(_getRequestProtocol) +export const getRequestIP = createWrapperFunction(_getRequestIP) +export const send = createWrapperFunction(_send) +export const sendNoContent = createWrapperFunction(_sendNoContent) +export const setResponseStatus = createWrapperFunction(_setResponseStatus) +export const getResponseStatus = createWrapperFunction(_getResponseStatus) +export const getResponseStatusText = createWrapperFunction( + _getResponseStatusText, +) +export const getResponseHeaders = createWrapperFunction(_getResponseHeaders) +export const getResponseHeader = createWrapperFunction(_getResponseHeader) +export const setResponseHeaders = createWrapperFunction(_setResponseHeaders) +type WrappedSetResponseHeader = ( + ...args: Tail>> +) => ReturnType> +export const setResponseHeader: PrependOverload< + typeof _setResponseHeader, + WrappedSetResponseHeader +> = createWrapperFunction(_setResponseHeader) +export const appendResponseHeaders = createWrapperFunction( + _appendResponseHeaders, +) +type WrappedAppendResponseHeader = ( + ...args: Tail>> +) => ReturnType> +export const appendResponseHeader: PrependOverload< + typeof _appendResponseHeader, + WrappedAppendResponseHeader +> = createWrapperFunction(_appendResponseHeader) +export const defaultContentType = createWrapperFunction(_defaultContentType) +export const sendRedirect = createWrapperFunction(_sendRedirect) +export const sendStream = createWrapperFunction(_sendStream) +export const writeEarlyHints = createWrapperFunction(_writeEarlyHints) +export const sendError = createWrapperFunction(_sendError) +export const sendProxy = createWrapperFunction(_sendProxy) +export const proxyRequest = createWrapperFunction(_proxyRequest) +type WrappedFetchWithEvent = < + T = unknown, + TResponse = any, + TFetch extends (req: RequestInfo | URL, opts?: any) => any = typeof fetch, +>( + ...args: Tail>> +) => ReturnType> +export const fetchWithEvent: PrependOverload< + typeof _fetchWithEvent, + WrappedFetchWithEvent +> = createWrapperFunction(_fetchWithEvent) +export const getProxyRequestHeaders = createWrapperFunction( + _getProxyRequestHeaders, +) + +export const parseCookies = createWrapperFunction(_parseCookies) +export const getCookie = createWrapperFunction(_getCookie) +export const setCookie = createWrapperFunction(_setCookie) +export const deleteCookie = createWrapperFunction(_deleteCookie) +// not exported :( +type SessionDataT = Record +type WrappedUseSession = ( + ...args: Tail>> +) => ReturnType> +// we need to `as` these because the WrapFunction doesn't work for them +// because they also accept CompatEvent instead of H3Event +export const useSession = createWrapperFunction(_useSession) as PrependOverload< + typeof _useSession, + WrappedUseSession +> +type WrappedGetSession = ( + ...args: Tail>> +) => ReturnType> +export const getSession = createWrapperFunction(_getSession) as PrependOverload< + typeof _getSession, + WrappedGetSession +> +type WrappedUpdateSession = ( + ...args: Tail>> +) => ReturnType> +export const updateSession: PrependOverload< + typeof _updateSession, + WrappedUpdateSession +> = createWrapperFunction(_updateSession) +type WrappedSealSession = ( + ...args: Tail>> +) => ReturnType> +export const sealSession = createWrapperFunction( + _sealSession, +) as PrependOverload +export const unsealSession = createWrapperFunction(_unsealSession) +export const clearSession = createWrapperFunction(_clearSession) +export const handleCacheHeaders = createWrapperFunction(_handleCacheHeaders) +export const handleCors = createWrapperFunction(_handleCors) +export const appendCorsHeaders = createWrapperFunction(_appendCorsHeaders) +export const appendCorsPreflightHeaders = createWrapperFunction( + _appendCorsPreflightHeaders, +) +export const sendWebResponse = createWrapperFunction(_sendWebResponse) +type WrappedAppendHeader = ( + ...args: Tail>> +) => ReturnType> +export const appendHeader: PrependOverload< + typeof _appendHeader, + WrappedAppendHeader +> = createWrapperFunction(_appendHeader) +export const appendHeaders = createWrapperFunction(_appendHeaders) +type WrappedSetHeader = ( + ...args: Tail>> +) => ReturnType> +export const setHeader: PrependOverload = + createWrapperFunction(_setHeader) +export const setHeaders = createWrapperFunction(_setHeaders) +export const getHeader = createWrapperFunction(_getHeader) +export const getHeaders = createWrapperFunction(_getHeaders) +export const getRequestFingerprint = createWrapperFunction( + _getRequestFingerprint, +) +export const getRequestWebStream = createWrapperFunction(_getRequestWebStream) +export const readFormData = createWrapperFunction(_readFormData) +export const readMultipartFormData = createWrapperFunction( + _readMultipartFormData, +) +type WrappedReadValidatedBody = < + T, + TEventInput = InferEventInput<'body', H3Event, T>, +>( + ...args: Tail>> +) => ReturnType> +export const readValidatedBody: PrependOverload< + typeof _readValidatedBody, + WrappedReadValidatedBody +> = createWrapperFunction(_readValidatedBody) +export const removeResponseHeader = createWrapperFunction(_removeResponseHeader) +export const getContext = createWrapperFunction(_getContext) +export const setContext = createWrapperFunction(_setContext) + +export const clearResponseHeaders = createWrapperFunction(_clearResponseHeaders) + +export const getWebRequest = createWrapperFunction(toWebRequest) + +export { createApp as createServer } from 'h3' + +function getNitroAsyncContext() { + const nitroAsyncContext = getUnctxContext('nitro-app', { + asyncContext: (globalThis as any).app.config.server.experimental + ?.asyncContext + ? true + : false, + AsyncLocalStorage, + }) + + return nitroAsyncContext +} + +export function getEvent() { + const event = (getNitroAsyncContext().use() as any).event as + | H3Event + | undefined + if (!event) { + throw new Error( + `No HTTPEvent found in AsyncLocalStorage. Make sure you are using the function within the server runtime.`, + ) + } + return event +} + +export async function handleHTTPEvent(event: H3Event) { + return await (globalThis as any).$handle(event) +} + +export { defineHandlerCallback } from './handlerCallback' +export type { HandlerCallback } from './handlerCallback' diff --git a/packages/solid-start-server/src/ssr-server.ts b/packages/solid-start-server/src/ssr-server.ts new file mode 100644 index 0000000000..9748cd684b --- /dev/null +++ b/packages/solid-start-server/src/ssr-server.ts @@ -0,0 +1,350 @@ +import { + TSR_DEFERRED_PROMISE, + defer, + isPlainArray, + isPlainObject, + pick, + warning, +} from '@tanstack/solid-router' +import jsesc from 'jsesc' +import { startSerializer } from '@tanstack/start-client' +import minifiedTsrBootStrapScript from './tsrScript?script-string' +import type { + ClientExtractedBaseEntry, + DehydratedRouter, + ResolvePromiseState, + SsrMatch, +} from '@tanstack/start-client' +import type { + AnyRouteMatch, + AnyRouter, + DeferredPromise, + Manifest, +} from '@tanstack/solid-router' + +export type ServerExtractedEntry = + | ServerExtractedStream + | ServerExtractedPromise + +export interface ServerExtractedBaseEntry extends ClientExtractedBaseEntry { + id: number + matchIndex: number +} + +export interface ServerExtractedStream extends ServerExtractedBaseEntry { + type: 'stream' + stream: ReadableStream +} + +export interface ServerExtractedPromise extends ServerExtractedBaseEntry { + type: 'promise' + promise: DeferredPromise +} + +export function attachRouterServerSsrUtils( + router: AnyRouter, + manifest: Manifest | undefined, +) { + router.ssr = { + manifest, + serializer: startSerializer, + } + + router.serverSsr = { + injectedHtml: [], + streamedKeys: new Set(), + injectHtml: (getHtml) => { + const promise = Promise.resolve().then(getHtml) + router.serverSsr!.injectedHtml.push(promise) + router.emit({ + type: 'onInjectedHtml', + promise, + }) + + return promise.then(() => {}) + }, + injectScript: (getScript, opts) => { + return router.serverSsr!.injectHtml(async () => { + const script = await getScript() + return `` + }) + }, + streamValue: (key, value) => { + warning( + !router.serverSsr!.streamedKeys.has(key), + 'Key has already been streamed: ' + key, + ) + + router.serverSsr!.streamedKeys.add(key) + router.serverSsr!.injectScript( + () => + `__TSR_SSR__.streamedValues['${key}'] = { value: ${jsesc( + router.ssr!.serializer.stringify(value), + { + isScriptContext: true, + wrap: true, + json: true, + }, + )}}`, + ) + }, + onMatchSettled, + } + + router.serverSsr.injectScript(() => minifiedTsrBootStrapScript, { + logScript: false, + }) +} + +export function dehydrateRouter(router: AnyRouter) { + const dehydratedRouter: DehydratedRouter = { + manifest: router.ssr!.manifest, + dehydratedData: router.options.dehydrate?.(), + } + + router.serverSsr!.injectScript( + () => + `__TSR_SSR__.dehydrated = ${jsesc( + router.ssr!.serializer.stringify(dehydratedRouter), + { + isScriptContext: true, + wrap: true, + json: true, + }, + )}`, + ) +} + +export function extractAsyncLoaderData( + loaderData: any, + ctx: { + match: AnyRouteMatch + router: AnyRouter + }, +) { + const extracted: Array = [] + + const replaced = replaceBy(loaderData, (value, path) => { + // If it's a stream, we need to tee it so we can read it multiple times + if (value instanceof ReadableStream) { + const [copy1, copy2] = value.tee() + const entry: ServerExtractedStream = { + type: 'stream', + path, + id: extracted.length, + matchIndex: ctx.match.index, + stream: copy2, + } + + extracted.push(entry) + return copy1 + } else if (value instanceof Promise) { + const deferredPromise = defer(value) + const entry: ServerExtractedPromise = { + type: 'promise', + path, + id: extracted.length, + matchIndex: ctx.match.index, + promise: deferredPromise, + } + extracted.push(entry) + } + + return value + }) + + return { replaced, extracted } +} + +export function onMatchSettled(opts: { + router: AnyRouter + match: AnyRouteMatch +}) { + const { router, match } = opts + + let extracted: Array | undefined = undefined + let serializedLoaderData: any = undefined + if (match.loaderData !== undefined) { + const result = extractAsyncLoaderData(match.loaderData, { + router, + match, + }) + match.loaderData = result.replaced + extracted = result.extracted + serializedLoaderData = extracted.reduce( + (acc: any, entry: ServerExtractedEntry) => { + return deepImmutableSetByPath(acc, ['temp', ...entry.path], undefined) + }, + { temp: result.replaced }, + ).temp + } + + const initCode = `__TSR_SSR__.initMatch(${jsesc( + { + id: match.id, + __beforeLoadContext: router.ssr!.serializer.stringify( + match.__beforeLoadContext, + ), + loaderData: router.ssr!.serializer.stringify(serializedLoaderData), + error: router.ssr!.serializer.stringify(match.error), + extracted: extracted?.map((entry) => pick(entry, ['type', 'path'])), + updatedAt: match.updatedAt, + status: match.status, + } satisfies SsrMatch, + { + isScriptContext: true, + wrap: true, + json: true, + }, + )})` + + router.serverSsr!.injectScript(() => initCode) + + if (extracted) { + extracted.forEach((entry) => { + if (entry.type === 'promise') return injectPromise(entry) + return injectStream(entry) + }) + } + + function injectPromise(entry: ServerExtractedPromise) { + router.serverSsr!.injectScript(async () => { + await entry.promise + + return `__TSR_SSR__.resolvePromise(${jsesc( + { + matchId: match.id, + id: entry.id, + promiseState: entry.promise[TSR_DEFERRED_PROMISE], + } satisfies ResolvePromiseState, + { + isScriptContext: true, + wrap: true, + json: true, + }, + )})` + }) + } + + function injectStream(entry: ServerExtractedStream) { + // Inject a promise that resolves when the stream is done + // We do this to keep the stream open until we're done + router.serverSsr!.injectHtml(async () => { + // + try { + const reader = entry.stream.getReader() + let chunk: ReadableStreamReadResult | null = null + while (!(chunk = await reader.read()).done) { + if (chunk.value) { + const code = `__TSR_SSR__.injectChunk(${jsesc( + { + matchId: match.id, + id: entry.id, + chunk: chunk.value, + }, + { + isScriptContext: true, + wrap: true, + json: true, + }, + )})` + + router.serverSsr!.injectScript(() => code) + } + } + + router.serverSsr!.injectScript( + () => + `__TSR_SSR__.closeStream(${jsesc( + { + matchId: match.id, + id: entry.id, + }, + { + isScriptContext: true, + wrap: true, + json: true, + }, + )})`, + ) + } catch (err) { + console.error('stream read error', err) + } + + return '' + }) + } +} + +function deepImmutableSetByPath(obj: T, path: Array, value: any): T { + // immutable set by path retaining array and object references + if (path.length === 0) { + return value + } + + const [key, ...rest] = path + + if (Array.isArray(obj)) { + return obj.map((item, i) => { + if (i === Number(key)) { + return deepImmutableSetByPath(item, rest, value) + } + return item + }) as T + } + + if (isPlainObject(obj)) { + return { + ...obj, + [key!]: deepImmutableSetByPath((obj as any)[key!], rest, value), + } + } + + return obj +} + +export function replaceBy( + obj: T, + cb: (value: any, path: Array) => any, + path: Array = [], +): T { + if (isPlainArray(obj)) { + return obj.map((value, i) => replaceBy(value, cb, [...path, `${i}`])) as any + } + + if (isPlainObject(obj)) { + // Do not allow objects with illegal + const newObj: any = {} + + for (const key in obj) { + newObj[key] = replaceBy(obj[key], cb, [...path, key]) + } + + return newObj + } + + // // Detect classes, functions, and other non-serializable objects + // // and return undefined. Exclude some known types that are serializable + // if ( + // typeof obj === 'function' || + // (typeof obj === 'object' && + // ![Object, Promise, ReadableStream].includes((obj as any)?.constructor)) + // ) { + // console.info(obj) + // warning(false, `Non-serializable value ☝️ found at ${path.join('.')}`) + // return undefined as any + // } + + const newObj = cb(obj, path) + + if (newObj !== obj) { + return newObj + } + + return obj +} diff --git a/packages/solid-start-server/src/transformStreamWithRouter.ts b/packages/solid-start-server/src/transformStreamWithRouter.ts new file mode 100644 index 0000000000..2c88b0517f --- /dev/null +++ b/packages/solid-start-server/src/transformStreamWithRouter.ts @@ -0,0 +1,258 @@ +import { ReadableStream } from 'node:stream/web' +import { Readable } from 'node:stream' +import { createControlledPromise } from '@tanstack/solid-router' +import type { AnyRouter } from '@tanstack/solid-router' + +export function transformReadableStreamWithRouter( + router: AnyRouter, + routerStream: ReadableStream, +) { + return transformStreamWithRouter(router, routerStream) +} + +export function transformPipeableStreamWithRouter( + router: AnyRouter, + routerStream: Readable, +) { + return Readable.fromWeb( + transformStreamWithRouter(router, Readable.toWeb(routerStream)), + ) +} + +// regex pattern for matching closing body and html tags +const patternBodyStart = /()/ +const patternHtmlEnd = /(<\/html>)/ +const patternHeadStart = /()/ +// regex pattern for matching closing tags +const patternClosingTag = /(<\/[a-zA-Z][\w:.-]*?>)/g + +const textDecoder = new TextDecoder() + +type ReadablePassthrough = { + stream: ReadableStream + write: (chunk: string) => void + end: (chunk?: string) => void + destroy: (error: unknown) => void + destroyed: boolean +} + +function createPassthrough() { + let controller: ReadableStreamDefaultController + const encoder = new TextEncoder() + const stream = new ReadableStream({ + start(c) { + controller = c + }, + }) + + const res: ReadablePassthrough = { + stream, + write: (chunk) => { + controller.enqueue(encoder.encode(chunk)) + }, + end: (chunk) => { + if (chunk) { + controller.enqueue(encoder.encode(chunk)) + } + controller.close() + res.destroyed = true + }, + destroy: (error) => { + controller.error(error) + }, + destroyed: false, + } + + return res +} + +async function readStream( + stream: ReadableStream, + opts: { + onData?: (chunk: ReadableStreamReadValueResult) => void + onEnd?: () => void + onError?: (error: unknown) => void + }, +) { + try { + const reader = stream.getReader() + let chunk + while (!(chunk = await reader.read()).done) { + opts.onData?.(chunk) + } + opts.onEnd?.() + } catch (error) { + opts.onError?.(error) + } +} + +export function transformStreamWithRouter( + router: AnyRouter, + appStream: ReadableStream, +) { + const finalPassThrough = createPassthrough() + + let isAppRendering = true as boolean + let routerStreamBuffer = '' + let pendingClosingTags = '' + let bodyStarted = false as boolean + let headStarted = false as boolean + let leftover = '' + let leftoverHtml = '' + + function getBufferedRouterStream() { + const html = routerStreamBuffer + routerStreamBuffer = '' + return html + } + + function decodeChunk(chunk: unknown): string { + if (chunk instanceof Uint8Array) { + return textDecoder.decode(chunk) + } + return String(chunk) + } + + const injectedHtmlDonePromise = createControlledPromise() + + let processingCount = 0 + + // Process any already-injected HTML + router.serverSsr!.injectedHtml.forEach((promise) => { + handleInjectedHtml(promise) + }) + + // Listen for any new injected HTML + const stopListeningToInjectedHtml = router.subscribe( + 'onInjectedHtml', + (e) => { + handleInjectedHtml(e.promise) + }, + ) + + function handleInjectedHtml(promise: Promise) { + processingCount++ + + promise + .then((html) => { + if (!bodyStarted) { + routerStreamBuffer += html + } else { + finalPassThrough.write(html) + } + }) + .catch(injectedHtmlDonePromise.reject) + .finally(() => { + processingCount-- + + if (!isAppRendering && processingCount === 0) { + stopListeningToInjectedHtml() + injectedHtmlDonePromise.resolve() + } + }) + } + + injectedHtmlDonePromise + .then(() => { + const finalHtml = + leftoverHtml + getBufferedRouterStream() + pendingClosingTags + + finalPassThrough.end(finalHtml) + }) + .catch((err) => { + console.error('Error reading routerStream:', err) + finalPassThrough.destroy(err) + }) + + // Transform the appStream + readStream(appStream, { + onData: (chunk) => { + const text = decodeChunk(chunk.value) + + let chunkString = leftover + text + const bodyEndMatch = chunkString.match(patternBodyEnd) + const htmlEndMatch = chunkString.match(patternHtmlEnd) + + if (!bodyStarted) { + const bodyStartMatch = chunkString.match(patternBodyStart) + if (bodyStartMatch) { + bodyStarted = true + } + } + + if (!headStarted) { + const headStartMatch = chunkString.match(patternHeadStart) + if (headStartMatch) { + headStarted = true + const index = headStartMatch.index! + const headTag = headStartMatch[0] + const remaining = chunkString.slice(index + headTag.length) + finalPassThrough.write( + chunkString.slice(0, index) + headTag + getBufferedRouterStream(), + ) + // make sure to only write `remaining` until the next closing tag + chunkString = remaining + } + } + + if (!bodyStarted) { + finalPassThrough.write(chunkString) + leftover = '' + return + } + + // If either the body end or html end is in the chunk, + // We need to get all of our data in asap + if ( + bodyEndMatch && + htmlEndMatch && + bodyEndMatch.index! < htmlEndMatch.index! + ) { + const bodyEndIndex = bodyEndMatch.index! + pendingClosingTags = chunkString.slice(bodyEndIndex) + + finalPassThrough.write( + chunkString.slice(0, bodyEndIndex) + getBufferedRouterStream(), + ) + + leftover = '' + return + } + + let result: RegExpExecArray | null + let lastIndex = 0 + while ((result = patternClosingTag.exec(chunkString)) !== null) { + lastIndex = result.index + result[0].length + } + + if (lastIndex > 0) { + const processed = + chunkString.slice(0, lastIndex) + + getBufferedRouterStream() + + leftoverHtml + + finalPassThrough.write(processed) + leftover = chunkString.slice(lastIndex) + } else { + leftover = chunkString + leftoverHtml += getBufferedRouterStream() + } + }, + onEnd: () => { + // Mark the app as done rendering + isAppRendering = false + + // If there are no pending promises, resolve the injectedHtmlDonePromise + if (processingCount === 0) { + injectedHtmlDonePromise.resolve() + } + }, + onError: (error) => { + console.error('Error reading appStream:', error) + finalPassThrough.destroy(error) + }, + }) + + return finalPassThrough.stream +} diff --git a/packages/solid-start-server/src/tsrScript.ts b/packages/solid-start-server/src/tsrScript.ts new file mode 100644 index 0000000000..80851fa0b1 --- /dev/null +++ b/packages/solid-start-server/src/tsrScript.ts @@ -0,0 +1,91 @@ +import type { ControllablePromise } from '@tanstack/solid-router' +import type { StartSsrGlobal } from '@tanstack/start-client' + +const __TSR_SSR__: StartSsrGlobal = { + matches: [], + streamedValues: {}, + initMatch: (match) => { + __TSR_SSR__.matches.push(match) + + match.extracted?.forEach((ex) => { + if (ex.type === 'stream') { + let controller + ex.value = new ReadableStream({ + start(c) { + controller = { + enqueue: (chunk: unknown) => { + try { + c.enqueue(chunk) + } catch {} + }, + close: () => { + try { + c.close() + } catch {} + }, + } + }, + }) + ex.value.controller = controller + } else { + let resolve: ControllablePromise['reject'] | undefined + let reject: ControllablePromise['reject'] | undefined + + ex.value = new Promise((_resolve, _reject) => { + reject = _reject + resolve = _resolve + }) as ControllablePromise + ex.value.reject = reject! + ex.value.resolve = resolve! + } + }) + + return true + }, + resolvePromise: ({ matchId, id, promiseState }) => { + const match = __TSR_SSR__.matches.find((m) => m.id === matchId) + if (match) { + const ex = match.extracted?.[id] + if ( + ex && + ex.type === 'promise' && + ex.value && + promiseState.status === 'success' + ) { + ex.value.resolve(promiseState.data) + return true + } + } + return false + }, + injectChunk: ({ matchId, id, chunk }) => { + const match = __TSR_SSR__.matches.find((m) => m.id === matchId) + + if (match) { + const ex = match.extracted?.[id] + if (ex && ex.type === 'stream' && ex.value?.controller) { + ex.value.controller.enqueue(new TextEncoder().encode(chunk.toString())) + return true + } + } + return false + }, + closeStream: ({ matchId, id }) => { + const match = __TSR_SSR__.matches.find((m) => m.id === matchId) + if (match) { + const ex = match.extracted?.[id] + if (ex && ex.type === 'stream' && ex.value?.controller) { + ex.value.controller.close() + return true + } + } + return false + }, + cleanScripts: () => { + document.querySelectorAll('.tsr-once').forEach((el) => { + el.remove() + }) + }, +} + +window.__TSR_SSR__ = __TSR_SSR__ diff --git a/packages/solid-start-server/src/vite-env.d.ts b/packages/solid-start-server/src/vite-env.d.ts new file mode 100644 index 0000000000..4267d23890 --- /dev/null +++ b/packages/solid-start-server/src/vite-env.d.ts @@ -0,0 +1,4 @@ +declare module '*?script-string' { + const content: string + export default content +} diff --git a/packages/solid-start-server/tests/h3-wrappers.test-d.ts b/packages/solid-start-server/tests/h3-wrappers.test-d.ts new file mode 100644 index 0000000000..55ab318ddb --- /dev/null +++ b/packages/solid-start-server/tests/h3-wrappers.test-d.ts @@ -0,0 +1,350 @@ +import { test } from 'vitest' +import * as h3 from 'h3' +import * as tssServer from '../src' + +interface Foo { + a: string +} + +type AnyFn = (...args: Array) => any +type Satisfies = T +type Tail = T extends [any, ...infer U] ? U : never +type PrependOverload< + TOriginal extends (...args: Array) => any, + TOverload extends (...args: Array) => any, +> = TOverload & TOriginal + +type SharedKeys = keyof typeof tssServer & keyof typeof h3 +// things we're not wrapping +type KeysToIgnore = Satisfies< + | 'H3Error' + | 'H3Event' + | 'MIMES' + | 'callNodeListener' + | 'createApp' + | 'createAppEventHandler' + | 'createError' + | 'createEvent' + | 'createRouter' + | 'defineEventHandler' + | 'defineLazyEventHandler' + | 'defineNodeListener' + | 'defineNodeMiddleware' + | 'defineRequestMiddleware' + | 'defineResponseMiddleware' + | 'defineWebSocket' + | 'dynamicEventHandler' + | 'eventHandler' + | 'fromNodeMiddleware' + | 'fromPlainHandler' + | 'fromWebHandler' + | 'isCorsOriginAllowed' + | 'isError' + | 'isEvent' + | 'isEventHandler' + | 'isStream' + | 'isWebResponse' + | 'lazyEventHandler' + | 'promisifyNodeListener' + | 'sanitizeStatusCode' + | 'sanitizeStatusMessage' + | 'serveStatic' + | 'splitCookiesString' + | 'toEventHandler' + | 'toNodeListener' + | 'toPlainHandler' + | 'toWebHandler' + | 'toWebRequest' + | 'useBase', + SharedKeys +> + +function expectWrappersToMatchH3< + TFnMap extends Record, AnyFn>, +>(_map: { + [K in keyof TFnMap]: { + h3: TFnMap[K] + tss: TFnMap[K] extends AnyFn + ? PrependOverload< + TFnMap[K], + (...args: Tail>) => ReturnType + > + : never + } +}) { + /* equivalent to the below for each key + + expectTypeOf>().toMatchTypeOf< + OverloadParameters + >() + expectTypeOf>>().toMatchTypeOf< + OverloadParameters + >() + + expectTypeOf(tssFunction).returns.toEqualTypeOf< + ReturnType + >() + + */ +} + +test('wrappers match original', () => { + expectWrappersToMatchH3({ + readRawBody: { + h3: h3.readRawBody<'hex'>, + tss: tssServer.readRawBody<'hex'>, + }, + readBody: { + h3: h3.readBody, + tss: tssServer.readBody, + }, + getQuery: { + h3: h3.getQuery, + tss: tssServer.getQuery, + }, + isMethod: { + h3: h3.isMethod, + tss: tssServer.isMethod, + }, + isPreflightRequest: { + h3: h3.isPreflightRequest, + tss: tssServer.isPreflightRequest, + }, + getValidatedQuery: { + h3: h3.getValidatedQuery, + tss: tssServer.getValidatedQuery, + }, + getRouterParams: { + h3: h3.getRouterParams, + tss: tssServer.getRouterParams, + }, + getRouterParam: { + h3: h3.getRouterParam, + tss: tssServer.getRouterParam, + }, + getValidatedRouterParams: { + h3: h3.getValidatedRouterParams, + tss: tssServer.getValidatedRouterParams, + }, + assertMethod: { + h3: h3.assertMethod, + tss: tssServer.assertMethod, + }, + getRequestHeaders: { + h3: h3.getRequestHeaders, + tss: tssServer.getRequestHeaders, + }, + getRequestHeader: { + h3: h3.getRequestHeader, + tss: tssServer.getRequestHeader, + }, + getRequestURL: { + h3: h3.getRequestURL, + tss: tssServer.getRequestURL, + }, + getRequestHost: { + h3: h3.getRequestHost, + tss: tssServer.getRequestHost, + }, + getRequestProtocol: { + h3: h3.getRequestProtocol, + tss: tssServer.getRequestProtocol, + }, + getRequestIP: { + h3: h3.getRequestIP, + tss: tssServer.getRequestIP, + }, + send: { + h3: h3.send, + tss: tssServer.send, + }, + sendNoContent: { + h3: h3.sendNoContent, + tss: tssServer.sendNoContent, + }, + setResponseStatus: { + h3: h3.setResponseStatus, + tss: tssServer.setResponseStatus, + }, + getResponseStatus: { + h3: h3.getResponseStatus, + tss: tssServer.getResponseStatus, + }, + getResponseStatusText: { + h3: h3.getResponseStatusText, + tss: tssServer.getResponseStatusText, + }, + getResponseHeaders: { + h3: h3.getResponseHeaders, + tss: tssServer.getResponseHeaders, + }, + getResponseHeader: { + h3: h3.getResponseHeader, + tss: tssServer.getResponseHeader, + }, + setResponseHeaders: { + h3: h3.setResponseHeaders, + tss: tssServer.setResponseHeaders, + }, + setResponseHeader: { + h3: h3.setResponseHeader<'Age'>, + tss: tssServer.setResponseHeader<'Age'>, + }, + appendResponseHeaders: { + h3: h3.appendResponseHeaders, + tss: tssServer.appendResponseHeaders, + }, + appendResponseHeader: { + h3: h3.appendResponseHeader<'Age'>, + tss: tssServer.appendResponseHeader<'Age'>, + }, + defaultContentType: { + h3: h3.defaultContentType, + tss: tssServer.defaultContentType, + }, + sendRedirect: { + h3: h3.sendRedirect, + tss: tssServer.sendRedirect, + }, + sendStream: { + h3: h3.sendStream, + tss: tssServer.sendStream, + }, + writeEarlyHints: { + h3: h3.writeEarlyHints, + tss: tssServer.writeEarlyHints, + }, + sendError: { + h3: h3.sendError, + tss: tssServer.sendError, + }, + sendProxy: { + h3: h3.sendProxy, + tss: tssServer.sendProxy, + }, + proxyRequest: { + h3: h3.proxyRequest, + tss: tssServer.proxyRequest, + }, + fetchWithEvent: { + h3: h3.fetchWithEvent, + tss: tssServer.fetchWithEvent, + }, + getProxyRequestHeaders: { + h3: h3.getProxyRequestHeaders, + tss: tssServer.getProxyRequestHeaders, + }, + parseCookies: { + h3: h3.parseCookies, + tss: tssServer.parseCookies, + }, + getCookie: { + h3: h3.getCookie, + tss: tssServer.getCookie, + }, + setCookie: { + h3: h3.setCookie, + tss: tssServer.setCookie, + }, + deleteCookie: { + h3: h3.deleteCookie, + tss: tssServer.deleteCookie, + }, + useSession: { + h3: h3.useSession, + tss: tssServer.useSession, + }, + getSession: { + h3: h3.getSession, + tss: tssServer.getSession, + }, + updateSession: { + h3: h3.updateSession, + tss: tssServer.updateSession, + }, + sealSession: { + h3: h3.sealSession, + tss: tssServer.sealSession, + }, + unsealSession: { + h3: h3.unsealSession, + tss: tssServer.unsealSession, + }, + clearSession: { + h3: h3.clearSession, + tss: tssServer.clearSession, + }, + handleCacheHeaders: { + h3: h3.handleCacheHeaders, + tss: tssServer.handleCacheHeaders, + }, + handleCors: { + h3: h3.handleCors, + tss: tssServer.handleCors, + }, + appendCorsHeaders: { + h3: h3.appendCorsHeaders, + tss: tssServer.appendCorsHeaders, + }, + appendCorsPreflightHeaders: { + h3: h3.appendCorsPreflightHeaders, + tss: tssServer.appendCorsPreflightHeaders, + }, + sendWebResponse: { + h3: h3.sendWebResponse, + tss: tssServer.sendWebResponse, + }, + appendHeader: { + h3: h3.appendHeader<'Age'>, + tss: tssServer.appendHeader<'Age'>, + }, + appendHeaders: { + h3: h3.appendHeaders, + tss: tssServer.appendHeaders, + }, + setHeader: { + h3: h3.setHeader<'Age'>, + tss: tssServer.setHeader<'Age'>, + }, + setHeaders: { + h3: h3.setHeaders, + tss: tssServer.setHeaders, + }, + getHeader: { + h3: h3.getHeader, + tss: tssServer.getHeader, + }, + getHeaders: { + h3: h3.getHeaders, + tss: tssServer.getHeaders, + }, + getRequestFingerprint: { + h3: h3.getRequestFingerprint, + tss: tssServer.getRequestFingerprint, + }, + getRequestWebStream: { + h3: h3.getRequestWebStream, + tss: tssServer.getRequestWebStream, + }, + readFormData: { + h3: h3.readFormData, + tss: tssServer.readFormData, + }, + readMultipartFormData: { + h3: h3.readMultipartFormData, + tss: tssServer.readMultipartFormData, + }, + readValidatedBody: { + h3: h3.readValidatedBody, + tss: tssServer.readValidatedBody, + }, + removeResponseHeader: { + h3: h3.removeResponseHeader, + tss: tssServer.removeResponseHeader, + }, + clearResponseHeaders: { + h3: h3.clearResponseHeaders, + tss: tssServer.clearResponseHeaders, + }, + }) +}) diff --git a/packages/solid-start-server/tsconfig.json b/packages/solid-start-server/tsconfig.json new file mode 100644 index 0000000000..4d747d628f --- /dev/null +++ b/packages/solid-start-server/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "module": "esnext" + }, + "include": [ + "src", + "tests", + "vite.config.ts", + "../start-client/src/tsrScript.ts" + ] +} diff --git a/packages/solid-start-server/vite-minify-plugin.ts b/packages/solid-start-server/vite-minify-plugin.ts new file mode 100644 index 0000000000..d8da290266 --- /dev/null +++ b/packages/solid-start-server/vite-minify-plugin.ts @@ -0,0 +1,25 @@ +import { transform as esbuildTransform } from 'esbuild' +import type { Plugin } from 'vite' + +export default function minifyScriptPlugin(): Plugin { + return { + name: 'vite-plugin-minify-script', + enforce: 'pre', + async transform(code, id) { + if (!id.endsWith('?script-string')) { + return null + } + + const result = await esbuildTransform(code, { + loader: 'ts', + minify: true, + target: 'esnext', + }) + + return { + code: `export default ${JSON.stringify(result.code)};`, + map: null, + } + }, + } +} diff --git a/packages/solid-start-server/vite.config.ts b/packages/solid-start-server/vite.config.ts new file mode 100644 index 0000000000..db10017130 --- /dev/null +++ b/packages/solid-start-server/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import { tanstackViteConfig } from '@tanstack/config/vite' +import solid from 'vite-plugin-solid' +import packageJson from './package.json' +import minifyScriptPlugin from './vite-minify-plugin' +import type { ViteUserConfig } from 'vitest/config' + +const config = defineConfig({ + plugins: [minifyScriptPlugin(), solid()] as ViteUserConfig['plugins'], + test: { + name: packageJson.name, + watch: false, + environment: 'jsdom', + }, +}) + +export default mergeConfig( + config, + tanstackViteConfig({ + srcDir: './src', + entry: './src/index.tsx', + }), +) From 2559ca43ca820129d273f401e5a8dcc03c88acc8 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Sat, 22 Feb 2025 12:46:54 +0100 Subject: [PATCH 003/124] solid-start-client --- packages/solid-start-client/package.json | 1 + packages/solid-start-client/src/serializer.ts | 4 +- .../src/tests/createServerFn.test-d.tsx | 24 +++--- .../src/tests/setupTests.tsx | 1 + packages/solid-start-client/tsconfig.json | 8 +- packages/solid-start-client/vite.config.ts | 2 + packages/solid-start-server/package.json | 2 +- pnpm-lock.yaml | 80 +++++++++++++++++++ 8 files changed, 105 insertions(+), 17 deletions(-) create mode 100644 packages/solid-start-client/src/tests/setupTests.tsx diff --git a/packages/solid-start-client/package.json b/packages/solid-start-client/package.json index f307bf808b..8029208897 100644 --- a/packages/solid-start-client/package.json +++ b/packages/solid-start-client/package.json @@ -65,6 +65,7 @@ "dependencies": { "@tanstack/solid-cross-context": "workspace:^", "@tanstack/solid-router": "workspace:^", + "@tanstack/router-core": "workspace:^", "cookie-es": "^1.2.2", "jsesc": "^3.1.0", "tiny-invariant": "^1.3.3", diff --git a/packages/solid-start-client/src/serializer.ts b/packages/solid-start-client/src/serializer.ts index 24a9b315c9..d9fd299b28 100644 --- a/packages/solid-start-client/src/serializer.ts +++ b/packages/solid-start-client/src/serializer.ts @@ -1,5 +1,5 @@ -import { isPlainObject } from '@tanstack/solid-router' -import type { StartSerializer } from '@tanstack/solid-router' +import { isPlainObject } from '@tanstack/router-core' +import type { StartSerializer } from '@tanstack/router-core' export const startSerializer: StartSerializer = { stringify: (value: any) => diff --git a/packages/solid-start-client/src/tests/createServerFn.test-d.tsx b/packages/solid-start-client/src/tests/createServerFn.test-d.tsx index 4ae3396ab6..be1671b790 100644 --- a/packages/solid-start-client/src/tests/createServerFn.test-d.tsx +++ b/packages/solid-start-client/src/tests/createServerFn.test-d.tsx @@ -317,18 +317,18 @@ test('createServerFn returns Date', () => { expectTypeOf(fn()).toEqualTypeOf>() }) -test('createServerFn returns RSC', () => { - const fn = createServerFn().handler(() => ({ - rscs: [ -
I'm an RSC
, -
I'm an RSC
, - ] as const, - })) - - expectTypeOf(fn()).toEqualTypeOf< - Promise<{ rscs: readonly [ReadableStream, ReadableStream] }> - >() -}) +// test('createServerFn returns RSC', () => { +// const fn = createServerFn().handler(() => ({ +// rscs: [ +//
I'm an RSC
, +//
I'm an RSC
, +// ] as const, +// })) + +// expectTypeOf(fn()).toEqualTypeOf< +// Promise<{ rscs: readonly [ReadableStream, ReadableStream] }> +// >() +// }) test('createServerFn returns undefined', () => { const fn = createServerFn().handler(() => ({ diff --git a/packages/solid-start-client/src/tests/setupTests.tsx b/packages/solid-start-client/src/tests/setupTests.tsx new file mode 100644 index 0000000000..e8ee517aed --- /dev/null +++ b/packages/solid-start-client/src/tests/setupTests.tsx @@ -0,0 +1 @@ +import '@testing-library/jest-dom/vitest' \ No newline at end of file diff --git a/packages/solid-start-client/tsconfig.json b/packages/solid-start-client/tsconfig.json index fdda84d59f..48a1415543 100644 --- a/packages/solid-start-client/tsconfig.json +++ b/packages/solid-start-client/tsconfig.json @@ -3,7 +3,11 @@ "compilerOptions": { "jsx": "preserve", "jsxImportSource": "solid-js", - "module": "esnext" + "module": "esnext", + "skipLibCheck": true + }, - "include": ["src", "vite.config.ts", "src/tsrScript.ts"] + "include": ["src", "vite.config.ts", "src/tsrScript.ts"], + "exclude": ["node_modules"] + } diff --git a/packages/solid-start-client/vite.config.ts b/packages/solid-start-client/vite.config.ts index 231533c36b..38e78635ea 100644 --- a/packages/solid-start-client/vite.config.ts +++ b/packages/solid-start-client/vite.config.ts @@ -7,9 +7,11 @@ import type { ViteUserConfig } from 'vitest/config' const config = defineConfig({ plugins: [solid()] as ViteUserConfig['plugins'], test: { + typecheck: { enabled: true }, name: packageJson.name, watch: false, environment: 'jsdom', + setupFiles: ['./src/tests/setupTests.tsx'], }, }) diff --git a/packages/solid-start-server/package.json b/packages/solid-start-server/package.json index 67cea9887b..36c4d7a0a6 100644 --- a/packages/solid-start-server/package.json +++ b/packages/solid-start-server/package.json @@ -64,7 +64,7 @@ "dependencies": { "@tanstack/solid-cross-context": "workspace:^", "@tanstack/solid-router": "workspace:^", - "@tanstack/start-client": "workspace:^", + "@tanstack/solid-start-client": "workspace:^", "h3": "1.13.0", "isbot": "^5.1.22", "jsesc": "^3.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 81e7ae115a..69cb63a9cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5678,6 +5678,86 @@ importers: specifier: ^3.23.8 version: 3.24.1 + packages/solid-start-client: + dependencies: + '@tanstack/router-core': + specifier: workspace:* + version: link:../router-core + '@tanstack/solid-cross-context': + specifier: workspace:^ + version: link:../solid-cross-context + '@tanstack/solid-router': + specifier: workspace:^ + version: link:../solid-router + cookie-es: + specifier: ^1.2.2 + version: 1.2.2 + jsesc: + specifier: ^3.1.0 + version: 3.1.0 + solid-js: + specifier: '>=1.0.0' + version: 1.9.4 + tiny-invariant: + specifier: ^1.3.3 + version: 1.3.3 + vinxi: + specifier: ^0.5.3 + version: 0.5.3(@types/node@22.13.4)(db0@0.2.3)(ioredis@5.4.2)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0) + devDependencies: + '@solidjs/testing-library': + specifier: ^0.8.10 + version: 0.8.10(solid-js@1.9.4) + '@testing-library/jest-dom': + specifier: ^6.6.3 + version: 6.6.3 + '@types/jsesc': + specifier: ^3.0.3 + version: 3.0.3 + vite-plugin-solid: + specifier: ^2.11.2 + version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.4)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + + packages/solid-start-server: + dependencies: + '@tanstack/solid-cross-context': + specifier: workspace:^ + version: link:../solid-cross-context + '@tanstack/solid-router': + specifier: workspace:^ + version: link:../solid-router + '@tanstack/solid-start-client': + specifier: workspace:^ + version: link:../solid-start-client + h3: + specifier: 1.13.0 + version: 1.13.0 + isbot: + specifier: ^5.1.22 + version: 5.1.22 + jsesc: + specifier: ^3.1.0 + version: 3.1.0 + unctx: + specifier: ^2.4.1 + version: 2.4.1 + devDependencies: + '@types/jsesc': + specifier: ^3.0.3 + version: 3.0.3 + esbuild: + specifier: ^0.25.0 + version: 0.25.0 + solid-js: + specifier: ^1.0.0 + version: 1.9.4 + typescript: + specifier: ^5.7.2 + version: 5.7.3 + vite-plugin-solid: + specifier: ^2.11.2 + version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.4)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + packages/start: dependencies: '@tanstack/react-start-api-routes': From 75d1a9811852a9444433d69745fc7eacf5711abb Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Sat, 22 Feb 2025 13:21:28 +0100 Subject: [PATCH 004/124] tanstack solid-start --- e2e/solid-start/basic/.gitignore | 22 + e2e/solid-start/basic/.prettierignore | 4 + e2e/solid-start/basic/app.config.ts | 12 + e2e/solid-start/basic/app/api.ts | 6 + e2e/solid-start/basic/app/client.tsx | 8 + .../basic/app/components/CustomMessage.tsx | 9 + .../app/components/DefaultCatchBoundary.tsx | 53 + .../basic/app/components/NotFound.tsx | 25 + .../basic/app/components/RedirectOnClick.tsx | 26 + .../basic/app/components/throwRedirect.ts | 20 + e2e/solid-start/basic/app/routeTree.gen.ts | 923 ++++++++++++++++++ e2e/solid-start/basic/app/router.tsx | 22 + e2e/solid-start/basic/app/routes/__root.tsx | 165 ++++ e2e/solid-start/basic/app/routes/_layout.tsx | 16 + .../basic/app/routes/_layout/_layout-2.tsx | 34 + .../app/routes/_layout/_layout-2/layout-a.tsx | 9 + .../app/routes/_layout/_layout-2/layout-b.tsx | 9 + e2e/solid-start/basic/app/routes/api.users.ts | 18 + .../basic/app/routes/api/users.$id.ts | 25 + e2e/solid-start/basic/app/routes/deferred.tsx | 62 ++ e2e/solid-start/basic/app/routes/index.tsx | 15 + e2e/solid-start/basic/app/routes/links.tsx | 47 + .../basic/app/routes/not-found/index.tsx | 31 + .../basic/app/routes/not-found/route.tsx | 8 + .../app/routes/not-found/via-beforeLoad.tsx | 23 + .../basic/app/routes/not-found/via-loader.tsx | 23 + .../basic/app/routes/posts.$postId.tsx | 39 + .../basic/app/routes/posts.index.tsx | 9 + e2e/solid-start/basic/app/routes/posts.tsx | 39 + .../basic/app/routes/posts_.$postId.deep.tsx | 27 + .../basic/app/routes/redirect/$target.tsx | 21 + .../app/routes/redirect/$target/index.tsx | 76 ++ .../redirect/$target/serverFn/index.tsx | 86 ++ .../$target/serverFn/via-beforeLoad.tsx | 12 + .../redirect/$target/serverFn/via-loader.tsx | 12 + .../$target/serverFn/via-useServerFn.tsx | 18 + .../redirect/$target/via-beforeLoad.tsx | 17 + .../routes/redirect/$target/via-loader.tsx | 18 + .../basic/app/routes/redirect/index.tsx | 28 + e2e/solid-start/basic/app/routes/scripts.tsx | 31 + .../basic/app/routes/search-params.tsx | 27 + e2e/solid-start/basic/app/routes/stream.tsx | 64 ++ .../basic/app/routes/users.$userId.tsx | 38 + .../basic/app/routes/users.index.tsx | 9 + e2e/solid-start/basic/app/routes/users.tsx | 49 + e2e/solid-start/basic/app/ssr.tsx | 12 + e2e/solid-start/basic/app/styles/app.css | 22 + e2e/solid-start/basic/app/utils/posts.tsx | 36 + e2e/solid-start/basic/app/utils/seo.ts | 33 + e2e/solid-start/basic/app/utils/users.tsx | 10 + e2e/solid-start/basic/package.json | 35 + e2e/solid-start/basic/playwright.config.ts | 35 + e2e/solid-start/basic/postcss.config.mjs | 6 + .../basic/public/android-chrome-192x192.png | Bin 0 -> 29964 bytes .../basic/public/android-chrome-512x512.png | Bin 0 -> 109271 bytes .../basic/public/apple-touch-icon.png | Bin 0 -> 27246 bytes .../basic/public/favicon-16x16.png | Bin 0 -> 832 bytes .../basic/public/favicon-32x32.png | Bin 0 -> 2115 bytes e2e/solid-start/basic/public/favicon.ico | Bin 0 -> 15406 bytes e2e/solid-start/basic/public/favicon.png | Bin 0 -> 1507 bytes e2e/solid-start/basic/public/script.js | 2 + e2e/solid-start/basic/public/script2.js | 2 + e2e/solid-start/basic/public/site.webmanifest | 19 + e2e/solid-start/basic/tailwind.config.mjs | 4 + e2e/solid-start/basic/tests/fixture.ts | 28 + .../basic/tests/navigation.spec.ts | 47 + e2e/solid-start/basic/tests/not-found.spec.ts | 74 ++ e2e/solid-start/basic/tests/redirect.spec.ts | 208 ++++ .../basic/tests/search-params.spec.ts | 22 + e2e/solid-start/basic/tests/streaming.spec.ts | 34 + e2e/solid-start/basic/tsconfig.json | 22 + packages/solid-start-config/README.md | 33 + packages/solid-start-config/eslint.config.js | 31 + packages/solid-start-config/package.json | 72 ++ packages/solid-start-config/src/index.ts | 660 +++++++++++++ packages/solid-start-config/src/schema.ts | 193 ++++ .../src/vinxi-file-router.ts | 87 ++ packages/solid-start-config/tsconfig.json | 10 + .../README.md | 33 + .../eslint.config.js | 31 + .../package.json | 71 ++ .../src/index.ts | 19 + .../tsconfig.json | 8 + .../tsconfigs/config.tsconfig.json | 10 + .../tsconfigs/router-manifest.tsconfig.json | 10 + .../vite.config.ts | 21 + .../README.md | 33 + .../eslint.config.js | 31 + .../package.json | 76 ++ .../src/index.tsx | 150 +++ .../tsconfig.json | 8 + .../vite.config.ts | 22 + .../README.md | 33 + .../eslint.config.js | 31 + .../package.json | 78 ++ .../src/index.tsx | 307 ++++++ .../tsconfig.json | 8 + .../vite.config.ts | 23 + packages/solid-start/README.md | 33 + packages/solid-start/eslint.config.js | 31 + packages/solid-start/package.json | 163 ++++ packages/solid-start/src/api.tsx | 1 + packages/solid-start/src/client.tsx | 1 + packages/solid-start/src/config.tsx | 1 + packages/solid-start/src/router-manifest.tsx | 1 + .../src/server-functions-client.tsx | 1 + .../src/server-functions-handler.tsx | 1 + .../src/server-functions-server.tsx | 1 + .../solid-start/src/server-functions-ssr.tsx | 1 + packages/solid-start/src/server.tsx | 1 + packages/solid-start/tsconfig.json | 8 + packages/solid-start/vite.config.ts | 38 + pnpm-lock.yaml | 142 +++ 113 files changed, 5399 insertions(+) create mode 100644 e2e/solid-start/basic/.gitignore create mode 100644 e2e/solid-start/basic/.prettierignore create mode 100644 e2e/solid-start/basic/app.config.ts create mode 100644 e2e/solid-start/basic/app/api.ts create mode 100644 e2e/solid-start/basic/app/client.tsx create mode 100644 e2e/solid-start/basic/app/components/CustomMessage.tsx create mode 100644 e2e/solid-start/basic/app/components/DefaultCatchBoundary.tsx create mode 100644 e2e/solid-start/basic/app/components/NotFound.tsx create mode 100644 e2e/solid-start/basic/app/components/RedirectOnClick.tsx create mode 100644 e2e/solid-start/basic/app/components/throwRedirect.ts create mode 100644 e2e/solid-start/basic/app/routeTree.gen.ts create mode 100644 e2e/solid-start/basic/app/router.tsx create mode 100644 e2e/solid-start/basic/app/routes/__root.tsx create mode 100644 e2e/solid-start/basic/app/routes/_layout.tsx create mode 100644 e2e/solid-start/basic/app/routes/_layout/_layout-2.tsx create mode 100644 e2e/solid-start/basic/app/routes/_layout/_layout-2/layout-a.tsx create mode 100644 e2e/solid-start/basic/app/routes/_layout/_layout-2/layout-b.tsx create mode 100644 e2e/solid-start/basic/app/routes/api.users.ts create mode 100644 e2e/solid-start/basic/app/routes/api/users.$id.ts create mode 100644 e2e/solid-start/basic/app/routes/deferred.tsx create mode 100644 e2e/solid-start/basic/app/routes/index.tsx create mode 100644 e2e/solid-start/basic/app/routes/links.tsx create mode 100644 e2e/solid-start/basic/app/routes/not-found/index.tsx create mode 100644 e2e/solid-start/basic/app/routes/not-found/route.tsx create mode 100644 e2e/solid-start/basic/app/routes/not-found/via-beforeLoad.tsx create mode 100644 e2e/solid-start/basic/app/routes/not-found/via-loader.tsx create mode 100644 e2e/solid-start/basic/app/routes/posts.$postId.tsx create mode 100644 e2e/solid-start/basic/app/routes/posts.index.tsx create mode 100644 e2e/solid-start/basic/app/routes/posts.tsx create mode 100644 e2e/solid-start/basic/app/routes/posts_.$postId.deep.tsx create mode 100644 e2e/solid-start/basic/app/routes/redirect/$target.tsx create mode 100644 e2e/solid-start/basic/app/routes/redirect/$target/index.tsx create mode 100644 e2e/solid-start/basic/app/routes/redirect/$target/serverFn/index.tsx create mode 100644 e2e/solid-start/basic/app/routes/redirect/$target/serverFn/via-beforeLoad.tsx create mode 100644 e2e/solid-start/basic/app/routes/redirect/$target/serverFn/via-loader.tsx create mode 100644 e2e/solid-start/basic/app/routes/redirect/$target/serverFn/via-useServerFn.tsx create mode 100644 e2e/solid-start/basic/app/routes/redirect/$target/via-beforeLoad.tsx create mode 100644 e2e/solid-start/basic/app/routes/redirect/$target/via-loader.tsx create mode 100644 e2e/solid-start/basic/app/routes/redirect/index.tsx create mode 100644 e2e/solid-start/basic/app/routes/scripts.tsx create mode 100644 e2e/solid-start/basic/app/routes/search-params.tsx create mode 100644 e2e/solid-start/basic/app/routes/stream.tsx create mode 100644 e2e/solid-start/basic/app/routes/users.$userId.tsx create mode 100644 e2e/solid-start/basic/app/routes/users.index.tsx create mode 100644 e2e/solid-start/basic/app/routes/users.tsx create mode 100644 e2e/solid-start/basic/app/ssr.tsx create mode 100644 e2e/solid-start/basic/app/styles/app.css create mode 100644 e2e/solid-start/basic/app/utils/posts.tsx create mode 100644 e2e/solid-start/basic/app/utils/seo.ts create mode 100644 e2e/solid-start/basic/app/utils/users.tsx create mode 100644 e2e/solid-start/basic/package.json create mode 100644 e2e/solid-start/basic/playwright.config.ts create mode 100644 e2e/solid-start/basic/postcss.config.mjs create mode 100644 e2e/solid-start/basic/public/android-chrome-192x192.png create mode 100644 e2e/solid-start/basic/public/android-chrome-512x512.png create mode 100644 e2e/solid-start/basic/public/apple-touch-icon.png create mode 100644 e2e/solid-start/basic/public/favicon-16x16.png create mode 100644 e2e/solid-start/basic/public/favicon-32x32.png create mode 100644 e2e/solid-start/basic/public/favicon.ico create mode 100644 e2e/solid-start/basic/public/favicon.png create mode 100644 e2e/solid-start/basic/public/script.js create mode 100644 e2e/solid-start/basic/public/script2.js create mode 100644 e2e/solid-start/basic/public/site.webmanifest create mode 100644 e2e/solid-start/basic/tailwind.config.mjs create mode 100644 e2e/solid-start/basic/tests/fixture.ts create mode 100644 e2e/solid-start/basic/tests/navigation.spec.ts create mode 100644 e2e/solid-start/basic/tests/not-found.spec.ts create mode 100644 e2e/solid-start/basic/tests/redirect.spec.ts create mode 100644 e2e/solid-start/basic/tests/search-params.spec.ts create mode 100644 e2e/solid-start/basic/tests/streaming.spec.ts create mode 100644 e2e/solid-start/basic/tsconfig.json create mode 100644 packages/solid-start-config/README.md create mode 100644 packages/solid-start-config/eslint.config.js create mode 100644 packages/solid-start-config/package.json create mode 100644 packages/solid-start-config/src/index.ts create mode 100644 packages/solid-start-config/src/schema.ts create mode 100644 packages/solid-start-config/src/vinxi-file-router.ts create mode 100644 packages/solid-start-config/tsconfig.json create mode 100644 packages/solid-start-server-functions-client/README.md create mode 100644 packages/solid-start-server-functions-client/eslint.config.js create mode 100644 packages/solid-start-server-functions-client/package.json create mode 100644 packages/solid-start-server-functions-client/src/index.ts create mode 100644 packages/solid-start-server-functions-client/tsconfig.json create mode 100644 packages/solid-start-server-functions-client/tsconfigs/config.tsconfig.json create mode 100644 packages/solid-start-server-functions-client/tsconfigs/router-manifest.tsconfig.json create mode 100644 packages/solid-start-server-functions-client/vite.config.ts create mode 100644 packages/solid-start-server-functions-fetcher/README.md create mode 100644 packages/solid-start-server-functions-fetcher/eslint.config.js create mode 100644 packages/solid-start-server-functions-fetcher/package.json create mode 100644 packages/solid-start-server-functions-fetcher/src/index.tsx create mode 100644 packages/solid-start-server-functions-fetcher/tsconfig.json create mode 100644 packages/solid-start-server-functions-fetcher/vite.config.ts create mode 100644 packages/solid-start-server-functions-handler/README.md create mode 100644 packages/solid-start-server-functions-handler/eslint.config.js create mode 100644 packages/solid-start-server-functions-handler/package.json create mode 100644 packages/solid-start-server-functions-handler/src/index.tsx create mode 100644 packages/solid-start-server-functions-handler/tsconfig.json create mode 100644 packages/solid-start-server-functions-handler/vite.config.ts create mode 100644 packages/solid-start/README.md create mode 100644 packages/solid-start/eslint.config.js create mode 100644 packages/solid-start/package.json create mode 100644 packages/solid-start/src/api.tsx create mode 100644 packages/solid-start/src/client.tsx create mode 100644 packages/solid-start/src/config.tsx create mode 100644 packages/solid-start/src/router-manifest.tsx create mode 100644 packages/solid-start/src/server-functions-client.tsx create mode 100644 packages/solid-start/src/server-functions-handler.tsx create mode 100644 packages/solid-start/src/server-functions-server.tsx create mode 100644 packages/solid-start/src/server-functions-ssr.tsx create mode 100644 packages/solid-start/src/server.tsx create mode 100644 packages/solid-start/tsconfig.json create mode 100644 packages/solid-start/vite.config.ts diff --git a/e2e/solid-start/basic/.gitignore b/e2e/solid-start/basic/.gitignore new file mode 100644 index 0000000000..be342025da --- /dev/null +++ b/e2e/solid-start/basic/.gitignore @@ -0,0 +1,22 @@ +node_modules +package-lock.json +yarn.lock + +.DS_Store +.cache +.env +.vercel +.output +.vinxi + +/build/ +/api/ +/server/build +/public/build +.vinxi +# Sentry Config File +.env.sentry-build-plugin +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/solid-start/basic/.prettierignore b/e2e/solid-start/basic/.prettierignore new file mode 100644 index 0000000000..2be5eaa6ec --- /dev/null +++ b/e2e/solid-start/basic/.prettierignore @@ -0,0 +1,4 @@ +**/build +**/public +pnpm-lock.yaml +routeTree.gen.ts \ No newline at end of file diff --git a/e2e/solid-start/basic/app.config.ts b/e2e/solid-start/basic/app.config.ts new file mode 100644 index 0000000000..5c531d7e3d --- /dev/null +++ b/e2e/solid-start/basic/app.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from '@tanstack/solid-start/config' +import tsConfigPaths from 'vite-tsconfig-paths' + +export default defineConfig({ + vite: { + plugins: [ + tsConfigPaths({ + projects: ['./tsconfig.json'], + }), + ], + }, +}) diff --git a/e2e/solid-start/basic/app/api.ts b/e2e/solid-start/basic/app/api.ts new file mode 100644 index 0000000000..ed511bcd26 --- /dev/null +++ b/e2e/solid-start/basic/app/api.ts @@ -0,0 +1,6 @@ +import { + createStartAPIHandler, + defaultAPIFileRouteHandler, +} from '@tanstack/solid-start/api' + +export default createStartAPIHandler(defaultAPIFileRouteHandler) diff --git a/e2e/solid-start/basic/app/client.tsx b/e2e/solid-start/basic/app/client.tsx new file mode 100644 index 0000000000..dd463c2622 --- /dev/null +++ b/e2e/solid-start/basic/app/client.tsx @@ -0,0 +1,8 @@ +/// +import { hydrateRoot } from 'react-dom/client' +import { StartClient } from '@tanstack/solid-start' +import { createRouter } from './router' + +const router = createRouter() + +hydrateRoot(document, ) diff --git a/e2e/solid-start/basic/app/components/CustomMessage.tsx b/e2e/solid-start/basic/app/components/CustomMessage.tsx new file mode 100644 index 0000000000..d417543fc3 --- /dev/null +++ b/e2e/solid-start/basic/app/components/CustomMessage.tsx @@ -0,0 +1,9 @@ + +export function CustomMessage({ message }: { message: string }) { + return ( +
+
This is a custom message:
+

{message}

+
+ ) +} diff --git a/e2e/solid-start/basic/app/components/DefaultCatchBoundary.tsx b/e2e/solid-start/basic/app/components/DefaultCatchBoundary.tsx new file mode 100644 index 0000000000..f5b7e91167 --- /dev/null +++ b/e2e/solid-start/basic/app/components/DefaultCatchBoundary.tsx @@ -0,0 +1,53 @@ +import { + ErrorComponent, + Link, + rootRouteId, + useMatch, + useRouter, +} from '@tanstack/solid-router' +import type { ErrorComponentProps } from '@tanstack/solid-router' + +export function DefaultCatchBoundary({ error }: ErrorComponentProps) { + const router = useRouter() + const isRoot = useMatch({ + strict: false, + select: (state) => state.id === rootRouteId, + }) + + console.error(error) + + return ( +
+ +
+ + {isRoot ? ( + + Home + + ) : ( + { + e.preventDefault() + window.history.back() + }} + > + Go Back + + )} +
+
+ ) +} diff --git a/e2e/solid-start/basic/app/components/NotFound.tsx b/e2e/solid-start/basic/app/components/NotFound.tsx new file mode 100644 index 0000000000..b299bdfb97 --- /dev/null +++ b/e2e/solid-start/basic/app/components/NotFound.tsx @@ -0,0 +1,25 @@ +import { Link } from '@tanstack/solid-router' + +export function NotFound({ children }: { children?: any }) { + return ( +
+
+ {children ||

The page you are looking for does not exist.

} +
+

+ + + Start Over + +

+
+ ) +} diff --git a/e2e/solid-start/basic/app/components/RedirectOnClick.tsx b/e2e/solid-start/basic/app/components/RedirectOnClick.tsx new file mode 100644 index 0000000000..385748e2a5 --- /dev/null +++ b/e2e/solid-start/basic/app/components/RedirectOnClick.tsx @@ -0,0 +1,26 @@ +import { useServerFn } from '@tanstack/solid-start' +import { throwRedirect } from './throwRedirect' + +interface RedirectOnClickProps { + target: 'internal' | 'external' + reloadDocument?: boolean + externalHost?: string +} + +export function RedirectOnClick({ + target, + reloadDocument, + externalHost, +}: RedirectOnClickProps) { + const execute = useServerFn(throwRedirect) + return ( + + ) +} diff --git a/e2e/solid-start/basic/app/components/throwRedirect.ts b/e2e/solid-start/basic/app/components/throwRedirect.ts new file mode 100644 index 0000000000..fd4a056324 --- /dev/null +++ b/e2e/solid-start/basic/app/components/throwRedirect.ts @@ -0,0 +1,20 @@ +import { redirect } from '@tanstack/solid-router' +import { createServerFn } from '@tanstack/solid-start' + +export const throwRedirect = createServerFn() + .validator( + (opts: { + target: 'internal' | 'external' + reloadDocument?: boolean + externalHost?: string + }) => opts, + ) + .handler((ctx) => { + if (ctx.data.target === 'internal') { + throw redirect({ to: '/posts', reloadDocument: ctx.data.reloadDocument }) + } + const href = ctx.data.externalHost ?? 'http://example.com' + throw redirect({ + href, + }) + }) diff --git a/e2e/solid-start/basic/app/routeTree.gen.ts b/e2e/solid-start/basic/app/routeTree.gen.ts new file mode 100644 index 0000000000..db7101d5f5 --- /dev/null +++ b/e2e/solid-start/basic/app/routeTree.gen.ts @@ -0,0 +1,923 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +// Import Routes + +import { Route as rootRoute } from './routes/__root' +import { Route as UsersImport } from './routes/users' +import { Route as StreamImport } from './routes/stream' +import { Route as SearchParamsImport } from './routes/search-params' +import { Route as ScriptsImport } from './routes/scripts' +import { Route as PostsImport } from './routes/posts' +import { Route as LinksImport } from './routes/links' +import { Route as DeferredImport } from './routes/deferred' +import { Route as LayoutImport } from './routes/_layout' +import { Route as NotFoundRouteImport } from './routes/not-found/route' +import { Route as IndexImport } from './routes/index' +import { Route as UsersIndexImport } from './routes/users.index' +import { Route as RedirectIndexImport } from './routes/redirect/index' +import { Route as PostsIndexImport } from './routes/posts.index' +import { Route as NotFoundIndexImport } from './routes/not-found/index' +import { Route as UsersUserIdImport } from './routes/users.$userId' +import { Route as RedirectTargetImport } from './routes/redirect/$target' +import { Route as PostsPostIdImport } from './routes/posts.$postId' +import { Route as NotFoundViaLoaderImport } from './routes/not-found/via-loader' +import { Route as NotFoundViaBeforeLoadImport } from './routes/not-found/via-beforeLoad' +import { Route as LayoutLayout2Import } from './routes/_layout/_layout-2' +import { Route as RedirectTargetIndexImport } from './routes/redirect/$target/index' +import { Route as RedirectTargetViaLoaderImport } from './routes/redirect/$target/via-loader' +import { Route as RedirectTargetViaBeforeLoadImport } from './routes/redirect/$target/via-beforeLoad' +import { Route as PostsPostIdDeepImport } from './routes/posts_.$postId.deep' +import { Route as LayoutLayout2LayoutBImport } from './routes/_layout/_layout-2/layout-b' +import { Route as LayoutLayout2LayoutAImport } from './routes/_layout/_layout-2/layout-a' +import { Route as RedirectTargetServerFnIndexImport } from './routes/redirect/$target/serverFn/index' +import { Route as RedirectTargetServerFnViaUseServerFnImport } from './routes/redirect/$target/serverFn/via-useServerFn' +import { Route as RedirectTargetServerFnViaLoaderImport } from './routes/redirect/$target/serverFn/via-loader' +import { Route as RedirectTargetServerFnViaBeforeLoadImport } from './routes/redirect/$target/serverFn/via-beforeLoad' + +// Create/Update Routes + +const UsersRoute = UsersImport.update({ + id: '/users', + path: '/users', + getParentRoute: () => rootRoute, +} as any) + +const StreamRoute = StreamImport.update({ + id: '/stream', + path: '/stream', + getParentRoute: () => rootRoute, +} as any) + +const SearchParamsRoute = SearchParamsImport.update({ + id: '/search-params', + path: '/search-params', + getParentRoute: () => rootRoute, +} as any) + +const ScriptsRoute = ScriptsImport.update({ + id: '/scripts', + path: '/scripts', + getParentRoute: () => rootRoute, +} as any) + +const PostsRoute = PostsImport.update({ + id: '/posts', + path: '/posts', + getParentRoute: () => rootRoute, +} as any) + +const LinksRoute = LinksImport.update({ + id: '/links', + path: '/links', + getParentRoute: () => rootRoute, +} as any) + +const DeferredRoute = DeferredImport.update({ + id: '/deferred', + path: '/deferred', + getParentRoute: () => rootRoute, +} as any) + +const LayoutRoute = LayoutImport.update({ + id: '/_layout', + getParentRoute: () => rootRoute, +} as any) + +const NotFoundRouteRoute = NotFoundRouteImport.update({ + id: '/not-found', + path: '/not-found', + getParentRoute: () => rootRoute, +} as any) + +const IndexRoute = IndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRoute, +} as any) + +const UsersIndexRoute = UsersIndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => UsersRoute, +} as any) + +const RedirectIndexRoute = RedirectIndexImport.update({ + id: '/redirect/', + path: '/redirect/', + getParentRoute: () => rootRoute, +} as any) + +const PostsIndexRoute = PostsIndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => PostsRoute, +} as any) + +const NotFoundIndexRoute = NotFoundIndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => NotFoundRouteRoute, +} as any) + +const UsersUserIdRoute = UsersUserIdImport.update({ + id: '/$userId', + path: '/$userId', + getParentRoute: () => UsersRoute, +} as any) + +const RedirectTargetRoute = RedirectTargetImport.update({ + id: '/redirect/$target', + path: '/redirect/$target', + getParentRoute: () => rootRoute, +} as any) + +const PostsPostIdRoute = PostsPostIdImport.update({ + id: '/$postId', + path: '/$postId', + getParentRoute: () => PostsRoute, +} as any) + +const NotFoundViaLoaderRoute = NotFoundViaLoaderImport.update({ + id: '/via-loader', + path: '/via-loader', + getParentRoute: () => NotFoundRouteRoute, +} as any) + +const NotFoundViaBeforeLoadRoute = NotFoundViaBeforeLoadImport.update({ + id: '/via-beforeLoad', + path: '/via-beforeLoad', + getParentRoute: () => NotFoundRouteRoute, +} as any) + +const LayoutLayout2Route = LayoutLayout2Import.update({ + id: '/_layout-2', + getParentRoute: () => LayoutRoute, +} as any) + +const RedirectTargetIndexRoute = RedirectTargetIndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => RedirectTargetRoute, +} as any) + +const RedirectTargetViaLoaderRoute = RedirectTargetViaLoaderImport.update({ + id: '/via-loader', + path: '/via-loader', + getParentRoute: () => RedirectTargetRoute, +} as any) + +const RedirectTargetViaBeforeLoadRoute = + RedirectTargetViaBeforeLoadImport.update({ + id: '/via-beforeLoad', + path: '/via-beforeLoad', + getParentRoute: () => RedirectTargetRoute, + } as any) + +const PostsPostIdDeepRoute = PostsPostIdDeepImport.update({ + id: '/posts_/$postId/deep', + path: '/posts/$postId/deep', + getParentRoute: () => rootRoute, +} as any) + +const LayoutLayout2LayoutBRoute = LayoutLayout2LayoutBImport.update({ + id: '/layout-b', + path: '/layout-b', + getParentRoute: () => LayoutLayout2Route, +} as any) + +const LayoutLayout2LayoutARoute = LayoutLayout2LayoutAImport.update({ + id: '/layout-a', + path: '/layout-a', + getParentRoute: () => LayoutLayout2Route, +} as any) + +const RedirectTargetServerFnIndexRoute = + RedirectTargetServerFnIndexImport.update({ + id: '/serverFn/', + path: '/serverFn/', + getParentRoute: () => RedirectTargetRoute, + } as any) + +const RedirectTargetServerFnViaUseServerFnRoute = + RedirectTargetServerFnViaUseServerFnImport.update({ + id: '/serverFn/via-useServerFn', + path: '/serverFn/via-useServerFn', + getParentRoute: () => RedirectTargetRoute, + } as any) + +const RedirectTargetServerFnViaLoaderRoute = + RedirectTargetServerFnViaLoaderImport.update({ + id: '/serverFn/via-loader', + path: '/serverFn/via-loader', + getParentRoute: () => RedirectTargetRoute, + } as any) + +const RedirectTargetServerFnViaBeforeLoadRoute = + RedirectTargetServerFnViaBeforeLoadImport.update({ + id: '/serverFn/via-beforeLoad', + path: '/serverFn/via-beforeLoad', + getParentRoute: () => RedirectTargetRoute, + } as any) + +// Populate the FileRoutesByPath interface + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexImport + parentRoute: typeof rootRoute + } + '/not-found': { + id: '/not-found' + path: '/not-found' + fullPath: '/not-found' + preLoaderRoute: typeof NotFoundRouteImport + parentRoute: typeof rootRoute + } + '/_layout': { + id: '/_layout' + path: '' + fullPath: '' + preLoaderRoute: typeof LayoutImport + parentRoute: typeof rootRoute + } + '/deferred': { + id: '/deferred' + path: '/deferred' + fullPath: '/deferred' + preLoaderRoute: typeof DeferredImport + parentRoute: typeof rootRoute + } + '/links': { + id: '/links' + path: '/links' + fullPath: '/links' + preLoaderRoute: typeof LinksImport + parentRoute: typeof rootRoute + } + '/posts': { + id: '/posts' + path: '/posts' + fullPath: '/posts' + preLoaderRoute: typeof PostsImport + parentRoute: typeof rootRoute + } + '/scripts': { + id: '/scripts' + path: '/scripts' + fullPath: '/scripts' + preLoaderRoute: typeof ScriptsImport + parentRoute: typeof rootRoute + } + '/search-params': { + id: '/search-params' + path: '/search-params' + fullPath: '/search-params' + preLoaderRoute: typeof SearchParamsImport + parentRoute: typeof rootRoute + } + '/stream': { + id: '/stream' + path: '/stream' + fullPath: '/stream' + preLoaderRoute: typeof StreamImport + parentRoute: typeof rootRoute + } + '/users': { + id: '/users' + path: '/users' + fullPath: '/users' + preLoaderRoute: typeof UsersImport + parentRoute: typeof rootRoute + } + '/_layout/_layout-2': { + id: '/_layout/_layout-2' + path: '' + fullPath: '' + preLoaderRoute: typeof LayoutLayout2Import + parentRoute: typeof LayoutImport + } + '/not-found/via-beforeLoad': { + id: '/not-found/via-beforeLoad' + path: '/via-beforeLoad' + fullPath: '/not-found/via-beforeLoad' + preLoaderRoute: typeof NotFoundViaBeforeLoadImport + parentRoute: typeof NotFoundRouteImport + } + '/not-found/via-loader': { + id: '/not-found/via-loader' + path: '/via-loader' + fullPath: '/not-found/via-loader' + preLoaderRoute: typeof NotFoundViaLoaderImport + parentRoute: typeof NotFoundRouteImport + } + '/posts/$postId': { + id: '/posts/$postId' + path: '/$postId' + fullPath: '/posts/$postId' + preLoaderRoute: typeof PostsPostIdImport + parentRoute: typeof PostsImport + } + '/redirect/$target': { + id: '/redirect/$target' + path: '/redirect/$target' + fullPath: '/redirect/$target' + preLoaderRoute: typeof RedirectTargetImport + parentRoute: typeof rootRoute + } + '/users/$userId': { + id: '/users/$userId' + path: '/$userId' + fullPath: '/users/$userId' + preLoaderRoute: typeof UsersUserIdImport + parentRoute: typeof UsersImport + } + '/not-found/': { + id: '/not-found/' + path: '/' + fullPath: '/not-found/' + preLoaderRoute: typeof NotFoundIndexImport + parentRoute: typeof NotFoundRouteImport + } + '/posts/': { + id: '/posts/' + path: '/' + fullPath: '/posts/' + preLoaderRoute: typeof PostsIndexImport + parentRoute: typeof PostsImport + } + '/redirect/': { + id: '/redirect/' + path: '/redirect' + fullPath: '/redirect' + preLoaderRoute: typeof RedirectIndexImport + parentRoute: typeof rootRoute + } + '/users/': { + id: '/users/' + path: '/' + fullPath: '/users/' + preLoaderRoute: typeof UsersIndexImport + parentRoute: typeof UsersImport + } + '/_layout/_layout-2/layout-a': { + id: '/_layout/_layout-2/layout-a' + path: '/layout-a' + fullPath: '/layout-a' + preLoaderRoute: typeof LayoutLayout2LayoutAImport + parentRoute: typeof LayoutLayout2Import + } + '/_layout/_layout-2/layout-b': { + id: '/_layout/_layout-2/layout-b' + path: '/layout-b' + fullPath: '/layout-b' + preLoaderRoute: typeof LayoutLayout2LayoutBImport + parentRoute: typeof LayoutLayout2Import + } + '/posts_/$postId/deep': { + id: '/posts_/$postId/deep' + path: '/posts/$postId/deep' + fullPath: '/posts/$postId/deep' + preLoaderRoute: typeof PostsPostIdDeepImport + parentRoute: typeof rootRoute + } + '/redirect/$target/via-beforeLoad': { + id: '/redirect/$target/via-beforeLoad' + path: '/via-beforeLoad' + fullPath: '/redirect/$target/via-beforeLoad' + preLoaderRoute: typeof RedirectTargetViaBeforeLoadImport + parentRoute: typeof RedirectTargetImport + } + '/redirect/$target/via-loader': { + id: '/redirect/$target/via-loader' + path: '/via-loader' + fullPath: '/redirect/$target/via-loader' + preLoaderRoute: typeof RedirectTargetViaLoaderImport + parentRoute: typeof RedirectTargetImport + } + '/redirect/$target/': { + id: '/redirect/$target/' + path: '/' + fullPath: '/redirect/$target/' + preLoaderRoute: typeof RedirectTargetIndexImport + parentRoute: typeof RedirectTargetImport + } + '/redirect/$target/serverFn/via-beforeLoad': { + id: '/redirect/$target/serverFn/via-beforeLoad' + path: '/serverFn/via-beforeLoad' + fullPath: '/redirect/$target/serverFn/via-beforeLoad' + preLoaderRoute: typeof RedirectTargetServerFnViaBeforeLoadImport + parentRoute: typeof RedirectTargetImport + } + '/redirect/$target/serverFn/via-loader': { + id: '/redirect/$target/serverFn/via-loader' + path: '/serverFn/via-loader' + fullPath: '/redirect/$target/serverFn/via-loader' + preLoaderRoute: typeof RedirectTargetServerFnViaLoaderImport + parentRoute: typeof RedirectTargetImport + } + '/redirect/$target/serverFn/via-useServerFn': { + id: '/redirect/$target/serverFn/via-useServerFn' + path: '/serverFn/via-useServerFn' + fullPath: '/redirect/$target/serverFn/via-useServerFn' + preLoaderRoute: typeof RedirectTargetServerFnViaUseServerFnImport + parentRoute: typeof RedirectTargetImport + } + '/redirect/$target/serverFn/': { + id: '/redirect/$target/serverFn/' + path: '/serverFn' + fullPath: '/redirect/$target/serverFn' + preLoaderRoute: typeof RedirectTargetServerFnIndexImport + parentRoute: typeof RedirectTargetImport + } + } +} + +// Create and export the route tree + +interface NotFoundRouteRouteChildren { + NotFoundViaBeforeLoadRoute: typeof NotFoundViaBeforeLoadRoute + NotFoundViaLoaderRoute: typeof NotFoundViaLoaderRoute + NotFoundIndexRoute: typeof NotFoundIndexRoute +} + +const NotFoundRouteRouteChildren: NotFoundRouteRouteChildren = { + NotFoundViaBeforeLoadRoute: NotFoundViaBeforeLoadRoute, + NotFoundViaLoaderRoute: NotFoundViaLoaderRoute, + NotFoundIndexRoute: NotFoundIndexRoute, +} + +const NotFoundRouteRouteWithChildren = NotFoundRouteRoute._addFileChildren( + NotFoundRouteRouteChildren, +) + +interface LayoutLayout2RouteChildren { + LayoutLayout2LayoutARoute: typeof LayoutLayout2LayoutARoute + LayoutLayout2LayoutBRoute: typeof LayoutLayout2LayoutBRoute +} + +const LayoutLayout2RouteChildren: LayoutLayout2RouteChildren = { + LayoutLayout2LayoutARoute: LayoutLayout2LayoutARoute, + LayoutLayout2LayoutBRoute: LayoutLayout2LayoutBRoute, +} + +const LayoutLayout2RouteWithChildren = LayoutLayout2Route._addFileChildren( + LayoutLayout2RouteChildren, +) + +interface LayoutRouteChildren { + LayoutLayout2Route: typeof LayoutLayout2RouteWithChildren +} + +const LayoutRouteChildren: LayoutRouteChildren = { + LayoutLayout2Route: LayoutLayout2RouteWithChildren, +} + +const LayoutRouteWithChildren = + LayoutRoute._addFileChildren(LayoutRouteChildren) + +interface PostsRouteChildren { + PostsPostIdRoute: typeof PostsPostIdRoute + PostsIndexRoute: typeof PostsIndexRoute +} + +const PostsRouteChildren: PostsRouteChildren = { + PostsPostIdRoute: PostsPostIdRoute, + PostsIndexRoute: PostsIndexRoute, +} + +const PostsRouteWithChildren = PostsRoute._addFileChildren(PostsRouteChildren) + +interface UsersRouteChildren { + UsersUserIdRoute: typeof UsersUserIdRoute + UsersIndexRoute: typeof UsersIndexRoute +} + +const UsersRouteChildren: UsersRouteChildren = { + UsersUserIdRoute: UsersUserIdRoute, + UsersIndexRoute: UsersIndexRoute, +} + +const UsersRouteWithChildren = UsersRoute._addFileChildren(UsersRouteChildren) + +interface RedirectTargetRouteChildren { + RedirectTargetViaBeforeLoadRoute: typeof RedirectTargetViaBeforeLoadRoute + RedirectTargetViaLoaderRoute: typeof RedirectTargetViaLoaderRoute + RedirectTargetIndexRoute: typeof RedirectTargetIndexRoute + RedirectTargetServerFnViaBeforeLoadRoute: typeof RedirectTargetServerFnViaBeforeLoadRoute + RedirectTargetServerFnViaLoaderRoute: typeof RedirectTargetServerFnViaLoaderRoute + RedirectTargetServerFnViaUseServerFnRoute: typeof RedirectTargetServerFnViaUseServerFnRoute + RedirectTargetServerFnIndexRoute: typeof RedirectTargetServerFnIndexRoute +} + +const RedirectTargetRouteChildren: RedirectTargetRouteChildren = { + RedirectTargetViaBeforeLoadRoute: RedirectTargetViaBeforeLoadRoute, + RedirectTargetViaLoaderRoute: RedirectTargetViaLoaderRoute, + RedirectTargetIndexRoute: RedirectTargetIndexRoute, + RedirectTargetServerFnViaBeforeLoadRoute: + RedirectTargetServerFnViaBeforeLoadRoute, + RedirectTargetServerFnViaLoaderRoute: RedirectTargetServerFnViaLoaderRoute, + RedirectTargetServerFnViaUseServerFnRoute: + RedirectTargetServerFnViaUseServerFnRoute, + RedirectTargetServerFnIndexRoute: RedirectTargetServerFnIndexRoute, +} + +const RedirectTargetRouteWithChildren = RedirectTargetRoute._addFileChildren( + RedirectTargetRouteChildren, +) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/not-found': typeof NotFoundRouteRouteWithChildren + '': typeof LayoutLayout2RouteWithChildren + '/deferred': typeof DeferredRoute + '/links': typeof LinksRoute + '/posts': typeof PostsRouteWithChildren + '/scripts': typeof ScriptsRoute + '/search-params': typeof SearchParamsRoute + '/stream': typeof StreamRoute + '/users': typeof UsersRouteWithChildren + '/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute + '/not-found/via-loader': typeof NotFoundViaLoaderRoute + '/posts/$postId': typeof PostsPostIdRoute + '/redirect/$target': typeof RedirectTargetRouteWithChildren + '/users/$userId': typeof UsersUserIdRoute + '/not-found/': typeof NotFoundIndexRoute + '/posts/': typeof PostsIndexRoute + '/redirect': typeof RedirectIndexRoute + '/users/': typeof UsersIndexRoute + '/layout-a': typeof LayoutLayout2LayoutARoute + '/layout-b': typeof LayoutLayout2LayoutBRoute + '/posts/$postId/deep': typeof PostsPostIdDeepRoute + '/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute + '/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute + '/redirect/$target/': typeof RedirectTargetIndexRoute + '/redirect/$target/serverFn/via-beforeLoad': typeof RedirectTargetServerFnViaBeforeLoadRoute + '/redirect/$target/serverFn/via-loader': typeof RedirectTargetServerFnViaLoaderRoute + '/redirect/$target/serverFn/via-useServerFn': typeof RedirectTargetServerFnViaUseServerFnRoute + '/redirect/$target/serverFn': typeof RedirectTargetServerFnIndexRoute +} + +export interface FileRoutesByTo { + '/': typeof IndexRoute + '': typeof LayoutLayout2RouteWithChildren + '/deferred': typeof DeferredRoute + '/links': typeof LinksRoute + '/scripts': typeof ScriptsRoute + '/search-params': typeof SearchParamsRoute + '/stream': typeof StreamRoute + '/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute + '/not-found/via-loader': typeof NotFoundViaLoaderRoute + '/posts/$postId': typeof PostsPostIdRoute + '/users/$userId': typeof UsersUserIdRoute + '/not-found': typeof NotFoundIndexRoute + '/posts': typeof PostsIndexRoute + '/redirect': typeof RedirectIndexRoute + '/users': typeof UsersIndexRoute + '/layout-a': typeof LayoutLayout2LayoutARoute + '/layout-b': typeof LayoutLayout2LayoutBRoute + '/posts/$postId/deep': typeof PostsPostIdDeepRoute + '/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute + '/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute + '/redirect/$target': typeof RedirectTargetIndexRoute + '/redirect/$target/serverFn/via-beforeLoad': typeof RedirectTargetServerFnViaBeforeLoadRoute + '/redirect/$target/serverFn/via-loader': typeof RedirectTargetServerFnViaLoaderRoute + '/redirect/$target/serverFn/via-useServerFn': typeof RedirectTargetServerFnViaUseServerFnRoute + '/redirect/$target/serverFn': typeof RedirectTargetServerFnIndexRoute +} + +export interface FileRoutesById { + __root__: typeof rootRoute + '/': typeof IndexRoute + '/not-found': typeof NotFoundRouteRouteWithChildren + '/_layout': typeof LayoutRouteWithChildren + '/deferred': typeof DeferredRoute + '/links': typeof LinksRoute + '/posts': typeof PostsRouteWithChildren + '/scripts': typeof ScriptsRoute + '/search-params': typeof SearchParamsRoute + '/stream': typeof StreamRoute + '/users': typeof UsersRouteWithChildren + '/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren + '/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute + '/not-found/via-loader': typeof NotFoundViaLoaderRoute + '/posts/$postId': typeof PostsPostIdRoute + '/redirect/$target': typeof RedirectTargetRouteWithChildren + '/users/$userId': typeof UsersUserIdRoute + '/not-found/': typeof NotFoundIndexRoute + '/posts/': typeof PostsIndexRoute + '/redirect/': typeof RedirectIndexRoute + '/users/': typeof UsersIndexRoute + '/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute + '/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute + '/posts_/$postId/deep': typeof PostsPostIdDeepRoute + '/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute + '/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute + '/redirect/$target/': typeof RedirectTargetIndexRoute + '/redirect/$target/serverFn/via-beforeLoad': typeof RedirectTargetServerFnViaBeforeLoadRoute + '/redirect/$target/serverFn/via-loader': typeof RedirectTargetServerFnViaLoaderRoute + '/redirect/$target/serverFn/via-useServerFn': typeof RedirectTargetServerFnViaUseServerFnRoute + '/redirect/$target/serverFn/': typeof RedirectTargetServerFnIndexRoute +} + +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/not-found' + | '' + | '/deferred' + | '/links' + | '/posts' + | '/scripts' + | '/search-params' + | '/stream' + | '/users' + | '/not-found/via-beforeLoad' + | '/not-found/via-loader' + | '/posts/$postId' + | '/redirect/$target' + | '/users/$userId' + | '/not-found/' + | '/posts/' + | '/redirect' + | '/users/' + | '/layout-a' + | '/layout-b' + | '/posts/$postId/deep' + | '/redirect/$target/via-beforeLoad' + | '/redirect/$target/via-loader' + | '/redirect/$target/' + | '/redirect/$target/serverFn/via-beforeLoad' + | '/redirect/$target/serverFn/via-loader' + | '/redirect/$target/serverFn/via-useServerFn' + | '/redirect/$target/serverFn' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '' + | '/deferred' + | '/links' + | '/scripts' + | '/search-params' + | '/stream' + | '/not-found/via-beforeLoad' + | '/not-found/via-loader' + | '/posts/$postId' + | '/users/$userId' + | '/not-found' + | '/posts' + | '/redirect' + | '/users' + | '/layout-a' + | '/layout-b' + | '/posts/$postId/deep' + | '/redirect/$target/via-beforeLoad' + | '/redirect/$target/via-loader' + | '/redirect/$target' + | '/redirect/$target/serverFn/via-beforeLoad' + | '/redirect/$target/serverFn/via-loader' + | '/redirect/$target/serverFn/via-useServerFn' + | '/redirect/$target/serverFn' + id: + | '__root__' + | '/' + | '/not-found' + | '/_layout' + | '/deferred' + | '/links' + | '/posts' + | '/scripts' + | '/search-params' + | '/stream' + | '/users' + | '/_layout/_layout-2' + | '/not-found/via-beforeLoad' + | '/not-found/via-loader' + | '/posts/$postId' + | '/redirect/$target' + | '/users/$userId' + | '/not-found/' + | '/posts/' + | '/redirect/' + | '/users/' + | '/_layout/_layout-2/layout-a' + | '/_layout/_layout-2/layout-b' + | '/posts_/$postId/deep' + | '/redirect/$target/via-beforeLoad' + | '/redirect/$target/via-loader' + | '/redirect/$target/' + | '/redirect/$target/serverFn/via-beforeLoad' + | '/redirect/$target/serverFn/via-loader' + | '/redirect/$target/serverFn/via-useServerFn' + | '/redirect/$target/serverFn/' + fileRoutesById: FileRoutesById +} + +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + NotFoundRouteRoute: typeof NotFoundRouteRouteWithChildren + LayoutRoute: typeof LayoutRouteWithChildren + DeferredRoute: typeof DeferredRoute + LinksRoute: typeof LinksRoute + PostsRoute: typeof PostsRouteWithChildren + ScriptsRoute: typeof ScriptsRoute + SearchParamsRoute: typeof SearchParamsRoute + StreamRoute: typeof StreamRoute + UsersRoute: typeof UsersRouteWithChildren + RedirectTargetRoute: typeof RedirectTargetRouteWithChildren + RedirectIndexRoute: typeof RedirectIndexRoute + PostsPostIdDeepRoute: typeof PostsPostIdDeepRoute +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + NotFoundRouteRoute: NotFoundRouteRouteWithChildren, + LayoutRoute: LayoutRouteWithChildren, + DeferredRoute: DeferredRoute, + LinksRoute: LinksRoute, + PostsRoute: PostsRouteWithChildren, + ScriptsRoute: ScriptsRoute, + SearchParamsRoute: SearchParamsRoute, + StreamRoute: StreamRoute, + UsersRoute: UsersRouteWithChildren, + RedirectTargetRoute: RedirectTargetRouteWithChildren, + RedirectIndexRoute: RedirectIndexRoute, + PostsPostIdDeepRoute: PostsPostIdDeepRoute, +} + +export const routeTree = rootRoute + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +/* ROUTE_MANIFEST_START +{ + "routes": { + "__root__": { + "filePath": "__root.tsx", + "children": [ + "/", + "/not-found", + "/_layout", + "/deferred", + "/links", + "/posts", + "/scripts", + "/search-params", + "/stream", + "/users", + "/redirect/$target", + "/redirect/", + "/posts_/$postId/deep" + ] + }, + "/": { + "filePath": "index.tsx" + }, + "/not-found": { + "filePath": "not-found/route.tsx", + "children": [ + "/not-found/via-beforeLoad", + "/not-found/via-loader", + "/not-found/" + ] + }, + "/_layout": { + "filePath": "_layout.tsx", + "children": [ + "/_layout/_layout-2" + ] + }, + "/deferred": { + "filePath": "deferred.tsx" + }, + "/links": { + "filePath": "links.tsx" + }, + "/posts": { + "filePath": "posts.tsx", + "children": [ + "/posts/$postId", + "/posts/" + ] + }, + "/scripts": { + "filePath": "scripts.tsx" + }, + "/search-params": { + "filePath": "search-params.tsx" + }, + "/stream": { + "filePath": "stream.tsx" + }, + "/users": { + "filePath": "users.tsx", + "children": [ + "/users/$userId", + "/users/" + ] + }, + "/_layout/_layout-2": { + "filePath": "_layout/_layout-2.tsx", + "parent": "/_layout", + "children": [ + "/_layout/_layout-2/layout-a", + "/_layout/_layout-2/layout-b" + ] + }, + "/not-found/via-beforeLoad": { + "filePath": "not-found/via-beforeLoad.tsx", + "parent": "/not-found" + }, + "/not-found/via-loader": { + "filePath": "not-found/via-loader.tsx", + "parent": "/not-found" + }, + "/posts/$postId": { + "filePath": "posts.$postId.tsx", + "parent": "/posts" + }, + "/redirect/$target": { + "filePath": "redirect/$target.tsx", + "children": [ + "/redirect/$target/via-beforeLoad", + "/redirect/$target/via-loader", + "/redirect/$target/", + "/redirect/$target/serverFn/via-beforeLoad", + "/redirect/$target/serverFn/via-loader", + "/redirect/$target/serverFn/via-useServerFn", + "/redirect/$target/serverFn/" + ] + }, + "/users/$userId": { + "filePath": "users.$userId.tsx", + "parent": "/users" + }, + "/not-found/": { + "filePath": "not-found/index.tsx", + "parent": "/not-found" + }, + "/posts/": { + "filePath": "posts.index.tsx", + "parent": "/posts" + }, + "/redirect/": { + "filePath": "redirect/index.tsx" + }, + "/users/": { + "filePath": "users.index.tsx", + "parent": "/users" + }, + "/_layout/_layout-2/layout-a": { + "filePath": "_layout/_layout-2/layout-a.tsx", + "parent": "/_layout/_layout-2" + }, + "/_layout/_layout-2/layout-b": { + "filePath": "_layout/_layout-2/layout-b.tsx", + "parent": "/_layout/_layout-2" + }, + "/posts_/$postId/deep": { + "filePath": "posts_.$postId.deep.tsx" + }, + "/redirect/$target/via-beforeLoad": { + "filePath": "redirect/$target/via-beforeLoad.tsx", + "parent": "/redirect/$target" + }, + "/redirect/$target/via-loader": { + "filePath": "redirect/$target/via-loader.tsx", + "parent": "/redirect/$target" + }, + "/redirect/$target/": { + "filePath": "redirect/$target/index.tsx", + "parent": "/redirect/$target" + }, + "/redirect/$target/serverFn/via-beforeLoad": { + "filePath": "redirect/$target/serverFn/via-beforeLoad.tsx", + "parent": "/redirect/$target" + }, + "/redirect/$target/serverFn/via-loader": { + "filePath": "redirect/$target/serverFn/via-loader.tsx", + "parent": "/redirect/$target" + }, + "/redirect/$target/serverFn/via-useServerFn": { + "filePath": "redirect/$target/serverFn/via-useServerFn.tsx", + "parent": "/redirect/$target" + }, + "/redirect/$target/serverFn/": { + "filePath": "redirect/$target/serverFn/index.tsx", + "parent": "/redirect/$target" + } + } +} +ROUTE_MANIFEST_END */ diff --git a/e2e/solid-start/basic/app/router.tsx b/e2e/solid-start/basic/app/router.tsx new file mode 100644 index 0000000000..c45bed4758 --- /dev/null +++ b/e2e/solid-start/basic/app/router.tsx @@ -0,0 +1,22 @@ +import { createRouter as createTanStackRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' +import { DefaultCatchBoundary } from './components/DefaultCatchBoundary' +import { NotFound } from './components/NotFound' + +export function createRouter() { + const router = createTanStackRouter({ + routeTree, + defaultPreload: 'intent', + defaultErrorComponent: DefaultCatchBoundary, + defaultNotFoundComponent: () => , + scrollRestoration: true, + }) + + return router +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/e2e/solid-start/basic/app/routes/__root.tsx b/e2e/solid-start/basic/app/routes/__root.tsx new file mode 100644 index 0000000000..dd290499f0 --- /dev/null +++ b/e2e/solid-start/basic/app/routes/__root.tsx @@ -0,0 +1,165 @@ +import * as React from 'react' +import { + HeadContent, + Link, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' + +import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary' +import { NotFound } from '~/components/NotFound' +import appCss from '~/styles/app.css?url' +import { seo } from '~/utils/seo' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { + charSet: 'utf-8', + }, + { + name: 'viewport', + content: 'width=device-width, initial-scale=1', + }, + ...seo({ + title: + 'TanStack Start | Type-Safe, Client-First, Full-Stack React Framework', + description: `TanStack Start is a type-safe, client-first, full-stack React framework. `, + }), + ], + links: [ + { rel: 'stylesheet', href: appCss }, + { + rel: 'apple-touch-icon', + sizes: '180x180', + href: '/apple-touch-icon.png', + }, + { + rel: 'icon', + type: 'image/png', + sizes: '32x32', + href: '/favicon-32x32.png', + }, + { + rel: 'icon', + type: 'image/png', + sizes: '16x16', + href: '/favicon-16x16.png', + }, + { rel: 'manifest', href: '/site.webmanifest', color: '#fffff' }, + { rel: 'icon', href: '/favicon.ico' }, + ], + }), + errorComponent: (props) => { + return ( + + + + ) + }, + notFoundComponent: () => , + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + ) +} + +const RouterDevtools = + process.env.NODE_ENV === 'production' + ? () => null // Render nothing in production + : React.lazy(() => + // Lazy load in development + import('@tanstack/router-devtools').then((res) => ({ + default: res.TanStackRouterDevtools, + })), + ) + +function RootDocument({ children }: { children: React.ReactNode }) { + return ( + + + + + +
+ + Home + {' '} + + Posts + {' '} + + Users + {' '} + + Layout + {' '} + + Scripts + {' '} + + Deferred + {' '} + + redirect + {' '} + + This Route Does Not Exist + +
+
+ {children} + + + + + ) +} diff --git a/e2e/solid-start/basic/app/routes/_layout.tsx b/e2e/solid-start/basic/app/routes/_layout.tsx new file mode 100644 index 0000000000..5514bb8913 --- /dev/null +++ b/e2e/solid-start/basic/app/routes/_layout.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a layout
+
+ +
+
+ ) +} diff --git a/e2e/solid-start/basic/app/routes/_layout/_layout-2.tsx b/e2e/solid-start/basic/app/routes/_layout/_layout-2.tsx new file mode 100644 index 0000000000..b5b52c44e2 --- /dev/null +++ b/e2e/solid-start/basic/app/routes/_layout/_layout-2.tsx @@ -0,0 +1,34 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout/_layout-2')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a nested layout
+
+ + Layout A + + + Layout B + +
+
+ +
+
+ ) +} diff --git a/e2e/solid-start/basic/app/routes/_layout/_layout-2/layout-a.tsx b/e2e/solid-start/basic/app/routes/_layout/_layout-2/layout-a.tsx new file mode 100644 index 0000000000..b69951b246 --- /dev/null +++ b/e2e/solid-start/basic/app/routes/_layout/_layout-2/layout-a.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout/_layout-2/layout-a')({ + component: LayoutAComponent, +}) + +function LayoutAComponent() { + return
I'm layout A!
+} diff --git a/e2e/solid-start/basic/app/routes/_layout/_layout-2/layout-b.tsx b/e2e/solid-start/basic/app/routes/_layout/_layout-2/layout-b.tsx new file mode 100644 index 0000000000..30dbcce90f --- /dev/null +++ b/e2e/solid-start/basic/app/routes/_layout/_layout-2/layout-b.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout/_layout-2/layout-b')({ + component: LayoutBComponent, +}) + +function LayoutBComponent() { + return
I'm layout B!
+} diff --git a/e2e/solid-start/basic/app/routes/api.users.ts b/e2e/solid-start/basic/app/routes/api.users.ts new file mode 100644 index 0000000000..45ac83b2f0 --- /dev/null +++ b/e2e/solid-start/basic/app/routes/api.users.ts @@ -0,0 +1,18 @@ +import { json } from '@tanstack/solid-start' +import { createAPIFileRoute } from '@tanstack/solid-start/api' +import axios from 'redaxios' + +import type { User } from '~/utils/users' + +export const APIRoute = createAPIFileRoute('/api/users')({ + GET: async ({ request }) => { + console.info('Fetching users... @', request.url) + const res = await axios.get>( + 'https://jsonplaceholder.typicode.com/users', + ) + + const list = res.data.slice(0, 10) + + return json(list.map((u) => ({ id: u.id, name: u.name, email: u.email }))) + }, +}) diff --git a/e2e/solid-start/basic/app/routes/api/users.$id.ts b/e2e/solid-start/basic/app/routes/api/users.$id.ts new file mode 100644 index 0000000000..7ee1fccbb4 --- /dev/null +++ b/e2e/solid-start/basic/app/routes/api/users.$id.ts @@ -0,0 +1,25 @@ +import { json } from '@tanstack/solid-start' +import { createAPIFileRoute } from '@tanstack/solid-start/api' +import axios from 'redaxios' + +import type { User } from '~/utils/users' + +export const APIRoute = createAPIFileRoute('/api/users/$id')({ + GET: async ({ request, params }) => { + console.info(`Fetching users by id=${params.id}... @`, request.url) + try { + const res = await axios.get( + 'https://jsonplaceholder.typicode.com/users/' + params.id, + ) + + return json({ + id: res.data.id, + name: res.data.name, + email: res.data.email, + }) + } catch (e) { + console.error(e) + return json({ error: 'User not found' }, { status: 404 }) + } + }, +}) diff --git a/e2e/solid-start/basic/app/routes/deferred.tsx b/e2e/solid-start/basic/app/routes/deferred.tsx new file mode 100644 index 0000000000..190b51b972 --- /dev/null +++ b/e2e/solid-start/basic/app/routes/deferred.tsx @@ -0,0 +1,62 @@ +import { Await, createFileRoute } from '@tanstack/solid-router' +import { createServerFn } from '@tanstack/solid-start' +import { Suspense, useState } from 'react' + +const personServerFn = createServerFn({ method: 'GET' }) + .validator((data: { name: string }) => data) + .handler(({ data }) => { + return { name: data.name, randomNumber: Math.floor(Math.random() * 100) } + }) + +const slowServerFn = createServerFn({ method: 'GET' }) + .validator((data: { name: string }) => data) + .handler(async ({ data }) => { + await new Promise((r) => setTimeout(r, 1000)) + return { name: data.name, randomNumber: Math.floor(Math.random() * 100) } + }) + +export const Route = createFileRoute('/deferred')({ + loader: async () => { + return { + deferredStuff: new Promise((r) => + setTimeout(() => r('Hello deferred!'), 2000), + ), + deferredPerson: slowServerFn({ data: { name: 'Tanner Linsley' } }), + person: await personServerFn({ data: { name: 'John Doe' } }), + } + }, + component: Deferred, +}) + +function Deferred() { + const [count, setCount] = useState(0) + const { deferredStuff, deferredPerson, person } = Route.useLoaderData() + + return ( +
+
+ {person.name} - {person.randomNumber} +
+ Loading person...
}> + ( +
+ {data.name} - {data.randomNumber} +
+ )} + /> + + Loading stuff...}> +

{data}

} + /> +
+
Count: {count}
+
+ +
+ + ) +} diff --git a/e2e/solid-start/basic/app/routes/index.tsx b/e2e/solid-start/basic/app/routes/index.tsx new file mode 100644 index 0000000000..c80321f4be --- /dev/null +++ b/e2e/solid-start/basic/app/routes/index.tsx @@ -0,0 +1,15 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { CustomMessage } from '~/components/CustomMessage' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return ( +
+

Welcome Home!!!

+ +
+ ) +} diff --git a/e2e/solid-start/basic/app/routes/links.tsx b/e2e/solid-start/basic/app/routes/links.tsx new file mode 100644 index 0000000000..9f5a67c6db --- /dev/null +++ b/e2e/solid-start/basic/app/routes/links.tsx @@ -0,0 +1,47 @@ +import { Link, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/links')({ + component: () => { + const navigate = Route.useNavigate() + return ( +
+

+ link test +

+
+ + Link to /posts + +
+
+ + Link to /posts (reloadDocument=true) + +
+
+ +
+
+ +
+
+ ) + }, +}) diff --git a/e2e/solid-start/basic/app/routes/not-found/index.tsx b/e2e/solid-start/basic/app/routes/not-found/index.tsx new file mode 100644 index 0000000000..17dd471cd1 --- /dev/null +++ b/e2e/solid-start/basic/app/routes/not-found/index.tsx @@ -0,0 +1,31 @@ +import { Link, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/not-found/')({ + component: () => { + const preload = Route.useSearch({ select: (s) => s.preload }) + return ( +
+
+ + via-beforeLoad + +
+
+ + via-loader + +
+
+ ) + }, +}) diff --git a/e2e/solid-start/basic/app/routes/not-found/route.tsx b/e2e/solid-start/basic/app/routes/not-found/route.tsx new file mode 100644 index 0000000000..84f5ef81a5 --- /dev/null +++ b/e2e/solid-start/basic/app/routes/not-found/route.tsx @@ -0,0 +1,8 @@ +import { createFileRoute } from '@tanstack/solid-router' +import z from 'zod' + +export const Route = createFileRoute('/not-found')({ + validateSearch: z.object({ + preload: z.literal(false).optional(), + }), +}) diff --git a/e2e/solid-start/basic/app/routes/not-found/via-beforeLoad.tsx b/e2e/solid-start/basic/app/routes/not-found/via-beforeLoad.tsx new file mode 100644 index 0000000000..5badde63bd --- /dev/null +++ b/e2e/solid-start/basic/app/routes/not-found/via-beforeLoad.tsx @@ -0,0 +1,23 @@ +import { createFileRoute, notFound } from '@tanstack/solid-router' + +export const Route = createFileRoute('/not-found/via-beforeLoad')({ + beforeLoad: () => { + throw notFound() + }, + component: RouteComponent, + notFoundComponent: () => { + return ( +
+ Not Found "/not-found/via-beforeLoad"! +
+ ) + }, +}) + +function RouteComponent() { + return ( +
+ Hello "/not-found/via-beforeLoad"! +
+ ) +} diff --git a/e2e/solid-start/basic/app/routes/not-found/via-loader.tsx b/e2e/solid-start/basic/app/routes/not-found/via-loader.tsx new file mode 100644 index 0000000000..20956cc43d --- /dev/null +++ b/e2e/solid-start/basic/app/routes/not-found/via-loader.tsx @@ -0,0 +1,23 @@ +import { createFileRoute, notFound } from '@tanstack/solid-router' + +export const Route = createFileRoute('/not-found/via-loader')({ + loader: () => { + throw notFound() + }, + component: RouteComponent, + notFoundComponent: () => { + return ( +
+ Not Found "/not-found/via-loader"! +
+ ) + }, +}) + +function RouteComponent() { + return ( +
+ Hello "/not-found/via-loader"! +
+ ) +} diff --git a/e2e/solid-start/basic/app/routes/posts.$postId.tsx b/e2e/solid-start/basic/app/routes/posts.$postId.tsx new file mode 100644 index 0000000000..7acdb4a89c --- /dev/null +++ b/e2e/solid-start/basic/app/routes/posts.$postId.tsx @@ -0,0 +1,39 @@ +import { ErrorComponent, Link, createFileRoute } from '@tanstack/solid-router' +import type { ErrorComponentProps } from '@tanstack/solid-router' + +import { fetchPost } from '~/utils/posts' +import { NotFound } from '~/components/NotFound' + +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params: { postId } }) => fetchPost({ data: postId }), + errorComponent: PostErrorComponent, + component: PostComponent, + notFoundComponent: () => { + return Post not found + }, +}) + +export function PostErrorComponent({ error }: ErrorComponentProps) { + return +} + +function PostComponent() { + const post = Route.useLoaderData() + + return ( +
+

{post.title}

+
{post.body}
+ + Deep View + +
+ ) +} diff --git a/e2e/solid-start/basic/app/routes/posts.index.tsx b/e2e/solid-start/basic/app/routes/posts.index.tsx new file mode 100644 index 0000000000..c7d8cfe19c --- /dev/null +++ b/e2e/solid-start/basic/app/routes/posts.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/posts/')({ + component: PostsIndexComponent, +}) + +function PostsIndexComponent() { + return
Select a post.
+} diff --git a/e2e/solid-start/basic/app/routes/posts.tsx b/e2e/solid-start/basic/app/routes/posts.tsx new file mode 100644 index 0000000000..d84eeb0e3d --- /dev/null +++ b/e2e/solid-start/basic/app/routes/posts.tsx @@ -0,0 +1,39 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/solid-router' + +import { fetchPosts } from '~/utils/posts' + +export const Route = createFileRoute('/posts')({ + loader: async () => fetchPosts(), + component: PostsComponent, +}) + +function PostsComponent() { + const posts = Route.useLoaderData() + + return ( +
+
    + {[...posts, { id: 'i-do-not-exist', title: 'Non-existent Post' }].map( + (post) => { + return ( +
  • + +
    {post.title.substring(0, 20)}
    + +
  • + ) + }, + )} +
+
+ +
+ ) +} diff --git a/e2e/solid-start/basic/app/routes/posts_.$postId.deep.tsx b/e2e/solid-start/basic/app/routes/posts_.$postId.deep.tsx new file mode 100644 index 0000000000..643b1a6497 --- /dev/null +++ b/e2e/solid-start/basic/app/routes/posts_.$postId.deep.tsx @@ -0,0 +1,27 @@ +import { Link, createFileRoute } from '@tanstack/solid-router' + +import { PostErrorComponent } from './posts.$postId' +import { fetchPost } from '~/utils/posts' + +export const Route = createFileRoute('/posts_/$postId/deep')({ + loader: async ({ params: { postId } }) => fetchPost({ data: postId }), + errorComponent: PostErrorComponent, + component: PostDeepComponent, +}) + +function PostDeepComponent() { + const post = Route.useLoaderData() + + return ( +
+ + ← All Posts + +

{post.title}

+
{post.body}
+
+ ) +} diff --git a/e2e/solid-start/basic/app/routes/redirect/$target.tsx b/e2e/solid-start/basic/app/routes/redirect/$target.tsx new file mode 100644 index 0000000000..525dd9da25 --- /dev/null +++ b/e2e/solid-start/basic/app/routes/redirect/$target.tsx @@ -0,0 +1,21 @@ +import { createFileRoute, retainSearchParams } from '@tanstack/solid-router' +import z from 'zod' + +export const Route = createFileRoute('/redirect/$target')({ + params: { + parse: (p) => + z + .object({ + target: z.union([z.literal('internal'), z.literal('external')]), + }) + .parse(p), + }, + validateSearch: z.object({ + reloadDocument: z.boolean().optional(), + preload: z.literal(false).optional(), + externalHost: z.string().optional(), + }), + search: { + middlewares: [retainSearchParams(['externalHost'])], + }, +}) diff --git a/e2e/solid-start/basic/app/routes/redirect/$target/index.tsx b/e2e/solid-start/basic/app/routes/redirect/$target/index.tsx new file mode 100644 index 0000000000..806c651f6c --- /dev/null +++ b/e2e/solid-start/basic/app/routes/redirect/$target/index.tsx @@ -0,0 +1,76 @@ +import { Link, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/redirect/$target/')({ + component: () => { + const preload = Route.useSearch({ select: (s) => s.preload }) + return ( +
+
+ + via-beforeLoad + +
+
+ + via-beforeLoad (reloadDocument=true) + +
+
+ + via-loader + +
+
+ + via-loader (reloadDocument=true) + +
+
+ + serverFn + +
+
+ ) + }, +}) diff --git a/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/index.tsx b/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/index.tsx new file mode 100644 index 0000000000..4b71331658 --- /dev/null +++ b/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/index.tsx @@ -0,0 +1,86 @@ +import { Link, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/redirect/$target/serverFn/')({ + component: () => ( +
+

+ redirect test with server functions (target {Route.useParams().target}) +

+
+ + via-beforeLoad + +
+
+ + via-beforeLoad (reloadDocument=true) + +
+
+ + via-loader + +
+
+ + via-loader (reloadDocument=true) + +
+
+ + via-useServerFn + +
+
+ + via-useServerFn (reloadDocument=true) + +
+
+ ), +}) diff --git a/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/via-beforeLoad.tsx b/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/via-beforeLoad.tsx new file mode 100644 index 0000000000..81f9dd2d36 --- /dev/null +++ b/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/via-beforeLoad.tsx @@ -0,0 +1,12 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { throwRedirect } from '~/components/throwRedirect' + +export const Route = createFileRoute( + '/redirect/$target/serverFn/via-beforeLoad', +)({ + beforeLoad: ({ + params: { target }, + search: { reloadDocument, externalHost }, + }) => throwRedirect({ data: { target, reloadDocument, externalHost } }), + component: () =>
{Route.fullPath}
, +}) diff --git a/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/via-loader.tsx b/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/via-loader.tsx new file mode 100644 index 0000000000..0b6ac91c15 --- /dev/null +++ b/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/via-loader.tsx @@ -0,0 +1,12 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { throwRedirect } from '~/components/throwRedirect' + +export const Route = createFileRoute('/redirect/$target/serverFn/via-loader')({ + loaderDeps: ({ search: { reloadDocument, externalHost } }) => ({ + reloadDocument, + externalHost, + }), + loader: ({ params: { target }, deps: { reloadDocument, externalHost } }) => + throwRedirect({ data: { target, reloadDocument, externalHost } }), + component: () =>
{Route.fullPath}
, +}) diff --git a/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/via-useServerFn.tsx b/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/via-useServerFn.tsx new file mode 100644 index 0000000000..c54d14944f --- /dev/null +++ b/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/via-useServerFn.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { RedirectOnClick } from '~/components/RedirectOnClick' + +export const Route = createFileRoute( + '/redirect/$target/serverFn/via-useServerFn', +)({ + component: () => { + const { target } = Route.useParams() + const { reloadDocument, externalHost } = Route.useSearch() + return ( + + ) + }, +}) diff --git a/e2e/solid-start/basic/app/routes/redirect/$target/via-beforeLoad.tsx b/e2e/solid-start/basic/app/routes/redirect/$target/via-beforeLoad.tsx new file mode 100644 index 0000000000..c88cc07986 --- /dev/null +++ b/e2e/solid-start/basic/app/routes/redirect/$target/via-beforeLoad.tsx @@ -0,0 +1,17 @@ +import { createFileRoute, redirect } from '@tanstack/solid-router' + +export const Route = createFileRoute('/redirect/$target/via-beforeLoad')({ + beforeLoad: ({ + params: { target }, + search: { reloadDocument, externalHost }, + }) => { + switch (target) { + case 'internal': + throw redirect({ to: '/posts', reloadDocument }) + case 'external': + const href = externalHost ?? 'http://example.com' + throw redirect({ href }) + } + }, + component: () =>
{Route.fullPath}
, +}) diff --git a/e2e/solid-start/basic/app/routes/redirect/$target/via-loader.tsx b/e2e/solid-start/basic/app/routes/redirect/$target/via-loader.tsx new file mode 100644 index 0000000000..5c059717c5 --- /dev/null +++ b/e2e/solid-start/basic/app/routes/redirect/$target/via-loader.tsx @@ -0,0 +1,18 @@ +import { createFileRoute, redirect } from '@tanstack/solid-router' + +export const Route = createFileRoute('/redirect/$target/via-loader')({ + loaderDeps: ({ search: { reloadDocument, externalHost } }) => ({ + reloadDocument, + externalHost, + }), + loader: ({ params: { target }, deps: { externalHost, reloadDocument } }) => { + switch (target) { + case 'internal': + throw redirect({ to: '/posts', reloadDocument }) + case 'external': + const href = externalHost ?? 'http://example.com' + throw redirect({ href }) + } + }, + component: () =>
{Route.fullPath}
, +}) diff --git a/e2e/solid-start/basic/app/routes/redirect/index.tsx b/e2e/solid-start/basic/app/routes/redirect/index.tsx new file mode 100644 index 0000000000..1c8cb8b9f8 --- /dev/null +++ b/e2e/solid-start/basic/app/routes/redirect/index.tsx @@ -0,0 +1,28 @@ +import { Link, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/redirect/')({ + component: () => ( +
+ + internal + {' '} + + external + +
+ ), +}) diff --git a/e2e/solid-start/basic/app/routes/scripts.tsx b/e2e/solid-start/basic/app/routes/scripts.tsx new file mode 100644 index 0000000000..ab9c46b50d --- /dev/null +++ b/e2e/solid-start/basic/app/routes/scripts.tsx @@ -0,0 +1,31 @@ +import { createFileRoute } from '@tanstack/solid-router' + +const isProd = import.meta.env.PROD + +export const Route = createFileRoute('/scripts')({ + head: () => ({ + scripts: [ + { + src: 'script.js', + }, + isProd + ? undefined + : { + src: 'script2.js', + }, + ], + }), + component: ScriptsComponent, +}) + +function ScriptsComponent() { + return ( +
+

Scripts Test

+

+ Both `script.js` and `script2.js` are included in development, but only + `script.js` is included in production. +

+
+ ) +} diff --git a/e2e/solid-start/basic/app/routes/search-params.tsx b/e2e/solid-start/basic/app/routes/search-params.tsx new file mode 100644 index 0000000000..20eb10b92a --- /dev/null +++ b/e2e/solid-start/basic/app/routes/search-params.tsx @@ -0,0 +1,27 @@ +import { createFileRoute, redirect } from '@tanstack/solid-router' +import { z } from 'zod' + +export const Route = createFileRoute('/search-params')({ + component: () => { + const search = Route.useSearch() + return ( +
+

SearchParams

+
{search.step}
+
+ ) + }, + validateSearch: z.object({ + step: z.enum(['a', 'b', 'c']).optional(), + }), + loaderDeps: ({ search: { step } }) => ({ step }), + loader: ({ deps: { step } }) => { + if (step === undefined) { + throw redirect({ + to: '/search-params', + from: '/search-params', + search: { step: 'a' }, + }) + } + }, +}) diff --git a/e2e/solid-start/basic/app/routes/stream.tsx b/e2e/solid-start/basic/app/routes/stream.tsx new file mode 100644 index 0000000000..293e76e42f --- /dev/null +++ b/e2e/solid-start/basic/app/routes/stream.tsx @@ -0,0 +1,64 @@ +import { Await, createFileRoute } from '@tanstack/solid-router' +import { useEffect, useState } from 'react' + +export const Route = createFileRoute('/stream')({ + component: Home, + loader() { + return { + promise: new Promise((resolve) => + setTimeout(() => resolve('promise-data'), 150), + ), + stream: new ReadableStream({ + async start(controller) { + for (let i = 0; i < 5; i++) { + await new Promise((resolve) => setTimeout(resolve, 200)) + controller.enqueue(`stream-data-${i} `) + } + controller.close() + }, + }), + } + }, +}) + +const decoder = new TextDecoder('utf-8') + +function Home() { + const { promise, stream } = Route.useLoaderData() + const [streamData, setStreamData] = useState>([]) + + useEffect(() => { + async function fetchStream() { + const reader = stream.getReader() + let chunk + + while (!(chunk = await reader.read()).done) { + let value = chunk.value + if (typeof value !== 'string') { + value = decoder.decode(value, { stream: !chunk.done }) + } + setStreamData((prev) => [...prev, value]) + } + } + + fetchStream() + }, []) + + return ( + <> + ( +
+ {promiseData} +
+ {streamData.map((d) => ( +
{d}
+ ))} +
+
+ )} + /> + + ) +} diff --git a/e2e/solid-start/basic/app/routes/users.$userId.tsx b/e2e/solid-start/basic/app/routes/users.$userId.tsx new file mode 100644 index 0000000000..5ad8da7d3e --- /dev/null +++ b/e2e/solid-start/basic/app/routes/users.$userId.tsx @@ -0,0 +1,38 @@ +import { ErrorComponent, createFileRoute } from '@tanstack/solid-router' +import axios from 'redaxios' +import type { ErrorComponentProps } from '@tanstack/solid-router' + +import type { User } from '~/utils/users' +import { DEPLOY_URL } from '~/utils/users' +import { NotFound } from '~/components/NotFound' + +export const Route = createFileRoute('/users/$userId')({ + loader: async ({ params: { userId } }) => { + return await axios + .get(DEPLOY_URL + '/api/users/' + userId) + .then((r) => r.data) + .catch(() => { + throw new Error('Failed to fetch user') + }) + }, + errorComponent: UserErrorComponent, + component: UserComponent, + notFoundComponent: () => { + return User not found + }, +}) + +export function UserErrorComponent({ error }: ErrorComponentProps) { + return +} + +function UserComponent() { + const user = Route.useLoaderData() + + return ( +
+

{user.name}

+
{user.email}
+
+ ) +} diff --git a/e2e/solid-start/basic/app/routes/users.index.tsx b/e2e/solid-start/basic/app/routes/users.index.tsx new file mode 100644 index 0000000000..bbc96801a9 --- /dev/null +++ b/e2e/solid-start/basic/app/routes/users.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/users/')({ + component: UsersIndexComponent, +}) + +function UsersIndexComponent() { + return
Select a user.
+} diff --git a/e2e/solid-start/basic/app/routes/users.tsx b/e2e/solid-start/basic/app/routes/users.tsx new file mode 100644 index 0000000000..f47114ee49 --- /dev/null +++ b/e2e/solid-start/basic/app/routes/users.tsx @@ -0,0 +1,49 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/solid-router' +import axios from 'redaxios' + +import type { User } from '~/utils/users' +import { DEPLOY_URL } from '~/utils/users' + +export const Route = createFileRoute('/users')({ + loader: async () => { + return await axios + .get>(DEPLOY_URL + '/api/users') + .then((r) => r.data) + .catch(() => { + throw new Error('Failed to fetch users') + }) + }, + component: UsersComponent, +}) + +function UsersComponent() { + const users = Route.useLoaderData() + + return ( +
+
    + {[ + ...users, + { id: 'i-do-not-exist', name: 'Non-existent User', email: '' }, + ].map((user) => { + return ( +
  • + +
    {user.name}
    + +
  • + ) + })} +
+
+ +
+ ) +} diff --git a/e2e/solid-start/basic/app/ssr.tsx b/e2e/solid-start/basic/app/ssr.tsx new file mode 100644 index 0000000000..6d10bea05f --- /dev/null +++ b/e2e/solid-start/basic/app/ssr.tsx @@ -0,0 +1,12 @@ +import { + createStartHandler, + defaultStreamHandler, +} from '@tanstack/solid-start/server' +import { getRouterManifest } from '@tanstack/solid-start/router-manifest' + +import { createRouter } from './router' + +export default createStartHandler({ + createRouter, + getRouterManifest, +})(defaultStreamHandler) diff --git a/e2e/solid-start/basic/app/styles/app.css b/e2e/solid-start/basic/app/styles/app.css new file mode 100644 index 0000000000..c53c870665 --- /dev/null +++ b/e2e/solid-start/basic/app/styles/app.css @@ -0,0 +1,22 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + html { + color-scheme: light dark; + } + + * { + @apply border-gray-200 dark:border-gray-800; + } + + html, + body { + @apply text-gray-900 bg-gray-50 dark:bg-gray-950 dark:text-gray-200; + } + + .using-mouse * { + outline: none !important; + } +} diff --git a/e2e/solid-start/basic/app/utils/posts.tsx b/e2e/solid-start/basic/app/utils/posts.tsx new file mode 100644 index 0000000000..6c12105ab8 --- /dev/null +++ b/e2e/solid-start/basic/app/utils/posts.tsx @@ -0,0 +1,36 @@ +import { notFound } from '@tanstack/solid-router' +import { createServerFn } from '@tanstack/solid-start' +import axios from 'redaxios' + +export type PostType = { + id: string + title: string + body: string +} + +export const fetchPost = createServerFn({ method: 'GET' }) + .validator((postId: string) => postId) + .handler(async ({ data: postId }) => { + console.info(`Fetching post with id ${postId}...`) + const post = await axios + .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) + .then((r) => r.data) + .catch((err) => { + console.error(err) + if (err.status === 404) { + throw notFound() + } + throw err + }) + + return post + }) + +export const fetchPosts = createServerFn({ method: 'GET' }).handler( + async () => { + console.info('Fetching posts...') + return axios + .get>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) + }, +) diff --git a/e2e/solid-start/basic/app/utils/seo.ts b/e2e/solid-start/basic/app/utils/seo.ts new file mode 100644 index 0000000000..d18ad84b74 --- /dev/null +++ b/e2e/solid-start/basic/app/utils/seo.ts @@ -0,0 +1,33 @@ +export const seo = ({ + title, + description, + keywords, + image, +}: { + title: string + description?: string + image?: string + keywords?: string +}) => { + const tags = [ + { title }, + { name: 'description', content: description }, + { name: 'keywords', content: keywords }, + { name: 'twitter:title', content: title }, + { name: 'twitter:description', content: description }, + { name: 'twitter:creator', content: '@tannerlinsley' }, + { name: 'twitter:site', content: '@tannerlinsley' }, + { name: 'og:type', content: 'website' }, + { name: 'og:title', content: title }, + { name: 'og:description', content: description }, + ...(image + ? [ + { name: 'twitter:image', content: image }, + { name: 'twitter:card', content: 'summary_large_image' }, + { name: 'og:image', content: image }, + ] + : []), + ] + + return tags +} diff --git a/e2e/solid-start/basic/app/utils/users.tsx b/e2e/solid-start/basic/app/utils/users.tsx new file mode 100644 index 0000000000..79b45867d5 --- /dev/null +++ b/e2e/solid-start/basic/app/utils/users.tsx @@ -0,0 +1,10 @@ +export type User = { + id: number + name: string + email: string +} + +const PORT = + import.meta.env.VITE_SERVER_PORT || process.env.VITE_SERVER_PORT || 3000 + +export const DEPLOY_URL = `http://localhost:${PORT}` diff --git a/e2e/solid-start/basic/package.json b/e2e/solid-start/basic/package.json new file mode 100644 index 0000000000..9ebe10882d --- /dev/null +++ b/e2e/solid-start/basic/package.json @@ -0,0 +1,35 @@ +{ + "name": "tanstack-start-e2e-basic", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev": "vinxi dev --port 3000", + "dev:e2e": "vinxi dev", + "build": "vinxi build && tsc --noEmit", + "start": "vinxi start", + "test:e2e": "playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/solid-router": "workspace:^", + "@tanstack/router-devtools": "workspace:^", + "@tanstack/solid-start": "workspace:^", + "solid-js": "^1.0.0", + "redaxios": "^0.5.1", + "tailwind-merge": "^2.6.0", + "vinxi": "0.5.3", + "zod": "^3.24.1" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@types/node": "^22.10.2", + "@tanstack/router-e2e-utils": "workspace:^", + "vite-plugin-solid": "^2.11.2", + "combinate": "^1.1.11", + "postcss": "^8.5.1", + "autoprefixer": "^10.4.20", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.2", + "vite-tsconfig-paths": "^5.1.4" + } +} diff --git a/e2e/solid-start/basic/playwright.config.ts b/e2e/solid-start/basic/playwright.config.ts new file mode 100644 index 0000000000..95d043d48b --- /dev/null +++ b/e2e/solid-start/basic/playwright.config.ts @@ -0,0 +1,35 @@ +import { defineConfig, devices } from '@playwright/test' +import { derivePort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const PORT = derivePort(packageJson.name) +const baseURL = `http://localhost:${PORT}` + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + webServer: { + command: `VITE_SERVER_PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm start --port ${PORT}`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/solid-start/basic/postcss.config.mjs b/e2e/solid-start/basic/postcss.config.mjs new file mode 100644 index 0000000000..2e7af2b7f1 --- /dev/null +++ b/e2e/solid-start/basic/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/e2e/solid-start/basic/public/android-chrome-192x192.png b/e2e/solid-start/basic/public/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..09c8324f8c6781bc90fcf5dd38e0702bd5f171f3 GIT binary patch literal 29964 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 b=FG literal 0 HcmV?d00001 diff --git a/e2e/solid-start/basic/public/android-chrome-512x512.png b/e2e/solid-start/basic/public/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..11d626ea3d00fddd52861bf0af5554a92fc30d69 GIT binary patch literal 109271 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 Ef@mE(_W%F@ literal 0 HcmV?d00001 diff --git a/e2e/solid-start/basic/public/apple-touch-icon.png b/e2e/solid-start/basic/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5a9423cc02c40ea066d2a061cdcdf621c0718cb1 GIT binary patch literal 27246 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⪼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 N002ovPDHLkV1k;v<;Va4 literal 0 HcmV?d00001 diff --git a/e2e/solid-start/basic/public/favicon-16x16.png b/e2e/solid-start/basic/public/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..e3389b00443d602a249b48ae0e21e333971c9c1e GIT binary patch literal 832 zcmV-G1Hb%Px%`bk7VR5(v%lW%BSRTRK~=OuZ4FYhI3vvy^4t8t~UDHGBS#44K%hSiDal!9Nj zeb^RZ;3&cb72F3w6s4;uUA7>Cf>1=Z56;a$8wZ=NKHtq z33GDV=_|V0E(QPTEB1=edhG{{=-+C??RQi7*e9J7YBnZR+PppIKjA8EV+}Yt`zH<7 zGR%O5D3|Ef)99hUXu19=Cxaki@6toZIe&u-X0(oDFRl|x6P7Bwc^mB(i9|gOY)Jh6DaPkmt z8Wr!@TP1|RPlYRlJe^eRB0K(gn91{(=zi{HrkpHIflw8&q%;+U!V&dFPYDQAWJh@< zwcklN9VEX?vG;}75GqCepKC25M^xV%BcR%A8c!)S5k4P0$g}HS#lA6zy`~K@9&EX4 zOrvr%_1U2dp~gg6G*;&`Jag*?jZTJv+$S`*#)zme_SFWOCm*F4U&hJihqxAu;8z}x zWv7!u>YKRa3PUJQs55wC)i4QvFOAV0jW$?1cnUJJ7Vl?z_ge{9q!zpfCJxd8vJ>-ql@0&`Bttn1~4;s>8tax%hr(^q#A_*wqAv( zh(T&>hJD?OhiP$sxAzkd1W1^EHV1WL3xM&6$*HhG@|8}OjPpG{+!;GC>w;ha0000< KMNUMnLSTZGXp5cz literal 0 HcmV?d00001 diff --git a/e2e/solid-start/basic/public/favicon-32x32.png b/e2e/solid-start/basic/public/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..900c77d444c9b31e6144b6ac0bb1ab7bb7ca767f GIT binary patch literal 2115 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 literal 0 HcmV?d00001 diff --git a/e2e/solid-start/basic/public/favicon.ico b/e2e/solid-start/basic/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1a1751676f7e22811b1070572093996c93c87617 GIT binary patch literal 15406 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 literal 0 HcmV?d00001 diff --git a/e2e/solid-start/basic/public/favicon.png b/e2e/solid-start/basic/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..1e77bc06091ade4496525a09d8900675afcf03f0 GIT binary patch literal 1507 zcmV<91swW`P)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91AfN*P1ONa40RR91AOHXW0IY^$^8f$^O-V#SR9Fd>S3Qp$MHK9ro!#4$ zEP@L_hX|b@f=!*_42h6mKu7{)4)_U*>1>0bCkUj;Z1X!7 zHe(Ew^Oi(|bW3J~xu+)XbtFF?4>!7TH$>(D_atUQVEj(8fGvYu2NF33#JZX>)(Vj8 zIi@z>Glt?6t~;Lf(|C8F>;WF^8F<^s7Scr!sZc01uB?HMHoL5+FZ>B(g+r-)?Sn)#3Zal#?G@GAwO5U27MpGOlC2+_saA)rl zP-<@-n~;PQOlm|Hi<+W;NdR;5+=zADzM&?!+CPD36=cGwHy6!D^vPEHG?rO`K>G|M z3FposX{yT132wuw1OR3Um_5JoKB#6?!QgBupIT;?YIr;WcpmuCE>S75mZid+ens#E zGPuYjiG0UNNVWu=f!Id^?9)34)eIpu-`j_~W0iAQzK(}XYc_!;87Tk~?4tq|h=2(! zuq0HCiNK)@+ocCKR3q1REdUju>HdYxd>JX@%oOibg+J~D+}rhz54D!NfC{h-OYk{M zkzmFtdrL@nL0bm8nF@pob1CeLC>12ef#in-Bzv2!wi)Iuwq24)`AH}|0QNQ^f$KHv z?5PBPo1*#GAuAk+Poe`?UJ>mP`@~d4a(103j0lwUx@_+$#B&VC%7r>#2$HIiD`KO8L|s3Yp%M}BT0;NJDzZtPnx=4%enhU zhW*pNN0t`^4%5MKAR+}=^Q?QeqQ`>bbK zf+-ji$Uz8V0?LpX@kh`k%DL)GCA2=@SJNKg56Wh>>pr=7{1PmHqG|~=AdLV3002ov JPDHLkV1ivgp)>#h literal 0 HcmV?d00001 diff --git a/e2e/solid-start/basic/public/script.js b/e2e/solid-start/basic/public/script.js new file mode 100644 index 0000000000..897477e7d0 --- /dev/null +++ b/e2e/solid-start/basic/public/script.js @@ -0,0 +1,2 @@ +console.log('SCRIPT_1 loaded') +window.SCRIPT_1 = true diff --git a/e2e/solid-start/basic/public/script2.js b/e2e/solid-start/basic/public/script2.js new file mode 100644 index 0000000000..819af30daf --- /dev/null +++ b/e2e/solid-start/basic/public/script2.js @@ -0,0 +1,2 @@ +console.log('SCRIPT_2 loaded') +window.SCRIPT_2 = true diff --git a/e2e/solid-start/basic/public/site.webmanifest b/e2e/solid-start/basic/public/site.webmanifest new file mode 100644 index 0000000000..fa99de77db --- /dev/null +++ b/e2e/solid-start/basic/public/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/e2e/solid-start/basic/tailwind.config.mjs b/e2e/solid-start/basic/tailwind.config.mjs new file mode 100644 index 0000000000..07c3598bac --- /dev/null +++ b/e2e/solid-start/basic/tailwind.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./app/**/*.{js,jsx,ts,tsx}'], +} diff --git a/e2e/solid-start/basic/tests/fixture.ts b/e2e/solid-start/basic/tests/fixture.ts new file mode 100644 index 0000000000..abb7b1d564 --- /dev/null +++ b/e2e/solid-start/basic/tests/fixture.ts @@ -0,0 +1,28 @@ +import { test as base, expect } from '@playwright/test' + +export interface TestFixtureOptions { + whitelistErrors: Array +} +export const test = base.extend({ + whitelistErrors: [[], { option: true }], + page: async ({ page, whitelistErrors }, use) => { + const errorMessages: Array = [] + page.on('console', (m) => { + if (m.type() === 'error') { + const text = m.text() + for (const whitelistError of whitelistErrors) { + if ( + (typeof whitelistError === 'string' && + text.includes(whitelistError)) || + (whitelistError instanceof RegExp && whitelistError.test(text)) + ) { + return + } + } + errorMessages.push(text) + } + }) + await use(page) + expect(errorMessages).toEqual([]) + }, +}) diff --git a/e2e/solid-start/basic/tests/navigation.spec.ts b/e2e/solid-start/basic/tests/navigation.spec.ts new file mode 100644 index 0000000000..ffa4ddb595 --- /dev/null +++ b/e2e/solid-start/basic/tests/navigation.spec.ts @@ -0,0 +1,47 @@ +import { expect, test } from '@playwright/test' + +test('Navigating to post', async ({ page }) => { + await page.goto('/') + + await page.getByRole('link', { name: 'Posts' }).click() + await page.getByRole('link', { name: 'sunt aut facere repe' }).click() + await page.getByRole('link', { name: 'Deep View' }).click() + await expect(page.getByRole('heading')).toContainText('sunt aut facere') +}) + +test('Navigating to user', async ({ page }) => { + await page.goto('/') + + await page.getByRole('link', { name: 'Users' }).click() + await page.getByRole('link', { name: 'Leanne Graham' }).click() + await expect(page.getByRole('heading')).toContainText('Leanne Graham') +}) + +test('Navigating nested layouts', async ({ page }) => { + await page.goto('/') + + await page.getByRole('link', { name: 'Layout', exact: true }).click() + + await expect(page.locator('body')).toContainText("I'm a layout") + await expect(page.locator('body')).toContainText("I'm a nested layout") + + await page.getByRole('link', { name: 'Layout A' }).click() + await expect(page.locator('body')).toContainText("I'm layout A!") + + await page.getByRole('link', { name: 'Layout B' }).click() + await expect(page.locator('body')).toContainText("I'm layout B!") +}) + +test('directly going to a route with scripts', async ({ page }) => { + await page.goto('/scripts') + expect(await page.evaluate('window.SCRIPT_1')).toBe(true) + expect(await page.evaluate('window.SCRIPT_2')).toBe(undefined) +}) + +test('Navigating to a not-found route', async ({ page }) => { + await page.goto('/') + + await page.getByRole('link', { name: 'This Route Does Not Exist' }).click() + await page.getByRole('link', { name: 'Start Over' }).click() + await expect(page.getByRole('heading')).toContainText('Welcome Home!') +}) diff --git a/e2e/solid-start/basic/tests/not-found.spec.ts b/e2e/solid-start/basic/tests/not-found.spec.ts new file mode 100644 index 0000000000..0d83ab5280 --- /dev/null +++ b/e2e/solid-start/basic/tests/not-found.spec.ts @@ -0,0 +1,74 @@ +import { expect } from '@playwright/test' +import combinateImport from 'combinate' +import { test } from './fixture' + +// somehow playwright does not correctly import default exports +const combinate = (combinateImport as any).default as typeof combinateImport + +test.use({ + whitelistErrors: [ + /Failed to load resource: the server responded with a status of 404/, + ], +}) +test.describe('not-found', () => { + test(`global not found`, async ({ page }) => { + const response = await page.goto(`/this-page-does-not-exist/foo/bar`) + + expect(response?.status()).toBe(404) + + await expect( + page.getByTestId('default-not-found-component'), + ).toBeInViewport() + }) + + test.describe('throw notFound()', () => { + const navigationTestMatrix = combinate({ + // TODO beforeLoad! + thrower: [/* 'beforeLoad',*/ 'loader'] as const, + preload: [false, true] as const, + }) + + navigationTestMatrix.forEach(({ thrower, preload }) => { + test(`navigation: thrower: ${thrower}, preload: ${preload}`, async ({ + page, + }) => { + await page.goto( + `/not-found/${preload === false ? '?preload=false' : ''}`, + ) + const link = page.getByTestId(`via-${thrower}`) + + if (preload) { + await link.focus() + await new Promise((r) => setTimeout(r, 250)) + } + + await link.click() + + await expect( + page.getByTestId(`via-${thrower}-notFound-component`), + ).toBeInViewport() + await expect( + page.getByTestId(`via-${thrower}-route-component`), + ).not.toBeInViewport() + }) + }) + const directVisitTestMatrix = combinate({ + // TODO beforeLoad! + + thrower: [/* 'beforeLoad',*/ 'loader'] as const, + }) + + directVisitTestMatrix.forEach(({ thrower }) => { + test(`direct visit: thrower: ${thrower}`, async ({ page }) => { + await page.goto(`/not-found/via-${thrower}`) + await page.waitForLoadState('networkidle') + await expect( + page.getByTestId(`via-${thrower}-notFound-component`), + ).toBeInViewport() + await expect( + page.getByTestId(`via-${thrower}-route-component`), + ).not.toBeInViewport() + }) + }) + }) +}) diff --git a/e2e/solid-start/basic/tests/redirect.spec.ts b/e2e/solid-start/basic/tests/redirect.spec.ts new file mode 100644 index 0000000000..1809b531df --- /dev/null +++ b/e2e/solid-start/basic/tests/redirect.spec.ts @@ -0,0 +1,208 @@ +import { expect } from '@playwright/test' +import combinateImport from 'combinate' +import { derivePort, localDummyServer } from '@tanstack/router-e2e-utils' +import packageJson from '../package.json' with { type: 'json' } +import { test } from './fixture' +import { Server } from 'node:http' +import queryString from 'node:querystring' + +// somehow playwright does not correctly import default exports +const combinate = (combinateImport as any).default as typeof combinateImport + +const PORT = derivePort(packageJson.name) +const EXTERNAL_HOST_PORT = derivePort(`${packageJson.name}-external`) + +test.describe('redirects', () => { + let server: Server + test.beforeAll(async () => { + server = await localDummyServer(EXTERNAL_HOST_PORT) + }) + test.afterAll(async () => { + server.close() + }) + + const internalNavigationTestMatrix = combinate({ + thrower: ['beforeLoad', 'loader'] as const, + reloadDocument: [false, true] as const, + preload: [false, true] as const, + }) + + internalNavigationTestMatrix.forEach( + ({ thrower, reloadDocument, preload }) => { + test(`internal target, navigation: thrower: ${thrower}, reloadDocument: ${reloadDocument}, preload: ${preload}`, async ({ + page, + }) => { + await page.goto( + `/redirect/internal${preload === false ? '?preload=false' : ''}`, + ) + const link = page.getByTestId( + `via-${thrower}${reloadDocument ? '-reloadDocument' : ''}`, + ) + + await page.waitForLoadState('networkidle') + let requestHappened = false + + const requestPromise = new Promise((resolve) => { + page.on('request', (request) => { + if (request.url().startsWith(`http://localhost:${PORT}/_server/`)) { + requestHappened = true + resolve() + } + }) + }) + await link.focus() + + const expectRequestHappened = preload && !reloadDocument + const timeoutPromise = new Promise((resolve) => + setTimeout(resolve, expectRequestHappened ? 5000 : 500), + ) + await Promise.race([requestPromise, timeoutPromise]) + expect(requestHappened).toBe(expectRequestHappened) + let fullPageLoad = false + page.on('domcontentloaded', () => { + fullPageLoad = true + }) + + await link.click() + + const url = `http://localhost:${PORT}/posts` + + await page.waitForURL(url) + expect(page.url()).toBe(url) + await expect(page.getByTestId('PostsIndexComponent')).toBeInViewport() + expect(fullPageLoad).toBe(reloadDocument) + }) + }, + ) + + const internalDirectVisitTestMatrix = combinate({ + thrower: ['beforeLoad', 'loader'] as const, + reloadDocument: [false, true] as const, + }) + + internalDirectVisitTestMatrix.forEach(({ thrower, reloadDocument }) => { + test(`internal target, direct visit: thrower: ${thrower}, reloadDocument: ${reloadDocument}`, async ({ + page, + }) => { + await page.goto(`/redirect/internal/via-${thrower}`) + + const url = `http://localhost:${PORT}/posts` + + await page.waitForURL(url) + expect(page.url()).toBe(url) + await page.waitForLoadState('networkidle') + await expect(page.getByTestId('PostsIndexComponent')).toBeInViewport() + }) + }) + + const externalTestMatrix = combinate({ + scenario: ['navigate', 'direct_visit'] as const, + thrower: ['beforeLoad', 'loader'] as const, + }) + + externalTestMatrix.forEach(({ scenario, thrower }) => { + test(`external target: scenario: ${scenario}, thrower: ${thrower}`, async ({ + page, + }) => { + let q = queryString.stringify({ + externalHost: `http://localhost:${EXTERNAL_HOST_PORT}/`, + }) + + if (scenario === 'navigate') { + await page.goto(`/redirect/external?${q}`) + await page.waitForLoadState('networkidle') + const link = page.getByTestId(`via-${thrower}`) + await link.focus() + await link.click() + } else { + await page.goto(`/redirect/external/via-${thrower}?${q}`) + } + + const url = `http://localhost:${EXTERNAL_HOST_PORT}/` + + await page.waitForURL(url) + expect(page.url()).toBe(url) + }) + }) + + const serverFnTestMatrix = combinate({ + target: ['internal', 'external'] as const, + scenario: ['navigate', 'direct_visit'] as const, + thrower: ['beforeLoad', 'loader'] as const, + reloadDocument: [false, true] as const, + }) + + serverFnTestMatrix.forEach( + ({ target, thrower, scenario, reloadDocument }) => { + test(`serverFn redirects to target: ${target}, scenario: ${scenario}, thrower: ${thrower}, reloadDocument: ${reloadDocument}`, async ({ + page, + }) => { + let fullPageLoad = false + let q = queryString.stringify({ + externalHost: `http://localhost:${EXTERNAL_HOST_PORT}/`, + reloadDocument, + }) + + if (scenario === 'navigate') { + await page.goto(`/redirect/${target}/serverFn?${q}`) + await page.waitForLoadState('networkidle') + const link = page.getByTestId( + `via-${thrower}${reloadDocument ? '-reloadDocument' : ''}`, + ) + page.on('domcontentloaded', () => { + fullPageLoad = true + }) + await link.focus() + await link.click() + } else { + await page.goto(`/redirect/${target}/serverFn/via-${thrower}?${q}`) + } + + const url = + target === 'internal' + ? `http://localhost:${PORT}/posts` + : `http://localhost:${EXTERNAL_HOST_PORT}/` + await page.waitForURL(url) + expect(page.url()).toBe(url) + if (target === 'internal' && scenario === 'navigate') { + await expect(page.getByTestId('PostsIndexComponent')).toBeInViewport() + expect(fullPageLoad).toBe(reloadDocument) + } + }) + }, + ) + + const useServerFnTestMatrix = combinate({ + target: ['internal', 'external'] as const, + reloadDocument: [false, true] as const, + }) + + useServerFnTestMatrix.forEach(({ target, reloadDocument }) => { + test(`useServerFn redirects to target: ${target}, reloadDocument: ${reloadDocument}`, async ({ + page, + }) => { + await page.goto( + `/redirect/${target}/serverFn/via-useServerFn${reloadDocument ? '?reloadDocument=true' : ''}`, + ) + const button = page.getByTestId('redirect-on-click') + + let fullPageLoad = false + page.on('domcontentloaded', () => { + fullPageLoad = true + }) + + await button.click() + + const url = + target === 'internal' + ? `http://localhost:${PORT}/posts` + : 'http://example.com/' + await page.waitForURL(url) + expect(page.url()).toBe(url) + if (target === 'internal') { + await expect(page.getByTestId('PostsIndexComponent')).toBeInViewport() + expect(fullPageLoad).toBe(reloadDocument) + } + }) + }) +}) diff --git a/e2e/solid-start/basic/tests/search-params.spec.ts b/e2e/solid-start/basic/tests/search-params.spec.ts new file mode 100644 index 0000000000..7d1ee6d374 --- /dev/null +++ b/e2e/solid-start/basic/tests/search-params.spec.ts @@ -0,0 +1,22 @@ +import { expect } from '@playwright/test' +import { test } from './fixture' + +test('Directly visiting the search-params route without search param set', async ({ + page, +}) => { + await page.goto('/search-params') + + await new Promise((r) => setTimeout(r, 500)) + await expect(page.getByTestId('search-param')).toContainText('a') + expect(page.url().endsWith('/search-params?step=a')) +}) + +test('Directly visiting the search-params route with search param set', async ({ + page, +}) => { + await page.goto('/search-params?step=b') + + await new Promise((r) => setTimeout(r, 500)) + await expect(page.getByTestId('search-param')).toContainText('b') + expect(page.url().endsWith('/search-params?step=b')) +}) diff --git a/e2e/solid-start/basic/tests/streaming.spec.ts b/e2e/solid-start/basic/tests/streaming.spec.ts new file mode 100644 index 0000000000..252bb192aa --- /dev/null +++ b/e2e/solid-start/basic/tests/streaming.spec.ts @@ -0,0 +1,34 @@ +import { expect, test } from '@playwright/test' + +test('Navigating to deferred route', async ({ page }) => { + await page.goto('/') + + await page.getByRole('link', { name: 'Deferred' }).click() + + await expect(page.getByTestId('regular-person')).toContainText('John Doe') + await expect(page.getByTestId('deferred-person')).toContainText( + 'Tanner Linsley', + ) + await expect(page.getByTestId('deferred-stuff')).toContainText( + 'Hello deferred!', + ) +}) + +test('Directly visiting the deferred route', async ({ page }) => { + await page.goto('/deferred') + + await expect(page.getByTestId('regular-person')).toContainText('John Doe') + await expect(page.getByTestId('deferred-person')).toContainText( + 'Tanner Linsley', + ) + await expect(page.getByTestId('deferred-stuff')).toContainText( + 'Hello deferred!', + ) +}) + +test('streaming loader data', async ({ page }) => { + await page.goto('/stream') + + await expect(page.getByTestId('promise-data')).toContainText('promise-data') + await expect(page.getByTestId('stream-data')).toContainText('stream-data') +}) diff --git a/e2e/solid-start/basic/tsconfig.json b/e2e/solid-start/basic/tsconfig.json new file mode 100644 index 0000000000..a6747faec5 --- /dev/null +++ b/e2e/solid-start/basic/tsconfig.json @@ -0,0 +1,22 @@ +{ + "include": ["**/*.ts", "**/*.tsx", "public/script*.js"], + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "isolatedModules": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "target": "ES2022", + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, + "noEmit": true + } +} diff --git a/packages/solid-start-config/README.md b/packages/solid-start-config/README.md new file mode 100644 index 0000000000..bb009b0c87 --- /dev/null +++ b/packages/solid-start-config/README.md @@ -0,0 +1,33 @@ +> 🤫 we're cooking up something special! + + + +# TanStack Start + +![TanStack Router Header](https://github.com/tanstack/router/raw/main/media/header.png) + +🤖 Type-safe router w/ built-in caching & URL state management for React! + + + #TanStack + + + + + + + + semantic-release + + Join the discussion on Github +Best of JS + + + + + + + +Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [React Query](https://github.com/tannerlinsley/react-query), [React Table](https://github.com/tanstack/react-table), [React Charts](https://github.com/tannerlinsley/react-charts), [React Virtual](https://github.com/tannerlinsley/react-virtual) + +## Visit [tanstack.com/router](https://tanstack.com/router) for docs, guides, API and more! diff --git a/packages/solid-start-config/eslint.config.js b/packages/solid-start-config/eslint.config.js new file mode 100644 index 0000000000..931f0ec774 --- /dev/null +++ b/packages/solid-start-config/eslint.config.js @@ -0,0 +1,31 @@ +// @ts-check + +import pluginReact from '@eslint-react/eslint-plugin' +import pluginReactHooks from 'eslint-plugin-react-hooks' +import rootConfig from '../../eslint.config.js' + +export default [ + ...rootConfig, + { + ...pluginReact.configs.recommended, + files: ['**/*.{ts,tsx}'], + }, + { + plugins: { + 'react-hooks': pluginReactHooks, + }, + rules: { + '@eslint-react/no-unstable-context-value': 'off', + '@eslint-react/no-unstable-default-props': 'off', + '@eslint-react/dom/no-missing-button-type': 'off', + 'react-hooks/exhaustive-deps': 'error', + 'react-hooks/rules-of-hooks': 'error', + }, + }, + { + files: ['**/__tests__/**'], + rules: { + '@typescript-eslint/no-unnecessary-condition': 'off', + }, + }, +] diff --git a/packages/solid-start-config/package.json b/packages/solid-start-config/package.json new file mode 100644 index 0000000000..bab610faa0 --- /dev/null +++ b/packages/solid-start-config/package.json @@ -0,0 +1,72 @@ +{ + "name": "@tanstack/solid-start-config", + "version": "1.109.2", + "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" + }, + "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", + "build": "tsc", + "test": "pnpm test:eslint && pnpm test:types && pnpm test:build && pnpm test:unit", + "test:unit": "exit 0;vitest", + "test:eslint": "eslint ./src", + "test:types": "exit 0; vitest" + }, + "type": "module", + "types": "dist/esm/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + } + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "files": [ + "dist", + "src" + ], + "engines": { + "node": ">=12" + }, + "dependencies": { + "@tanstack/solid-router": "workspace:^", + "@tanstack/router-generator": "workspace:^", + "@tanstack/router-plugin": "workspace:^", + "@tanstack/server-functions-plugin": "workspace:^", + "@tanstack/start-plugin": "workspace:^", + "@tanstack/solid-start-server-functions-handler": "workspace:^", + "vite-plugin-solid": "^2.11.2", + "import-meta-resolve": "^4.1.0", + "nitropack": "^2.10.4", + "ofetch": "^1.4.1", + "vite": "^6.1.0", + "vinxi": "0.5.3", + "zod": "^3.24.1" + }, + "peerDependencies": { + "solid-js": ">=1.0.0", + "vite": "^6.0.0" + } +} diff --git a/packages/solid-start-config/src/index.ts b/packages/solid-start-config/src/index.ts new file mode 100644 index 0000000000..4370e8084c --- /dev/null +++ b/packages/solid-start-config/src/index.ts @@ -0,0 +1,660 @@ +import path from 'node:path' +import { existsSync, readFileSync } from 'node:fs' +import { readFile } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' +import viteSolid from 'vite-plugin-solid' +import { resolve } from 'import-meta-resolve' +import { TanStackRouterVite } from '@tanstack/router-plugin/vite' +import { TanStackStartVitePlugin } from '@tanstack/start-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/server-functions-plugin' +import { createFetch } from 'ofetch' +import { createNitro } from 'nitropack' +import { tanstackStartVinxiFileRouter } from './vinxi-file-router.js' +import { + checkDeploymentPresetInput, + getUserViteConfig, + inlineConfigSchema, + serverSchema, +} from './schema.js' +import type { configSchema } from '@tanstack/router-generator' +import type { z } from 'zod' +import type { + TanStackStartInputConfig, + TanStackStartOutputConfig, +} from './schema.js' +import type { App as VinxiApp } from 'vinxi' +import type { Manifest } from '@tanstack/solid-router' +import type * as vite from 'vite' + +export type { + TanStackStartInputConfig, + TanStackStartOutputConfig, +} from './schema.js' + +function setTsrDefaults(config: TanStackStartOutputConfig['tsr']) { + // Normally these are `./src/___`, but we're using `./app/___` for Start stuff + const appDirectory = config?.appDirectory ?? './app' + return { + ...config, + appDirectory: config?.appDirectory ?? appDirectory, + routesDirectory: + config?.routesDirectory ?? path.join(appDirectory, 'routes'), + generatedRouteTree: + config?.generatedRouteTree ?? path.join(appDirectory, 'routeTree.gen.ts'), + } +} + +function mergeSsrOptions(options: Array) { + let ssrOptions: vite.SSROptions = {} + let noExternal: vite.SSROptions['noExternal'] = [] + for (const option of options) { + if (!option) { + continue + } + + if (option.noExternal) { + if (option.noExternal === true) { + noExternal = true + } else if (noExternal !== true) { + if (Array.isArray(option.noExternal)) { + noExternal.push(...option.noExternal) + } else { + noExternal.push(option.noExternal) + } + } + } + + ssrOptions = { + ...ssrOptions, + ...option, + noExternal, + } + } + + return ssrOptions +} + +export async function defineConfig( + inlineConfig: TanStackStartInputConfig = {}, +): Promise { + const opts = inlineConfigSchema.parse(inlineConfig) + + const { preset: configDeploymentPreset, ...serverOptions } = + serverSchema.parse(opts.server || {}) + + const deploymentPreset = checkDeploymentPresetInput( + configDeploymentPreset || 'node-server', + ) + const tsr = setTsrDefaults(opts.tsr) + const tsrConfig = getConfig(tsr) + + const appDirectory = tsr.appDirectory + const publicDir = opts.routers?.public?.dir || './public' + + const publicBase = opts.routers?.public?.base || '/' + const clientBase = opts.routers?.client?.base || '/_build' + const apiBase = opts.tsr?.apiBase || '/api' + const serverBase = opts.routers?.server?.base || '/_server' + + const apiMiddleware = opts.routers?.api?.middleware || undefined + const serverMiddleware = opts.routers?.server?.middleware || undefined + const ssrMiddleware = opts.routers?.ssr?.middleware || undefined + + const clientEntry = + opts.routers?.client?.entry || path.join(appDirectory, 'client.tsx') + const ssrEntry = + opts.routers?.ssr?.entry || path.join(appDirectory, 'ssr.tsx') + const apiEntry = opts.routers?.api?.entry || path.join(appDirectory, 'api.ts') + + const apiEntryExists = existsSync(apiEntry) + + const viteConfig = getUserViteConfig(opts.vite) + + const TanStackServerFnsPlugin = createTanStackServerFnPlugin({ + // This is the ID that will be available to look up and import + // our server function manifest and resolve its module + manifestVirtualImportId: 'tsr:server-fn-manifest', + client: { + getRuntimeCode: () => + `import { createClientRpc } from '@tanstack/start/server-functions-client'`, + replacer: (opts) => + `createClientRpc('${opts.functionId}', '${serverBase}')`, + }, + ssr: { + getRuntimeCode: () => + `import { createSsrRpc } from '@tanstack/start/server-functions-ssr'`, + replacer: (opts) => `createSsrRpc('${opts.functionId}', '${serverBase}')`, + }, + server: { + getRuntimeCode: () => + `import { createServerRpc } from '@tanstack/start/server-functions-server'`, + replacer: (opts) => + `createServerRpc('${opts.functionId}', '${serverBase}', ${opts.fn})`, + }, + }) + + // Create a dummy nitro app to get the resolved public output path + const dummyNitroApp = await createNitro({ + preset: deploymentPreset, + compatibilityDate: '2024-12-01', + }) + + const nitroOutputPublicDir = dummyNitroApp.options.output.publicDir + await dummyNitroApp.close() + + let vinxiApp = createApp({ + server: { + ...serverOptions, + preset: deploymentPreset, + experimental: { + ...serverOptions.experimental, + asyncContext: true, + }, + }, + routers: [ + { + name: 'public', + type: 'static', + dir: publicDir, + base: publicBase, + }, + { + name: 'client', + type: 'client', + target: 'browser', + handler: clientEntry, + base: clientBase, + // @ts-expect-error + build: { + sourcemap: true, + }, + plugins: () => { + const routerType = 'client' + const clientViteConfig = getUserViteConfig( + opts.routers?.[routerType]?.vite, + ) + + return [ + config('tss-vite-config-client', { + ...viteConfig.userConfig, + ...clientViteConfig.userConfig, + define: { + ...(viteConfig.userConfig.define || {}), + ...(clientViteConfig.userConfig.define || {}), + ...injectDefineEnv('TSS_PUBLIC_BASE', publicBase), + ...injectDefineEnv('TSS_CLIENT_BASE', clientBase), + ...injectDefineEnv('TSS_API_BASE', apiBase), + ...injectDefineEnv( + 'TSS_OUTPUT_PUBLIC_DIR', + nitroOutputPublicDir, + ), + }, + ssr: mergeSsrOptions([ + viteConfig.userConfig.ssr, + clientViteConfig.userConfig.ssr, + { + noExternal, + }, + ]), + optimizeDeps: { + entries: [], + ...(viteConfig.userConfig.optimizeDeps || {}), + ...(clientViteConfig.userConfig.optimizeDeps || {}), + }, + }), + TanStackRouterVite({ + ...tsrConfig, + enableRouteGeneration: true, + autoCodeSplitting: true, + experimental: { + ...tsrConfig.experimental, + }, + }), + TanStackStartVitePlugin({ + env: 'client', + }), + TanStackServerFnsPlugin.client, + ...(viteConfig.plugins || []), + ...(clientViteConfig.plugins || []), + viteSolid(opts.solid), + // TODO: RSCS - enable this + // serverComponents.client(), + ] + }, + }, + { + name: 'ssr', + type: 'http', + target: 'server', + handler: ssrEntry, + middleware: ssrMiddleware, + // @ts-expect-error + link: { + client: 'client', + }, + plugins: () => { + const routerType = 'ssr' + const ssrViteConfig = getUserViteConfig( + opts.routers?.[routerType]?.vite, + ) + + return [ + config('tss-vite-config-ssr', { + ...viteConfig.userConfig, + ...ssrViteConfig.userConfig, + define: { + ...(viteConfig.userConfig.define || {}), + ...(ssrViteConfig.userConfig.define || {}), + ...injectDefineEnv('TSS_PUBLIC_BASE', publicBase), + ...injectDefineEnv('TSS_CLIENT_BASE', clientBase), + ...injectDefineEnv('TSS_API_BASE', apiBase), + ...injectDefineEnv( + 'TSS_OUTPUT_PUBLIC_DIR', + nitroOutputPublicDir, + ), + }, + ssr: mergeSsrOptions([ + viteConfig.userConfig.ssr, + ssrViteConfig.userConfig.ssr, + { + noExternal, + external: ['@vinxi/react-server-dom/client'], + }, + ]), + optimizeDeps: { + entries: [], + ...(viteConfig.userConfig.optimizeDeps || {}), + ...(ssrViteConfig.userConfig.optimizeDeps || {}), + }, + }), + TanStackRouterVite({ + ...tsrConfig, + enableRouteGeneration: false, + autoCodeSplitting: true, + experimental: { + ...tsrConfig.experimental, + }, + }), + TanStackStartVitePlugin({ + env: 'ssr', + }), + TanStackServerFnsPlugin.ssr, + tsrRoutesManifest({ + tsrConfig, + clientBase, + }), + ...(getUserViteConfig(opts.vite).plugins || []), + ...(getUserViteConfig(opts.routers?.ssr?.vite).plugins || []), + viteSolid(opts.solid), + ] + }, + }, + { + name: 'server', + type: 'http', + target: 'server', + base: serverBase, + middleware: serverMiddleware, + // TODO: RSCS - enable this + // worker: true, + handler: importToProjectRelative( + '@tanstack/start-server-functions-handler', + ), + plugins: () => { + const routerType = 'server' + const serverViteConfig = getUserViteConfig( + opts.routers?.[routerType]?.vite, + ) + + return [ + config('tss-vite-config-server', { + ...viteConfig.userConfig, + ...serverViteConfig.userConfig, + define: { + ...(viteConfig.userConfig.define || {}), + ...(serverViteConfig.userConfig.define || {}), + ...injectDefineEnv('TSS_PUBLIC_BASE', publicBase), + ...injectDefineEnv('TSS_CLIENT_BASE', clientBase), + ...injectDefineEnv('TSS_API_BASE', apiBase), + ...injectDefineEnv('TSS_SERVER_FN_BASE', serverBase), + ...injectDefineEnv( + 'TSS_OUTPUT_PUBLIC_DIR', + nitroOutputPublicDir, + ), + }, + ssr: mergeSsrOptions([ + viteConfig.userConfig.ssr, + serverViteConfig.userConfig.ssr, + { + noExternal, + }, + ]), + optimizeDeps: { + entries: [], + ...(viteConfig.userConfig.optimizeDeps || {}), + ...(serverViteConfig.userConfig.optimizeDeps || {}), + }, + }), + TanStackRouterVite({ + ...tsrConfig, + enableRouteGeneration: false, + autoCodeSplitting: true, + experimental: { + ...tsrConfig.experimental, + }, + }), + TanStackStartVitePlugin({ + env: 'server', + }), + TanStackServerFnsPlugin.server, + // TODO: RSCS - remove this + // resolve: { + // conditions: [], + // }, + // TODO: RSCs - add this + // serverComponents.serverActions({ + // resolve: { + // conditions: [ + // 'react-server', + // // 'node', + // 'import', + // process.env.NODE_ENV, + // ], + // }, + // runtime: '@vinxi/react-server-dom/runtime', + // transpileDeps: ['react', 'react-dom', '@vinxi/react-server-dom'], + // }), + ...(viteConfig.plugins || []), + ...(serverViteConfig.plugins || []), + ] + }, + }, + ], + }) + + const noExternal = [ + '@tanstack/start', + '@tanstack/start/server', + '@tanstack/start-client', + '@tanstack/start-server', + '@tanstack/start-server-functions-fetcher', + '@tanstack/start-server-functions-handler', + '@tanstack/start-server-functions-client', + '@tanstack/start-server-functions-ssr', + '@tanstack/start-server-functions-server', + '@tanstack/start-router-manifest', + '@tanstack/start-config', + '@tanstack/start-api-routes', + '@tanstack/server-functions-plugin', + 'tsr:routes-manifest', + 'tsr:server-fn-manifest', + ] + + // If API routes handler exists, add a router for it + if (apiEntryExists) { + vinxiApp = vinxiApp.addRouter({ + name: 'api', + type: 'http', + target: 'server', + base: apiBase, + handler: apiEntry, + middleware: apiMiddleware, + routes: tanstackStartVinxiFileRouter({ tsrConfig, apiBase }), + plugins: () => { + const viteConfig = getUserViteConfig(opts.vite) + const apiViteConfig = getUserViteConfig(opts.routers?.api?.vite) + + return [ + config('tsr-vite-config-api', { + ...viteConfig.userConfig, + ...apiViteConfig.userConfig, + ssr: mergeSsrOptions([ + viteConfig.userConfig.ssr, + apiViteConfig.userConfig.ssr, + { + noExternal, + }, + ]), + optimizeDeps: { + entries: [], + ...(viteConfig.userConfig.optimizeDeps || {}), + ...(apiViteConfig.userConfig.optimizeDeps || {}), + }, + define: { + ...(viteConfig.userConfig.define || {}), + ...(apiViteConfig.userConfig.define || {}), + ...injectDefineEnv('TSS_PUBLIC_BASE', publicBase), + ...injectDefineEnv('TSS_CLIENT_BASE', clientBase), + ...injectDefineEnv('TSS_API_BASE', apiBase), + ...injectDefineEnv('TSS_OUTPUT_PUBLIC_DIR', nitroOutputPublicDir), + }, + }), + TanStackRouterVite({ + ...tsrConfig, + enableRouteGeneration: false, + autoCodeSplitting: true, + experimental: { + ...tsrConfig.experimental, + }, + }), + ...(viteConfig.plugins || []), + ...(apiViteConfig.plugins || []), + ] + }, + }) + } + + // Because Vinxi doesn't use the normal nitro dev server, it doesn't + // supply $fetch during dev. We need to hook into the dev server creation, + // nab the proper utils from the custom nitro instance that is used + // during dev and supply the $fetch to app. + // Hopefully and likely, this will just get removed when we move to + // Nitro directly. + vinxiApp.hooks.hook('app:dev:nitro:config', (devServer) => { + vinxiApp.hooks.hook( + 'app:dev:server:created', + ({ devApp: { localFetch } }) => { + const $fetch = createFetch({ + fetch: localFetch, + defaults: { + baseURL: devServer.nitro.options.runtimeConfig.app.baseURL, + }, + }) + + // @ts-expect-error + globalThis.$fetch = $fetch + }, + ) + }) + + return vinxiApp +} + +function importToProjectRelative(p: string) { + const resolved = fileURLToPath(resolve(p, import.meta.url)) + + const relative = path.relative(process.cwd(), resolved) + + return relative +} + +function tsrRoutesManifest(opts: { + tsrConfig: z.infer + clientBase: string +}): vite.Plugin { + let config: vite.ResolvedConfig + + return { + name: 'tsr-routes-manifest', + configResolved(resolvedConfig) { + config = resolvedConfig + }, + resolveId(id) { + if (id === 'tsr:routes-manifest') { + return id + } + return + }, + async load(id) { + if (id === 'tsr:routes-manifest') { + // If we're in development, return a dummy manifest + + if (config.command === 'serve') { + return `export default () => ({ + routes: {} + })` + } + + const clientViteManifestPath = path.resolve( + config.build.outDir, + `../client/${opts.clientBase}/.vite/manifest.json`, + ) + + type ViteManifest = Record< + string, + { + file: string + isEntry: boolean + imports: Array + } + > + + let manifest: ViteManifest + try { + manifest = JSON.parse(await readFile(clientViteManifestPath, 'utf-8')) + } catch (err) { + console.error(err) + throw new Error( + `Could not find the production client vite manifest at '${clientViteManifestPath}'!`, + ) + } + + const routeTreePath = path.resolve(opts.tsrConfig.generatedRouteTree) + + let routeTreeContent: string + try { + routeTreeContent = readFileSync(routeTreePath, 'utf-8') + } catch (err) { + console.error(err) + throw new Error( + `Could not find the generated route tree at '${routeTreePath}'!`, + ) + } + + // Extract the routesManifest JSON from the route tree file. + // It's located between the /* ROUTE_MANIFEST_START and ROUTE_MANIFEST_END */ comment block. + + const routerManifest = JSON.parse( + routeTreeContent.match( + /\/\* ROUTE_MANIFEST_START([\s\S]*?)ROUTE_MANIFEST_END \*\//, + )?.[1] || '{ routes: {} }', + ) as Manifest + + const routes = routerManifest.routes + + let entryFile: + | { + file: string + imports: Array + } + | undefined + + const filesByRouteFilePath: ViteManifest = Object.fromEntries( + Object.entries(manifest).map(([k, v]) => { + if (v.isEntry) { + entryFile = v + } + + const rPath = k.split('?')[0] + + return [rPath, v] + }, {}), + ) + + // Add preloads to the routes from the vite manifest + Object.entries(routes).forEach(([k, v]) => { + const file = + filesByRouteFilePath[ + path.join(opts.tsrConfig.routesDirectory, v.filePath as string) + ] + + if (file) { + const preloads = file.imports.map((d) => + path.join(opts.clientBase, manifest[d]!.file), + ) + + preloads.unshift(path.join(opts.clientBase, file.file)) + + routes[k] = { + ...v, + preloads, + } + } + }) + + if (entryFile) { + routes.__root__!.preloads = [ + path.join(opts.clientBase, entryFile.file), + ...entryFile.imports.map((d) => + path.join(opts.clientBase, manifest[d]!.file), + ), + ] + } + + const recurseRoute = ( + route: { + preloads?: Array + children?: Array + }, + seenPreloads = {} as Record, + ) => { + route.preloads = route.preloads?.filter((preload) => { + if (seenPreloads[preload]) { + return false + } + seenPreloads[preload] = true + return true + }) + + if (route.children) { + route.children.forEach((child) => { + const childRoute = routes[child]! + recurseRoute(childRoute, { ...seenPreloads }) + }) + } + } + + // @ts-expect-error + recurseRoute(routes.__root__) + + const routesManifest = { + routes, + } + + if (process.env.TSR_VITE_DEBUG) { + console.info( + 'Routes Manifest: \n' + JSON.stringify(routesManifest, null, 2), + ) + } + + return `export default () => (${JSON.stringify(routesManifest)})` + } + return + }, + } +} + +function injectDefineEnv( + key: TKey, + value: TValue, +): { [P in `process.env.${TKey}` | `import.meta.env.${TKey}`]: TValue } { + return { + [`process.env.${key}`]: JSON.stringify(value), + [`import.meta.env.${key}`]: JSON.stringify(value), + } as { [P in `process.env.${TKey}` | `import.meta.env.${TKey}`]: TValue } +} diff --git a/packages/solid-start-config/src/schema.ts b/packages/solid-start-config/src/schema.ts new file mode 100644 index 0000000000..378ce21c93 --- /dev/null +++ b/packages/solid-start-config/src/schema.ts @@ -0,0 +1,193 @@ +import { configSchema } from '@tanstack/router-generator' +import { z } from 'zod' +import type { PluginOption } from 'vite' +import type { AppOptions as VinxiAppOptions } from 'vinxi' +import type { NitroOptions } from 'nitropack' +import type { Options as ViteSolidOptions } from 'vite-plugin-solid' +import type { CustomizableConfig } from 'vinxi/dist/types/lib/vite-dev' + +type StartUserViteConfig = CustomizableConfig | (() => CustomizableConfig) + +export function getUserViteConfig(config?: StartUserViteConfig): { + plugins: Array | undefined + userConfig: CustomizableConfig +} { + const { plugins, ...userConfig } = + typeof config === 'function' ? config() : { ...config } + return { plugins, userConfig } +} + +/** + * Not all the deployment presets are fully functional or tested. + * @see https://github.com/TanStack/router/pull/2002 + */ +const vinxiDeploymentPresets = [ + 'alwaysdata', // untested + 'aws-amplify', // untested + 'aws-lambda', // untested + 'azure', // untested + 'azure-functions', // untested + 'base-worker', // untested + 'bun', // ✅ working + 'cleavr', // untested + 'cli', // untested + 'cloudflare', // untested + 'cloudflare-module', // untested + 'cloudflare-pages', // ✅ working + 'cloudflare-pages-static', // untested + 'deno', // untested + 'deno-deploy', // untested + 'deno-server', // untested + 'digital-ocean', // untested + 'edgio', // untested + 'firebase', // untested + 'flight-control', // untested + 'github-pages', // untested + 'heroku', // untested + 'iis', // untested + 'iis-handler', // untested + 'iis-node', // untested + 'koyeb', // untested + 'layer0', // untested + 'netlify', // ✅ working + 'netlify-builder', // untested + 'netlify-edge', // untested + 'netlify-static', // untested + 'nitro-dev', // untested + 'nitro-prerender', // untested + 'node', // partially working + 'node-cluster', // untested + 'node-server', // ✅ working + 'platform-sh', // untested + 'service-worker', // untested + 'static', // 🟧 partially working + 'stormkit', // untested + 'vercel', // ✅ working + 'vercel-edge', // untested + 'vercel-static', // untested + 'winterjs', // untested + 'zeabur', // untested + 'zeabur-static', // untested +] as const + +type DeploymentPreset = (typeof vinxiDeploymentPresets)[number] | (string & {}) + +const testedDeploymentPresets: Array = [ + 'bun', + 'netlify', + 'vercel', + 'cloudflare-pages', + 'node-server', +] + +export function checkDeploymentPresetInput(preset: string): DeploymentPreset { + if (!vinxiDeploymentPresets.includes(preset as any)) { + console.warn( + `Invalid deployment preset "${preset}". Available presets are: ${vinxiDeploymentPresets + .map((p) => `"${p}"`) + .join(', ')}.`, + ) + } + + if (!testedDeploymentPresets.includes(preset as any)) { + console.warn( + `The deployment preset '${preset}' is not fully supported yet and may not work as expected.`, + ) + } + + return preset +} + +type HTTPSOptions = { + cert?: string + key?: string + pfx?: string + passphrase?: string + validityDays?: number + domains?: Array +} + +type ServerOptions_ = VinxiAppOptions['server'] & { + https?: boolean | HTTPSOptions +} + +type ServerOptions = { + [K in keyof ServerOptions_]: ServerOptions_[K] +} + +export const serverSchema = z + .object({ + routeRules: z.custom().optional(), + preset: z.custom().optional(), + static: z.boolean().optional(), + prerender: z + .object({ + routes: z.array(z.string()), + ignore: z + .array( + z.custom< + string | RegExp | ((path: string) => undefined | null | boolean) + >(), + ) + .optional(), + crawlLinks: z.boolean().optional(), + }) + .optional(), + }) + .and(z.custom()) + +const viteSchema = z.custom() + +const viteSolidSchema = z.custom() + +const routersSchema = z.object({ + ssr: z + .object({ + entry: z.string().optional(), + middleware: z.string().optional(), + vite: viteSchema.optional(), + }) + .optional(), + client: z + .object({ + entry: z.string().optional(), + base: z.string().optional(), + vite: viteSchema.optional(), + }) + .optional(), + server: z + .object({ + base: z.string().optional(), + middleware: z.string().optional(), + vite: viteSchema.optional(), + }) + .optional(), + api: z + .object({ + entry: z.string().optional(), + middleware: z.string().optional(), + vite: viteSchema.optional(), + }) + .optional(), + public: z + .object({ + dir: z.string().optional(), + base: z.string().optional(), + }) + .optional(), +}) + +const tsrConfig = configSchema.partial().extend({ + appDirectory: z.string().optional(), +}) + +export const inlineConfigSchema = z.object({ + solid: viteSolidSchema.optional(), + vite: viteSchema.optional(), + tsr: tsrConfig.optional(), + routers: routersSchema.optional(), + server: serverSchema.optional(), +}) + +export type TanStackStartInputConfig = z.input +export type TanStackStartOutputConfig = z.infer diff --git a/packages/solid-start-config/src/vinxi-file-router.ts b/packages/solid-start-config/src/vinxi-file-router.ts new file mode 100644 index 0000000000..9e6d829d1b --- /dev/null +++ b/packages/solid-start-config/src/vinxi-file-router.ts @@ -0,0 +1,87 @@ +import { + BaseFileSystemRouter as VinxiBaseFileSystemRouter, + analyzeModule as vinxiFsRouterAnalyzeModule, + cleanPath as vinxiFsRouterCleanPath, +} from 'vinxi/fs-router' +import { + CONSTANTS as GENERATOR_CONSTANTS, + startAPIRouteSegmentsFromTSRFilePath, +} from '@tanstack/router-generator' +import type { configSchema } from '@tanstack/router-generator' +import type { + AppOptions as VinxiAppOptions, + RouterSchemaInput as VinxiRouterSchemaInput, +} from 'vinxi' +import type { z } from 'zod' + +export function tanstackStartVinxiFileRouter(opts: { + tsrConfig: z.infer + apiBase: string +}) { + const apiBaseSegment = opts.apiBase.split('/').filter(Boolean).join('/') + const isAPIPath = new RegExp(`/${apiBaseSegment}/`) + + return function (router: VinxiRouterSchemaInput, app: VinxiAppOptions) { + // Our own custom File Router that extends the VinxiBaseFileSystemRouter + // for splitting the API routes into its own "bundle" + // and adding the $APIRoute metadata to the route object + // This could be customized in future to support more complex splits + class TanStackStartFsRouter extends VinxiBaseFileSystemRouter { + toPath(src: string): string { + const inputPath = vinxiFsRouterCleanPath(src, this.config) + + const segments = startAPIRouteSegmentsFromTSRFilePath( + inputPath, + opts.tsrConfig, + ) + + const pathname = segments + .map((part) => { + if (part.type === 'splat') { + return `*splat` + } + + if (part.type === 'param') { + return `:${part.value}?` + } + + return part.value + }) + .join('/') + + return pathname.length > 0 ? `/${pathname}` : '/' + } + + toRoute(src: string) { + const webPath = this.toPath(src) + + const [_, exports] = vinxiFsRouterAnalyzeModule(src) + + const hasAPIRoute = exports.find( + (exp) => exp.n === GENERATOR_CONSTANTS.APIRouteExportVariable, + ) + + return { + path: webPath, + filePath: src, + $APIRoute: + isAPIPath.test(webPath) && hasAPIRoute + ? { + src, + pick: [GENERATOR_CONSTANTS.APIRouteExportVariable], + } + : undefined, + } + } + } + + return new TanStackStartFsRouter( + { + dir: opts.tsrConfig.routesDirectory, + extensions: ['js', 'jsx', 'ts', 'tsx'], + }, + router, + app, + ) + } +} diff --git a/packages/solid-start-config/tsconfig.json b/packages/solid-start-config/tsconfig.json new file mode 100644 index 0000000000..940a9cce0a --- /dev/null +++ b/packages/solid-start-config/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/index.ts"], + "compilerOptions": { + "rootDir": "src", + "outDir": "dist/esm", + "target": "esnext", + "noEmit": false + } +} diff --git a/packages/solid-start-server-functions-client/README.md b/packages/solid-start-server-functions-client/README.md new file mode 100644 index 0000000000..bb009b0c87 --- /dev/null +++ b/packages/solid-start-server-functions-client/README.md @@ -0,0 +1,33 @@ +> 🤫 we're cooking up something special! + + + +# TanStack Start + +![TanStack Router Header](https://github.com/tanstack/router/raw/main/media/header.png) + +🤖 Type-safe router w/ built-in caching & URL state management for React! + + + #TanStack + + + + + + + + semantic-release + + Join the discussion on Github +Best of JS + + + + + + + +Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [React Query](https://github.com/tannerlinsley/react-query), [React Table](https://github.com/tanstack/react-table), [React Charts](https://github.com/tannerlinsley/react-charts), [React Virtual](https://github.com/tannerlinsley/react-virtual) + +## Visit [tanstack.com/router](https://tanstack.com/router) for docs, guides, API and more! diff --git a/packages/solid-start-server-functions-client/eslint.config.js b/packages/solid-start-server-functions-client/eslint.config.js new file mode 100644 index 0000000000..931f0ec774 --- /dev/null +++ b/packages/solid-start-server-functions-client/eslint.config.js @@ -0,0 +1,31 @@ +// @ts-check + +import pluginReact from '@eslint-react/eslint-plugin' +import pluginReactHooks from 'eslint-plugin-react-hooks' +import rootConfig from '../../eslint.config.js' + +export default [ + ...rootConfig, + { + ...pluginReact.configs.recommended, + files: ['**/*.{ts,tsx}'], + }, + { + plugins: { + 'react-hooks': pluginReactHooks, + }, + rules: { + '@eslint-react/no-unstable-context-value': 'off', + '@eslint-react/no-unstable-default-props': 'off', + '@eslint-react/dom/no-missing-button-type': 'off', + 'react-hooks/exhaustive-deps': 'error', + 'react-hooks/rules-of-hooks': 'error', + }, + }, + { + files: ['**/__tests__/**'], + rules: { + '@typescript-eslint/no-unnecessary-condition': 'off', + }, + }, +] diff --git a/packages/solid-start-server-functions-client/package.json b/packages/solid-start-server-functions-client/package.json new file mode 100644 index 0000000000..0903dcd4a5 --- /dev/null +++ b/packages/solid-start-server-functions-client/package.json @@ -0,0 +1,71 @@ +{ + "name": "@tanstack/solid-start-server-functions-client", + "version": "1.109.2", + "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" + }, + "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": "exit 0; 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", + "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/server-functions-plugin": "workspace:^", + "@tanstack/solid-start-server-functions-fetcher": "workspace:^" + }, + "devDependencies": { + "typescript": "^5.7.2" + } +} diff --git a/packages/solid-start-server-functions-client/src/index.ts b/packages/solid-start-server-functions-client/src/index.ts new file mode 100644 index 0000000000..8094b34192 --- /dev/null +++ b/packages/solid-start-server-functions-client/src/index.ts @@ -0,0 +1,19 @@ +import { serverFnFetcher } from '@tanstack/solid-start-server-functions-fetcher' +import type { CreateRpcFn } from '@tanstack/server-functions-plugin' + +function sanitizeBase(base: string) { + return base.replace(/^\/|\/$/g, '') +} + +export const createClientRpc: CreateRpcFn = (functionId, serverBase) => { + const url = `/${sanitizeBase(serverBase)}/${functionId}` + + const clientFn = (...args: Array) => { + return serverFnFetcher(url, args, fetch) + } + + return Object.assign(clientFn, { + url, + functionId, + }) +} diff --git a/packages/solid-start-server-functions-client/tsconfig.json b/packages/solid-start-server-functions-client/tsconfig.json new file mode 100644 index 0000000000..51dda9abf2 --- /dev/null +++ b/packages/solid-start-server-functions-client/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "module": "esnext" + }, + "include": ["src", "vite.config.ts"] +} diff --git a/packages/solid-start-server-functions-client/tsconfigs/config.tsconfig.json b/packages/solid-start-server-functions-client/tsconfigs/config.tsconfig.json new file mode 100644 index 0000000000..58fc33fb0a --- /dev/null +++ b/packages/solid-start-server-functions-client/tsconfigs/config.tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.json", + "include": ["../src/config/index.ts"], + "compilerOptions": { + "rootDir": "../src/config", + "outDir": "../dist/esm/config", + "target": "esnext", + "noEmit": false + } +} diff --git a/packages/solid-start-server-functions-client/tsconfigs/router-manifest.tsconfig.json b/packages/solid-start-server-functions-client/tsconfigs/router-manifest.tsconfig.json new file mode 100644 index 0000000000..cc108d2494 --- /dev/null +++ b/packages/solid-start-server-functions-client/tsconfigs/router-manifest.tsconfig.json @@ -0,0 +1,10 @@ +{ + "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/solid-start-server-functions-client/vite.config.ts b/packages/solid-start-server-functions-client/vite.config.ts new file mode 100644 index 0000000000..976bb5c87f --- /dev/null +++ b/packages/solid-start-server-functions-client/vite.config.ts @@ -0,0 +1,21 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import { tanstackViteConfig } from '@tanstack/config/vite' +import packageJson from './package.json' +import type { ViteUserConfig } from 'vitest/config' + +const config = defineConfig({ + plugins: [] as ViteUserConfig['plugins'], + test: { + name: packageJson.name, + watch: false, + environment: 'jsdom', + }, +}) + +export default mergeConfig( + config, + tanstackViteConfig({ + entry: './src/index.ts', + srcDir: './src', + }), +) diff --git a/packages/solid-start-server-functions-fetcher/README.md b/packages/solid-start-server-functions-fetcher/README.md new file mode 100644 index 0000000000..bb009b0c87 --- /dev/null +++ b/packages/solid-start-server-functions-fetcher/README.md @@ -0,0 +1,33 @@ +> 🤫 we're cooking up something special! + + + +# TanStack Start + +![TanStack Router Header](https://github.com/tanstack/router/raw/main/media/header.png) + +🤖 Type-safe router w/ built-in caching & URL state management for React! + + + #TanStack + + + + + + + + semantic-release + + Join the discussion on Github +Best of JS + + + + + + + +Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [React Query](https://github.com/tannerlinsley/react-query), [React Table](https://github.com/tanstack/react-table), [React Charts](https://github.com/tannerlinsley/react-charts), [React Virtual](https://github.com/tannerlinsley/react-virtual) + +## Visit [tanstack.com/router](https://tanstack.com/router) for docs, guides, API and more! diff --git a/packages/solid-start-server-functions-fetcher/eslint.config.js b/packages/solid-start-server-functions-fetcher/eslint.config.js new file mode 100644 index 0000000000..931f0ec774 --- /dev/null +++ b/packages/solid-start-server-functions-fetcher/eslint.config.js @@ -0,0 +1,31 @@ +// @ts-check + +import pluginReact from '@eslint-react/eslint-plugin' +import pluginReactHooks from 'eslint-plugin-react-hooks' +import rootConfig from '../../eslint.config.js' + +export default [ + ...rootConfig, + { + ...pluginReact.configs.recommended, + files: ['**/*.{ts,tsx}'], + }, + { + plugins: { + 'react-hooks': pluginReactHooks, + }, + rules: { + '@eslint-react/no-unstable-context-value': 'off', + '@eslint-react/no-unstable-default-props': 'off', + '@eslint-react/dom/no-missing-button-type': 'off', + 'react-hooks/exhaustive-deps': 'error', + 'react-hooks/rules-of-hooks': 'error', + }, + }, + { + files: ['**/__tests__/**'], + rules: { + '@typescript-eslint/no-unnecessary-condition': 'off', + }, + }, +] diff --git a/packages/solid-start-server-functions-fetcher/package.json b/packages/solid-start-server-functions-fetcher/package.json new file mode 100644 index 0000000000..fe1b4cf5e0 --- /dev/null +++ b/packages/solid-start-server-functions-fetcher/package.json @@ -0,0 +1,76 @@ +{ + "name": "@tanstack/solid-start-server-functions-fetcher", + "version": "1.109.2", + "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" + }, + "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": "exit 0; 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", + "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/solid-router": "workspace:^", + "@tanstack/solid-start-client": "workspace:^" + }, + "devDependencies": { + "vite-plugin-solid": "^2.11.2", + "solid-js": "^1.0.0", + "typescript": "^5.7.2" + }, + "peerDependencies": { + "solid-js": ">=1.0.0" + } +} diff --git a/packages/solid-start-server-functions-fetcher/src/index.tsx b/packages/solid-start-server-functions-fetcher/src/index.tsx new file mode 100644 index 0000000000..f5442b8ebb --- /dev/null +++ b/packages/solid-start-server-functions-fetcher/src/index.tsx @@ -0,0 +1,150 @@ +import { + encode, + isNotFound, + isPlainObject, + isRedirect, +} from '@tanstack/solid-router' +import { startSerializer } from '@tanstack/solid-start-client' +import type { MiddlewareClientFnOptions } from '@tanstack/start-client' + +export async function serverFnFetcher( + url: string, + args: Array, + handler: (url: string, requestInit: RequestInit) => Promise, +) { + const _first = args[0] + + // If createServerFn was used to wrap the fetcher, + // We need to handle the arguments differently + if (isPlainObject(_first) && _first.method) { + const first = _first as MiddlewareClientFnOptions & { + headers: HeadersInit + } + const type = first.data instanceof FormData ? 'formData' : 'payload' + + // Arrange the headers + const headers = new Headers({ + ...(type === 'payload' + ? { + 'content-type': 'application/json', + accept: 'application/json', + } + : {}), + ...(first.headers instanceof Headers + ? Object.fromEntries(first.headers.entries()) + : first.headers), + }) + + // If the method is GET, we need to move the payload to the query string + if (first.method === 'GET') { + // If the method is GET, we need to move the payload to the query string + const encodedPayload = encode({ + payload: startSerializer.stringify({ + data: first.data, + context: first.context, + }), + }) + + if (encodedPayload) { + if (url.includes('?')) { + url += `&${encodedPayload}` + } else { + url += `?${encodedPayload}` + } + } + } + + if (url.includes('?')) { + url += `&createServerFn` + } else { + url += `?createServerFn` + } + + const handlerResponse = await handler(url, { + method: first.method, + headers, + signal: first.signal, + ...getFetcherRequestOptions(first), + }) + + const response = await handleResponseErrors(handlerResponse) + + // Check if the response is JSON + if (response.headers.get('content-type')?.includes('application/json')) { + // Even though the response is JSON, we need to decode it + // because the server may have transformed it + const json = startSerializer.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) || json instanceof Error) { + throw json + } + + return json + } + + // Must be a raw response + return response + } + + // If not a custom fetcher, it was probably + // a `use server` function, so just proxy the arguments + // through as a POST request + const response = await handleResponseErrors( + await handler(url, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(args), + }), + ) + + // If the response is JSON, return it parsed + const contentType = response.headers.get('content-type') + if (contentType && contentType.includes('application/json')) { + return startSerializer.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 response.text() + } +} + +function getFetcherRequestOptions(opts: MiddlewareClientFnOptions) { + if (opts.method === 'POST') { + if (opts.data instanceof FormData) { + opts.data.set('__TSR_CONTEXT', startSerializer.stringify(opts.context)) + return { + body: opts.data, + } + } + + return { + body: startSerializer.stringify({ + data: opts.data ?? null, + context: opts.context, + }), + } + } + + return {} +} + +async function handleResponseErrors(response: Response) { + if (!response.ok) { + const contentType = response.headers.get('content-type') + const isJson = contentType && contentType.includes('application/json') + + if (isJson) { + throw startSerializer.decode(await response.json()) + } + + throw new Error(await response.text()) + } + + return response +} diff --git a/packages/solid-start-server-functions-fetcher/tsconfig.json b/packages/solid-start-server-functions-fetcher/tsconfig.json new file mode 100644 index 0000000000..51dda9abf2 --- /dev/null +++ b/packages/solid-start-server-functions-fetcher/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "module": "esnext" + }, + "include": ["src", "vite.config.ts"] +} diff --git a/packages/solid-start-server-functions-fetcher/vite.config.ts b/packages/solid-start-server-functions-fetcher/vite.config.ts new file mode 100644 index 0000000000..d88917fad1 --- /dev/null +++ b/packages/solid-start-server-functions-fetcher/vite.config.ts @@ -0,0 +1,22 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import { tanstackViteConfig } from '@tanstack/config/vite' +import solid from 'vite-plugin-solid' +import packageJson from './package.json' +import type { ViteUserConfig } from 'vitest/config' + +const config = defineConfig({ + plugins: [solid()] as ViteUserConfig['plugins'], + test: { + name: packageJson.name, + watch: false, + environment: 'jsdom', + }, +}) + +export default mergeConfig( + config, + tanstackViteConfig({ + entry: './src/index.tsx', + srcDir: './src', + }), +) diff --git a/packages/solid-start-server-functions-handler/README.md b/packages/solid-start-server-functions-handler/README.md new file mode 100644 index 0000000000..bb009b0c87 --- /dev/null +++ b/packages/solid-start-server-functions-handler/README.md @@ -0,0 +1,33 @@ +> 🤫 we're cooking up something special! + + + +# TanStack Start + +![TanStack Router Header](https://github.com/tanstack/router/raw/main/media/header.png) + +🤖 Type-safe router w/ built-in caching & URL state management for React! + + + #TanStack + + + + + + + + semantic-release + + Join the discussion on Github +Best of JS + + + + + + + +Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [React Query](https://github.com/tannerlinsley/react-query), [React Table](https://github.com/tanstack/react-table), [React Charts](https://github.com/tannerlinsley/react-charts), [React Virtual](https://github.com/tannerlinsley/react-virtual) + +## Visit [tanstack.com/router](https://tanstack.com/router) for docs, guides, API and more! diff --git a/packages/solid-start-server-functions-handler/eslint.config.js b/packages/solid-start-server-functions-handler/eslint.config.js new file mode 100644 index 0000000000..931f0ec774 --- /dev/null +++ b/packages/solid-start-server-functions-handler/eslint.config.js @@ -0,0 +1,31 @@ +// @ts-check + +import pluginReact from '@eslint-react/eslint-plugin' +import pluginReactHooks from 'eslint-plugin-react-hooks' +import rootConfig from '../../eslint.config.js' + +export default [ + ...rootConfig, + { + ...pluginReact.configs.recommended, + files: ['**/*.{ts,tsx}'], + }, + { + plugins: { + 'react-hooks': pluginReactHooks, + }, + rules: { + '@eslint-react/no-unstable-context-value': 'off', + '@eslint-react/no-unstable-default-props': 'off', + '@eslint-react/dom/no-missing-button-type': 'off', + 'react-hooks/exhaustive-deps': 'error', + 'react-hooks/rules-of-hooks': 'error', + }, + }, + { + files: ['**/__tests__/**'], + rules: { + '@typescript-eslint/no-unnecessary-condition': 'off', + }, + }, +] diff --git a/packages/solid-start-server-functions-handler/package.json b/packages/solid-start-server-functions-handler/package.json new file mode 100644 index 0000000000..b76d1ec535 --- /dev/null +++ b/packages/solid-start-server-functions-handler/package.json @@ -0,0 +1,78 @@ +{ + "name": "@tanstack/solid-start-server-functions-handler", + "version": "1.109.2", + "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" + }, + "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": "exit 0; 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", + "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/solid-router": "workspace:^", + "@tanstack/solid-start-client": "workspace:^", + "@tanstack/solid-start-server": "workspace:^", + "tiny-invariant": "^1.3.3" + }, + "devDependencies": { + "vite-plugin-solid": "^2.11.2", + "solid-js": "^1.0.0", + "typescript": "^5.7.2" + }, + "peerDependencies": { + "solid-js": ">=1.0.0" + } +} diff --git a/packages/solid-start-server-functions-handler/src/index.tsx b/packages/solid-start-server-functions-handler/src/index.tsx new file mode 100644 index 0000000000..3cfd145e85 --- /dev/null +++ b/packages/solid-start-server-functions-handler/src/index.tsx @@ -0,0 +1,307 @@ +import { isNotFound, isRedirect } from '@tanstack/solid-router' +import invariant from 'tiny-invariant' +import { + eventHandler, + getEvent, + getResponseStatus, + toWebRequest, +} from '@tanstack/solid-start-server' +import { startSerializer } from '@tanstack/solid-start-client' +// @ts-expect-error +import _serverFnManifest from 'tsr:server-fn-manifest' +import type { H3Event } from '@tanstack/solid-start-server' + +// NOTE: This is a dummy export to silence warnings about +// only having a default export. +export const dummy = 1 + +export default eventHandler(handleServerAction) + +const serverFnManifest = _serverFnManifest as Record< + string, + { + functionName: string + extractedFilename: string + importer: () => Promise + } +> + +async function handleServerAction(event: H3Event) { + const request = toWebRequest(event)! + + const response = await handleServerRequest({ + request, + event, + }) + return response +} + +function sanitizeBase(base: string | undefined) { + if (!base) { + throw new Error( + '🚨 process.env.TSS_SERVER_FN_BASE is required in start/server-handler/index', + ) + } + + return base.replace(/^\/|\/$/g, '') +} + +async function handleServerRequest({ + request, + event, +}: { + request: Request + event: H3Event +}) { + const controller = new AbortController() + const signal = controller.signal + const abort = () => controller.abort() + event.node.req.on('close', abort) + + 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 = new RegExp( + `${sanitizeBase(process.env.TSS_SERVER_FN_BASE)}/([^/?#]+)`, + ) + + // Execute the regex + const match = url.pathname.match(regex) + const serverFnId = match ? match[1] : null + const search = Object.fromEntries(url.searchParams.entries()) as { + payload?: any + createServerFn?: boolean + } + + const isCreateServerFn = 'createServerFn' in search + + if (typeof serverFnId !== 'string') { + throw new Error('Invalid server action param for serverFnId: ' + serverFnId) + } + + const serverFnInfo = serverFnManifest[serverFnId] + + 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 fnModule: undefined | { [key: string]: any } + + if (process.env.NODE_ENV === 'development') { + fnModule = await (globalThis as any).app + .getRouter('server') + .internals.devServer.ssrLoadModule(serverFnInfo.extractedFilename) + } else { + fnModule = await serverFnInfo.importer() + } + + if (!fnModule) { + console.log('serverFnManifest', serverFnManifest) + throw new Error('Server function module not resolved for ' + serverFnId) + } + + const action = fnModule[serverFnInfo.functionName] + + if (!action) { + console.log('serverFnManifest', serverFnManifest) + console.log('fnModule', fnModule) + throw new Error( + `Server function module export not resolved for serverFn ID: ${serverFnId}`, + ) + } + + // Known FormData 'Content-Type' header values + const formDataContentTypes = [ + 'multipart/form-data', + 'application/x-www-form-urlencoded', + ] + + const response = await (async () => { + try { + let result = await (async () => { + // FormData + if ( + request.headers.get('Content-Type') && + formDataContentTypes.some((type) => + request.headers.get('Content-Type')?.includes(type), + ) + ) { + // We don't support GET requests with FormData payloads... that seems impossible + invariant( + method.toLowerCase() !== 'get', + 'GET requests with FormData payloads are not supported', + ) + + return await action(await request.formData(), signal) + } + + // Get requests use the query string + if (method.toLowerCase() === 'get') { + // By default the payload is the search params + let payload: any = search + + // If this GET request was created by createServerFn, + // then the payload will be on the payload param + if (isCreateServerFn) { + payload = search.payload + } + + // If there's a payload, we should try to parse it + payload = payload ? startSerializer.parse(payload) : payload + + // Send it through! + return await action(payload, signal) + } + + // This must be a POST request, likely JSON??? + const jsonPayloadAsString = await request.text() + + // We should probably try to deserialize the payload + // as JSON, but we'll just pass it through for now. + const payload = startSerializer.parse(jsonPayloadAsString) + + // If this POST request was created by createServerFn, + // it's payload will be the only argument + if (isCreateServerFn) { + return await action(payload, signal) + } + + // Otherwise, we'll spread the payload. Need to + // support `use server` functions that take multiple + // arguments. + return await action(...(payload as any), signal) + })() + + // Any time we get a Response back, we should just + // return it immediately. + if (result instanceof Response) { + return result + } + + // If this is a non createServerFn request, we need to + // pull out the result from the result object + if (!isCreateServerFn) { + result = result.result + + // The result might again be a response, + // and if it is, return it. + if (result instanceof Response) { + return result + } + } + + // if (!search.createServerFn) { + // result = result.result + // } + + // else if ( + // isPlainObject(result) && + // 'result' in result && + // result.result instanceof Response + // ) { + // return result.result + // } + + // TODO: RSCs + // if (isValidElement(result)) { + // const { renderToPipeableStream } = await import( + // // @ts-expect-error + // '@vinxi/react-server-dom/server' + // ) + + // const pipeableStream = renderToPipeableStream(result) + + // setHeaders(event, { + // 'Content-Type': 'text/x-component', + // } as any) + + // sendStream(event, response) + // event._handled = true + + // return new Response(null, { status: 200 }) + // } + + if (isRedirect(result) || isNotFound(result)) { + return redirectOrNotFoundResponse(result) + } + + return new Response( + result !== undefined ? startSerializer.stringify(result) : undefined, + { + status: getResponseStatus(getEvent()), + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + } 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 + // build final URLs, so we need to defer that to the client. + // The client will check for __redirect and __notFound keys, + // and if they exist, it will handle them appropriately. + + if (isRedirect(error) || isNotFound(error)) { + return redirectOrNotFoundResponse(error) + } + + console.info() + console.info('Server Fn Error!') + console.info() + console.error(error) + console.info() + + return new Response(startSerializer.stringify(error), { + status: 500, + headers: { + 'Content-Type': 'application/json', + }, + }) + } + })() + event.node.req.removeListener('close', abort) + + if (process.env.NODE_ENV === 'development') + console.info(`ServerFn Response: ${response.status}`) + + if (response.headers.get('Content-Type') === 'application/json') { + const cloned = response.clone() + const text = await cloned.text() + const payload = text ? JSON.stringify(JSON.parse(text)) : 'undefined' + + if (process.env.NODE_ENV === 'development') + console.info( + ` - Payload: ${payload.length > 100 ? payload.substring(0, 100) + '...' : payload}`, + ) + } + if (process.env.NODE_ENV === 'development') console.info() + + return response +} + +function redirectOrNotFoundResponse(error: any) { + const { headers, ...rest } = error + + return new Response(JSON.stringify(rest), { + status: 200, + headers: { + 'Content-Type': 'application/json', + ...(headers || {}), + }, + }) +} diff --git a/packages/solid-start-server-functions-handler/tsconfig.json b/packages/solid-start-server-functions-handler/tsconfig.json new file mode 100644 index 0000000000..51dda9abf2 --- /dev/null +++ b/packages/solid-start-server-functions-handler/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "module": "esnext" + }, + "include": ["src", "vite.config.ts"] +} diff --git a/packages/solid-start-server-functions-handler/vite.config.ts b/packages/solid-start-server-functions-handler/vite.config.ts new file mode 100644 index 0000000000..7be2947863 --- /dev/null +++ b/packages/solid-start-server-functions-handler/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import { tanstackViteConfig } from '@tanstack/config/vite' +import solid from 'vite-plugin-solid' +import packageJson from './package.json' +import type { ViteUserConfig } from 'vitest/config' + +const config = defineConfig({ + plugins: [solid()] as ViteUserConfig['plugins'], + test: { + name: packageJson.name, + watch: false, + environment: 'jsdom', + }, +}) + +export default mergeConfig( + config, + tanstackViteConfig({ + entry: './src/index.tsx', + srcDir: './src', + externalDeps: ['tsr:server-fn-manifest'], + }), +) diff --git a/packages/solid-start/README.md b/packages/solid-start/README.md new file mode 100644 index 0000000000..bb009b0c87 --- /dev/null +++ b/packages/solid-start/README.md @@ -0,0 +1,33 @@ +> 🤫 we're cooking up something special! + + + +# TanStack Start + +![TanStack Router Header](https://github.com/tanstack/router/raw/main/media/header.png) + +🤖 Type-safe router w/ built-in caching & URL state management for React! + + + #TanStack + + + + + + + + semantic-release + + Join the discussion on Github +Best of JS + + + + + + + +Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [React Query](https://github.com/tannerlinsley/react-query), [React Table](https://github.com/tanstack/react-table), [React Charts](https://github.com/tannerlinsley/react-charts), [React Virtual](https://github.com/tannerlinsley/react-virtual) + +## Visit [tanstack.com/router](https://tanstack.com/router) for docs, guides, API and more! diff --git a/packages/solid-start/eslint.config.js b/packages/solid-start/eslint.config.js new file mode 100644 index 0000000000..931f0ec774 --- /dev/null +++ b/packages/solid-start/eslint.config.js @@ -0,0 +1,31 @@ +// @ts-check + +import pluginReact from '@eslint-react/eslint-plugin' +import pluginReactHooks from 'eslint-plugin-react-hooks' +import rootConfig from '../../eslint.config.js' + +export default [ + ...rootConfig, + { + ...pluginReact.configs.recommended, + files: ['**/*.{ts,tsx}'], + }, + { + plugins: { + 'react-hooks': pluginReactHooks, + }, + rules: { + '@eslint-react/no-unstable-context-value': 'off', + '@eslint-react/no-unstable-default-props': 'off', + '@eslint-react/dom/no-missing-button-type': 'off', + 'react-hooks/exhaustive-deps': 'error', + 'react-hooks/rules-of-hooks': 'error', + }, + }, + { + files: ['**/__tests__/**'], + rules: { + '@typescript-eslint/no-unnecessary-condition': 'off', + }, + }, +] diff --git a/packages/solid-start/package.json b/packages/solid-start/package.json new file mode 100644 index 0000000000..b86fe01c1b --- /dev/null +++ b/packages/solid-start/package.json @@ -0,0 +1,163 @@ +{ + "name": "@tanstack/solid-start", + "version": "1.111.0", + "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" + }, + "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:build", + "test:build": "exit 0; vitest", + "build": "vite build" + }, + "type": "module", + "types": "dist/esm/client.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/esm/client.d.ts", + "default": "./dist/esm/client.js" + }, + "require": { + "types": "./dist/cjs/client.d.cts", + "default": "./dist/cjs/client.cjs" + } + }, + "./client": { + "import": { + "types": "./dist/esm/client.d.ts", + "default": "./dist/esm/client.js" + }, + "require": { + "types": "./dist/cjs/client.d.cts", + "default": "./dist/cjs/client.cjs" + } + }, + "./server": { + "import": { + "types": "./dist/esm/server.d.ts", + "default": "./dist/esm/server.js" + }, + "require": { + "types": "./dist/cjs/server.d.cts", + "default": "./dist/cjs/server.cjs" + } + }, + "./config": { + "import": { + "types": "./dist/esm/config.d.ts", + "default": "./dist/esm/config.js" + }, + "require": { + "types": "./dist/cjs/config.d.cts", + "default": "./dist/cjs/config.cjs" + } + }, + "./api": { + "import": { + "types": "./dist/esm/api.d.ts", + "default": "./dist/esm/api.js" + }, + "require": { + "types": "./dist/cjs/api.d.cts", + "default": "./dist/cjs/api.cjs" + } + }, + "./router-manifest": { + "import": { + "types": "./dist/esm/router-manifest.d.ts", + "default": "./dist/esm/router-manifest.js" + }, + "require": { + "types": "./dist/cjs/router-manifest.d.cts", + "default": "./dist/cjs/router-manifest.cjs" + } + }, + "./server-functions-client": { + "import": { + "types": "./dist/esm/server-functions-client.d.ts", + "default": "./dist/esm/server-functions-client.js" + }, + "require": { + "types": "./dist/cjs/server-functions-client.d.cts", + "default": "./dist/cjs/server-functions-client.cjs" + } + }, + "./server-functions-server": { + "import": { + "types": "./dist/esm/server-functions-server.d.ts", + "default": "./dist/esm/server-functions-server.js" + }, + "require": { + "types": "./dist/cjs/server-functions-server.d.cts", + "default": "./dist/cjs/server-functions-server.cjs" + } + }, + "./server-functions-handler": { + "import": { + "types": "./dist/esm/server-functions-handler.d.ts", + "default": "./dist/esm/server-functions-handler.js" + }, + "require": { + "types": "./dist/cjs/server-functions-handler.d.cts", + "default": "./dist/cjs/server-functions-handler.cjs" + } + }, + "./server-functions-ssr": { + "import": { + "types": "./dist/esm/server-functions-ssr.d.ts", + "default": "./dist/esm/server-functions-ssr.js" + }, + "require": { + "types": "./dist/cjs/server-functions-ssr.d.cts", + "default": "./dist/cjs/server-functions-ssr.cjs" + } + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "files": [ + "dist", + "src" + ], + "engines": { + "node": ">=12" + }, + "dependencies": { + "@tanstack/solid-start-client": "workspace:^", + "@tanstack/solid-start-server": "workspace:^", + "@tanstack/solid-start-config": "workspace:^", + "@tanstack/start-router-manifest": "workspace:^", + "@tanstack/solid-start-server-functions-client": "workspace:^", + "@tanstack/start-server-functions-server": "workspace:^", + "@tanstack/solid-start-server-functions-handler": "workspace:^", + "@tanstack/start-server-functions-ssr": "workspace:^", + "@tanstack/start-api-routes": "workspace:^" + }, + "peerDependencies": { + "solid-js": ">=1.0.0", + "vite": "^6.0.0" + }, + "devDependencies": { + "esbuild": "^0.25.0" + } +} diff --git a/packages/solid-start/src/api.tsx b/packages/solid-start/src/api.tsx new file mode 100644 index 0000000000..fff3bec5ea --- /dev/null +++ b/packages/solid-start/src/api.tsx @@ -0,0 +1 @@ +export * from '@tanstack/start-api-routes' diff --git a/packages/solid-start/src/client.tsx b/packages/solid-start/src/client.tsx new file mode 100644 index 0000000000..ea588dbb26 --- /dev/null +++ b/packages/solid-start/src/client.tsx @@ -0,0 +1 @@ +export * from '@tanstack/solid-start-client' diff --git a/packages/solid-start/src/config.tsx b/packages/solid-start/src/config.tsx new file mode 100644 index 0000000000..5a71360c3a --- /dev/null +++ b/packages/solid-start/src/config.tsx @@ -0,0 +1 @@ +export * from '@tanstack/solid-start-config' diff --git a/packages/solid-start/src/router-manifest.tsx b/packages/solid-start/src/router-manifest.tsx new file mode 100644 index 0000000000..38f6fdac0a --- /dev/null +++ b/packages/solid-start/src/router-manifest.tsx @@ -0,0 +1 @@ +export * from '@tanstack/start-router-manifest' diff --git a/packages/solid-start/src/server-functions-client.tsx b/packages/solid-start/src/server-functions-client.tsx new file mode 100644 index 0000000000..1747f07086 --- /dev/null +++ b/packages/solid-start/src/server-functions-client.tsx @@ -0,0 +1 @@ +export * from '@tanstack/solid-start-server-functions-client' diff --git a/packages/solid-start/src/server-functions-handler.tsx b/packages/solid-start/src/server-functions-handler.tsx new file mode 100644 index 0000000000..ce5c8f988d --- /dev/null +++ b/packages/solid-start/src/server-functions-handler.tsx @@ -0,0 +1 @@ +export * from '@tanstack/solid-start-server-functions-handler' diff --git a/packages/solid-start/src/server-functions-server.tsx b/packages/solid-start/src/server-functions-server.tsx new file mode 100644 index 0000000000..cbbdd07a89 --- /dev/null +++ b/packages/solid-start/src/server-functions-server.tsx @@ -0,0 +1 @@ +export * from '@tanstack/start-server-functions-server' diff --git a/packages/solid-start/src/server-functions-ssr.tsx b/packages/solid-start/src/server-functions-ssr.tsx new file mode 100644 index 0000000000..7359e26f45 --- /dev/null +++ b/packages/solid-start/src/server-functions-ssr.tsx @@ -0,0 +1 @@ +export * from '@tanstack/start-server-functions-ssr' diff --git a/packages/solid-start/src/server.tsx b/packages/solid-start/src/server.tsx new file mode 100644 index 0000000000..5c03f993a3 --- /dev/null +++ b/packages/solid-start/src/server.tsx @@ -0,0 +1 @@ +export * from '@tanstack/start-server' diff --git a/packages/solid-start/tsconfig.json b/packages/solid-start/tsconfig.json new file mode 100644 index 0000000000..51dda9abf2 --- /dev/null +++ b/packages/solid-start/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "module": "esnext" + }, + "include": ["src", "vite.config.ts"] +} diff --git a/packages/solid-start/vite.config.ts b/packages/solid-start/vite.config.ts new file mode 100644 index 0000000000..3cd9726fba --- /dev/null +++ b/packages/solid-start/vite.config.ts @@ -0,0 +1,38 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import { tanstackViteConfig } from '@tanstack/config/vite' +import packageJson from './package.json' + +const config = defineConfig({ + test: { + name: packageJson.name, + watch: false, + environment: 'jsdom', + }, +}) + +export default mergeConfig( + config, + tanstackViteConfig({ + srcDir: './src', + entry: [ + './src/client.tsx', + './src/server.tsx', + './src/config.tsx', + './src/router-manifest.tsx', + './src/server-functions-client.tsx', + './src/server-functions-server.tsx', + './src/server-functions-ssr.tsx', + './src/api.tsx', + ], + externalDeps: [ + '@tanstack/start-client', + '@tanstack/start-server', + '@tanstack/start-config', + '@tanstack/start-router-manifest', + '@tanstack/start-server-functions-client', + '@tanstack/start-server-functions-server', + '@tanstack/start-server-functions-ssr', + '@tanstack/start-api-routes', + ], + }), +) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 69cb63a9cc..988a2bf291 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5678,6 +5678,46 @@ importers: specifier: ^3.23.8 version: 3.24.1 + packages/solid-start: + dependencies: + '@tanstack/solid-start-client': + specifier: workspace:^ + version: link:../solid-start-client + '@tanstack/solid-start-config': + specifier: workspace:^ + version: link:../solid-start-config + '@tanstack/solid-start-server': + specifier: workspace:^ + version: link:../solid-start-server + '@tanstack/solid-start-server-functions-client': + specifier: workspace:^ + version: link:../solid-start-server-functions-client + '@tanstack/solid-start-server-functions-handler': + specifier: workspace:^ + version: link:../solid-start-server-functions-handler + '@tanstack/start-api-routes': + specifier: workspace:* + version: link:../start-api-routes + '@tanstack/start-router-manifest': + specifier: workspace:* + version: link:../start-router-manifest + '@tanstack/start-server-functions-server': + specifier: workspace:* + version: link:../start-server-functions-server + '@tanstack/start-server-functions-ssr': + specifier: workspace:* + version: link:../start-server-functions-ssr + solid-js: + specifier: '>=1.0.0' + version: 1.9.4 + vite: + specifier: 6.1.0 + version: 6.1.0(@types/node@22.13.4)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) + devDependencies: + esbuild: + specifier: ^0.25.0 + version: 0.25.0 + packages/solid-start-client: dependencies: '@tanstack/router-core': @@ -5718,6 +5758,51 @@ importers: specifier: ^2.11.2 version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.4)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + packages/solid-start-config: + dependencies: + '@tanstack/router-generator': + specifier: workspace:* + version: link:../router-generator + '@tanstack/router-plugin': + specifier: workspace:* + version: link:../router-plugin + '@tanstack/server-functions-plugin': + specifier: workspace:* + version: link:../server-functions-plugin + '@tanstack/solid-router': + specifier: workspace:^ + version: link:../solid-router + '@tanstack/solid-start-server-functions-handler': + specifier: workspace:^ + version: link:../solid-start-server-functions-handler + '@tanstack/start-plugin': + specifier: workspace:* + version: link:../start-plugin + import-meta-resolve: + specifier: ^4.1.0 + version: 4.1.0 + nitropack: + specifier: ^2.10.4 + version: 2.10.4(typescript@5.7.3) + ofetch: + specifier: ^1.4.1 + version: 1.4.1 + solid-js: + specifier: '>=1.0.0' + version: 1.9.4 + vinxi: + specifier: 0.5.3 + version: 0.5.3(@types/node@22.13.4)(db0@0.2.3)(ioredis@5.4.2)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0) + vite: + specifier: 6.1.0 + version: 6.1.0(@types/node@22.13.4)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) + vite-plugin-solid: + specifier: ^2.11.2 + version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.4)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + zod: + specifier: ^3.24.1 + version: 3.24.1 + packages/solid-start-server: dependencies: '@tanstack/solid-cross-context': @@ -5758,6 +5843,63 @@ importers: specifier: ^2.11.2 version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.4)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + packages/solid-start-server-functions-client: + dependencies: + '@tanstack/server-functions-plugin': + specifier: workspace:* + version: link:../server-functions-plugin + '@tanstack/solid-start-server-functions-fetcher': + specifier: workspace:^ + version: link:../solid-start-server-functions-fetcher + devDependencies: + typescript: + specifier: ^5.7.2 + version: 5.7.3 + + packages/solid-start-server-functions-fetcher: + dependencies: + '@tanstack/solid-router': + specifier: workspace:^ + version: link:../solid-router + '@tanstack/solid-start-client': + specifier: workspace:^ + version: link:../solid-start-client + devDependencies: + solid-js: + specifier: ^1.0.0 + version: 1.9.4 + typescript: + specifier: ^5.7.2 + version: 5.7.3 + vite-plugin-solid: + specifier: ^2.11.2 + version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.4)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + + packages/solid-start-server-functions-handler: + dependencies: + '@tanstack/solid-router': + specifier: workspace:^ + version: link:../solid-router + '@tanstack/solid-start-client': + specifier: workspace:^ + version: link:../solid-start-client + '@tanstack/solid-start-server': + specifier: workspace:^ + version: link:../solid-start-server + tiny-invariant: + specifier: ^1.3.3 + version: 1.3.3 + devDependencies: + solid-js: + specifier: ^1.0.0 + version: 1.9.4 + typescript: + specifier: ^5.7.2 + version: 5.7.3 + vite-plugin-solid: + specifier: ^2.11.2 + version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.4)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + packages/start: dependencies: '@tanstack/react-start-api-routes': From ebc6bd1032e95b5f2a8717c41a3386c7ff117c47 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 22 Feb 2025 12:23:37 +0000 Subject: [PATCH 005/124] ci: apply automated fixes --- e2e/solid-start/basic/app/components/CustomMessage.tsx | 1 - packages/solid-start-client/eslint.config.js | 4 +--- packages/solid-start-client/src/tests/setupTests.tsx | 2 +- packages/solid-start-client/tsconfig.json | 2 -- packages/solid-start-server/src/defaultRenderHandler.tsx | 2 +- packages/solid-start-server/src/defaultStreamHandler.tsx | 6 +++--- 6 files changed, 6 insertions(+), 11 deletions(-) diff --git a/e2e/solid-start/basic/app/components/CustomMessage.tsx b/e2e/solid-start/basic/app/components/CustomMessage.tsx index d417543fc3..6faae2e08f 100644 --- a/e2e/solid-start/basic/app/components/CustomMessage.tsx +++ b/e2e/solid-start/basic/app/components/CustomMessage.tsx @@ -1,4 +1,3 @@ - export function CustomMessage({ message }: { message: string }) { return (
diff --git a/packages/solid-start-client/eslint.config.js b/packages/solid-start-client/eslint.config.js index 7699a9bcc0..2135cedbd4 100644 --- a/packages/solid-start-client/eslint.config.js +++ b/packages/solid-start-client/eslint.config.js @@ -14,9 +14,7 @@ export default [ plugins: { 'react-hooks': pluginReactHooks, }, - rules: { - - }, + rules: {}, }, { files: ['**/__tests__/**'], diff --git a/packages/solid-start-client/src/tests/setupTests.tsx b/packages/solid-start-client/src/tests/setupTests.tsx index e8ee517aed..a9d0dd31aa 100644 --- a/packages/solid-start-client/src/tests/setupTests.tsx +++ b/packages/solid-start-client/src/tests/setupTests.tsx @@ -1 +1 @@ -import '@testing-library/jest-dom/vitest' \ No newline at end of file +import '@testing-library/jest-dom/vitest' diff --git a/packages/solid-start-client/tsconfig.json b/packages/solid-start-client/tsconfig.json index 48a1415543..84a07f3808 100644 --- a/packages/solid-start-client/tsconfig.json +++ b/packages/solid-start-client/tsconfig.json @@ -5,9 +5,7 @@ "jsxImportSource": "solid-js", "module": "esnext", "skipLibCheck": true - }, "include": ["src", "vite.config.ts", "src/tsrScript.ts"], "exclude": ["node_modules"] - } diff --git a/packages/solid-start-server/src/defaultRenderHandler.tsx b/packages/solid-start-server/src/defaultRenderHandler.tsx index afc30aedb6..0bbd9854a8 100644 --- a/packages/solid-start-server/src/defaultRenderHandler.tsx +++ b/packages/solid-start-server/src/defaultRenderHandler.tsx @@ -5,7 +5,7 @@ import { defineHandlerCallback } from './handlerCallback' export const defaultRenderHandler = defineHandlerCallback( async ({ router, responseHeaders }) => { try { - let html = Solid.renderToString(()=>) + let html = Solid.renderToString(() => ) const injectedHtml = await Promise.all( router.serverSsr!.injectedHtml, ).then((htmls) => htmls.join('')) diff --git a/packages/solid-start-server/src/defaultStreamHandler.tsx b/packages/solid-start-server/src/defaultStreamHandler.tsx index cefeaa78d5..a4d2fefef7 100644 --- a/packages/solid-start-server/src/defaultStreamHandler.tsx +++ b/packages/solid-start-server/src/defaultStreamHandler.tsx @@ -15,9 +15,9 @@ import type { ReadableStream } from 'node:stream/web' export const defaultStreamHandler = defineHandlerCallback( async ({ request, router, responseHeaders }) => { if (typeof Solid.renderToStream === 'function') { - const stream = await Solid.renderToStream(()=> + const stream = await Solid.renderToStream(() => ( - ) + )) const responseStream = transformReadableStreamWithRouter( router, @@ -34,7 +34,7 @@ export const defaultStreamHandler = defineHandlerCallback( try { const pipeable = Solid.renderToStream( - ()=>, + () => , { ...(isbot(request.headers.get('User-Agent')) ? { From 5647d66b95e3e03b5228f4dd4e8c80ed02ead915 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Sat, 22 Feb 2025 15:07:26 +0100 Subject: [PATCH 006/124] cleanup --- packages/solid-start-client/tsconfig.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/solid-start-client/tsconfig.json b/packages/solid-start-client/tsconfig.json index 84a07f3808..fdda84d59f 100644 --- a/packages/solid-start-client/tsconfig.json +++ b/packages/solid-start-client/tsconfig.json @@ -3,9 +3,7 @@ "compilerOptions": { "jsx": "preserve", "jsxImportSource": "solid-js", - "module": "esnext", - "skipLibCheck": true + "module": "esnext" }, - "include": ["src", "vite.config.ts", "src/tsrScript.ts"], - "exclude": ["node_modules"] + "include": ["src", "vite.config.ts", "src/tsrScript.ts"] } From 8d0eb6d4708fa78c0a26eca7df38898dbaa2327c Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Sat, 22 Feb 2025 15:21:07 +0100 Subject: [PATCH 007/124] fix the workspace --- e2e/solid-start/basic/app/routeTree.gen.ts | 2 +- e2e/solid-start/basic/package.json | 2 +- pnpm-workspace.yaml | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/e2e/solid-start/basic/app/routeTree.gen.ts b/e2e/solid-start/basic/app/routeTree.gen.ts index db7101d5f5..1335898845 100644 --- a/e2e/solid-start/basic/app/routeTree.gen.ts +++ b/e2e/solid-start/basic/app/routeTree.gen.ts @@ -229,7 +229,7 @@ const RedirectTargetServerFnViaBeforeLoadRoute = // Populate the FileRoutesByPath interface -declare module '@tanstack/solid-router' { +declare module '@tanstack/react-router' { interface FileRoutesByPath { '/': { id: '/' diff --git a/e2e/solid-start/basic/package.json b/e2e/solid-start/basic/package.json index 9ebe10882d..eba86ffc14 100644 --- a/e2e/solid-start/basic/package.json +++ b/e2e/solid-start/basic/package.json @@ -1,5 +1,5 @@ { - "name": "tanstack-start-e2e-basic", + "name": "tanstack-solid-start-e2e-basic", "private": true, "sideEffects": false, "type": "module", diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 34603ea7b6..994a1e57ca 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,3 +9,4 @@ packages: - 'e2e/react-router/*' - 'e2e/solid-router/*' - 'e2e/react-start/*' + - 'e2e/solid-start/*' From 48402b05f45afdd638d6fbec443379757f90801c Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Sat, 22 Feb 2025 15:25:18 +0100 Subject: [PATCH 008/124] class --- .../basic/app/components/CustomMessage.tsx | 4 +-- .../app/components/DefaultCatchBoundary.tsx | 12 ++++----- .../basic/app/components/NotFound.tsx | 10 +++---- e2e/solid-start/basic/app/routes/__root.tsx | 18 ++++++------- e2e/solid-start/basic/app/routes/_layout.tsx | 4 +-- .../basic/app/routes/_layout/_layout-2.tsx | 6 ++--- e2e/solid-start/basic/app/routes/deferred.tsx | 2 +- e2e/solid-start/basic/app/routes/index.tsx | 2 +- e2e/solid-start/basic/app/routes/links.tsx | 14 +++++----- .../basic/app/routes/not-found/index.tsx | 4 +-- .../basic/app/routes/posts.$postId.tsx | 10 +++---- e2e/solid-start/basic/app/routes/posts.tsx | 10 +++---- .../basic/app/routes/posts_.$postId.deep.tsx | 8 +++--- .../app/routes/redirect/$target/index.tsx | 20 +++++++------- .../redirect/$target/serverFn/index.tsx | 26 +++++++++---------- .../basic/app/routes/redirect/index.tsx | 4 +-- e2e/solid-start/basic/app/routes/scripts.tsx | 2 +- e2e/solid-start/basic/app/routes/stream.tsx | 2 +- .../basic/app/routes/users.$userId.tsx | 6 ++--- e2e/solid-start/basic/app/routes/users.tsx | 12 ++++----- e2e/solid-start/basic/tsconfig.json | 3 ++- 21 files changed, 90 insertions(+), 89 deletions(-) diff --git a/e2e/solid-start/basic/app/components/CustomMessage.tsx b/e2e/solid-start/basic/app/components/CustomMessage.tsx index 6faae2e08f..c038b66d2f 100644 --- a/e2e/solid-start/basic/app/components/CustomMessage.tsx +++ b/e2e/solid-start/basic/app/components/CustomMessage.tsx @@ -1,7 +1,7 @@ export function CustomMessage({ message }: { message: string }) { return ( -
-
This is a custom message:
+
+
This is a custom message:

{message}

) diff --git a/e2e/solid-start/basic/app/components/DefaultCatchBoundary.tsx b/e2e/solid-start/basic/app/components/DefaultCatchBoundary.tsx index f5b7e91167..32aed20e67 100644 --- a/e2e/solid-start/basic/app/components/DefaultCatchBoundary.tsx +++ b/e2e/solid-start/basic/app/components/DefaultCatchBoundary.tsx @@ -17,28 +17,28 @@ export function DefaultCatchBoundary({ error }: ErrorComponentProps) { console.error(error) return ( -
+
-
+
- {isRoot ? ( + {isRoot() ? ( Home ) : ( { e.preventDefault() window.history.back() diff --git a/e2e/solid-start/basic/app/components/NotFound.tsx b/e2e/solid-start/basic/app/components/NotFound.tsx index b299bdfb97..eb0a968d39 100644 --- a/e2e/solid-start/basic/app/components/NotFound.tsx +++ b/e2e/solid-start/basic/app/components/NotFound.tsx @@ -2,20 +2,20 @@ import { Link } from '@tanstack/solid-router' export function NotFound({ children }: { children?: any }) { return ( -
-
+
+
{children ||

The page you are looking for does not exist.

}
-

+

Start Over diff --git a/e2e/solid-start/basic/app/routes/__root.tsx b/e2e/solid-start/basic/app/routes/__root.tsx index dd290499f0..3bc7bde561 100644 --- a/e2e/solid-start/basic/app/routes/__root.tsx +++ b/e2e/solid-start/basic/app/routes/__root.tsx @@ -87,11 +87,11 @@ function RootDocument({ children }: { children: React.ReactNode }) { -

+
@@ -100,7 +100,7 @@ function RootDocument({ children }: { children: React.ReactNode }) { Posts @@ -108,7 +108,7 @@ function RootDocument({ children }: { children: React.ReactNode }) { Users @@ -116,7 +116,7 @@ function RootDocument({ children }: { children: React.ReactNode }) { Layout @@ -124,7 +124,7 @@ function RootDocument({ children }: { children: React.ReactNode }) { Scripts @@ -132,7 +132,7 @@ function RootDocument({ children }: { children: React.ReactNode }) { Deferred @@ -140,7 +140,7 @@ function RootDocument({ children }: { children: React.ReactNode }) { redirect @@ -149,7 +149,7 @@ function RootDocument({ children }: { children: React.ReactNode }) { // @ts-expect-error to="/this-route-does-not-exist" activeProps={{ - className: 'font-bold', + class: 'font-bold', }} > This Route Does Not Exist diff --git a/e2e/solid-start/basic/app/routes/_layout.tsx b/e2e/solid-start/basic/app/routes/_layout.tsx index 5514bb8913..d43b4ef5f5 100644 --- a/e2e/solid-start/basic/app/routes/_layout.tsx +++ b/e2e/solid-start/basic/app/routes/_layout.tsx @@ -6,8 +6,8 @@ export const Route = createFileRoute('/_layout')({ function LayoutComponent() { return ( -
-
I'm a layout
+
+
I'm a layout
diff --git a/e2e/solid-start/basic/app/routes/_layout/_layout-2.tsx b/e2e/solid-start/basic/app/routes/_layout/_layout-2.tsx index b5b52c44e2..7a5a3623a0 100644 --- a/e2e/solid-start/basic/app/routes/_layout/_layout-2.tsx +++ b/e2e/solid-start/basic/app/routes/_layout/_layout-2.tsx @@ -8,11 +8,11 @@ function LayoutComponent() { return (
I'm a nested layout
-
+
Layout A @@ -20,7 +20,7 @@ function LayoutComponent() { Layout B diff --git a/e2e/solid-start/basic/app/routes/deferred.tsx b/e2e/solid-start/basic/app/routes/deferred.tsx index 190b51b972..924499bb90 100644 --- a/e2e/solid-start/basic/app/routes/deferred.tsx +++ b/e2e/solid-start/basic/app/routes/deferred.tsx @@ -33,7 +33,7 @@ function Deferred() { const { deferredStuff, deferredPerson, person } = Route.useLoaderData() return ( -
+
{person.name} - {person.randomNumber}
diff --git a/e2e/solid-start/basic/app/routes/index.tsx b/e2e/solid-start/basic/app/routes/index.tsx index c80321f4be..6b23caf87b 100644 --- a/e2e/solid-start/basic/app/routes/index.tsx +++ b/e2e/solid-start/basic/app/routes/index.tsx @@ -7,7 +7,7 @@ export const Route = createFileRoute('/')({ function Home() { return ( -
+

Welcome Home!!!

diff --git a/e2e/solid-start/basic/app/routes/links.tsx b/e2e/solid-start/basic/app/routes/links.tsx index 9f5a67c6db..d6ce6a449b 100644 --- a/e2e/solid-start/basic/app/routes/links.tsx +++ b/e2e/solid-start/basic/app/routes/links.tsx @@ -5,36 +5,36 @@ export const Route = createFileRoute('/links')({ const navigate = Route.useNavigate() return (
-

+

link test

-
+
Link to /posts
-
+
Link to /posts (reloadDocument=true)
-
+
-
+
+
) diff --git a/e2e/solid-start/basic/app/routes/search-params.tsx b/e2e/solid-start/basic/app/routes/search-params.tsx index 20eb10b92a..ec941d9a4c 100644 --- a/e2e/solid-start/basic/app/routes/search-params.tsx +++ b/e2e/solid-start/basic/app/routes/search-params.tsx @@ -7,7 +7,7 @@ export const Route = createFileRoute('/search-params')({ return (

SearchParams

-
{search.step}
+
{search().step}
) }, diff --git a/e2e/solid-start/basic/app/routes/stream.tsx b/e2e/solid-start/basic/app/routes/stream.tsx index a1c0bad786..79c8591545 100644 --- a/e2e/solid-start/basic/app/routes/stream.tsx +++ b/e2e/solid-start/basic/app/routes/stream.tsx @@ -1,5 +1,5 @@ import { Await, createFileRoute } from '@tanstack/solid-router' -import { useEffect, useState } from 'react' +import { createEffect, createSignal } from 'solid-js' export const Route = createFileRoute('/stream')({ component: Home, @@ -24,12 +24,12 @@ export const Route = createFileRoute('/stream')({ const decoder = new TextDecoder('utf-8') function Home() { - const { promise, stream } = Route.useLoaderData() - const [streamData, setStreamData] = useState>([]) + const loaderData: any = Route.useLoaderData() + const [streamData, setStreamData] = createSignal>([]) - useEffect(() => { + createEffect(() => { async function fetchStream() { - const reader = stream.getReader() + const reader = loaderData().stream.getReader() let chunk while (!(chunk = await reader.read()).done) { @@ -42,18 +42,18 @@ function Home() { } fetchStream() - }, []) + }) return ( <> (
{promiseData}
- {streamData.map((d) => ( -
{d}
+ {streamData().map((d) => ( +
{d}
))}
diff --git a/e2e/solid-start/basic/package.json b/e2e/solid-start/basic/package.json index eba86ffc14..163d84222c 100644 --- a/e2e/solid-start/basic/package.json +++ b/e2e/solid-start/basic/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@tanstack/solid-router": "workspace:^", - "@tanstack/router-devtools": "workspace:^", + "@tanstack/solid-start": "workspace:^", "solid-js": "^1.0.0", "redaxios": "^0.5.1", From b285aa05451a899391c3c2c05dbc72d9bf25f7a3 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 10:13:42 +0000 Subject: [PATCH 015/124] ci: apply automated fixes --- e2e/solid-start/basic/app/routes/__root.tsx | 2 +- e2e/solid-start/basic/package.json | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/e2e/solid-start/basic/app/routes/__root.tsx b/e2e/solid-start/basic/app/routes/__root.tsx index 0fa6b7f6d1..d54a788452 100644 --- a/e2e/solid-start/basic/app/routes/__root.tsx +++ b/e2e/solid-start/basic/app/routes/__root.tsx @@ -6,7 +6,7 @@ import { createRootRoute, } from '@tanstack/solid-router' -import * as Solid from 'solid-js'; +import * as Solid from 'solid-js' import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary' import { NotFound } from '~/components/NotFound' diff --git a/e2e/solid-start/basic/package.json b/e2e/solid-start/basic/package.json index 163d84222c..b99fb82624 100644 --- a/e2e/solid-start/basic/package.json +++ b/e2e/solid-start/basic/package.json @@ -12,7 +12,6 @@ }, "dependencies": { "@tanstack/solid-router": "workspace:^", - "@tanstack/solid-start": "workspace:^", "solid-js": "^1.0.0", "redaxios": "^0.5.1", From 519f23f88fc7b4751be885b2130814cd10b49f5c Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Mon, 24 Feb 2025 12:13:32 +0100 Subject: [PATCH 016/124] use agnostic start --- packages/solid-start-client/package.json | 2 +- .../solid-start-client/src/createServerFn.ts | 6 +++--- .../solid-start-client/src/ssr-client.tsx | 6 +++--- .../src/tests/createServerFn.test-d.tsx | 2 +- .../tests/createServerMiddleware.test-d.ts | 2 +- .../package.json | 1 + .../src/index.tsx | 5 ++--- packages/solid-start-server/package.json | 4 +++- .../src/createRequestHandler.ts | 7 ++++--- .../src/createStartHandler.ts | 7 ++++--- packages/solid-start-server/src/ssr-server.ts | 18 +++++++--------- packages/solid-start-server/src/tsrScript.ts | 2 +- packages/solid-start/src/server.tsx | 2 +- pnpm-lock.yaml | 21 +++++++++++++------ 14 files changed, 47 insertions(+), 38 deletions(-) diff --git a/packages/solid-start-client/package.json b/packages/solid-start-client/package.json index 8029208897..7d65e36186 100644 --- a/packages/solid-start-client/package.json +++ b/packages/solid-start-client/package.json @@ -63,12 +63,12 @@ "node": ">=12" }, "dependencies": { - "@tanstack/solid-cross-context": "workspace:^", "@tanstack/solid-router": "workspace:^", "@tanstack/router-core": "workspace:^", "cookie-es": "^1.2.2", "jsesc": "^3.1.0", "tiny-invariant": "^1.3.3", + "tiny-warning": "^1.0.3", "vinxi": "^0.5.3" }, "devDependencies": { diff --git a/packages/solid-start-client/src/createServerFn.ts b/packages/solid-start-client/src/createServerFn.ts index add2f22bb2..da4dd4ba75 100644 --- a/packages/solid-start-client/src/createServerFn.ts +++ b/packages/solid-start-client/src/createServerFn.ts @@ -1,8 +1,8 @@ +import { default as invariant } from 'tiny-invariant' +import { default as warning } from 'tiny-warning' import { - invariant, isNotFound, isRedirect, - warning, } from '@tanstack/solid-router' import { mergeHeaders } from './headers' import { globalMiddleware } from './registerGlobalMiddleware' @@ -16,7 +16,7 @@ import type { SerializerStringify, SerializerStringifyBy, Validator, -} from '@tanstack/solid-router' +} from '@tanstack/router-core' import type { AnyMiddleware, AssignAllClientSendContext, diff --git a/packages/solid-start-client/src/ssr-client.tsx b/packages/solid-start-client/src/ssr-client.tsx index 2cb95914d0..b84fc9f919 100644 --- a/packages/solid-start-client/src/ssr-client.tsx +++ b/packages/solid-start-client/src/ssr-client.tsx @@ -1,4 +1,4 @@ -import { isPlainObject } from '@tanstack/solid-router' +import { isPlainObject } from '@tanstack/router-core' import invariant from 'tiny-invariant' @@ -6,11 +6,11 @@ import { startSerializer } from './serializer' import type { AnyRouter, ControllablePromise, - DeferredPromiseState, MakeRouteMatch, - Manifest, } from '@tanstack/solid-router' +import type { DeferredPromiseState, Manifest } from '@tanstack/router-core' + declare global { interface Window { __TSR_SSR__?: StartSsrGlobal diff --git a/packages/solid-start-client/src/tests/createServerFn.test-d.tsx b/packages/solid-start-client/src/tests/createServerFn.test-d.tsx index be1671b790..981b48b461 100644 --- a/packages/solid-start-client/src/tests/createServerFn.test-d.tsx +++ b/packages/solid-start-client/src/tests/createServerFn.test-d.tsx @@ -1,7 +1,7 @@ import { expectTypeOf, test } from 'vitest' import { createServerFn } from '../createServerFn' import { createMiddleware } from '../createMiddleware' -import type { Constrain, Validator } from '@tanstack/solid-router' +import type { Constrain, Validator } from '@tanstack/router-core' test('createServerFn method with autocomplete', () => { createServerFn().handler((options) => { diff --git a/packages/solid-start-client/src/tests/createServerMiddleware.test-d.ts b/packages/solid-start-client/src/tests/createServerMiddleware.test-d.ts index f19e92c722..22caa76e3d 100644 --- a/packages/solid-start-client/src/tests/createServerMiddleware.test-d.ts +++ b/packages/solid-start-client/src/tests/createServerMiddleware.test-d.ts @@ -1,6 +1,6 @@ import { expectTypeOf, test } from 'vitest' import { createMiddleware } from '../createMiddleware' -import type { Constrain, Validator } from '@tanstack/solid-router' +import type { Constrain, Validator } from '@tanstack/router-core' test('createServeMiddleware removes middleware after middleware,', () => { const middleware = createMiddleware() diff --git a/packages/solid-start-server-functions-fetcher/package.json b/packages/solid-start-server-functions-fetcher/package.json index fe1b4cf5e0..26a1433d64 100644 --- a/packages/solid-start-server-functions-fetcher/package.json +++ b/packages/solid-start-server-functions-fetcher/package.json @@ -62,6 +62,7 @@ "node": ">=12" }, "dependencies": { + "@tanstack/router-core": "workspace:^", "@tanstack/solid-router": "workspace:^", "@tanstack/solid-start-client": "workspace:^" }, diff --git a/packages/solid-start-server-functions-fetcher/src/index.tsx b/packages/solid-start-server-functions-fetcher/src/index.tsx index f5442b8ebb..6f7b944240 100644 --- a/packages/solid-start-server-functions-fetcher/src/index.tsx +++ b/packages/solid-start-server-functions-fetcher/src/index.tsx @@ -1,11 +1,10 @@ import { - encode, isNotFound, - isPlainObject, isRedirect, } from '@tanstack/solid-router' +import { encode, isPlainObject } from '@tanstack/router-core' import { startSerializer } from '@tanstack/solid-start-client' -import type { MiddlewareClientFnOptions } from '@tanstack/start-client' +import type { MiddlewareClientFnOptions } from '@tanstack/solid-start-client' export async function serverFnFetcher( url: string, diff --git a/packages/solid-start-server/package.json b/packages/solid-start-server/package.json index 36c4d7a0a6..c2fb17b205 100644 --- a/packages/solid-start-server/package.json +++ b/packages/solid-start-server/package.json @@ -62,12 +62,14 @@ "node": ">=12" }, "dependencies": { - "@tanstack/solid-cross-context": "workspace:^", + "@tanstack/history": "workspace:^", + "@tanstack/router-core": "workspace:^", "@tanstack/solid-router": "workspace:^", "@tanstack/solid-start-client": "workspace:^", "h3": "1.13.0", "isbot": "^5.1.22", "jsesc": "^3.1.0", + "tiny-warning": "^1.0.3", "unctx": "^2.4.1" }, "devDependencies": { diff --git a/packages/solid-start-server/src/createRequestHandler.ts b/packages/solid-start-server/src/createRequestHandler.ts index c5156296b5..0a7bb09095 100644 --- a/packages/solid-start-server/src/createRequestHandler.ts +++ b/packages/solid-start-server/src/createRequestHandler.ts @@ -1,8 +1,9 @@ -import { createMemoryHistory } from '@tanstack/solid-router' -import { mergeHeaders } from '@tanstack/start-client' +import { createMemoryHistory } from '@tanstack/history' +import { mergeHeaders } from '@tanstack/solid-start-client' import { attachRouterServerSsrUtils, dehydrateRouter } from './ssr-server' import type { HandlerCallback } from './handlerCallback' -import type { AnyRouter, Manifest } from '@tanstack/solid-router' +import type { AnyRouter } from '@tanstack/solid-router' +import type { Manifest } from '@tanstack/router-core' export type RequestHandler = ( cb: HandlerCallback, diff --git a/packages/solid-start-server/src/createStartHandler.ts b/packages/solid-start-server/src/createStartHandler.ts index 135dc0f80a..55a314aa9b 100644 --- a/packages/solid-start-server/src/createStartHandler.ts +++ b/packages/solid-start-server/src/createStartHandler.ts @@ -1,10 +1,11 @@ -import { createMemoryHistory } from '@tanstack/solid-router' -import { mergeHeaders } from '@tanstack/start-client' +import { createMemoryHistory } from '@tanstack/history' +import { mergeHeaders } from '@tanstack/solid-start-client' import { eventHandler, getResponseHeaders, toWebRequest } from 'h3' import { attachRouterServerSsrUtils, dehydrateRouter } from './ssr-server' import type { HandlerCallback } from './handlerCallback' import type { EventHandlerResponse, H3Event } from 'h3' -import type { AnyRouter, Manifest } from '@tanstack/solid-router' +import type { AnyRouter } from '@tanstack/solid-router' +import type { Manifest } from '@tanstack/router-core' export type CustomizeStartHandler< TRouter extends AnyRouter, diff --git a/packages/solid-start-server/src/ssr-server.ts b/packages/solid-start-server/src/ssr-server.ts index 9748cd684b..ccd36fe9f0 100644 --- a/packages/solid-start-server/src/ssr-server.ts +++ b/packages/solid-start-server/src/ssr-server.ts @@ -1,26 +1,22 @@ +import { default as warning } from 'tiny-warning' import { TSR_DEFERRED_PROMISE, defer, isPlainArray, isPlainObject, - pick, - warning, -} from '@tanstack/solid-router' + pick +} from '@tanstack/router-core' import jsesc from 'jsesc' -import { startSerializer } from '@tanstack/start-client' +import { startSerializer } from '@tanstack/solid-start-client' import minifiedTsrBootStrapScript from './tsrScript?script-string' import type { ClientExtractedBaseEntry, DehydratedRouter, ResolvePromiseState, SsrMatch, -} from '@tanstack/start-client' -import type { - AnyRouteMatch, - AnyRouter, - DeferredPromise, - Manifest, -} from '@tanstack/solid-router' +} from '@tanstack/solid-start-client' +import type { AnyRouteMatch, AnyRouter } from '@tanstack/solid-router' +import type { Manifest, DeferredPromise } from '@tanstack/router-core' export type ServerExtractedEntry = | ServerExtractedStream diff --git a/packages/solid-start-server/src/tsrScript.ts b/packages/solid-start-server/src/tsrScript.ts index 80851fa0b1..205190e49f 100644 --- a/packages/solid-start-server/src/tsrScript.ts +++ b/packages/solid-start-server/src/tsrScript.ts @@ -1,5 +1,5 @@ import type { ControllablePromise } from '@tanstack/solid-router' -import type { StartSsrGlobal } from '@tanstack/start-client' +import type { StartSsrGlobal } from '@tanstack/solid-start-client' const __TSR_SSR__: StartSsrGlobal = { matches: [], diff --git a/packages/solid-start/src/server.tsx b/packages/solid-start/src/server.tsx index 5c03f993a3..5e6da97d48 100644 --- a/packages/solid-start/src/server.tsx +++ b/packages/solid-start/src/server.tsx @@ -1 +1 @@ -export * from '@tanstack/start-server' +export * from '@tanstack/solid-start-server' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 988a2bf291..c50466e8c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5723,9 +5723,6 @@ importers: '@tanstack/router-core': specifier: workspace:* version: link:../router-core - '@tanstack/solid-cross-context': - specifier: workspace:^ - version: link:../solid-cross-context '@tanstack/solid-router': specifier: workspace:^ version: link:../solid-router @@ -5741,6 +5738,9 @@ importers: tiny-invariant: specifier: ^1.3.3 version: 1.3.3 + tiny-warning: + specifier: ^1.0.3 + version: 1.0.3 vinxi: specifier: ^0.5.3 version: 0.5.3(@types/node@22.13.4)(db0@0.2.3)(ioredis@5.4.2)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0) @@ -5805,9 +5805,12 @@ importers: packages/solid-start-server: dependencies: - '@tanstack/solid-cross-context': - specifier: workspace:^ - version: link:../solid-cross-context + '@tanstack/history': + specifier: workspace:* + version: link:../history + '@tanstack/router-core': + specifier: workspace:* + version: link:../router-core '@tanstack/solid-router': specifier: workspace:^ version: link:../solid-router @@ -5823,6 +5826,9 @@ importers: jsesc: specifier: ^3.1.0 version: 3.1.0 + tiny-warning: + specifier: ^1.0.3 + version: 1.0.3 unctx: specifier: ^2.4.1 version: 2.4.1 @@ -5858,6 +5864,9 @@ importers: packages/solid-start-server-functions-fetcher: dependencies: + '@tanstack/router-core': + specifier: workspace:* + version: link:../router-core '@tanstack/solid-router': specifier: workspace:^ version: link:../solid-router From 23a18e2779542ab9883bbbc4e59e5b6955a97f34 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 11:14:51 +0000 Subject: [PATCH 017/124] ci: apply automated fixes --- packages/solid-start-client/src/createServerFn.ts | 5 +---- packages/solid-start-server-functions-fetcher/src/index.tsx | 5 +---- packages/solid-start-server/src/ssr-server.ts | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/solid-start-client/src/createServerFn.ts b/packages/solid-start-client/src/createServerFn.ts index da4dd4ba75..c685af88fc 100644 --- a/packages/solid-start-client/src/createServerFn.ts +++ b/packages/solid-start-client/src/createServerFn.ts @@ -1,9 +1,6 @@ import { default as invariant } from 'tiny-invariant' import { default as warning } from 'tiny-warning' -import { - isNotFound, - isRedirect, -} from '@tanstack/solid-router' +import { isNotFound, isRedirect } from '@tanstack/solid-router' import { mergeHeaders } from './headers' import { globalMiddleware } from './registerGlobalMiddleware' import { startSerializer } from './serializer' diff --git a/packages/solid-start-server-functions-fetcher/src/index.tsx b/packages/solid-start-server-functions-fetcher/src/index.tsx index 6f7b944240..655f91dc33 100644 --- a/packages/solid-start-server-functions-fetcher/src/index.tsx +++ b/packages/solid-start-server-functions-fetcher/src/index.tsx @@ -1,7 +1,4 @@ -import { - isNotFound, - isRedirect, -} from '@tanstack/solid-router' +import { isNotFound, isRedirect } from '@tanstack/solid-router' import { encode, isPlainObject } from '@tanstack/router-core' import { startSerializer } from '@tanstack/solid-start-client' import type { MiddlewareClientFnOptions } from '@tanstack/solid-start-client' diff --git a/packages/solid-start-server/src/ssr-server.ts b/packages/solid-start-server/src/ssr-server.ts index ccd36fe9f0..af996f7822 100644 --- a/packages/solid-start-server/src/ssr-server.ts +++ b/packages/solid-start-server/src/ssr-server.ts @@ -4,7 +4,7 @@ import { defer, isPlainArray, isPlainObject, - pick + pick, } from '@tanstack/router-core' import jsesc from 'jsesc' import { startSerializer } from '@tanstack/solid-start-client' From 45ad2c948a609593a1935d394206bc7ab16903d5 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Mon, 24 Feb 2025 12:31:55 +0100 Subject: [PATCH 018/124] fix default config for solid-start --- e2e/solid-start/basic/app/routeTree.gen.ts | 163 +-------------------- packages/solid-start-config/src/index.ts | 26 ++-- 2 files changed, 15 insertions(+), 174 deletions(-) diff --git a/e2e/solid-start/basic/app/routeTree.gen.ts b/e2e/solid-start/basic/app/routeTree.gen.ts index 1335898845..a1391d211b 100644 --- a/e2e/solid-start/basic/app/routeTree.gen.ts +++ b/e2e/solid-start/basic/app/routeTree.gen.ts @@ -229,7 +229,7 @@ const RedirectTargetServerFnViaBeforeLoadRoute = // Populate the FileRoutesByPath interface -declare module '@tanstack/react-router' { +declare module '@tanstack/solid-router' { interface FileRoutesByPath { '/': { id: '/' @@ -760,164 +760,3 @@ const rootRouteChildren: RootRouteChildren = { export const routeTree = rootRoute ._addFileChildren(rootRouteChildren) ._addFileTypes() - -/* ROUTE_MANIFEST_START -{ - "routes": { - "__root__": { - "filePath": "__root.tsx", - "children": [ - "/", - "/not-found", - "/_layout", - "/deferred", - "/links", - "/posts", - "/scripts", - "/search-params", - "/stream", - "/users", - "/redirect/$target", - "/redirect/", - "/posts_/$postId/deep" - ] - }, - "/": { - "filePath": "index.tsx" - }, - "/not-found": { - "filePath": "not-found/route.tsx", - "children": [ - "/not-found/via-beforeLoad", - "/not-found/via-loader", - "/not-found/" - ] - }, - "/_layout": { - "filePath": "_layout.tsx", - "children": [ - "/_layout/_layout-2" - ] - }, - "/deferred": { - "filePath": "deferred.tsx" - }, - "/links": { - "filePath": "links.tsx" - }, - "/posts": { - "filePath": "posts.tsx", - "children": [ - "/posts/$postId", - "/posts/" - ] - }, - "/scripts": { - "filePath": "scripts.tsx" - }, - "/search-params": { - "filePath": "search-params.tsx" - }, - "/stream": { - "filePath": "stream.tsx" - }, - "/users": { - "filePath": "users.tsx", - "children": [ - "/users/$userId", - "/users/" - ] - }, - "/_layout/_layout-2": { - "filePath": "_layout/_layout-2.tsx", - "parent": "/_layout", - "children": [ - "/_layout/_layout-2/layout-a", - "/_layout/_layout-2/layout-b" - ] - }, - "/not-found/via-beforeLoad": { - "filePath": "not-found/via-beforeLoad.tsx", - "parent": "/not-found" - }, - "/not-found/via-loader": { - "filePath": "not-found/via-loader.tsx", - "parent": "/not-found" - }, - "/posts/$postId": { - "filePath": "posts.$postId.tsx", - "parent": "/posts" - }, - "/redirect/$target": { - "filePath": "redirect/$target.tsx", - "children": [ - "/redirect/$target/via-beforeLoad", - "/redirect/$target/via-loader", - "/redirect/$target/", - "/redirect/$target/serverFn/via-beforeLoad", - "/redirect/$target/serverFn/via-loader", - "/redirect/$target/serverFn/via-useServerFn", - "/redirect/$target/serverFn/" - ] - }, - "/users/$userId": { - "filePath": "users.$userId.tsx", - "parent": "/users" - }, - "/not-found/": { - "filePath": "not-found/index.tsx", - "parent": "/not-found" - }, - "/posts/": { - "filePath": "posts.index.tsx", - "parent": "/posts" - }, - "/redirect/": { - "filePath": "redirect/index.tsx" - }, - "/users/": { - "filePath": "users.index.tsx", - "parent": "/users" - }, - "/_layout/_layout-2/layout-a": { - "filePath": "_layout/_layout-2/layout-a.tsx", - "parent": "/_layout/_layout-2" - }, - "/_layout/_layout-2/layout-b": { - "filePath": "_layout/_layout-2/layout-b.tsx", - "parent": "/_layout/_layout-2" - }, - "/posts_/$postId/deep": { - "filePath": "posts_.$postId.deep.tsx" - }, - "/redirect/$target/via-beforeLoad": { - "filePath": "redirect/$target/via-beforeLoad.tsx", - "parent": "/redirect/$target" - }, - "/redirect/$target/via-loader": { - "filePath": "redirect/$target/via-loader.tsx", - "parent": "/redirect/$target" - }, - "/redirect/$target/": { - "filePath": "redirect/$target/index.tsx", - "parent": "/redirect/$target" - }, - "/redirect/$target/serverFn/via-beforeLoad": { - "filePath": "redirect/$target/serverFn/via-beforeLoad.tsx", - "parent": "/redirect/$target" - }, - "/redirect/$target/serverFn/via-loader": { - "filePath": "redirect/$target/serverFn/via-loader.tsx", - "parent": "/redirect/$target" - }, - "/redirect/$target/serverFn/via-useServerFn": { - "filePath": "redirect/$target/serverFn/via-useServerFn.tsx", - "parent": "/redirect/$target" - }, - "/redirect/$target/serverFn/": { - "filePath": "redirect/$target/serverFn/index.tsx", - "parent": "/redirect/$target" - } - } -} -ROUTE_MANIFEST_END */ diff --git a/packages/solid-start-config/src/index.ts b/packages/solid-start-config/src/index.ts index 8898bb58db..85e1768a5c 100644 --- a/packages/solid-start-config/src/index.ts +++ b/packages/solid-start-config/src/index.ts @@ -125,18 +125,18 @@ export async function defineConfig( manifestVirtualImportId: 'tsr:server-fn-manifest', client: { getRuntimeCode: () => - `import { createClientRpc } from '@tanstack/start/server-functions-client'`, + `import { createClientRpc } from '@tanstack/solid-start/server-functions-client'`, replacer: (opts) => `createClientRpc('${opts.functionId}', '${serverBase}')`, }, ssr: { getRuntimeCode: () => - `import { createSsrRpc } from '@tanstack/start/server-functions-ssr'`, + `import { createSsrRpc } from '@tanstack/solid-start/server-functions-ssr'`, replacer: (opts) => `createSsrRpc('${opts.functionId}', '${serverBase}')`, }, server: { getRuntimeCode: () => - `import { createServerRpc } from '@tanstack/start/server-functions-server'`, + `import { createServerRpc } from '@tanstack/solid-start/server-functions-server'`, replacer: (opts) => `createServerRpc('${opts.functionId}', '${serverBase}', ${opts.fn})`, }, @@ -156,6 +156,7 @@ export async function defineConfig( await dummyNitroApp.close() let vinxiApp = createApp({ + server: { ...serverOptions, preset: deploymentPreset, @@ -217,6 +218,7 @@ export async function defineConfig( }), TanStackRouterVite({ ...tsrConfig, + target: 'solid', enableRouteGeneration: true, autoCodeSplitting: true, experimental: { @@ -307,7 +309,7 @@ export async function defineConfig( // TODO: RSCS - enable this // worker: true, handler: importToProjectRelative( - '@tanstack/start-server-functions-handler', + '@tanstack/solid-start-server-functions-handler', ), plugins: () => { const routerType = 'server' @@ -380,17 +382,17 @@ export async function defineConfig( }) const noExternal = [ - '@tanstack/start', - '@tanstack/start/server', - '@tanstack/start-client', - '@tanstack/start-server', - '@tanstack/start-server-functions-fetcher', - '@tanstack/start-server-functions-handler', - '@tanstack/start-server-functions-client', + '@tanstack/solid-start', + '@tanstack/solid-start/server', + '@tanstack/solid-start-client', + '@tanstack/solid-start-server', + '@tanstack/solid-start-server-functions-fetcher', + '@tanstack/solid-start-server-functions-handler', + '@tanstack/solid-start-server-functions-client', '@tanstack/start-server-functions-ssr', '@tanstack/start-server-functions-server', '@tanstack/start-router-manifest', - '@tanstack/start-config', + '@tanstack/solid-start-config', '@tanstack/start-api-routes', '@tanstack/server-functions-plugin', 'tsr:routes-manifest', From 224078957a2fc5d024332c1ca40abe90f785f31c Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 11:33:03 +0000 Subject: [PATCH 019/124] ci: apply automated fixes --- packages/solid-start-config/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/solid-start-config/src/index.ts b/packages/solid-start-config/src/index.ts index 85e1768a5c..d25b436d0d 100644 --- a/packages/solid-start-config/src/index.ts +++ b/packages/solid-start-config/src/index.ts @@ -156,7 +156,6 @@ export async function defineConfig( await dummyNitroApp.close() let vinxiApp = createApp({ - server: { ...serverOptions, preset: deploymentPreset, From 6f2a30d1ec3c342c3d2501dc37149f66ff14d140 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Mon, 24 Feb 2025 13:09:09 +0100 Subject: [PATCH 020/124] fix syntax --- e2e/solid-start/basic/app/client.tsx | 4 ++-- e2e/solid-start/basic/app/routes/not-found/index.tsx | 4 ++-- e2e/solid-start/basic/app/routes/posts.$postId.tsx | 6 +++--- e2e/solid-start/basic/app/routes/posts.tsx | 4 ++-- .../basic/app/routes/posts_.$postId.deep.tsx | 4 ++-- .../app/routes/redirect/$target/serverFn/index.tsx | 2 +- .../redirect/$target/serverFn/via-useServerFn.tsx | 10 +++++----- e2e/solid-start/basic/app/routes/users.$userId.tsx | 4 ++-- 8 files changed, 19 insertions(+), 19 deletions(-) diff --git a/e2e/solid-start/basic/app/client.tsx b/e2e/solid-start/basic/app/client.tsx index dd463c2622..8ab1e8854c 100644 --- a/e2e/solid-start/basic/app/client.tsx +++ b/e2e/solid-start/basic/app/client.tsx @@ -1,8 +1,8 @@ /// -import { hydrateRoot } from 'react-dom/client' import { StartClient } from '@tanstack/solid-start' import { createRouter } from './router' +import { render } from 'solid-js/web' const router = createRouter() -hydrateRoot(document, ) +render(()=> , document) diff --git a/e2e/solid-start/basic/app/routes/not-found/index.tsx b/e2e/solid-start/basic/app/routes/not-found/index.tsx index 2e247c5d83..34c8ef6146 100644 --- a/e2e/solid-start/basic/app/routes/not-found/index.tsx +++ b/e2e/solid-start/basic/app/routes/not-found/index.tsx @@ -9,7 +9,7 @@ export const Route = createFileRoute('/not-found/')({ via-beforeLoad @@ -19,7 +19,7 @@ export const Route = createFileRoute('/not-found/')({ via-loader diff --git a/e2e/solid-start/basic/app/routes/posts.$postId.tsx b/e2e/solid-start/basic/app/routes/posts.$postId.tsx index d736b82fea..6df924a588 100644 --- a/e2e/solid-start/basic/app/routes/posts.$postId.tsx +++ b/e2e/solid-start/basic/app/routes/posts.$postId.tsx @@ -22,12 +22,12 @@ function PostComponent() { return (
-

{post.title}

-
{post.body}
+

{post().title}

+
{post().body}
    - {[...posts, { id: 'i-do-not-exist', title: 'Non-existent Post' }].map( + {[...posts(), { id: 'i-do-not-exist', title: 'Non-existent Post' }].map( (post) => { return ( -
  • +
  • ← All Posts -

    {post.title}

    -
    {post.body}
    +

    {post().title}

    +
    {post().body}
) } diff --git a/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/index.tsx b/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/index.tsx index f2b4533d1a..510103e4c2 100644 --- a/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/index.tsx +++ b/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/index.tsx @@ -4,7 +4,7 @@ export const Route = createFileRoute('/redirect/$target/serverFn/')({ component: () => (

- redirect test with server functions (target {Route.useParams().target}) + redirect test with server functions (target {Route.useParams()().target})

{ - const { target } = Route.useParams() - const { reloadDocument, externalHost } = Route.useSearch() + const params = Route.useParams() + const search = Route.useSearch() return ( ) }, diff --git a/e2e/solid-start/basic/app/routes/users.$userId.tsx b/e2e/solid-start/basic/app/routes/users.$userId.tsx index 7dac80bdcf..4f4062326d 100644 --- a/e2e/solid-start/basic/app/routes/users.$userId.tsx +++ b/e2e/solid-start/basic/app/routes/users.$userId.tsx @@ -31,8 +31,8 @@ function UserComponent() { return (
-

{user.name}

-
{user.email}
+

{user().name}

+
{user().email}
) } From e775037d713422cfb807158390b9a349154315dd Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 12:10:21 +0000 Subject: [PATCH 021/124] ci: apply automated fixes --- e2e/solid-start/basic/app/client.tsx | 2 +- .../basic/app/routes/redirect/$target/serverFn/index.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/e2e/solid-start/basic/app/client.tsx b/e2e/solid-start/basic/app/client.tsx index 8ab1e8854c..993d626cdf 100644 --- a/e2e/solid-start/basic/app/client.tsx +++ b/e2e/solid-start/basic/app/client.tsx @@ -5,4 +5,4 @@ import { render } from 'solid-js/web' const router = createRouter() -render(()=> , document) +render(() => , document) diff --git a/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/index.tsx b/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/index.tsx index 510103e4c2..d404672372 100644 --- a/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/index.tsx +++ b/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/index.tsx @@ -4,7 +4,8 @@ export const Route = createFileRoute('/redirect/$target/serverFn/')({ component: () => (

- redirect test with server functions (target {Route.useParams()().target}) + redirect test with server functions (target {Route.useParams()().target} + )

Date: Mon, 24 Feb 2025 13:13:51 +0100 Subject: [PATCH 022/124] work on agnostic packages --- .../src/{index.tsx => index.ts} | 0 packages/solid-start-server-functions-fetcher/tsconfig.json | 1 - packages/solid-start-server-functions-fetcher/vite.config.ts | 2 +- .../src/{index.tsx => index.ts} | 0 packages/solid-start-server-functions-handler/tsconfig.json | 1 - packages/solid-start-server-functions-handler/vite.config.ts | 2 +- 6 files changed, 2 insertions(+), 4 deletions(-) rename packages/solid-start-server-functions-fetcher/src/{index.tsx => index.ts} (100%) rename packages/solid-start-server-functions-handler/src/{index.tsx => index.ts} (100%) diff --git a/packages/solid-start-server-functions-fetcher/src/index.tsx b/packages/solid-start-server-functions-fetcher/src/index.ts similarity index 100% rename from packages/solid-start-server-functions-fetcher/src/index.tsx rename to packages/solid-start-server-functions-fetcher/src/index.ts diff --git a/packages/solid-start-server-functions-fetcher/tsconfig.json b/packages/solid-start-server-functions-fetcher/tsconfig.json index 51dda9abf2..0484835e6b 100644 --- a/packages/solid-start-server-functions-fetcher/tsconfig.json +++ b/packages/solid-start-server-functions-fetcher/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "jsx": "react-jsx", "module": "esnext" }, "include": ["src", "vite.config.ts"] diff --git a/packages/solid-start-server-functions-fetcher/vite.config.ts b/packages/solid-start-server-functions-fetcher/vite.config.ts index d88917fad1..5c86d4f549 100644 --- a/packages/solid-start-server-functions-fetcher/vite.config.ts +++ b/packages/solid-start-server-functions-fetcher/vite.config.ts @@ -16,7 +16,7 @@ const config = defineConfig({ export default mergeConfig( config, tanstackViteConfig({ - entry: './src/index.tsx', + entry: './src/index.ts', srcDir: './src', }), ) diff --git a/packages/solid-start-server-functions-handler/src/index.tsx b/packages/solid-start-server-functions-handler/src/index.ts similarity index 100% rename from packages/solid-start-server-functions-handler/src/index.tsx rename to packages/solid-start-server-functions-handler/src/index.ts diff --git a/packages/solid-start-server-functions-handler/tsconfig.json b/packages/solid-start-server-functions-handler/tsconfig.json index 51dda9abf2..0484835e6b 100644 --- a/packages/solid-start-server-functions-handler/tsconfig.json +++ b/packages/solid-start-server-functions-handler/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "jsx": "react-jsx", "module": "esnext" }, "include": ["src", "vite.config.ts"] diff --git a/packages/solid-start-server-functions-handler/vite.config.ts b/packages/solid-start-server-functions-handler/vite.config.ts index 7be2947863..7a427b185d 100644 --- a/packages/solid-start-server-functions-handler/vite.config.ts +++ b/packages/solid-start-server-functions-handler/vite.config.ts @@ -16,7 +16,7 @@ const config = defineConfig({ export default mergeConfig( config, tanstackViteConfig({ - entry: './src/index.tsx', + entry: './src/index.ts', srcDir: './src', externalDeps: ['tsr:server-fn-manifest'], }), From 570d4f45dd6db46475b1cfdf056ef4c4f9c6babe Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Mon, 24 Feb 2025 14:07:32 +0100 Subject: [PATCH 023/124] add solid-start-api-routers and solid-start-server-functions-ssr --- e2e/solid-start/basic/app/routeTree.gen.ts | 161 ++++++++ examples/solid/start-bare/app.config.ts | 14 + examples/solid/start-bare/app/api.ts | 6 + examples/solid/start-bare/app/client.tsx | 8 + .../start-bare/app/components/Counter.css | 21 + .../start-bare/app/components/Counter.tsx | 15 + .../solid/start-bare/app/routeTree.gen.ts | 111 ++++++ examples/solid/start-bare/app/router.tsx | 20 + .../solid/start-bare/app/routes/__root.tsx | 39 ++ .../solid/start-bare/app/routes/about.tsx | 13 + .../solid/start-bare/app/routes/index.tsx | 14 + examples/solid/start-bare/app/ssr.tsx | 12 + examples/solid/start-bare/app/styles/app.css | 17 + examples/solid/start-bare/package.json | 29 ++ examples/solid/start-bare/public/favicon.ico | Bin 0 -> 15406 bytes examples/solid/start-bare/tailwind.config.mjs | 4 + examples/solid/start-bare/tsconfig.json | 23 ++ packages/solid-start-api-routes/README.md | 33 ++ .../solid-start-api-routes/eslint.config.js | 31 ++ packages/solid-start-api-routes/package.json | 72 ++++ packages/solid-start-api-routes/src/index.ts | 369 ++++++++++++++++++ packages/solid-start-api-routes/tsconfig.json | 8 + .../solid-start-api-routes/vite.config.ts | 21 + .../src/createMiddleware.ts | 2 +- packages/solid-start-config/src/index.ts | 6 +- .../README.md | 33 ++ .../eslint.config.js | 31 ++ .../package.json | 73 ++++ .../src/index.ts | 37 ++ .../tsconfig.json | 8 + .../tsconfigs/config.tsconfig.json | 10 + .../tsconfigs/router-manifest.tsconfig.json | 10 + .../vite.config.ts | 21 + .../src/defaultStreamHandler.tsx | 11 +- packages/solid-start/package.json | 4 +- packages/solid-start/src/api.tsx | 2 +- .../solid-start/src/server-functions-ssr.tsx | 2 +- packages/solid-start/vite.config.ts | 12 +- pnpm-lock.yaml | 68 +++- 39 files changed, 1337 insertions(+), 34 deletions(-) create mode 100644 examples/solid/start-bare/app.config.ts create mode 100644 examples/solid/start-bare/app/api.ts create mode 100644 examples/solid/start-bare/app/client.tsx create mode 100644 examples/solid/start-bare/app/components/Counter.css create mode 100644 examples/solid/start-bare/app/components/Counter.tsx create mode 100644 examples/solid/start-bare/app/routeTree.gen.ts create mode 100644 examples/solid/start-bare/app/router.tsx create mode 100644 examples/solid/start-bare/app/routes/__root.tsx create mode 100644 examples/solid/start-bare/app/routes/about.tsx create mode 100644 examples/solid/start-bare/app/routes/index.tsx create mode 100644 examples/solid/start-bare/app/ssr.tsx create mode 100644 examples/solid/start-bare/app/styles/app.css create mode 100644 examples/solid/start-bare/package.json create mode 100644 examples/solid/start-bare/public/favicon.ico create mode 100644 examples/solid/start-bare/tailwind.config.mjs create mode 100644 examples/solid/start-bare/tsconfig.json create mode 100644 packages/solid-start-api-routes/README.md create mode 100644 packages/solid-start-api-routes/eslint.config.js create mode 100644 packages/solid-start-api-routes/package.json create mode 100644 packages/solid-start-api-routes/src/index.ts create mode 100644 packages/solid-start-api-routes/tsconfig.json create mode 100644 packages/solid-start-api-routes/vite.config.ts create mode 100644 packages/solid-start-server-functions-ssr/README.md create mode 100644 packages/solid-start-server-functions-ssr/eslint.config.js create mode 100644 packages/solid-start-server-functions-ssr/package.json create mode 100644 packages/solid-start-server-functions-ssr/src/index.ts create mode 100644 packages/solid-start-server-functions-ssr/tsconfig.json create mode 100644 packages/solid-start-server-functions-ssr/tsconfigs/config.tsconfig.json create mode 100644 packages/solid-start-server-functions-ssr/tsconfigs/router-manifest.tsconfig.json create mode 100644 packages/solid-start-server-functions-ssr/vite.config.ts diff --git a/e2e/solid-start/basic/app/routeTree.gen.ts b/e2e/solid-start/basic/app/routeTree.gen.ts index a1391d211b..db7101d5f5 100644 --- a/e2e/solid-start/basic/app/routeTree.gen.ts +++ b/e2e/solid-start/basic/app/routeTree.gen.ts @@ -760,3 +760,164 @@ const rootRouteChildren: RootRouteChildren = { export const routeTree = rootRoute ._addFileChildren(rootRouteChildren) ._addFileTypes() + +/* ROUTE_MANIFEST_START +{ + "routes": { + "__root__": { + "filePath": "__root.tsx", + "children": [ + "/", + "/not-found", + "/_layout", + "/deferred", + "/links", + "/posts", + "/scripts", + "/search-params", + "/stream", + "/users", + "/redirect/$target", + "/redirect/", + "/posts_/$postId/deep" + ] + }, + "/": { + "filePath": "index.tsx" + }, + "/not-found": { + "filePath": "not-found/route.tsx", + "children": [ + "/not-found/via-beforeLoad", + "/not-found/via-loader", + "/not-found/" + ] + }, + "/_layout": { + "filePath": "_layout.tsx", + "children": [ + "/_layout/_layout-2" + ] + }, + "/deferred": { + "filePath": "deferred.tsx" + }, + "/links": { + "filePath": "links.tsx" + }, + "/posts": { + "filePath": "posts.tsx", + "children": [ + "/posts/$postId", + "/posts/" + ] + }, + "/scripts": { + "filePath": "scripts.tsx" + }, + "/search-params": { + "filePath": "search-params.tsx" + }, + "/stream": { + "filePath": "stream.tsx" + }, + "/users": { + "filePath": "users.tsx", + "children": [ + "/users/$userId", + "/users/" + ] + }, + "/_layout/_layout-2": { + "filePath": "_layout/_layout-2.tsx", + "parent": "/_layout", + "children": [ + "/_layout/_layout-2/layout-a", + "/_layout/_layout-2/layout-b" + ] + }, + "/not-found/via-beforeLoad": { + "filePath": "not-found/via-beforeLoad.tsx", + "parent": "/not-found" + }, + "/not-found/via-loader": { + "filePath": "not-found/via-loader.tsx", + "parent": "/not-found" + }, + "/posts/$postId": { + "filePath": "posts.$postId.tsx", + "parent": "/posts" + }, + "/redirect/$target": { + "filePath": "redirect/$target.tsx", + "children": [ + "/redirect/$target/via-beforeLoad", + "/redirect/$target/via-loader", + "/redirect/$target/", + "/redirect/$target/serverFn/via-beforeLoad", + "/redirect/$target/serverFn/via-loader", + "/redirect/$target/serverFn/via-useServerFn", + "/redirect/$target/serverFn/" + ] + }, + "/users/$userId": { + "filePath": "users.$userId.tsx", + "parent": "/users" + }, + "/not-found/": { + "filePath": "not-found/index.tsx", + "parent": "/not-found" + }, + "/posts/": { + "filePath": "posts.index.tsx", + "parent": "/posts" + }, + "/redirect/": { + "filePath": "redirect/index.tsx" + }, + "/users/": { + "filePath": "users.index.tsx", + "parent": "/users" + }, + "/_layout/_layout-2/layout-a": { + "filePath": "_layout/_layout-2/layout-a.tsx", + "parent": "/_layout/_layout-2" + }, + "/_layout/_layout-2/layout-b": { + "filePath": "_layout/_layout-2/layout-b.tsx", + "parent": "/_layout/_layout-2" + }, + "/posts_/$postId/deep": { + "filePath": "posts_.$postId.deep.tsx" + }, + "/redirect/$target/via-beforeLoad": { + "filePath": "redirect/$target/via-beforeLoad.tsx", + "parent": "/redirect/$target" + }, + "/redirect/$target/via-loader": { + "filePath": "redirect/$target/via-loader.tsx", + "parent": "/redirect/$target" + }, + "/redirect/$target/": { + "filePath": "redirect/$target/index.tsx", + "parent": "/redirect/$target" + }, + "/redirect/$target/serverFn/via-beforeLoad": { + "filePath": "redirect/$target/serverFn/via-beforeLoad.tsx", + "parent": "/redirect/$target" + }, + "/redirect/$target/serverFn/via-loader": { + "filePath": "redirect/$target/serverFn/via-loader.tsx", + "parent": "/redirect/$target" + }, + "/redirect/$target/serverFn/via-useServerFn": { + "filePath": "redirect/$target/serverFn/via-useServerFn.tsx", + "parent": "/redirect/$target" + }, + "/redirect/$target/serverFn/": { + "filePath": "redirect/$target/serverFn/index.tsx", + "parent": "/redirect/$target" + } + } +} +ROUTE_MANIFEST_END */ diff --git a/examples/solid/start-bare/app.config.ts b/examples/solid/start-bare/app.config.ts new file mode 100644 index 0000000000..cb3e4af661 --- /dev/null +++ b/examples/solid/start-bare/app.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from '@tanstack/solid-start/config' +import tsConfigPaths from 'vite-tsconfig-paths' +import tailwindcss from '@tailwindcss/vite' + +export default defineConfig({ + vite: { + plugins: [ + tsConfigPaths({ + projects: ['./tsconfig.json'], + }), + tailwindcss(), + ], + }, +}) diff --git a/examples/solid/start-bare/app/api.ts b/examples/solid/start-bare/app/api.ts new file mode 100644 index 0000000000..ed511bcd26 --- /dev/null +++ b/examples/solid/start-bare/app/api.ts @@ -0,0 +1,6 @@ +import { + createStartAPIHandler, + defaultAPIFileRouteHandler, +} from '@tanstack/solid-start/api' + +export default createStartAPIHandler(defaultAPIFileRouteHandler) diff --git a/examples/solid/start-bare/app/client.tsx b/examples/solid/start-bare/app/client.tsx new file mode 100644 index 0000000000..993d626cdf --- /dev/null +++ b/examples/solid/start-bare/app/client.tsx @@ -0,0 +1,8 @@ +/// +import { StartClient } from '@tanstack/solid-start' +import { createRouter } from './router' +import { render } from 'solid-js/web' + +const router = createRouter() + +render(() => , document) diff --git a/examples/solid/start-bare/app/components/Counter.css b/examples/solid/start-bare/app/components/Counter.css new file mode 100644 index 0000000000..308965720f --- /dev/null +++ b/examples/solid/start-bare/app/components/Counter.css @@ -0,0 +1,21 @@ +.increment { + font-family: inherit; + font-size: inherit; + padding: 1em 2em; + color: #335d92; + background-color: rgba(68, 107, 158, 0.1); + border-radius: 2em; + border: 2px solid rgba(68, 107, 158, 0); + outline: none; + width: 200px; + font-variant-numeric: tabular-nums; + cursor: pointer; +} + +.increment:focus { + border: 2px solid #335d92; +} + +.increment:active { + background-color: rgba(68, 107, 158, 0.2); +} diff --git a/examples/solid/start-bare/app/components/Counter.tsx b/examples/solid/start-bare/app/components/Counter.tsx new file mode 100644 index 0000000000..fc3e5120cd --- /dev/null +++ b/examples/solid/start-bare/app/components/Counter.tsx @@ -0,0 +1,15 @@ +import { createSignal } from 'solid-js' +import './Counter.css' + +export default function Counter() { + const [count, setCount] = createSignal(0) + return ( + + ) +} diff --git a/examples/solid/start-bare/app/routeTree.gen.ts b/examples/solid/start-bare/app/routeTree.gen.ts new file mode 100644 index 0000000000..cb367c516c --- /dev/null +++ b/examples/solid/start-bare/app/routeTree.gen.ts @@ -0,0 +1,111 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +// Import Routes + +import { Route as rootRoute } from './routes/__root' +import { Route as AboutImport } from './routes/about' +import { Route as IndexImport } from './routes/index' + +// Create/Update Routes + +const AboutRoute = AboutImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => rootRoute, +} as any) + +const IndexRoute = IndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRoute, +} as any) + +// Populate the FileRoutesByPath interface + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexImport + parentRoute: typeof rootRoute + } + '/about': { + id: '/about' + path: '/about' + fullPath: '/about' + preLoaderRoute: typeof AboutImport + parentRoute: typeof rootRoute + } + } +} + +// Create and export the route tree + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/about': typeof AboutRoute +} + +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/about': typeof AboutRoute +} + +export interface FileRoutesById { + __root__: typeof rootRoute + '/': typeof IndexRoute + '/about': typeof AboutRoute +} + +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/about' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/about' + id: '__root__' | '/' | '/about' + fileRoutesById: FileRoutesById +} + +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AboutRoute: typeof AboutRoute +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + AboutRoute: AboutRoute, +} + +export const routeTree = rootRoute + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +/* ROUTE_MANIFEST_START +{ + "routes": { + "__root__": { + "filePath": "__root.tsx", + "children": [ + "/", + "/about" + ] + }, + "/": { + "filePath": "index.tsx" + }, + "/about": { + "filePath": "about.tsx" + } + } +} +ROUTE_MANIFEST_END */ diff --git a/examples/solid/start-bare/app/router.tsx b/examples/solid/start-bare/app/router.tsx new file mode 100644 index 0000000000..a8d9cb45e7 --- /dev/null +++ b/examples/solid/start-bare/app/router.tsx @@ -0,0 +1,20 @@ +import { createRouter as createTanStackRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createRouter() { + const router = createTanStackRouter({ + routeTree, + defaultPreload: 'intent', + defaultErrorComponent: (err) =>

{err.error.stack}

, + defaultNotFoundComponent: () =>

not found

, + scrollRestoration: true, + }) + + return router +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/examples/solid/start-bare/app/routes/__root.tsx b/examples/solid/start-bare/app/routes/__root.tsx new file mode 100644 index 0000000000..dae225f9f5 --- /dev/null +++ b/examples/solid/start-bare/app/routes/__root.tsx @@ -0,0 +1,39 @@ +import { + createRootRoute, + HeadContent, + Link, + Outlet, + Scripts, +} from '@tanstack/solid-router' +import * as Solid from 'solid-js' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + ) +} + +function RootDocument({ children }: { children: Solid.JSX.Element }) { + return ( + + + + + +
+ Index + About +
+ + {children} + + + + ) +} diff --git a/examples/solid/start-bare/app/routes/about.tsx b/examples/solid/start-bare/app/routes/about.tsx new file mode 100644 index 0000000000..d66c7db828 --- /dev/null +++ b/examples/solid/start-bare/app/routes/about.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/about')({ + component: RouteComponent, +}) + +function RouteComponent() { + return ( +
+

About

+
+ ) +} diff --git a/examples/solid/start-bare/app/routes/index.tsx b/examples/solid/start-bare/app/routes/index.tsx new file mode 100644 index 0000000000..6a91bd1999 --- /dev/null +++ b/examples/solid/start-bare/app/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/solid-router' +import Counter from '~/components/Counter' +export const Route = createFileRoute('/')({ + component: RouteComponent, +}) + +function RouteComponent() { + return ( +
+

Hello world!

+ +
+ ) +} diff --git a/examples/solid/start-bare/app/ssr.tsx b/examples/solid/start-bare/app/ssr.tsx new file mode 100644 index 0000000000..6d10bea05f --- /dev/null +++ b/examples/solid/start-bare/app/ssr.tsx @@ -0,0 +1,12 @@ +import { + createStartHandler, + defaultStreamHandler, +} from '@tanstack/solid-start/server' +import { getRouterManifest } from '@tanstack/solid-start/router-manifest' + +import { createRouter } from './router' + +export default createStartHandler({ + createRouter, + getRouterManifest, +})(defaultStreamHandler) diff --git a/examples/solid/start-bare/app/styles/app.css b/examples/solid/start-bare/app/styles/app.css new file mode 100644 index 0000000000..43d97fa7a7 --- /dev/null +++ b/examples/solid/start-bare/app/styles/app.css @@ -0,0 +1,17 @@ +@import 'tailwindcss'; + +body { + font-family: + Gordita, Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', + sans-serif; +} + +a { + margin-right: 1rem; +} + +main { + text-align: center; + padding: 1em; + margin: 0 auto; +} diff --git a/examples/solid/start-bare/package.json b/examples/solid/start-bare/package.json new file mode 100644 index 0000000000..4e2bb3dbac --- /dev/null +++ b/examples/solid/start-bare/package.json @@ -0,0 +1,29 @@ +{ + "name": "tanstack-solid-start-example-bare", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev": "vinxi dev", + "build": "vinxi build", + "start": "vinxi start" + }, + "dependencies": { + "@tanstack/solid-router": "workspace:^", + "@tanstack/solid-start": "workspace:^", + "solid-js": "^1.0.0", + "redaxios": "^0.5.1", + "tailwind-merge": "^2.6.0", + "vinxi": "0.5.3", + "zod": "^3.24.1" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.8", + "@types/node": "^22.10.2", + "vite-plugin-solid": "^2.11.2", + "combinate": "^1.1.11", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.2", + "vite-tsconfig-paths": "^5.1.4" + } +} diff --git a/examples/solid/start-bare/public/favicon.ico b/examples/solid/start-bare/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1a1751676f7e22811b1070572093996c93c87617 GIT binary patch literal 15406 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 literal 0 HcmV?d00001 diff --git a/examples/solid/start-bare/tailwind.config.mjs b/examples/solid/start-bare/tailwind.config.mjs new file mode 100644 index 0000000000..07c3598bac --- /dev/null +++ b/examples/solid/start-bare/tailwind.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./app/**/*.{js,jsx,ts,tsx}'], +} diff --git a/examples/solid/start-bare/tsconfig.json b/examples/solid/start-bare/tsconfig.json new file mode 100644 index 0000000000..73e4856648 --- /dev/null +++ b/examples/solid/start-bare/tsconfig.json @@ -0,0 +1,23 @@ +{ + "include": ["**/*.ts", "**/*.tsx", "public/script*.js"], + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "isolatedModules": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "target": "ES2022", + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, + "noEmit": true + } +} diff --git a/packages/solid-start-api-routes/README.md b/packages/solid-start-api-routes/README.md new file mode 100644 index 0000000000..bb009b0c87 --- /dev/null +++ b/packages/solid-start-api-routes/README.md @@ -0,0 +1,33 @@ +> 🤫 we're cooking up something special! + + + +# TanStack Start + +![TanStack Router Header](https://github.com/tanstack/router/raw/main/media/header.png) + +🤖 Type-safe router w/ built-in caching & URL state management for React! + + + #TanStack + + + + + + + + semantic-release + + Join the discussion on Github +Best of JS + + + + + + + +Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [React Query](https://github.com/tannerlinsley/react-query), [React Table](https://github.com/tanstack/react-table), [React Charts](https://github.com/tannerlinsley/react-charts), [React Virtual](https://github.com/tannerlinsley/react-virtual) + +## Visit [tanstack.com/router](https://tanstack.com/router) for docs, guides, API and more! diff --git a/packages/solid-start-api-routes/eslint.config.js b/packages/solid-start-api-routes/eslint.config.js new file mode 100644 index 0000000000..931f0ec774 --- /dev/null +++ b/packages/solid-start-api-routes/eslint.config.js @@ -0,0 +1,31 @@ +// @ts-check + +import pluginReact from '@eslint-react/eslint-plugin' +import pluginReactHooks from 'eslint-plugin-react-hooks' +import rootConfig from '../../eslint.config.js' + +export default [ + ...rootConfig, + { + ...pluginReact.configs.recommended, + files: ['**/*.{ts,tsx}'], + }, + { + plugins: { + 'react-hooks': pluginReactHooks, + }, + rules: { + '@eslint-react/no-unstable-context-value': 'off', + '@eslint-react/no-unstable-default-props': 'off', + '@eslint-react/dom/no-missing-button-type': 'off', + 'react-hooks/exhaustive-deps': 'error', + 'react-hooks/rules-of-hooks': 'error', + }, + }, + { + files: ['**/__tests__/**'], + rules: { + '@typescript-eslint/no-unnecessary-condition': 'off', + }, + }, +] diff --git a/packages/solid-start-api-routes/package.json b/packages/solid-start-api-routes/package.json new file mode 100644 index 0000000000..554e6e59dc --- /dev/null +++ b/packages/solid-start-api-routes/package.json @@ -0,0 +1,72 @@ +{ + "name": "@tanstack/solid-start-api-routes", + "version": "1.111.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" + }, + "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": "exit 0; 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", + "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/router-core": "workspace:^", + "@tanstack/solid-start-server": "workspace:^", + "vinxi": "0.5.3" + }, + "devDependencies": { + "typescript": "^5.7.2" + } +} diff --git a/packages/solid-start-api-routes/src/index.ts b/packages/solid-start-api-routes/src/index.ts new file mode 100644 index 0000000000..46b5d3db43 --- /dev/null +++ b/packages/solid-start-api-routes/src/index.ts @@ -0,0 +1,369 @@ +import { eventHandler, toWebRequest } from '@tanstack/solid-start-server' +import vinxiFileRoutes from 'vinxi/routes' +import type { ResolveParams } from '@tanstack/router-core' + +export type StartAPIHandlerCallback = (ctx: { + request: Request +}) => Response | Promise + +export type StartAPIMethodCallback = (ctx: { + request: Request + params: ResolveParams +}) => Response | Promise + +const HTTP_API_METHODS = [ + 'GET', + 'POST', + 'PUT', + 'PATCH', + 'DELETE', + 'OPTIONS', + 'HEAD', +] as const +export type HTTP_API_METHOD = (typeof HTTP_API_METHODS)[number] + +/** + * + * @param cb The callback function that will be called when the API handler is invoked + * @returns The response from the callback function + */ +export function createStartAPIHandler(cb: StartAPIHandlerCallback) { + return eventHandler(async (event) => { + const request = toWebRequest(event)! + const res = await cb({ request }) + return res + }) +} + +type APIRoute = { + path: TPath + methods: Partial>> +} + +type CreateAPIRouteFn = ( + methods: Partial>>, +) => APIRoute + +type CreateAPIRoute = ( + path: TPath, +) => CreateAPIRouteFn + +type APIRouteReturnType = ReturnType> + +/** + * This function is used to create an API route that will be listening on a specific path when you are not using the file-based routes. + * + * @param path The path that the API route will be listening on. You need to make sure that this is a valid TanStack Router path in order for the route to be matched. This means that you can use the following syntax: + * /api/foo/$bar/name/$ + * - The `$bar` is a parameter that will be extracted from the URL and passed to the handler + * - The `$` is a wildcard that will match any number of segments in the URL + * @returns A function that takes the methods that the route will be listening on and returns the API route object + */ +export const createAPIRoute: CreateAPIRoute = (path) => (methods) => ({ + path, + methods, +}) + +/** + * This function is used to create an API route that will be listening on a specific path when you are using the file-based routes. + * + * @param filePath The path that the API file route will be listening on. This filePath should automatically be generated by the TSR plugin and should be a valid TanStack Router path + * @returns A function that takes the methods that the route will be listening on and returns the API route object + */ +export const createAPIFileRoute: CreateAPIRoute = (filePath) => (methods) => ({ + path: filePath, + methods, +}) + +/** + * This function takes a URL object and a list of routes and finds the route that matches the URL. + * + * @param url URL object + * @param entryRoutes List of routes entries in the TSR format to find the current match by the URL + * @returns Returns the route that matches the URL or undefined if no route matches + */ +function findRoute( + url: URL, + entryRoutes: Array<{ routePath: string; payload: TPayload }>, +): + | { + routePath: string + params: Record + payload: TPayload + } + | undefined { + const urlSegments = url.pathname.split('/').filter(Boolean) + + const routes = entryRoutes + .sort((a, b) => { + const aParts = a.routePath.split('/').filter(Boolean) + const bParts = b.routePath.split('/').filter(Boolean) + + return bParts.length - aParts.length + }) + .filter((r) => { + const routeSegments = r.routePath.split('/').filter(Boolean) + return urlSegments.length >= routeSegments.length + }) + + for (const route of routes) { + const routeSegments = route.routePath.split('/').filter(Boolean) + const params: Record = {} + let matches = true + for (let i = 0; i < routeSegments.length; i++) { + const routeSegment = routeSegments[i] as string + const urlSegment = urlSegments[i] as string + if (routeSegment.startsWith('$')) { + if (routeSegment === '$') { + const wildcardValue = urlSegments.slice(i).join('/') + if (wildcardValue !== '') { + params['*'] = wildcardValue + params['_splat'] = wildcardValue + } else { + matches = false + break + } + } else { + const paramName = routeSegment.slice(1) + params[paramName] = urlSegment + } + } else if (routeSegment !== urlSegment) { + matches = false + break + } + } + if (matches) { + return { routePath: route.routePath, params, payload: route.payload } + } + } + + return undefined +} + +/** + * You should only be using this function if you are not using the file-based routes. + * + * + * @param opts - A map of TSR routes with the values being the route handlers + * @returns The handler for the incoming request + * + * @example + * ```ts + * // app/foo.ts + * import { createAPIRoute } from '@tanstack/start-api-routes' + * const fooBarRoute = createAPIRoute('/api/foo/$bar')({ + * GET: ({ params }) => { + * return new Response(JSON.stringify({ params })) + * } + * }) + * + * // app/api.ts + * import { + * createStartAPIHandler, + * defaultAPIRoutesHandler + * } from '@tanstack/start-api-routes' + * + * export default createStartAPIHandler( + * defaultAPIRoutesHandler({ + * '/api/foo/$bar': fooBarRoute + * }) + * ) + * ``` + */ +export const defaultAPIRoutesHandler: (opts: { + routes: { [TPath in string]: APIRoute } +}) => StartAPIHandlerCallback = (opts) => { + return async ({ request }) => { + if (!HTTP_API_METHODS.includes(request.method as HTTP_API_METHOD)) { + return new Response('Method not allowed', { status: 405 }) + } + + const url = new URL(request.url, 'http://localhost:3000') + + const routes = Object.entries(opts.routes).map(([routePath, route]) => ({ + routePath, + payload: route, + })) + + // Find the route that matches the request by the request URL + const match = findRoute(url, routes) + + // If we don't have a route that could possibly handle the request, return a 404 + if (!match) { + return new Response('Not found', { status: 404 }) + } + + // If the route path doesn't match the payload path, return a 404 + if (match.routePath !== match.payload.path) { + console.error( + `Route path mismatch: ${match.routePath} !== ${match.payload.path}. Please make sure that the route path in \`createAPIRoute\` matches the path in the handler map in \`defaultAPIRoutesHandler\``, + ) + return new Response('Not found', { status: 404 }) + } + + const method = request.method as HTTP_API_METHOD + + // Get the handler for the request method based on the Request Method + const handler = match.payload.methods[method] + + // If the handler is not defined, return a 405 + if (!handler) { + return new Response('Method not allowed', { status: 405 }) + } + + return await handler({ request, params: match.params }) + } +} + +interface CustomizedVinxiFileRoute { + path: string // this path adheres to the h3 router path format + filePath: string // this is the file path on the system + $APIRoute?: { + src: string // this is the path to the source file + import: () => Promise<{ + APIRoute: APIRouteReturnType + }> + } +} + +/** + * This is populated by the work done in the config file using the tsrFileRouter + */ +const vinxiRoutes = ( + vinxiFileRoutes as unknown as Array +).filter((route) => route['$APIRoute']) + +/** + * This function takes the vinxi routes and interpolates them into a format that can be worked with in the API handler + * + * @param routes The vinxi routes that have been filtered to only include those with a $APIRoute property + * @returns An array of objects where the path `key` is interpolated to a valid TanStack Router path, with the `payload` being the original route object + * + * @example + * ``` + * const input = [ + * { + * path: '/api/boo/:$id?/name/*splat', + * filePath: '..../code/tanstack/router/examples/react/start-basic/app/routes/api.boo.$id.name.$.tsx', + * '$APIRoute': [Object] + * } + * ] + * + * toTSRFileBasedRoutes(input) + * [ + * { + * path: '/api/boo/$id/name/$', + * route: { + * path: '/api/boo/:$id?/name/*splat', + * filePath: '..../code/tanstack/router/examples/react/start-basic/app/routes/api.boo.$id.name.$.tsx', + * '$APIRoute': [Object] + * } + * } + * ] + * ``` + */ +function toTSRFileBasedRoutes( + routes: Array, +): Array<{ routePath: string; payload: CustomizedVinxiFileRoute }> { + const pairs: Array<{ + routePath: string + payload: CustomizedVinxiFileRoute + }> = [] + + routes.forEach((route) => { + const parts = route.path.split('/').filter(Boolean) + + const path = parts + .map((part) => { + if (part === '*splat') { + return '$' + } + + if (part.startsWith(':$') && part.endsWith('?')) { + return part.slice(1, -1) + } + + return part + }) + .join('/') + + pairs.push({ routePath: `/${path}`, payload: route }) + }) + + return pairs +} + +/** + * This function is the default handler for the API routes when using file-based routes. + * + * @param StartAPIHandlerCallbackContext + * @returns The handler for the incoming request + * + * @example + * ```ts + * // app/api.ts + * import { + * createStartAPIHandler, + * defaultAPIFileRouteHandler + * } from '@tanstack/start-api-routes' + * + * export default createStartAPIHandler(defaultAPIFileRouteHandler) + * ``` + */ +export const defaultAPIFileRouteHandler: StartAPIHandlerCallback = async ({ + request, +}) => { + // Simple early abort if there are no routes + if (!vinxiRoutes.length) { + return new Response('No routes found', { status: 404 }) + } + + if (!HTTP_API_METHODS.includes(request.method as HTTP_API_METHOD)) { + return new Response('Method not allowed', { status: 405 }) + } + + const routes = toTSRFileBasedRoutes(vinxiRoutes) + + const url = new URL(request.url, 'http://localhost:3000') + + // Find the route that file that matches the request by the request URL + const match = findRoute(url, routes) + + // If we don't have a route that could possibly handle the request, return a 404 + if (!match) { + return new Response('Not found', { status: 404 }) + } + + // The action is the route file that we need to import + // which contains the possible handlers for the incoming request + let action: APIRouteReturnType | undefined = undefined + + try { + // We can guarantee that action is defined since we filtered for it earlier + action = await match.payload.$APIRoute!.import().then((m) => m.APIRoute) + } catch (err) { + // If we can't import the route file, return a 500 + console.error('Error importing route file:', err) + return new Response('Internal server error', { status: 500 }) + } + + // If we don't have an action, return a 500 + if (!action) { + return new Response('Internal server error', { status: 500 }) + } + + const method = request.method as HTTP_API_METHOD + + // Get the handler for the request method based on the Request Method + const handler = action.methods[method] + + // If the handler is not defined, return a 405 + // What this means is that we have a route that matches the request + // but we don't have a handler for the request method + // i.e we have a route that matches /api/foo/$ but we don't have a POST handler + if (!handler) { + return new Response('Method not allowed', { status: 405 }) + } + + return await handler({ request, params: match.params }) +} diff --git a/packages/solid-start-api-routes/tsconfig.json b/packages/solid-start-api-routes/tsconfig.json new file mode 100644 index 0000000000..51dda9abf2 --- /dev/null +++ b/packages/solid-start-api-routes/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "module": "esnext" + }, + "include": ["src", "vite.config.ts"] +} diff --git a/packages/solid-start-api-routes/vite.config.ts b/packages/solid-start-api-routes/vite.config.ts new file mode 100644 index 0000000000..976bb5c87f --- /dev/null +++ b/packages/solid-start-api-routes/vite.config.ts @@ -0,0 +1,21 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import { tanstackViteConfig } from '@tanstack/config/vite' +import packageJson from './package.json' +import type { ViteUserConfig } from 'vitest/config' + +const config = defineConfig({ + plugins: [] as ViteUserConfig['plugins'], + test: { + name: packageJson.name, + watch: false, + environment: 'jsdom', + }, +}) + +export default mergeConfig( + config, + tanstackViteConfig({ + entry: './src/index.ts', + srcDir: './src', + }), +) diff --git a/packages/solid-start-client/src/createMiddleware.ts b/packages/solid-start-client/src/createMiddleware.ts index 0a09412dc1..34c4920597 100644 --- a/packages/solid-start-client/src/createMiddleware.ts +++ b/packages/solid-start-client/src/createMiddleware.ts @@ -11,7 +11,7 @@ import type { ResolveValidatorInput, ResolveValidatorOutput, SerializerStringify, -} from '@tanstack/solid-router' +} from '@tanstack/router-core' export type AssignAllMiddleware< TMiddlewares, diff --git a/packages/solid-start-config/src/index.ts b/packages/solid-start-config/src/index.ts index d25b436d0d..ce6aa770f2 100644 --- a/packages/solid-start-config/src/index.ts +++ b/packages/solid-start-config/src/index.ts @@ -295,7 +295,7 @@ export async function defineConfig( }), ...(getUserViteConfig(opts.vite).plugins || []), ...(getUserViteConfig(opts.routers?.ssr?.vite).plugins || []), - viteSolid(opts.solid), + viteSolid({ ...opts.solid, ssr: true }), ] }, }, @@ -388,11 +388,11 @@ export async function defineConfig( '@tanstack/solid-start-server-functions-fetcher', '@tanstack/solid-start-server-functions-handler', '@tanstack/solid-start-server-functions-client', - '@tanstack/start-server-functions-ssr', + '@tanstack/solid-start-server-functions-ssr', '@tanstack/start-server-functions-server', '@tanstack/start-router-manifest', '@tanstack/solid-start-config', - '@tanstack/start-api-routes', + '@tanstack/solid-start-api-routes', '@tanstack/server-functions-plugin', 'tsr:routes-manifest', 'tsr:server-fn-manifest', diff --git a/packages/solid-start-server-functions-ssr/README.md b/packages/solid-start-server-functions-ssr/README.md new file mode 100644 index 0000000000..bb009b0c87 --- /dev/null +++ b/packages/solid-start-server-functions-ssr/README.md @@ -0,0 +1,33 @@ +> 🤫 we're cooking up something special! + + + +# TanStack Start + +![TanStack Router Header](https://github.com/tanstack/router/raw/main/media/header.png) + +🤖 Type-safe router w/ built-in caching & URL state management for React! + + + #TanStack + + + + + + + + semantic-release + + Join the discussion on Github +Best of JS + + + + + + + +Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [React Query](https://github.com/tannerlinsley/react-query), [React Table](https://github.com/tanstack/react-table), [React Charts](https://github.com/tannerlinsley/react-charts), [React Virtual](https://github.com/tannerlinsley/react-virtual) + +## Visit [tanstack.com/router](https://tanstack.com/router) for docs, guides, API and more! diff --git a/packages/solid-start-server-functions-ssr/eslint.config.js b/packages/solid-start-server-functions-ssr/eslint.config.js new file mode 100644 index 0000000000..931f0ec774 --- /dev/null +++ b/packages/solid-start-server-functions-ssr/eslint.config.js @@ -0,0 +1,31 @@ +// @ts-check + +import pluginReact from '@eslint-react/eslint-plugin' +import pluginReactHooks from 'eslint-plugin-react-hooks' +import rootConfig from '../../eslint.config.js' + +export default [ + ...rootConfig, + { + ...pluginReact.configs.recommended, + files: ['**/*.{ts,tsx}'], + }, + { + plugins: { + 'react-hooks': pluginReactHooks, + }, + rules: { + '@eslint-react/no-unstable-context-value': 'off', + '@eslint-react/no-unstable-default-props': 'off', + '@eslint-react/dom/no-missing-button-type': 'off', + 'react-hooks/exhaustive-deps': 'error', + 'react-hooks/rules-of-hooks': 'error', + }, + }, + { + files: ['**/__tests__/**'], + rules: { + '@typescript-eslint/no-unnecessary-condition': 'off', + }, + }, +] diff --git a/packages/solid-start-server-functions-ssr/package.json b/packages/solid-start-server-functions-ssr/package.json new file mode 100644 index 0000000000..dec723ad3a --- /dev/null +++ b/packages/solid-start-server-functions-ssr/package.json @@ -0,0 +1,73 @@ +{ + "name": "@tanstack/solid-start-server-functions-ssr", + "version": "1.111.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" + }, + "homepage": "https://tanstack.com/start", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "keywords": [ + "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": "exit 0; 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", + "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/server-functions-plugin": "workspace:^", + "@tanstack/solid-start-client": "workspace:^", + "@tanstack/solid-start-server": "workspace:^", + "@tanstack/solid-start-server-functions-fetcher": "workspace:^", + "tiny-invariant": "^1.3.3" + }, + "devDependencies": { + "typescript": "^5.7.2" + } +} diff --git a/packages/solid-start-server-functions-ssr/src/index.ts b/packages/solid-start-server-functions-ssr/src/index.ts new file mode 100644 index 0000000000..f3eeea30c4 --- /dev/null +++ b/packages/solid-start-server-functions-ssr/src/index.ts @@ -0,0 +1,37 @@ +/// +import { serverFnFetcher } from '@tanstack/solid-start-server-functions-fetcher' +import { mergeHeaders } from '@tanstack/solid-start-client' +import { getEvent, getHeaders } from '@tanstack/solid-start-server' +import type { CreateRpcFn } from '@tanstack/server-functions-plugin' + +function sanitizeBase(base: string) { + return base.replace(/^\/|\/$/g, '') +} + +export const createSsrRpc: CreateRpcFn = (functionId, serverBase) => { + const url = `/${sanitizeBase(serverBase)}/${functionId}` + + const ssrFn = (...args: Array) => { + return serverFnFetcher(url, args, async (url, requestInit) => { + // pass on the headers from the document request to the server function fetch + requestInit.headers = mergeHeaders(getHeaders(), requestInit.headers) + // @ts-expect-error The $fetch.native method is not typed yet + const res: Response = await $fetch.native(url, requestInit) + const event = getEvent() + const mergedHeaders = mergeHeaders( + res.headers, + (event as any).___ssrRpcResponseHeaders, + ) + + // any response headers set in the server function need to be set on the document response + // we attach the headers to the event so we can later set them + ;(event as any).___ssrRpcResponseHeaders = mergedHeaders + return res + }) + } + + return Object.assign(ssrFn, { + url, + functionId, + }) +} diff --git a/packages/solid-start-server-functions-ssr/tsconfig.json b/packages/solid-start-server-functions-ssr/tsconfig.json new file mode 100644 index 0000000000..51dda9abf2 --- /dev/null +++ b/packages/solid-start-server-functions-ssr/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "module": "esnext" + }, + "include": ["src", "vite.config.ts"] +} diff --git a/packages/solid-start-server-functions-ssr/tsconfigs/config.tsconfig.json b/packages/solid-start-server-functions-ssr/tsconfigs/config.tsconfig.json new file mode 100644 index 0000000000..58fc33fb0a --- /dev/null +++ b/packages/solid-start-server-functions-ssr/tsconfigs/config.tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.json", + "include": ["../src/config/index.ts"], + "compilerOptions": { + "rootDir": "../src/config", + "outDir": "../dist/esm/config", + "target": "esnext", + "noEmit": false + } +} diff --git a/packages/solid-start-server-functions-ssr/tsconfigs/router-manifest.tsconfig.json b/packages/solid-start-server-functions-ssr/tsconfigs/router-manifest.tsconfig.json new file mode 100644 index 0000000000..cc108d2494 --- /dev/null +++ b/packages/solid-start-server-functions-ssr/tsconfigs/router-manifest.tsconfig.json @@ -0,0 +1,10 @@ +{ + "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/solid-start-server-functions-ssr/vite.config.ts b/packages/solid-start-server-functions-ssr/vite.config.ts new file mode 100644 index 0000000000..976bb5c87f --- /dev/null +++ b/packages/solid-start-server-functions-ssr/vite.config.ts @@ -0,0 +1,21 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import { tanstackViteConfig } from '@tanstack/config/vite' +import packageJson from './package.json' +import type { ViteUserConfig } from 'vitest/config' + +const config = defineConfig({ + plugins: [] as ViteUserConfig['plugins'], + test: { + name: packageJson.name, + watch: false, + environment: 'jsdom', + }, +}) + +export default mergeConfig( + config, + tanstackViteConfig({ + entry: './src/index.ts', + srcDir: './src', + }), +) diff --git a/packages/solid-start-server/src/defaultStreamHandler.tsx b/packages/solid-start-server/src/defaultStreamHandler.tsx index a4d2fefef7..e11de7ee96 100644 --- a/packages/solid-start-server/src/defaultStreamHandler.tsx +++ b/packages/solid-start-server/src/defaultStreamHandler.tsx @@ -13,15 +13,16 @@ import { defineHandlerCallback } from './handlerCallback' import type { ReadableStream } from 'node:stream/web' export const defaultStreamHandler = defineHandlerCallback( - async ({ request, router, responseHeaders }) => { + ({ request, router, responseHeaders }) => { if (typeof Solid.renderToStream === 'function') { - const stream = await Solid.renderToStream(() => ( - - )) + const stream = Solid.renderToStream(() => ) + + const { writable, readable } = new TransformStream() + stream.pipeTo(writable) const responseStream = transformReadableStreamWithRouter( router, - stream as unknown as ReadableStream, + readable as unknown as ReadableStream, ) return new Response(responseStream as any, { status: router.state.statusCode, diff --git a/packages/solid-start/package.json b/packages/solid-start/package.json index b86fe01c1b..49eb342689 100644 --- a/packages/solid-start/package.json +++ b/packages/solid-start/package.json @@ -150,8 +150,8 @@ "@tanstack/solid-start-server-functions-client": "workspace:^", "@tanstack/start-server-functions-server": "workspace:^", "@tanstack/solid-start-server-functions-handler": "workspace:^", - "@tanstack/start-server-functions-ssr": "workspace:^", - "@tanstack/start-api-routes": "workspace:^" + "@tanstack/solid-start-server-functions-ssr": "workspace:^", + "@tanstack/solid-start-api-routes": "workspace:^" }, "peerDependencies": { "solid-js": ">=1.0.0", diff --git a/packages/solid-start/src/api.tsx b/packages/solid-start/src/api.tsx index fff3bec5ea..83ae0dada9 100644 --- a/packages/solid-start/src/api.tsx +++ b/packages/solid-start/src/api.tsx @@ -1 +1 @@ -export * from '@tanstack/start-api-routes' +export * from '@tanstack/solid-start-api-routes' diff --git a/packages/solid-start/src/server-functions-ssr.tsx b/packages/solid-start/src/server-functions-ssr.tsx index 7359e26f45..dbb63ec9e4 100644 --- a/packages/solid-start/src/server-functions-ssr.tsx +++ b/packages/solid-start/src/server-functions-ssr.tsx @@ -1 +1 @@ -export * from '@tanstack/start-server-functions-ssr' +export * from '@tanstack/solid-start-server-functions-ssr' diff --git a/packages/solid-start/vite.config.ts b/packages/solid-start/vite.config.ts index 3cd9726fba..c9114be9da 100644 --- a/packages/solid-start/vite.config.ts +++ b/packages/solid-start/vite.config.ts @@ -25,14 +25,14 @@ export default mergeConfig( './src/api.tsx', ], externalDeps: [ - '@tanstack/start-client', - '@tanstack/start-server', - '@tanstack/start-config', + '@tanstack/solid-start-client', + '@tanstack/solid-start-server', + '@tanstack/solid-start-config', '@tanstack/start-router-manifest', - '@tanstack/start-server-functions-client', + '@tanstack/solid-start-server-functions-client', '@tanstack/start-server-functions-server', - '@tanstack/start-server-functions-ssr', - '@tanstack/start-api-routes', + '@tanstack/solid-start-server-functions-ssr', + '@tanstack/solid-start-api-routes', ], }), ) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c50466e8c9..bfa6a83970 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5680,6 +5680,9 @@ importers: packages/solid-start: dependencies: + '@tanstack/solid-start-api-routes': + specifier: workspace:^ + version: link:../solid-start-api-routes '@tanstack/solid-start-client': specifier: workspace:^ version: link:../solid-start-client @@ -5695,29 +5698,42 @@ importers: '@tanstack/solid-start-server-functions-handler': specifier: workspace:^ version: link:../solid-start-server-functions-handler - '@tanstack/start-api-routes': - specifier: workspace:* - version: link:../start-api-routes + '@tanstack/solid-start-server-functions-ssr': + specifier: workspace:^ + version: link:../solid-start-server-functions-ssr '@tanstack/start-router-manifest': specifier: workspace:* version: link:../start-router-manifest '@tanstack/start-server-functions-server': specifier: workspace:* version: link:../start-server-functions-server - '@tanstack/start-server-functions-ssr': - specifier: workspace:* - version: link:../start-server-functions-ssr solid-js: specifier: '>=1.0.0' version: 1.9.4 vite: specifier: 6.1.0 - version: 6.1.0(@types/node@22.13.4)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) + version: 6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) devDependencies: esbuild: specifier: ^0.25.0 version: 0.25.0 + packages/solid-start-api-routes: + dependencies: + '@tanstack/router-core': + specifier: workspace:* + version: link:../router-core + '@tanstack/solid-start-server': + specifier: workspace:^ + version: link:../solid-start-server + vinxi: + specifier: 0.5.3 + version: 0.5.3(@types/node@22.13.4)(db0@0.2.3)(ioredis@5.4.2)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0) + devDependencies: + typescript: + specifier: ^5.7.2 + version: 5.7.3 + packages/solid-start-client: dependencies: '@tanstack/router-core': @@ -5743,7 +5759,7 @@ importers: version: 1.0.3 vinxi: specifier: ^0.5.3 - version: 0.5.3(@types/node@22.13.4)(db0@0.2.3)(ioredis@5.4.2)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0) + version: 0.5.3(@types/node@22.13.4)(db0@0.2.3)(ioredis@5.4.2)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0) devDependencies: '@solidjs/testing-library': specifier: ^0.8.10 @@ -5756,7 +5772,7 @@ importers: version: 3.0.3 vite-plugin-solid: specifier: ^2.11.2 - version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.4)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.4)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) packages/solid-start-config: dependencies: @@ -5792,13 +5808,13 @@ importers: version: 1.9.4 vinxi: specifier: 0.5.3 - version: 0.5.3(@types/node@22.13.4)(db0@0.2.3)(ioredis@5.4.2)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0) + version: 0.5.3(@types/node@22.13.4)(db0@0.2.3)(ioredis@5.4.2)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0) vite: specifier: 6.1.0 - version: 6.1.0(@types/node@22.13.4)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) + version: 6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) vite-plugin-solid: specifier: ^2.11.2 - version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.4)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.4)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) zod: specifier: ^3.24.1 version: 3.24.1 @@ -5847,7 +5863,7 @@ importers: version: 5.7.3 vite-plugin-solid: specifier: ^2.11.2 - version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.4)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.4)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) packages/solid-start-server-functions-client: dependencies: @@ -5882,7 +5898,7 @@ importers: version: 5.7.3 vite-plugin-solid: specifier: ^2.11.2 - version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.4)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.4)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) packages/solid-start-server-functions-handler: dependencies: @@ -5907,7 +5923,29 @@ importers: version: 5.7.3 vite-plugin-solid: specifier: ^2.11.2 - version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.4)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.4)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + + packages/solid-start-server-functions-ssr: + dependencies: + '@tanstack/server-functions-plugin': + specifier: workspace:* + version: link:../server-functions-plugin + '@tanstack/solid-start-client': + specifier: workspace:^ + version: link:../solid-start-client + '@tanstack/solid-start-server': + specifier: workspace:^ + version: link:../solid-start-server + '@tanstack/solid-start-server-functions-fetcher': + specifier: workspace:^ + version: link:../solid-start-server-functions-fetcher + tiny-invariant: + specifier: ^1.3.3 + version: 1.3.3 + devDependencies: + typescript: + specifier: ^5.7.2 + version: 5.7.3 packages/start: dependencies: From d01848626456166c7d2b6107107ee486fdcd2f9e Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Mon, 24 Feb 2025 23:09:10 +0100 Subject: [PATCH 024/124] fix solid-start-config --- packages/solid-start-config/src/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/solid-start-config/src/index.ts b/packages/solid-start-config/src/index.ts index ce6aa770f2..ed9f957b35 100644 --- a/packages/solid-start-config/src/index.ts +++ b/packages/solid-start-config/src/index.ts @@ -281,6 +281,7 @@ export async function defineConfig( }), TanStackRouterVite({ ...tsrConfig, + target: 'solid', enableRouteGeneration: false, autoCodeSplitting: true, experimental: { @@ -347,6 +348,7 @@ export async function defineConfig( }), TanStackRouterVite({ ...tsrConfig, + target: 'solid', enableRouteGeneration: false, autoCodeSplitting: true, experimental: { @@ -439,6 +441,7 @@ export async function defineConfig( }), TanStackRouterVite({ ...tsrConfig, + target: 'solid', enableRouteGeneration: false, autoCodeSplitting: true, experimental: { From b7da45b056c16eb4042291968a651d4b5e1f5226 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Mon, 24 Feb 2025 23:15:14 +0100 Subject: [PATCH 025/124] improve solid-start/basic e2e --- .../basic/app/components/UserErrorComponent.tsx | 5 +++++ e2e/solid-start/basic/app/routes/__root.tsx | 4 +--- .../basic/app/routes/redirect/$target/index.tsx | 8 ++++---- e2e/solid-start/basic/app/routes/stream.tsx | 2 +- e2e/solid-start/basic/app/routes/users.$userId.tsx | 5 ++--- 5 files changed, 13 insertions(+), 11 deletions(-) create mode 100644 e2e/solid-start/basic/app/components/UserErrorComponent.tsx diff --git a/e2e/solid-start/basic/app/components/UserErrorComponent.tsx b/e2e/solid-start/basic/app/components/UserErrorComponent.tsx new file mode 100644 index 0000000000..d0b69bc2ab --- /dev/null +++ b/e2e/solid-start/basic/app/components/UserErrorComponent.tsx @@ -0,0 +1,5 @@ +import { ErrorComponent, ErrorComponentProps } from "@tanstack/solid-router"; + +export function UserErrorComponent({ error }: ErrorComponentProps) { + return +} \ No newline at end of file diff --git a/e2e/solid-start/basic/app/routes/__root.tsx b/e2e/solid-start/basic/app/routes/__root.tsx index d54a788452..1e6f8db85c 100644 --- a/e2e/solid-start/basic/app/routes/__root.tsx +++ b/e2e/solid-start/basic/app/routes/__root.tsx @@ -16,9 +16,7 @@ import { seo } from '~/utils/seo' export const Route = createRootRoute({ head: () => ({ meta: [ - { - charSet: 'utf-8', - }, + { name: 'viewport', content: 'width=device-width, initial-scale=1', diff --git a/e2e/solid-start/basic/app/routes/redirect/$target/index.tsx b/e2e/solid-start/basic/app/routes/redirect/$target/index.tsx index c9fb508be9..916afd450b 100644 --- a/e2e/solid-start/basic/app/routes/redirect/$target/index.tsx +++ b/e2e/solid-start/basic/app/routes/redirect/$target/index.tsx @@ -12,7 +12,7 @@ export const Route = createFileRoute('/redirect/$target/')({ activeProps={{ class: 'font-bold', }} - preload={preload} + preload={preload()} data-testid="via-beforeLoad" > via-beforeLoad @@ -26,7 +26,7 @@ export const Route = createFileRoute('/redirect/$target/')({ activeProps={{ class: 'font-bold', }} - preload={preload} + preload={preload()} data-testid="via-beforeLoad-reloadDocument" > via-beforeLoad (reloadDocument=true) @@ -39,7 +39,7 @@ export const Route = createFileRoute('/redirect/$target/')({ activeProps={{ class: 'font-bold', }} - preload={preload} + preload={preload()} data-testid="via-loader" > via-loader @@ -53,7 +53,7 @@ export const Route = createFileRoute('/redirect/$target/')({ class: 'font-bold', }} search={{ reloadDocument: true }} - preload={preload} + preload={preload()} data-testid="via-loader-reloadDocument" > via-loader (reloadDocument=true) diff --git a/e2e/solid-start/basic/app/routes/stream.tsx b/e2e/solid-start/basic/app/routes/stream.tsx index 79c8591545..d1d21e344a 100644 --- a/e2e/solid-start/basic/app/routes/stream.tsx +++ b/e2e/solid-start/basic/app/routes/stream.tsx @@ -24,7 +24,7 @@ export const Route = createFileRoute('/stream')({ const decoder = new TextDecoder('utf-8') function Home() { - const loaderData: any = Route.useLoaderData() + const loaderData = Route.useLoaderData() const [streamData, setStreamData] = createSignal>([]) createEffect(() => { diff --git a/e2e/solid-start/basic/app/routes/users.$userId.tsx b/e2e/solid-start/basic/app/routes/users.$userId.tsx index 4f4062326d..fe7a110434 100644 --- a/e2e/solid-start/basic/app/routes/users.$userId.tsx +++ b/e2e/solid-start/basic/app/routes/users.$userId.tsx @@ -5,6 +5,7 @@ import type { ErrorComponentProps } from '@tanstack/solid-router' import type { User } from '~/utils/users' import { DEPLOY_URL } from '~/utils/users' import { NotFound } from '~/components/NotFound' +import { UserErrorComponent } from '~/components/UserErrorComponent' export const Route = createFileRoute('/users/$userId')({ loader: async ({ params: { userId } }) => { @@ -22,9 +23,7 @@ export const Route = createFileRoute('/users/$userId')({ }, }) -export function UserErrorComponent({ error }: ErrorComponentProps) { - return -} + function UserComponent() { const user = Route.useLoaderData() From 1073de96b8d01ce77561486638928ac74bf892ed Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 22:16:43 +0000 Subject: [PATCH 026/124] ci: apply automated fixes --- e2e/solid-start/basic/app/components/UserErrorComponent.tsx | 4 ++-- e2e/solid-start/basic/app/routes/__root.tsx | 1 - e2e/solid-start/basic/app/routes/users.$userId.tsx | 2 -- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/e2e/solid-start/basic/app/components/UserErrorComponent.tsx b/e2e/solid-start/basic/app/components/UserErrorComponent.tsx index d0b69bc2ab..69762add4c 100644 --- a/e2e/solid-start/basic/app/components/UserErrorComponent.tsx +++ b/e2e/solid-start/basic/app/components/UserErrorComponent.tsx @@ -1,5 +1,5 @@ -import { ErrorComponent, ErrorComponentProps } from "@tanstack/solid-router"; +import { ErrorComponent, ErrorComponentProps } from '@tanstack/solid-router' export function UserErrorComponent({ error }: ErrorComponentProps) { return -} \ No newline at end of file +} diff --git a/e2e/solid-start/basic/app/routes/__root.tsx b/e2e/solid-start/basic/app/routes/__root.tsx index 1e6f8db85c..539e4a866d 100644 --- a/e2e/solid-start/basic/app/routes/__root.tsx +++ b/e2e/solid-start/basic/app/routes/__root.tsx @@ -16,7 +16,6 @@ import { seo } from '~/utils/seo' export const Route = createRootRoute({ head: () => ({ meta: [ - { name: 'viewport', content: 'width=device-width, initial-scale=1', diff --git a/e2e/solid-start/basic/app/routes/users.$userId.tsx b/e2e/solid-start/basic/app/routes/users.$userId.tsx index fe7a110434..6f95652e53 100644 --- a/e2e/solid-start/basic/app/routes/users.$userId.tsx +++ b/e2e/solid-start/basic/app/routes/users.$userId.tsx @@ -23,8 +23,6 @@ export const Route = createFileRoute('/users/$userId')({ }, }) - - function UserComponent() { const user = Route.useLoaderData() From bf40998a528046ff52cbe953fffc87e3c86fefd7 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Mon, 24 Feb 2025 23:38:29 +0100 Subject: [PATCH 027/124] fix tailwind --- e2e/solid-start/basic/app/routes/posts.tsx | 10 ++++++---- examples/solid/start-bare/app/routes/__root.tsx | 4 ++++ examples/solid/start-bare/package.json | 2 +- examples/solid/start-bare/tailwind.config.mjs | 4 ---- 4 files changed, 11 insertions(+), 9 deletions(-) delete mode 100644 examples/solid/start-bare/tailwind.config.mjs diff --git a/e2e/solid-start/basic/app/routes/posts.tsx b/e2e/solid-start/basic/app/routes/posts.tsx index 008a1dd20c..02db90a324 100644 --- a/e2e/solid-start/basic/app/routes/posts.tsx +++ b/e2e/solid-start/basic/app/routes/posts.tsx @@ -1,4 +1,5 @@ import { Link, Outlet, createFileRoute } from '@tanstack/solid-router' +import { For } from 'solid-js' import { fetchPosts } from '~/utils/posts' @@ -13,8 +14,8 @@ function PostsComponent() { return (
    - {[...posts(), { id: 'i-do-not-exist', title: 'Non-existent Post' }].map( - (post) => { + + {(post) => { return (
  • ) - }, - )} + }} +
    +

diff --git a/examples/solid/start-bare/app/routes/__root.tsx b/examples/solid/start-bare/app/routes/__root.tsx index dae225f9f5..f57d3f24c3 100644 --- a/examples/solid/start-bare/app/routes/__root.tsx +++ b/examples/solid/start-bare/app/routes/__root.tsx @@ -5,9 +5,13 @@ import { Outlet, Scripts, } from '@tanstack/solid-router' +import appCss from '~/styles/app.css?url' import * as Solid from 'solid-js' export const Route = createRootRoute({ + head: () => ({ + links: [{ rel: 'stylesheet', href: appCss }], + }), component: RootComponent, }) diff --git a/examples/solid/start-bare/package.json b/examples/solid/start-bare/package.json index 4e2bb3dbac..dd18fc7807 100644 --- a/examples/solid/start-bare/package.json +++ b/examples/solid/start-bare/package.json @@ -22,7 +22,7 @@ "@types/node": "^22.10.2", "vite-plugin-solid": "^2.11.2", "combinate": "^1.1.11", - "tailwindcss": "^3.4.17", + "tailwindcss": "^4.0.0", "typescript": "^5.7.2", "vite-tsconfig-paths": "^5.1.4" } diff --git a/examples/solid/start-bare/tailwind.config.mjs b/examples/solid/start-bare/tailwind.config.mjs deleted file mode 100644 index 07c3598bac..0000000000 --- a/examples/solid/start-bare/tailwind.config.mjs +++ /dev/null @@ -1,4 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -export default { - content: ['./app/**/*.{js,jsx,ts,tsx}'], -} From fd12f74ad0d4d2fd8b8701b7f6478f68d50156a1 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 22:39:43 +0000 Subject: [PATCH 028/124] ci: apply automated fixes --- e2e/solid-start/basic/app/routes/posts.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/e2e/solid-start/basic/app/routes/posts.tsx b/e2e/solid-start/basic/app/routes/posts.tsx index 02db90a324..90d87d8a28 100644 --- a/e2e/solid-start/basic/app/routes/posts.tsx +++ b/e2e/solid-start/basic/app/routes/posts.tsx @@ -31,8 +31,7 @@ function PostsComponent() { ) }} - - +
From c42d8188964d7a9fabd86f3f6a152f23a39e9d53 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Mon, 24 Feb 2025 23:55:41 +0100 Subject: [PATCH 029/124] disable react-refresh for solid --- packages/react-start-router-manifest/src/index.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/react-start-router-manifest/src/index.ts b/packages/react-start-router-manifest/src/index.ts index 3f0a37f609..73b5505e44 100644 --- a/packages/react-start-router-manifest/src/index.ts +++ b/packages/react-start-router-manifest/src/index.ts @@ -31,11 +31,12 @@ export function getFullRouterManifest() { 'tanstack/start-router-manifest: TSS_CLIENT_BASE must be defined in your environment for getFullRouterManifest()', ) } - script = `import RefreshRuntime from "/${CLIENT_BASE}/@react-refresh"; - RefreshRuntime.injectIntoGlobalHook(window) - window.$RefreshReg$ = () => {} - window.$RefreshSig$ = () => (type) => type - window.__vite_plugin_react_preamble_installed__ = true;` + // TODO: WE NEED TO CONDITIONALLY DISABLE THIS FOR SOLID + // script = `import RefreshRuntime from "/${CLIENT_BASE}/@react-refresh"; + // RefreshRuntime.injectIntoGlobalHook(window) + // window.$RefreshReg$ = () => {} + // window.$RefreshSig$ = () => (type) => type + // window.__vite_plugin_react_preamble_installed__ = true;` } // Get the entry for the client from vinxi From b12bca39005520e6592b5abf4b0ba1af0ed19a12 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Tue, 25 Feb 2025 00:01:08 +0100 Subject: [PATCH 030/124] solid-refresh --- packages/react-start-plugin/src/index.ts | 25 ++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/react-start-plugin/src/index.ts b/packages/react-start-plugin/src/index.ts index 0909c923f9..9fbfce2ac2 100644 --- a/packages/react-start-plugin/src/index.ts +++ b/packages/react-start-plugin/src/index.ts @@ -110,18 +110,19 @@ export function TanStackStartServerFnsAndMiddleware(opts: { return null } - if (code.includes('@react-refresh')) { - throw new Error( - `We detected that the '@vitejs/plugin-react' was passed before '@tanstack/react-start-plugin'. Please make sure that '@tanstack/router-vite-plugin' is passed before '@vitejs/plugin-react' and try again: -e.g. - -plugins: [ - TanStackStartVite(), // Place this before viteReact() - viteReact(), -] -`, - ) - } + // TODO: solid-refresh +// if (code.includes('@react-refresh')) { +// throw new Error( +// `We detected that the '@vitejs/plugin-react' was passed before '@tanstack/start-plugin'. Please make sure that '@tanstack/router-vite-plugin' is passed before '@vitejs/plugin-react' and try again: +// e.g. + +// plugins: [ +// TanStackStartVite(), // Place this before viteReact() +// viteReact(), +// ] +// `, +// ) +// } if (debug) console.info(`${opts.env} Compiling Start: `, id) From b581141d994ea508e4378181640c08fa2777c3fe Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 23:10:38 +0000 Subject: [PATCH 031/124] ci: apply automated fixes --- packages/react-start-plugin/src/index.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/react-start-plugin/src/index.ts b/packages/react-start-plugin/src/index.ts index 9fbfce2ac2..43d2f03293 100644 --- a/packages/react-start-plugin/src/index.ts +++ b/packages/react-start-plugin/src/index.ts @@ -111,18 +111,18 @@ export function TanStackStartServerFnsAndMiddleware(opts: { } // TODO: solid-refresh -// if (code.includes('@react-refresh')) { -// throw new Error( -// `We detected that the '@vitejs/plugin-react' was passed before '@tanstack/start-plugin'. Please make sure that '@tanstack/router-vite-plugin' is passed before '@vitejs/plugin-react' and try again: -// e.g. - -// plugins: [ -// TanStackStartVite(), // Place this before viteReact() -// viteReact(), -// ] -// `, -// ) -// } + // if (code.includes('@react-refresh')) { + // throw new Error( + // `We detected that the '@vitejs/plugin-react' was passed before '@tanstack/start-plugin'. Please make sure that '@tanstack/router-vite-plugin' is passed before '@vitejs/plugin-react' and try again: + // e.g. + + // plugins: [ + // TanStackStartVite(), // Place this before viteReact() + // viteReact(), + // ] + // `, + // ) + // } if (debug) console.info(`${opts.env} Compiling Start: `, id) From 4a939dfe06beaad6d204faf679491c5f3df7e83c Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Mon, 24 Feb 2025 17:12:54 -0600 Subject: [PATCH 032/124] Fix use hydration --- examples/solid/start-bare/app/client.tsx | 5 +-- .../solid/start-bare/app/routes/__root.tsx | 31 +++++++++++-------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/examples/solid/start-bare/app/client.tsx b/examples/solid/start-bare/app/client.tsx index 993d626cdf..76c88d714c 100644 --- a/examples/solid/start-bare/app/client.tsx +++ b/examples/solid/start-bare/app/client.tsx @@ -1,8 +1,9 @@ /// import { StartClient } from '@tanstack/solid-start' + +import { hydrate } from 'solid-js/web' import { createRouter } from './router' -import { render } from 'solid-js/web' const router = createRouter() -render(() => , document) +hydrate(() => , document) diff --git a/examples/solid/start-bare/app/routes/__root.tsx b/examples/solid/start-bare/app/routes/__root.tsx index f57d3f24c3..6ba3fe6981 100644 --- a/examples/solid/start-bare/app/routes/__root.tsx +++ b/examples/solid/start-bare/app/routes/__root.tsx @@ -7,6 +7,7 @@ import { } from '@tanstack/solid-router' import appCss from '~/styles/app.css?url' import * as Solid from 'solid-js' +import { Hydration, HydrationScript, NoHydration } from 'solid-js/web' export const Route = createRootRoute({ head: () => ({ @@ -25,19 +26,23 @@ function RootComponent() { function RootDocument({ children }: { children: Solid.JSX.Element }) { return ( - - - - - -
- Index - About -
+ + + + + + + +
+ Index + About +
- {children} - - - + {children} + + + + +
) } From 544d8be67881d88abfe46c4f205136f8957b0e07 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Tue, 25 Feb 2025 00:32:02 +0100 Subject: [PATCH 033/124] add back react-refresh --- packages/react-start-plugin/src/index.ts | 25 +++++++++---------- .../react-start-router-manifest/src/index.ts | 10 ++++---- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/packages/react-start-plugin/src/index.ts b/packages/react-start-plugin/src/index.ts index 43d2f03293..f35485eb06 100644 --- a/packages/react-start-plugin/src/index.ts +++ b/packages/react-start-plugin/src/index.ts @@ -110,19 +110,18 @@ export function TanStackStartServerFnsAndMiddleware(opts: { return null } - // TODO: solid-refresh - // if (code.includes('@react-refresh')) { - // throw new Error( - // `We detected that the '@vitejs/plugin-react' was passed before '@tanstack/start-plugin'. Please make sure that '@tanstack/router-vite-plugin' is passed before '@vitejs/plugin-react' and try again: - // e.g. - - // plugins: [ - // TanStackStartVite(), // Place this before viteReact() - // viteReact(), - // ] - // `, - // ) - // } + if (code.includes('@react-refresh')) { + throw new Error( + `We detected that the '@vitejs/plugin-react' was passed before '@tanstack/start-plugin'. Please make sure that '@tanstack/router-vite-plugin' is passed before '@vitejs/plugin-react' and try again: + e.g. + + plugins: [ + TanStackStartVite(), // Place this before viteReact() + viteReact(), + ] + `, + ) + } if (debug) console.info(`${opts.env} Compiling Start: `, id) diff --git a/packages/react-start-router-manifest/src/index.ts b/packages/react-start-router-manifest/src/index.ts index 73b5505e44..a62061ca30 100644 --- a/packages/react-start-router-manifest/src/index.ts +++ b/packages/react-start-router-manifest/src/index.ts @@ -32,11 +32,11 @@ export function getFullRouterManifest() { ) } // TODO: WE NEED TO CONDITIONALLY DISABLE THIS FOR SOLID - // script = `import RefreshRuntime from "/${CLIENT_BASE}/@react-refresh"; - // RefreshRuntime.injectIntoGlobalHook(window) - // window.$RefreshReg$ = () => {} - // window.$RefreshSig$ = () => (type) => type - // window.__vite_plugin_react_preamble_installed__ = true;` + script = `import RefreshRuntime from "/${CLIENT_BASE}/@react-refresh"; + RefreshRuntime.injectIntoGlobalHook(window) + window.$RefreshReg$ = () => {} + window.$RefreshSig$ = () => (type) => type + window.__vite_plugin_react_preamble_installed__ = true;` } // Get the entry for the client from vinxi From 2b80efc8dd328dd5a37ea51d539c71608be7886d Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Tue, 25 Feb 2025 00:46:45 +0100 Subject: [PATCH 034/124] fix malforemd html error --- .../solid/start-bare/app/routes/__root.tsx | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/examples/solid/start-bare/app/routes/__root.tsx b/examples/solid/start-bare/app/routes/__root.tsx index 6ba3fe6981..0faf23067c 100644 --- a/examples/solid/start-bare/app/routes/__root.tsx +++ b/examples/solid/start-bare/app/routes/__root.tsx @@ -27,22 +27,19 @@ function RootComponent() { function RootDocument({ children }: { children: Solid.JSX.Element }) { return ( - - - - - - -
- Index - About -
+ <> + + - {children} +
+ Index + About +
- - - + {children} + + +
) } From 47ea9b1e0511e63ca2081a1b27566849d79816eb Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Tue, 25 Feb 2025 00:48:12 +0100 Subject: [PATCH 035/124] format --- .../solid/start-bare/app/routes/__root.tsx | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/examples/solid/start-bare/app/routes/__root.tsx b/examples/solid/start-bare/app/routes/__root.tsx index 0faf23067c..96e853f613 100644 --- a/examples/solid/start-bare/app/routes/__root.tsx +++ b/examples/solid/start-bare/app/routes/__root.tsx @@ -28,17 +28,20 @@ function RootDocument({ children }: { children: Solid.JSX.Element }) { return ( <> - - + + + + + +
+ Index + About +
-
- Index - About -
+ {children} - {children} - - + +
) From a2ab150a0140c7fc27248f42f344f845662fc60c Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Tue, 25 Feb 2025 00:51:01 +0100 Subject: [PATCH 036/124] remove head/body --- .../solid/start-bare/app/routes/__root.tsx | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/examples/solid/start-bare/app/routes/__root.tsx b/examples/solid/start-bare/app/routes/__root.tsx index 96e853f613..0faf23067c 100644 --- a/examples/solid/start-bare/app/routes/__root.tsx +++ b/examples/solid/start-bare/app/routes/__root.tsx @@ -28,20 +28,17 @@ function RootDocument({ children }: { children: Solid.JSX.Element }) { return ( <> - - - - - -
- Index - About -
+ + - {children} +
+ Index + About +
- - + {children} + +
) From cfd8dca5474a0ea644e26deedf7d2802f33a7065 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Tue, 25 Feb 2025 00:56:22 +0100 Subject: [PATCH 037/124] remove api routes --- examples/solid/start-bare/app/api.ts | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 examples/solid/start-bare/app/api.ts diff --git a/examples/solid/start-bare/app/api.ts b/examples/solid/start-bare/app/api.ts deleted file mode 100644 index ed511bcd26..0000000000 --- a/examples/solid/start-bare/app/api.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { - createStartAPIHandler, - defaultAPIFileRouteHandler, -} from '@tanstack/solid-start/api' - -export default createStartAPIHandler(defaultAPIFileRouteHandler) From cb8681728f3e2f509afd4799108e1abb21163c6e Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Tue, 25 Feb 2025 03:52:20 +0100 Subject: [PATCH 038/124] working hydration --- examples/solid/start-bare/app/client.tsx | 4 +++- .../solid/start-bare/app/routes/__root.tsx | 19 ++++++++++--------- packages/react-start-plugin/src/index.ts | 12 ------------ .../react-start-router-manifest/src/index.ts | 9 ++------- .../src/core/router-code-splitter-plugin.ts | 7 +------ 5 files changed, 16 insertions(+), 35 deletions(-) diff --git a/examples/solid/start-bare/app/client.tsx b/examples/solid/start-bare/app/client.tsx index 76c88d714c..2dcbbce594 100644 --- a/examples/solid/start-bare/app/client.tsx +++ b/examples/solid/start-bare/app/client.tsx @@ -6,4 +6,6 @@ import { createRouter } from './router' const router = createRouter() -hydrate(() => , document) +const appDiv = document.getElementById('app')! + +hydrate(() => , appDiv) diff --git a/examples/solid/start-bare/app/routes/__root.tsx b/examples/solid/start-bare/app/routes/__root.tsx index 0faf23067c..7ae1783e07 100644 --- a/examples/solid/start-bare/app/routes/__root.tsx +++ b/examples/solid/start-bare/app/routes/__root.tsx @@ -28,17 +28,18 @@ function RootDocument({ children }: { children: Solid.JSX.Element }) { return ( <> - - +
+ + +
+ Index + About +
-
- Index - About -
- - {children} + {children} - + +
) diff --git a/packages/react-start-plugin/src/index.ts b/packages/react-start-plugin/src/index.ts index f35485eb06..0fde2cde88 100644 --- a/packages/react-start-plugin/src/index.ts +++ b/packages/react-start-plugin/src/index.ts @@ -110,18 +110,6 @@ export function TanStackStartServerFnsAndMiddleware(opts: { return null } - if (code.includes('@react-refresh')) { - throw new Error( - `We detected that the '@vitejs/plugin-react' was passed before '@tanstack/start-plugin'. Please make sure that '@tanstack/router-vite-plugin' is passed before '@vitejs/plugin-react' and try again: - e.g. - - plugins: [ - TanStackStartVite(), // Place this before viteReact() - viteReact(), - ] - `, - ) - } if (debug) console.info(`${opts.env} Compiling Start: `, id) diff --git a/packages/react-start-router-manifest/src/index.ts b/packages/react-start-router-manifest/src/index.ts index a62061ca30..3a26a806da 100644 --- a/packages/react-start-router-manifest/src/index.ts +++ b/packages/react-start-router-manifest/src/index.ts @@ -31,12 +31,7 @@ export function getFullRouterManifest() { 'tanstack/start-router-manifest: TSS_CLIENT_BASE must be defined in your environment for getFullRouterManifest()', ) } - // TODO: WE NEED TO CONDITIONALLY DISABLE THIS FOR SOLID - script = `import RefreshRuntime from "/${CLIENT_BASE}/@react-refresh"; - RefreshRuntime.injectIntoGlobalHook(window) - window.$RefreshReg$ = () => {} - window.$RefreshSig$ = () => (type) => type - window.__vite_plugin_react_preamble_installed__ = true;` + } // Get the entry for the client from vinxi @@ -52,7 +47,7 @@ export function getFullRouterManifest() { tag: 'script', attrs: { type: 'module', - suppressHydrationWarning: true, + suppressHydrationWarning: false, async: true, }, children: `${script}import("${importPath}")`, 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 7c1ecc17a8..da0fd12ea5 100644 --- a/packages/router-plugin/src/core/router-code-splitter-plugin.ts +++ b/packages/router-plugin/src/core/router-code-splitter-plugin.ts @@ -56,12 +56,7 @@ type BannedBeforeExternalPlugin = { } const bannedBeforeExternalPlugins: Array = [ - { - identifier: '@react-refresh', - pkg: '@vitejs/plugin-react', - usage: 'viteReact()', - frameworks: ['vite'], - }, + ] class FoundPluginInBeforeCode extends Error { From 8a4dc9120ee448f82a9ec6fc2233d875bd40e4fd Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2025 02:53:24 +0000 Subject: [PATCH 039/124] ci: apply automated fixes --- packages/react-start-plugin/src/index.ts | 1 - packages/react-start-router-manifest/src/index.ts | 1 - .../router-plugin/src/core/router-code-splitter-plugin.ts | 4 +--- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/react-start-plugin/src/index.ts b/packages/react-start-plugin/src/index.ts index 0fde2cde88..4f27b30f1a 100644 --- a/packages/react-start-plugin/src/index.ts +++ b/packages/react-start-plugin/src/index.ts @@ -110,7 +110,6 @@ export function TanStackStartServerFnsAndMiddleware(opts: { return null } - if (debug) console.info(`${opts.env} Compiling Start: `, id) const compiled = compileStartOutput({ diff --git a/packages/react-start-router-manifest/src/index.ts b/packages/react-start-router-manifest/src/index.ts index 3a26a806da..879d991382 100644 --- a/packages/react-start-router-manifest/src/index.ts +++ b/packages/react-start-router-manifest/src/index.ts @@ -31,7 +31,6 @@ export function getFullRouterManifest() { 'tanstack/start-router-manifest: TSS_CLIENT_BASE must be defined in your environment for getFullRouterManifest()', ) } - } // Get the entry for the client from vinxi 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 da0fd12ea5..7a5df622d2 100644 --- a/packages/router-plugin/src/core/router-code-splitter-plugin.ts +++ b/packages/router-plugin/src/core/router-code-splitter-plugin.ts @@ -55,9 +55,7 @@ type BannedBeforeExternalPlugin = { frameworks: Array } -const bannedBeforeExternalPlugins: Array = [ - -] +const bannedBeforeExternalPlugins: Array = [] class FoundPluginInBeforeCode extends Error { constructor(externalPlugin: BannedBeforeExternalPlugin, framework: string) { From 72575f34a49a802ad36b3fbbc446ca206b292298 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Tue, 25 Feb 2025 03:58:36 +0100 Subject: [PATCH 040/124] cleanup e2e/solid-start/basic --- e2e/solid-start/basic/app/client.tsx | 6 +- .../app/components/PostErrorComponent.tsx | 5 + e2e/solid-start/basic/app/routes/__root.tsx | 158 +++++++++--------- .../basic/app/routes/posts.$postId.tsx | 4 +- .../basic/app/routes/posts_.$postId.deep.tsx | 2 +- 5 files changed, 91 insertions(+), 84 deletions(-) create mode 100644 e2e/solid-start/basic/app/components/PostErrorComponent.tsx diff --git a/e2e/solid-start/basic/app/client.tsx b/e2e/solid-start/basic/app/client.tsx index 993d626cdf..c0189b46fd 100644 --- a/e2e/solid-start/basic/app/client.tsx +++ b/e2e/solid-start/basic/app/client.tsx @@ -1,8 +1,10 @@ /// import { StartClient } from '@tanstack/solid-start' import { createRouter } from './router' -import { render } from 'solid-js/web' +import { hydrate } from 'solid-js/web' const router = createRouter() -render(() => , document) +const appDiv = document.getElementById('app')! + +hydrate(() => , appDiv) diff --git a/e2e/solid-start/basic/app/components/PostErrorComponent.tsx b/e2e/solid-start/basic/app/components/PostErrorComponent.tsx new file mode 100644 index 0000000000..0dc1f2e8a8 --- /dev/null +++ b/e2e/solid-start/basic/app/components/PostErrorComponent.tsx @@ -0,0 +1,5 @@ +import { ErrorComponent, ErrorComponentProps } from "@tanstack/solid-router"; + +export function PostErrorComponent({ error }: ErrorComponentProps) { + return +} \ No newline at end of file diff --git a/e2e/solid-start/basic/app/routes/__root.tsx b/e2e/solid-start/basic/app/routes/__root.tsx index 539e4a866d..6a89ae8749 100644 --- a/e2e/solid-start/basic/app/routes/__root.tsx +++ b/e2e/solid-start/basic/app/routes/__root.tsx @@ -7,6 +7,7 @@ import { } from '@tanstack/solid-router' import * as Solid from 'solid-js' +import { Hydration, HydrationScript, NoHydration } from 'solid-js/web' import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary' import { NotFound } from '~/components/NotFound' @@ -80,84 +81,85 @@ function RootComponent() { function RootDocument({ children }: { children: Solid.JSX.Element }) { return ( - - - - - -
- - Home - {' '} - - Posts - {' '} - - Users - {' '} - - Layout - {' '} - - Scripts - {' '} - - Deferred - {' '} - - redirect - {' '} - - This Route Does Not Exist - + + <> +
+ + +
+ + Home + {' '} + + Posts + {' '} + + Users + {' '} + + Layout + {' '} + + Scripts + {' '} + + Deferred + {' '} + + redirect + {' '} + + This Route Does Not Exist + +
+ + {children} + +
-
- {children} - {/* */} - - - + +
) } diff --git a/e2e/solid-start/basic/app/routes/posts.$postId.tsx b/e2e/solid-start/basic/app/routes/posts.$postId.tsx index 6df924a588..bc908e774c 100644 --- a/e2e/solid-start/basic/app/routes/posts.$postId.tsx +++ b/e2e/solid-start/basic/app/routes/posts.$postId.tsx @@ -3,6 +3,7 @@ import type { ErrorComponentProps } from '@tanstack/solid-router' import { fetchPost } from '~/utils/posts' import { NotFound } from '~/components/NotFound' +import { PostErrorComponent } from '~/components/PostErrorComponent' export const Route = createFileRoute('/posts/$postId')({ loader: async ({ params: { postId } }) => fetchPost({ data: postId }), @@ -13,9 +14,6 @@ export const Route = createFileRoute('/posts/$postId')({ }, }) -export function PostErrorComponent({ error }: ErrorComponentProps) { - return -} function PostComponent() { const post = Route.useLoaderData() diff --git a/e2e/solid-start/basic/app/routes/posts_.$postId.deep.tsx b/e2e/solid-start/basic/app/routes/posts_.$postId.deep.tsx index d2bbb37bb5..f8d4627914 100644 --- a/e2e/solid-start/basic/app/routes/posts_.$postId.deep.tsx +++ b/e2e/solid-start/basic/app/routes/posts_.$postId.deep.tsx @@ -1,6 +1,6 @@ import { Link, createFileRoute } from '@tanstack/solid-router' +import { PostErrorComponent } from '~/components/PostErrorComponent' -import { PostErrorComponent } from './posts.$postId' import { fetchPost } from '~/utils/posts' export const Route = createFileRoute('/posts_/$postId/deep')({ From 079a58c1e8a17ad9e37e6a1ec1fa4abba56dbc1d Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2025 02:59:47 +0000 Subject: [PATCH 041/124] ci: apply automated fixes --- e2e/solid-start/basic/app/components/PostErrorComponent.tsx | 4 ++-- e2e/solid-start/basic/app/routes/posts.$postId.tsx | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/e2e/solid-start/basic/app/components/PostErrorComponent.tsx b/e2e/solid-start/basic/app/components/PostErrorComponent.tsx index 0dc1f2e8a8..3e5d62c79b 100644 --- a/e2e/solid-start/basic/app/components/PostErrorComponent.tsx +++ b/e2e/solid-start/basic/app/components/PostErrorComponent.tsx @@ -1,5 +1,5 @@ -import { ErrorComponent, ErrorComponentProps } from "@tanstack/solid-router"; +import { ErrorComponent, ErrorComponentProps } from '@tanstack/solid-router' export function PostErrorComponent({ error }: ErrorComponentProps) { return -} \ No newline at end of file +} diff --git a/e2e/solid-start/basic/app/routes/posts.$postId.tsx b/e2e/solid-start/basic/app/routes/posts.$postId.tsx index bc908e774c..c6b2fcf5f9 100644 --- a/e2e/solid-start/basic/app/routes/posts.$postId.tsx +++ b/e2e/solid-start/basic/app/routes/posts.$postId.tsx @@ -14,7 +14,6 @@ export const Route = createFileRoute('/posts/$postId')({ }, }) - function PostComponent() { const post = Route.useLoaderData() From 6b24e23e25840158e4682a9787ec2a76ddd0b499 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Tue, 25 Feb 2025 11:56:08 +0100 Subject: [PATCH 042/124] add separate solid-start-router-manifest --- .../react-start-router-manifest/src/index.ts | 7 +- .../solid-start-router-manifest/README.md | 33 ++++++++ .../eslint.config.js | 31 +++++++ .../solid-start-router-manifest/package.json | 68 +++++++++++++++ .../solid-start-router-manifest/src/index.ts | 83 +++++++++++++++++++ .../solid-start-router-manifest/tsconfig.json | 10 +++ .../vite.config.ts | 22 +++++ packages/solid-start/package.json | 2 +- packages/solid-start/src/router-manifest.tsx | 2 +- packages/solid-start/vite.config.ts | 2 +- pnpm-lock.yaml | 22 ++++- 11 files changed, 275 insertions(+), 7 deletions(-) create mode 100644 packages/solid-start-router-manifest/README.md create mode 100644 packages/solid-start-router-manifest/eslint.config.js create mode 100644 packages/solid-start-router-manifest/package.json create mode 100644 packages/solid-start-router-manifest/src/index.ts create mode 100644 packages/solid-start-router-manifest/tsconfig.json create mode 100644 packages/solid-start-router-manifest/vite.config.ts diff --git a/packages/react-start-router-manifest/src/index.ts b/packages/react-start-router-manifest/src/index.ts index 879d991382..3f0a37f609 100644 --- a/packages/react-start-router-manifest/src/index.ts +++ b/packages/react-start-router-manifest/src/index.ts @@ -31,6 +31,11 @@ export function getFullRouterManifest() { 'tanstack/start-router-manifest: TSS_CLIENT_BASE must be defined in your environment for getFullRouterManifest()', ) } + script = `import RefreshRuntime from "/${CLIENT_BASE}/@react-refresh"; + RefreshRuntime.injectIntoGlobalHook(window) + window.$RefreshReg$ = () => {} + window.$RefreshSig$ = () => (type) => type + window.__vite_plugin_react_preamble_installed__ = true;` } // Get the entry for the client from vinxi @@ -46,7 +51,7 @@ export function getFullRouterManifest() { tag: 'script', attrs: { type: 'module', - suppressHydrationWarning: false, + suppressHydrationWarning: true, async: true, }, children: `${script}import("${importPath}")`, diff --git a/packages/solid-start-router-manifest/README.md b/packages/solid-start-router-manifest/README.md new file mode 100644 index 0000000000..bb009b0c87 --- /dev/null +++ b/packages/solid-start-router-manifest/README.md @@ -0,0 +1,33 @@ +> 🤫 we're cooking up something special! + + + +# TanStack Start + +![TanStack Router Header](https://github.com/tanstack/router/raw/main/media/header.png) + +🤖 Type-safe router w/ built-in caching & URL state management for React! + + + #TanStack + + + + + + + + semantic-release + + Join the discussion on Github +Best of JS + + + + + + + +Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [React Query](https://github.com/tannerlinsley/react-query), [React Table](https://github.com/tanstack/react-table), [React Charts](https://github.com/tannerlinsley/react-charts), [React Virtual](https://github.com/tannerlinsley/react-virtual) + +## Visit [tanstack.com/router](https://tanstack.com/router) for docs, guides, API and more! diff --git a/packages/solid-start-router-manifest/eslint.config.js b/packages/solid-start-router-manifest/eslint.config.js new file mode 100644 index 0000000000..931f0ec774 --- /dev/null +++ b/packages/solid-start-router-manifest/eslint.config.js @@ -0,0 +1,31 @@ +// @ts-check + +import pluginReact from '@eslint-react/eslint-plugin' +import pluginReactHooks from 'eslint-plugin-react-hooks' +import rootConfig from '../../eslint.config.js' + +export default [ + ...rootConfig, + { + ...pluginReact.configs.recommended, + files: ['**/*.{ts,tsx}'], + }, + { + plugins: { + 'react-hooks': pluginReactHooks, + }, + rules: { + '@eslint-react/no-unstable-context-value': 'off', + '@eslint-react/no-unstable-default-props': 'off', + '@eslint-react/dom/no-missing-button-type': 'off', + 'react-hooks/exhaustive-deps': 'error', + 'react-hooks/rules-of-hooks': 'error', + }, + }, + { + files: ['**/__tests__/**'], + rules: { + '@typescript-eslint/no-unnecessary-condition': 'off', + }, + }, +] diff --git a/packages/solid-start-router-manifest/package.json b/packages/solid-start-router-manifest/package.json new file mode 100644 index 0000000000..031e9b864d --- /dev/null +++ b/packages/solid-start-router-manifest/package.json @@ -0,0 +1,68 @@ +{ + "name": "@tanstack/solid-start-router-manifest", + "version": "1.111.7", + "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" + }, + "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": "exit 0; 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": "tsc" + }, + "type": "module", + "types": "dist/esm/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + } + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "files": [ + "dist", + "src" + ], + "engines": { + "node": ">=12" + }, + "dependencies": { + "@tanstack/router-core": "workspace:^", + "tiny-invariant": "^1.3.3", + "vinxi": "0.5.3" + }, + "devDependencies": { + "typescript": "^5.7.2" + } +} diff --git a/packages/solid-start-router-manifest/src/index.ts b/packages/solid-start-router-manifest/src/index.ts new file mode 100644 index 0000000000..b1565c0ed7 --- /dev/null +++ b/packages/solid-start-router-manifest/src/index.ts @@ -0,0 +1,83 @@ +// @ts-expect-error +import tsrGetManifest from 'tsr:routes-manifest' +import { getManifest } from 'vinxi/manifest' +import { default as invariant } from 'tiny-invariant' +import type { Manifest } from '@tanstack/router-core' + +function sanitizeBase(base: string) { + return base.replace(/^\/|\/$/g, '') +} + +/** + * @description Returns the full, unfiltered router manifest. This includes relationships + * between routes, assets, and preloads and is NOT what you want to serialize and + * send to the client. + */ +export function getFullRouterManifest() { + const routerManifest = tsrGetManifest() as Manifest + + const rootRoute = (routerManifest.routes.__root__ = + routerManifest.routes.__root__ || {}) + + rootRoute.assets = rootRoute.assets || [] + + let script = '' + // Always fake that HMR is ready + if (process.env.NODE_ENV === 'development') { + const CLIENT_BASE = sanitizeBase(process.env.TSS_CLIENT_BASE || '') + + if (!CLIENT_BASE) { + throw new Error( + 'tanstack/solid-start-router-manifest: TSS_CLIENT_BASE must be defined in your environment for getFullRouterManifest()', + ) + } + } + + // Get the entry for the client from vinxi + const vinxiClientManifest = getManifest('client') + + const importPath = + vinxiClientManifest.inputs[vinxiClientManifest.handler]?.output.path + if (!importPath) { + invariant(importPath, 'Could not find client entry in vinxi manifest') + } + + rootRoute.assets.push({ + tag: 'script', + attrs: { + type: 'module', + suppressHydrationWarning: true, + async: true, + }, + children: `${script}import("${importPath}")`, + }) + + return routerManifest +} + +/** + * @description Returns the router manifest that should be sent to the client. + * This includes only the assets and preloads for the current route and any + * special assets that are needed for the client. It does not include relationships + * between routes or any other data that is not needed for the client. + */ +export function getRouterManifest() { + const routerManifest = getFullRouterManifest() + + // Strip out anything that isn't needed for the client + return { + ...routerManifest, + routes: Object.fromEntries( + Object.entries(routerManifest.routes).map(([k, v]: any) => { + const { preloads, assets } = v + return [ + k, + { + preloads, + assets, + }, + ] + }), + ), + } +} diff --git a/packages/solid-start-router-manifest/tsconfig.json b/packages/solid-start-router-manifest/tsconfig.json new file mode 100644 index 0000000000..940a9cce0a --- /dev/null +++ b/packages/solid-start-router-manifest/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/index.ts"], + "compilerOptions": { + "rootDir": "src", + "outDir": "dist/esm", + "target": "esnext", + "noEmit": false + } +} diff --git a/packages/solid-start-router-manifest/vite.config.ts b/packages/solid-start-router-manifest/vite.config.ts new file mode 100644 index 0000000000..d6472068fb --- /dev/null +++ b/packages/solid-start-router-manifest/vite.config.ts @@ -0,0 +1,22 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import { tanstackViteConfig } from '@tanstack/config/vite' +import packageJson from './package.json' +import type { ViteUserConfig } from 'vitest/config' + +const config = defineConfig({ + plugins: [] as ViteUserConfig['plugins'], + test: { + name: packageJson.name, + watch: false, + environment: 'jsdom', + }, +}) + +export default mergeConfig( + config, + tanstackViteConfig({ + entry: './src/index.ts', + srcDir: './src', + externalDeps: ['tsr:routes-manifest'], + }), +) diff --git a/packages/solid-start/package.json b/packages/solid-start/package.json index 49eb342689..f3a6181970 100644 --- a/packages/solid-start/package.json +++ b/packages/solid-start/package.json @@ -146,7 +146,7 @@ "@tanstack/solid-start-client": "workspace:^", "@tanstack/solid-start-server": "workspace:^", "@tanstack/solid-start-config": "workspace:^", - "@tanstack/start-router-manifest": "workspace:^", + "@tanstack/solid-start-router-manifest": "workspace:^", "@tanstack/solid-start-server-functions-client": "workspace:^", "@tanstack/start-server-functions-server": "workspace:^", "@tanstack/solid-start-server-functions-handler": "workspace:^", diff --git a/packages/solid-start/src/router-manifest.tsx b/packages/solid-start/src/router-manifest.tsx index 38f6fdac0a..4e939be275 100644 --- a/packages/solid-start/src/router-manifest.tsx +++ b/packages/solid-start/src/router-manifest.tsx @@ -1 +1 @@ -export * from '@tanstack/start-router-manifest' +export * from '@tanstack/solid-start-router-manifest' diff --git a/packages/solid-start/vite.config.ts b/packages/solid-start/vite.config.ts index c9114be9da..eb44034657 100644 --- a/packages/solid-start/vite.config.ts +++ b/packages/solid-start/vite.config.ts @@ -28,7 +28,7 @@ export default mergeConfig( '@tanstack/solid-start-client', '@tanstack/solid-start-server', '@tanstack/solid-start-config', - '@tanstack/start-router-manifest', + '@tanstack/solid-start-router-manifest', '@tanstack/solid-start-server-functions-client', '@tanstack/start-server-functions-server', '@tanstack/solid-start-server-functions-ssr', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bfa6a83970..ef18ab1bc7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5689,6 +5689,9 @@ importers: '@tanstack/solid-start-config': specifier: workspace:^ version: link:../solid-start-config + '@tanstack/solid-start-router-manifest': + specifier: workspace:^ + version: link:../solid-start-router-manifest '@tanstack/solid-start-server': specifier: workspace:^ version: link:../solid-start-server @@ -5701,9 +5704,6 @@ importers: '@tanstack/solid-start-server-functions-ssr': specifier: workspace:^ version: link:../solid-start-server-functions-ssr - '@tanstack/start-router-manifest': - specifier: workspace:* - version: link:../start-router-manifest '@tanstack/start-server-functions-server': specifier: workspace:* version: link:../start-server-functions-server @@ -5819,6 +5819,22 @@ importers: specifier: ^3.24.1 version: 3.24.1 + packages/solid-start-router-manifest: + dependencies: + '@tanstack/router-core': + specifier: workspace:* + version: link:../router-core + tiny-invariant: + specifier: ^1.3.3 + version: 1.3.3 + vinxi: + specifier: 0.5.3 + version: 0.5.3(@types/node@22.13.4)(db0@0.2.3)(ioredis@5.4.2)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0) + devDependencies: + typescript: + specifier: ^5.7.2 + version: 5.7.3 + packages/solid-start-server: dependencies: '@tanstack/history': From b0f87688ab29686ab0ae2ae011afe0cf79d4fef2 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Tue, 25 Feb 2025 12:44:42 +0100 Subject: [PATCH 043/124] use simple error component --- e2e/solid-start/basic/app/routes/__root.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/e2e/solid-start/basic/app/routes/__root.tsx b/e2e/solid-start/basic/app/routes/__root.tsx index 6a89ae8749..da15dfb999 100644 --- a/e2e/solid-start/basic/app/routes/__root.tsx +++ b/e2e/solid-start/basic/app/routes/__root.tsx @@ -9,7 +9,6 @@ import { import * as Solid from 'solid-js' import { Hydration, HydrationScript, NoHydration } from 'solid-js/web' -import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary' import { NotFound } from '~/components/NotFound' import appCss from '~/styles/app.css?url' import { seo } from '~/utils/seo' @@ -50,13 +49,7 @@ export const Route = createRootRoute({ { rel: 'icon', href: '/favicon.ico' }, ], }), - errorComponent: (props) => { - return ( - - - - ) - }, + errorComponent: (props)=>

{props.error.stack}

, notFoundComponent: () => , component: RootComponent, }) From 911fd9ec687a74cf47c9b66340789fd08fce877b Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2025 11:45:44 +0000 Subject: [PATCH 044/124] ci: apply automated fixes --- e2e/solid-start/basic/app/routes/__root.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/solid-start/basic/app/routes/__root.tsx b/e2e/solid-start/basic/app/routes/__root.tsx index da15dfb999..2993bc40e2 100644 --- a/e2e/solid-start/basic/app/routes/__root.tsx +++ b/e2e/solid-start/basic/app/routes/__root.tsx @@ -49,7 +49,7 @@ export const Route = createRootRoute({ { rel: 'icon', href: '/favicon.ico' }, ], }), - errorComponent: (props)=>

{props.error.stack}

, + errorComponent: (props) =>

{props.error.stack}

, notFoundComponent: () => , component: RootComponent, }) From 4ae479bc10b125f6358df0250667f706621d9830 Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Tue, 25 Feb 2025 06:12:31 -0600 Subject: [PATCH 045/124] add vite-plugin-solid --- packages/solid-start-config/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/solid-start-config/src/index.ts b/packages/solid-start-config/src/index.ts index ed9f957b35..a8c06042d6 100644 --- a/packages/solid-start-config/src/index.ts +++ b/packages/solid-start-config/src/index.ts @@ -228,7 +228,7 @@ export async function defineConfig( TanStackServerFnsPlugin.client, ...(viteConfig.plugins || []), ...(clientViteConfig.plugins || []), - viteSolid(opts.solid), + viteSolid({ ...opts.solid, ssr: true }), // TODO: RSCS - enable this // serverComponents.client(), ] @@ -376,6 +376,7 @@ export async function defineConfig( // }), ...(viteConfig.plugins || []), ...(serverViteConfig.plugins || []), + viteSolid({ ...opts.solid, ssr: true }), ] }, }, From fe7ececaad79a39980e282e18d066a9e5f74008b Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Tue, 25 Feb 2025 13:25:23 +0100 Subject: [PATCH 046/124] use prev from setCount --- e2e/solid-start/basic/app/routes/deferred.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/solid-start/basic/app/routes/deferred.tsx b/e2e/solid-start/basic/app/routes/deferred.tsx index 72df78ebf5..01b49a0b28 100644 --- a/e2e/solid-start/basic/app/routes/deferred.tsx +++ b/e2e/solid-start/basic/app/routes/deferred.tsx @@ -56,7 +56,7 @@ function Deferred() {
Count: {count()}
- +
) From 4781c0b3366678a812556242baef4e1a28aabe82 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2025 12:26:32 +0000 Subject: [PATCH 047/124] ci: apply automated fixes --- e2e/solid-start/basic/app/routes/deferred.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/e2e/solid-start/basic/app/routes/deferred.tsx b/e2e/solid-start/basic/app/routes/deferred.tsx index 01b49a0b28..2a53643453 100644 --- a/e2e/solid-start/basic/app/routes/deferred.tsx +++ b/e2e/solid-start/basic/app/routes/deferred.tsx @@ -56,7 +56,9 @@ function Deferred() {
Count: {count()}
- +
) From 049a4e1e69c538e380df6eeab43c193eda8d564f Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Tue, 25 Feb 2025 06:33:36 -0600 Subject: [PATCH 048/124] Fix api routes --- packages/solid-start-config/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/solid-start-config/src/index.ts b/packages/solid-start-config/src/index.ts index a8c06042d6..876f0a322b 100644 --- a/packages/solid-start-config/src/index.ts +++ b/packages/solid-start-config/src/index.ts @@ -451,6 +451,7 @@ export async function defineConfig( }), ...(viteConfig.plugins || []), ...(apiViteConfig.plugins || []), + viteSolid({ ...opts.solid, ssr: true }), ] }, }) From 69079ecae82edbdfb447eecb9094484dae4d681c Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Tue, 25 Feb 2025 22:13:39 +0100 Subject: [PATCH 049/124] add solid-start-plugin --- packages/solid-start-config/package.json | 2 +- packages/solid-start-plugin/README.md | 5 + packages/solid-start-plugin/eslint.config.js | 5 + packages/solid-start-plugin/package.json | 86 +++ packages/solid-start-plugin/src/compilers.ts | 584 ++++++++++++++++++ packages/solid-start-plugin/src/index.ts | 130 ++++ .../createIsomorphicFn.test.ts | 94 +++ .../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 | 37 ++ .../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 | 176 ++++++ .../client/createServerFnDestructured.tsx | 56 ++ .../createServerFnDestructuredRename.tsx | 35 ++ .../client/createServerFnStarImport.tsx | 35 ++ .../client/createServerFnValidator.tsx | 9 + .../server/createServerFnDestructured.tsx | 77 +++ .../createServerFnDestructuredRename.tsx | 52 ++ .../server/createServerFnStarImport.tsx | 54 ++ .../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 | 61 ++ .../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 | 8 + .../envOnly/test-files/envOnlyStarImport.tsx | 5 + packages/solid-start-plugin/tsconfig.json | 8 + packages/solid-start-plugin/vite.config.ts | 20 + pnpm-lock.yaml | 55 +- 55 files changed, 2373 insertions(+), 4 deletions(-) create mode 100644 packages/solid-start-plugin/README.md create mode 100644 packages/solid-start-plugin/eslint.config.js create mode 100644 packages/solid-start-plugin/package.json create mode 100644 packages/solid-start-plugin/src/compilers.ts create mode 100644 packages/solid-start-plugin/src/index.ts create mode 100644 packages/solid-start-plugin/tests/createIsomorphicFn/createIsomorphicFn.test.ts create mode 100644 packages/solid-start-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructured.tsx create mode 100644 packages/solid-start-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructuredRename.tsx create mode 100644 packages/solid-start-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnStarImport.tsx create mode 100644 packages/solid-start-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructured.tsx create mode 100644 packages/solid-start-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructuredRename.tsx create mode 100644 packages/solid-start-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnStarImport.tsx create mode 100644 packages/solid-start-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnDestructured.tsx create mode 100644 packages/solid-start-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnDestructuredRename.tsx create mode 100644 packages/solid-start-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnStarImport.tsx create mode 100644 packages/solid-start-plugin/tests/createMiddleware/createMiddleware.test.ts create mode 100644 packages/solid-start-plugin/tests/createMiddleware/snapshots/client/createMiddlewareDestructured.tsx create mode 100644 packages/solid-start-plugin/tests/createMiddleware/snapshots/client/createMiddlewareDestructuredRename.tsx create mode 100644 packages/solid-start-plugin/tests/createMiddleware/snapshots/client/createMiddlewareStarImport.tsx create mode 100644 packages/solid-start-plugin/tests/createMiddleware/snapshots/client/createMiddlewareValidator.tsx create mode 100644 packages/solid-start-plugin/tests/createMiddleware/snapshots/server/createMiddlewareDestructured.tsx create mode 100644 packages/solid-start-plugin/tests/createMiddleware/snapshots/server/createMiddlewareDestructuredRename.tsx create mode 100644 packages/solid-start-plugin/tests/createMiddleware/snapshots/server/createMiddlewareStarImport.tsx create mode 100644 packages/solid-start-plugin/tests/createMiddleware/snapshots/server/createMiddlewareValidator.tsx create mode 100644 packages/solid-start-plugin/tests/createMiddleware/test-files/createMiddlewareDestructured.tsx create mode 100644 packages/solid-start-plugin/tests/createMiddleware/test-files/createMiddlewareDestructuredRename.tsx create mode 100644 packages/solid-start-plugin/tests/createMiddleware/test-files/createMiddlewareStarImport.tsx create mode 100644 packages/solid-start-plugin/tests/createMiddleware/test-files/createMiddlewareValidator.tsx create mode 100644 packages/solid-start-plugin/tests/createServerFn/createServerFn.test.ts create mode 100644 packages/solid-start-plugin/tests/createServerFn/snapshots/client/createServerFnDestructured.tsx create mode 100644 packages/solid-start-plugin/tests/createServerFn/snapshots/client/createServerFnDestructuredRename.tsx create mode 100644 packages/solid-start-plugin/tests/createServerFn/snapshots/client/createServerFnStarImport.tsx create mode 100644 packages/solid-start-plugin/tests/createServerFn/snapshots/client/createServerFnValidator.tsx create mode 100644 packages/solid-start-plugin/tests/createServerFn/snapshots/server/createServerFnDestructured.tsx create mode 100644 packages/solid-start-plugin/tests/createServerFn/snapshots/server/createServerFnDestructuredRename.tsx create mode 100644 packages/solid-start-plugin/tests/createServerFn/snapshots/server/createServerFnStarImport.tsx create mode 100644 packages/solid-start-plugin/tests/createServerFn/snapshots/server/createServerFnValidator.tsx create mode 100644 packages/solid-start-plugin/tests/createServerFn/test-files/createServerFnDestructured.tsx create mode 100644 packages/solid-start-plugin/tests/createServerFn/test-files/createServerFnDestructuredRename.tsx create mode 100644 packages/solid-start-plugin/tests/createServerFn/test-files/createServerFnStarImport.tsx create mode 100644 packages/solid-start-plugin/tests/createServerFn/test-files/createServerFnValidator.tsx create mode 100644 packages/solid-start-plugin/tests/envOnly/envOnly.test.ts create mode 100644 packages/solid-start-plugin/tests/envOnly/snapshots/client/envOnlyDestructured.tsx create mode 100644 packages/solid-start-plugin/tests/envOnly/snapshots/client/envOnlyDestructuredRename.tsx create mode 100644 packages/solid-start-plugin/tests/envOnly/snapshots/client/envOnlyStarImport.tsx create mode 100644 packages/solid-start-plugin/tests/envOnly/snapshots/server/envOnlyDestructured.tsx create mode 100644 packages/solid-start-plugin/tests/envOnly/snapshots/server/envOnlyDestructuredRename.tsx create mode 100644 packages/solid-start-plugin/tests/envOnly/snapshots/server/envOnlyStarImport.tsx create mode 100644 packages/solid-start-plugin/tests/envOnly/test-files/envOnlyDestructured.tsx create mode 100644 packages/solid-start-plugin/tests/envOnly/test-files/envOnlyDestructuredRename.tsx create mode 100644 packages/solid-start-plugin/tests/envOnly/test-files/envOnlyStarImport.tsx create mode 100644 packages/solid-start-plugin/tsconfig.json create mode 100644 packages/solid-start-plugin/vite.config.ts diff --git a/packages/solid-start-config/package.json b/packages/solid-start-config/package.json index bab610faa0..55a644c221 100644 --- a/packages/solid-start-config/package.json +++ b/packages/solid-start-config/package.json @@ -55,7 +55,7 @@ "@tanstack/router-generator": "workspace:^", "@tanstack/router-plugin": "workspace:^", "@tanstack/server-functions-plugin": "workspace:^", - "@tanstack/start-plugin": "workspace:^", + "@tanstack/solid-start-plugin": "workspace:^", "@tanstack/solid-start-server-functions-handler": "workspace:^", "vite-plugin-solid": "^2.11.2", "import-meta-resolve": "^4.1.0", diff --git a/packages/solid-start-plugin/README.md b/packages/solid-start-plugin/README.md new file mode 100644 index 0000000000..798fa094a3 --- /dev/null +++ b/packages/solid-start-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/solid-start-plugin/eslint.config.js b/packages/solid-start-plugin/eslint.config.js new file mode 100644 index 0000000000..8ce6ad05fc --- /dev/null +++ b/packages/solid-start-plugin/eslint.config.js @@ -0,0 +1,5 @@ +// @ts-check + +import rootConfig from '../../eslint.config.js' + +export default [...rootConfig] diff --git a/packages/solid-start-plugin/package.json b/packages/solid-start-plugin/package.json new file mode 100644 index 0000000000..96e0d3bebd --- /dev/null +++ b/packages/solid-start-plugin/package.json @@ -0,0 +1,86 @@ +{ + "name": "@tanstack/solid-start-plugin", + "version": "1.111.2", + "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/solid-start-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", + "clean:snapshots": "rimraf **/*snapshot* --glob", + "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/code-frame": "7.26.2", + "@babel/core": "^7.26.8", + "@babel/plugin-syntax-jsx": "^7.25.9", + "@babel/plugin-syntax-typescript": "^7.25.9", + "@babel/template": "^7.26.8", + "@babel/traverse": "^7.26.8", + "@babel/types": "^7.26.8", + "@tanstack/router-utils": "workspace:^", + "babel-dead-code-elimination": "^1.0.9", + "tiny-invariant": "^1.3.3", + "vite": "6.1.0" + }, + "devDependencies": { + "@types/babel__code-frame": "^7.0.6", + "@types/babel__core": "^7.20.5", + "@types/babel__template": "^7.4.4", + "@types/babel__traverse": "^7.20.6" + } +} diff --git a/packages/solid-start-plugin/src/compilers.ts b/packages/solid-start-plugin/src/compilers.ts new file mode 100644 index 0000000000..0fd65af65c --- /dev/null +++ b/packages/solid-start-plugin/src/compilers.ts @@ -0,0 +1,584 @@ +import * as babel from '@babel/core' +import * as t from '@babel/types' +import { codeFrameColumns } from '@babel/code-frame' +import { + deadCodeElimination, + findReferencedIdentifiers, +} from 'babel-dead-code-elimination' +import { generateFromAst, parseAst } from '@tanstack/router-utils' +import type { GeneratorResult, ParseAstOptions } from '@tanstack/router-utils' + +// build these once and reuse them +const handleServerOnlyCallExpression = + buildEnvOnlyCallExpressionHandler('server') +const handleClientOnlyCallExpression = + buildEnvOnlyCallExpressionHandler('client') + +type CompileOptions = ParseAstOptions & { + env: 'server' | 'client' | 'ssr' + dce?: boolean +} + +type IdentifierConfig = { + name: string + type: 'ImportSpecifier' | 'ImportNamespaceSpecifier' + namespaceId: string + handleCallExpression: ( + path: babel.NodePath, + opts: CompileOptions, + ) => void + paths: Array +} + +export function compileStartOutput(opts: CompileOptions): GeneratorResult { + const ast = parseAst(opts) + + const doDce = opts.dce ?? true + // find referenced identifiers *before* we transform anything + const refIdents = doDce ? findReferencedIdentifiers(ast) : undefined + + 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/solid-start') { + return + } + + // handle a destructured imports being renamed like "import { createServerFn as myCreateServerFn } from '@tanstack/solid-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/solid-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, + ) + }) + }) + }, + }, + }) + + if (doDce) { + deadCodeElimination(ast, refIdents) + } + + return generateFromAst(ast, { + sourceMaps: true, + sourceFileName: opts.filename, + filename: opts.filename, + }) +} + +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) + // }) + + // 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( + [t.identifier('opts'), t.identifier('signal')], + t.blockStatement( + // Everything in here is server-only, since the client + // will strip out anything in the 'use server' directive. + [ + t.returnStatement( + t.callExpression( + t.identifier(`${existingVariableName}.__executeServer`), + [t.identifier('opts'), t.identifier('signal')], + ), + ), + ], + [t.directive(t.directiveLiteral('use server'))], + ), + ), + ) + + if (opts.env === 'server') { + callExpressionPaths.handler.node.arguments.push(handlerFn) + } +} + +function handleCreateMiddlewareCallExpression( + path: babel.NodePath, + opts: ParseAstOptions, +) { + const rootCallExpression = getRootCallExpression(path) + + // if (debug) + // console.info( + // 'Handling createMiddleware call expression:', + // rootCallExpression.toString(), + // ) + + 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 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, + ) + } + } + } + + const serverFnPath = callExpressionPaths.server?.get( + 'arguments.0', + ) as babel.NodePath + + if ( + callExpressionPaths.server && + serverFnPath.node && + (opts.env === 'client' || opts.env === 'ssr') + ) { + // If we're on the client, remove the server call expression + 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()) + + 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] + + 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: CompileOptions, +) { + const rootCallExpression = getRootCallExpression(path) + + // if (debug) + // console.info( + // 'Handling createIsomorphicFn call expression:', + // rootCallExpression.toString(), + // ) + + const callExpressionPaths = { + client: null as babel.NodePath | null, + server: null as babel.NodePath | null, + } + + const validMethods = Object.keys(callExpressionPaths) + + rootCallExpression.traverse({ + MemberExpression(memberExpressionPath) { + if (t.isIdentifier(memberExpressionPath.node.property)) { + const name = memberExpressionPath.node.property + .name as keyof typeof callExpressionPaths + + if ( + validMethods.includes(name) && + memberExpressionPath.parentPath.isCallExpression() + ) { + callExpressionPaths[name] = memberExpressionPath.parentPath + } + } + }, + }) + + if ( + validMethods.every( + (method) => + !callExpressionPaths[method as keyof typeof callExpressionPaths], + ) + ) { + const variableId = rootCallExpression.parentPath.isVariableDeclarator() + ? rootCallExpression.parentPath.node.id + : null + console.warn( + 'createIsomorphicFn called without a client or server implementation!', + 'This will result in a no-op function.', + 'Variable name:', + t.isIdentifier(variableId) ? variableId.name : 'unknown', + ) + } + + const resolvedEnv = opts.env === 'ssr' ? 'server' : opts.env + + const envCallExpression = callExpressionPaths[resolvedEnv] + + 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().${resolvedEnv}(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/solid-start-plugin/src/index.ts b/packages/solid-start-plugin/src/index.ts new file mode 100644 index 0000000000..f24f09d18a --- /dev/null +++ b/packages/solid-start-plugin/src/index.ts @@ -0,0 +1,130 @@ +import { fileURLToPath, pathToFileURL } from 'node:url' +import path from 'node:path' +import { existsSync } from 'node:fs' +import { logDiff } from '@tanstack/router-utils' +import { compileStartOutput } from './compilers' +import type { Plugin } from 'vite' + +const debug = + process.env.TSR_VITE_DEBUG && + ['true', 'solid-start-plugin'].includes(process.env.TSR_VITE_DEBUG) + +export type TanStackStartViteOptions = { + globalMiddlewareEntry: string +} + +const transformFuncs = [ + 'createServerFn', + 'createMiddleware', + 'serverOnly', + 'clientOnly', + 'createIsomorphicFn', +] +const tokenRegex = new RegExp(transformFuncs.join('|')) +// const eitherFuncRegex = new RegExp( +// `(function ${transformFuncs.join('|function ')})`, +// ) + +export function createTanStackStartPlugin(opts: TanStackStartViteOptions): { + client: Array + ssr: Array + server: Array +} { + const globalMiddlewarePlugin = (): Plugin => { + let entry: string | null = null + let resolvedGlobalMiddlewareEntry: string | null = null + let globalMiddlewareEntryExists = false + let ROOT: string = process.cwd() + return { + name: 'vite-plugin-tanstack-start-ensure-global-middleware', + enforce: 'pre', + configResolved: (config) => { + ROOT = config.root + entry = path.resolve(ROOT, (config as any).router.handler) + resolvedGlobalMiddlewareEntry = path.resolve( + ROOT, + opts.globalMiddlewareEntry, + ) + globalMiddlewareEntryExists = existsSync(resolvedGlobalMiddlewareEntry) + + if (!entry) { + throw new Error( + '@tanstack/solid-start-plugin: No server entry found!', + ) + } + }, + transform(code, id) { + if (entry && id.includes(entry)) { + if (globalMiddlewareEntryExists) { + return { + code: `${code}\n\nimport '${path.resolve(ROOT, opts.globalMiddlewareEntry)}'`, + map: null, + } + } + } + return code + }, + } + } + + return { + client: [ + globalMiddlewarePlugin(), + TanStackStartServerFnsAndMiddleware({ ...opts, env: 'client' }), + ], + ssr: [ + globalMiddlewarePlugin(), + TanStackStartServerFnsAndMiddleware({ ...opts, env: 'ssr' }), + ], + server: [ + globalMiddlewarePlugin(), + TanStackStartServerFnsAndMiddleware({ ...opts, env: 'server' }), + ], + } +} + +export function TanStackStartServerFnsAndMiddleware(opts: { + env: 'server' | 'ssr' | 'client' +}): Plugin { + let ROOT: string = process.cwd() + + return { + name: 'vite-plugin-tanstack-start-create-server-fn', + enforce: 'pre', + configResolved: (config) => { + ROOT = config.root + }, + transform(code, id) { + const url = pathToFileURL(id) + url.searchParams.delete('v') + id = fileURLToPath(url).replace(/\\/g, '/') + + const includesToken = tokenRegex.test(code) + // const includesEitherFunc = eitherFuncRegex.test(code) + + if ( + !includesToken + // includesEitherFunc + // /node_modules/.test(id) + ) { + return null + } + + if (debug) console.info(`${opts.env} Compiling Start: `, id) + + const compiled = compileStartOutput({ + code, + root: ROOT, + filename: id, + env: opts.env, + }) + + if (debug) { + logDiff(code, compiled.code) + console.log('Output:\n', compiled.code + '\n\n') + } + + return compiled + }, + } +} diff --git a/packages/solid-start-plugin/tests/createIsomorphicFn/createIsomorphicFn.test.ts b/packages/solid-start-plugin/tests/createIsomorphicFn/createIsomorphicFn.test.ts new file mode 100644 index 0000000000..9d1e78228e --- /dev/null +++ b/packages/solid-start-plugin/tests/createIsomorphicFn/createIsomorphicFn.test.ts @@ -0,0 +1,94 @@ +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, + dce: false, + }) + + await expect(compiledResult.code).toMatchFileSnapshot( + `./snapshots/${env}/${filename}`, + ) + }, + ) + }) + test('should error if implementation not provided', () => { + expect(() => { + compileStartOutput({ + env: 'client', + code: ` + import { createIsomorphicFn } from '@tanstack/solid-start' + const clientOnly = createIsomorphicFn().client()`, + root: './test-files', + filename: 'no-fn.ts', + dce: false, + }) + }).toThrowError() + expect(() => { + compileStartOutput({ + env: 'server', + code: ` + import { createIsomorphicFn } from '@tanstack/solid-start' + const serverOnly = createIsomorphicFn().server()`, + root: './test-files', + filename: 'no-fn.ts', + dce: false, + }) + }).toThrowError() + }) + test('should warn to console if no implementations provided', () => { + compileStartOutput({ + env: 'client', + code: ` + import { createIsomorphicFn } from '@tanstack/solid-start' + const noImpl = createIsomorphicFn()`, + root: './test-files', + filename: 'no-fn.ts', + dce: false, + }) + expect(consoleSpy).toHaveBeenCalledWith( + noImplWarning, + 'This will result in a no-op function.', + 'Variable name:', + 'noImpl', + ) + }) +}) diff --git a/packages/solid-start-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructured.tsx b/packages/solid-start-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructured.tsx new file mode 100644 index 0000000000..269cab1607 --- /dev/null +++ b/packages/solid-start-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructured.tsx @@ -0,0 +1,16 @@ +import { createIsomorphicFn } from '@tanstack/solid-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/solid-start-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructuredRename.tsx b/packages/solid-start-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructuredRename.tsx new file mode 100644 index 0000000000..3c5c041e90 --- /dev/null +++ b/packages/solid-start-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructuredRename.tsx @@ -0,0 +1,16 @@ +import { createIsomorphicFn as isomorphicFn } from '@tanstack/solid-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/solid-start-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnStarImport.tsx b/packages/solid-start-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnStarImport.tsx new file mode 100644 index 0000000000..5b90ddeaf1 --- /dev/null +++ b/packages/solid-start-plugin/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnStarImport.tsx @@ -0,0 +1,16 @@ +import * as TanStackStart from '@tanstack/solid-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/solid-start-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructured.tsx b/packages/solid-start-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructured.tsx new file mode 100644 index 0000000000..126e2404c5 --- /dev/null +++ b/packages/solid-start-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructured.tsx @@ -0,0 +1,16 @@ +import { createIsomorphicFn } from '@tanstack/solid-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/solid-start-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructuredRename.tsx b/packages/solid-start-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructuredRename.tsx new file mode 100644 index 0000000000..442156f4e3 --- /dev/null +++ b/packages/solid-start-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructuredRename.tsx @@ -0,0 +1,16 @@ +import { createIsomorphicFn as isomorphicFn } from '@tanstack/solid-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/solid-start-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnStarImport.tsx b/packages/solid-start-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnStarImport.tsx new file mode 100644 index 0000000000..318745b993 --- /dev/null +++ b/packages/solid-start-plugin/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnStarImport.tsx @@ -0,0 +1,16 @@ +import * as TanStackStart from '@tanstack/solid-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/solid-start-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnDestructured.tsx b/packages/solid-start-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnDestructured.tsx new file mode 100644 index 0000000000..51263bea85 --- /dev/null +++ b/packages/solid-start-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnDestructured.tsx @@ -0,0 +1,35 @@ +import { createIsomorphicFn } from '@tanstack/solid-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/solid-start-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnDestructuredRename.tsx b/packages/solid-start-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnDestructuredRename.tsx new file mode 100644 index 0000000000..6606a86c50 --- /dev/null +++ b/packages/solid-start-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnDestructuredRename.tsx @@ -0,0 +1,35 @@ +import { createIsomorphicFn as isomorphicFn } from '@tanstack/solid-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/solid-start-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnStarImport.tsx b/packages/solid-start-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnStarImport.tsx new file mode 100644 index 0000000000..9ca4b15b94 --- /dev/null +++ b/packages/solid-start-plugin/tests/createIsomorphicFn/test-files/createIsomorphicFnStarImport.tsx @@ -0,0 +1,37 @@ +import * as TanStackStart from '@tanstack/solid-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/solid-start-plugin/tests/createMiddleware/createMiddleware.test.ts b/packages/solid-start-plugin/tests/createMiddleware/createMiddleware.test.ts new file mode 100644 index 0000000000..05d57dd32d --- /dev/null +++ b/packages/solid-start-plugin/tests/createMiddleware/createMiddleware.test.ts @@ -0,0 +1,37 @@ +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, + dce: false, + }) + + await expect(compiledResult.code).toMatchFileSnapshot( + `./snapshots/${env}/${filename}`, + ) + }, + ) + }) +}) diff --git a/packages/solid-start-plugin/tests/createMiddleware/snapshots/client/createMiddlewareDestructured.tsx b/packages/solid-start-plugin/tests/createMiddleware/snapshots/client/createMiddlewareDestructured.tsx new file mode 100644 index 0000000000..ac0d1d2a9a --- /dev/null +++ b/packages/solid-start-plugin/tests/createMiddleware/snapshots/client/createMiddlewareDestructured.tsx @@ -0,0 +1,24 @@ +import { createMiddleware } from '@tanstack/solid-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/solid-start-plugin/tests/createMiddleware/snapshots/client/createMiddlewareDestructuredRename.tsx b/packages/solid-start-plugin/tests/createMiddleware/snapshots/client/createMiddlewareDestructuredRename.tsx new file mode 100644 index 0000000000..3e917e901f --- /dev/null +++ b/packages/solid-start-plugin/tests/createMiddleware/snapshots/client/createMiddlewareDestructuredRename.tsx @@ -0,0 +1,24 @@ +import { createMiddleware as middlewareFn } from '@tanstack/solid-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/solid-start-plugin/tests/createMiddleware/snapshots/client/createMiddlewareStarImport.tsx b/packages/solid-start-plugin/tests/createMiddleware/snapshots/client/createMiddlewareStarImport.tsx new file mode 100644 index 0000000000..00d22094e8 --- /dev/null +++ b/packages/solid-start-plugin/tests/createMiddleware/snapshots/client/createMiddlewareStarImport.tsx @@ -0,0 +1,24 @@ +import * as TanStackStart from '@tanstack/solid-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/solid-start-plugin/tests/createMiddleware/snapshots/client/createMiddlewareValidator.tsx b/packages/solid-start-plugin/tests/createMiddleware/snapshots/client/createMiddlewareValidator.tsx new file mode 100644 index 0000000000..40a49dca05 --- /dev/null +++ b/packages/solid-start-plugin/tests/createMiddleware/snapshots/client/createMiddlewareValidator.tsx @@ -0,0 +1,5 @@ +import { createMiddleware } from '@tanstack/solid-start'; +import { z } from 'zod'; +export const withUseServer = createMiddleware({ + id: 'test' +}); \ No newline at end of file diff --git a/packages/solid-start-plugin/tests/createMiddleware/snapshots/server/createMiddlewareDestructured.tsx b/packages/solid-start-plugin/tests/createMiddleware/snapshots/server/createMiddlewareDestructured.tsx new file mode 100644 index 0000000000..dda24f7c4e --- /dev/null +++ b/packages/solid-start-plugin/tests/createMiddleware/snapshots/server/createMiddlewareDestructured.tsx @@ -0,0 +1,36 @@ +import { createMiddleware } from '@tanstack/solid-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/solid-start-plugin/tests/createMiddleware/snapshots/server/createMiddlewareDestructuredRename.tsx b/packages/solid-start-plugin/tests/createMiddleware/snapshots/server/createMiddlewareDestructuredRename.tsx new file mode 100644 index 0000000000..7fcc6b7b5a --- /dev/null +++ b/packages/solid-start-plugin/tests/createMiddleware/snapshots/server/createMiddlewareDestructuredRename.tsx @@ -0,0 +1,36 @@ +import { createMiddleware as middlewareFn } from '@tanstack/solid-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/solid-start-plugin/tests/createMiddleware/snapshots/server/createMiddlewareStarImport.tsx b/packages/solid-start-plugin/tests/createMiddleware/snapshots/server/createMiddlewareStarImport.tsx new file mode 100644 index 0000000000..92047bfa0a --- /dev/null +++ b/packages/solid-start-plugin/tests/createMiddleware/snapshots/server/createMiddlewareStarImport.tsx @@ -0,0 +1,38 @@ +import * as TanStackStart from '@tanstack/solid-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/solid-start-plugin/tests/createMiddleware/snapshots/server/createMiddlewareValidator.tsx b/packages/solid-start-plugin/tests/createMiddleware/snapshots/server/createMiddlewareValidator.tsx new file mode 100644 index 0000000000..eb3d933b77 --- /dev/null +++ b/packages/solid-start-plugin/tests/createMiddleware/snapshots/server/createMiddlewareValidator.tsx @@ -0,0 +1,7 @@ +import { createMiddleware } from '@tanstack/solid-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/solid-start-plugin/tests/createMiddleware/test-files/createMiddlewareDestructured.tsx b/packages/solid-start-plugin/tests/createMiddleware/test-files/createMiddlewareDestructured.tsx new file mode 100644 index 0000000000..595099cdbe --- /dev/null +++ b/packages/solid-start-plugin/tests/createMiddleware/test-files/createMiddlewareDestructured.tsx @@ -0,0 +1,51 @@ +import { createMiddleware } from '@tanstack/solid-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/solid-start-plugin/tests/createMiddleware/test-files/createMiddlewareDestructuredRename.tsx b/packages/solid-start-plugin/tests/createMiddleware/test-files/createMiddlewareDestructuredRename.tsx new file mode 100644 index 0000000000..700da4fe63 --- /dev/null +++ b/packages/solid-start-plugin/tests/createMiddleware/test-files/createMiddlewareDestructuredRename.tsx @@ -0,0 +1,51 @@ +import { createMiddleware as middlewareFn } from '@tanstack/solid-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/solid-start-plugin/tests/createMiddleware/test-files/createMiddlewareStarImport.tsx b/packages/solid-start-plugin/tests/createMiddleware/test-files/createMiddlewareStarImport.tsx new file mode 100644 index 0000000000..315d29e1c7 --- /dev/null +++ b/packages/solid-start-plugin/tests/createMiddleware/test-files/createMiddlewareStarImport.tsx @@ -0,0 +1,52 @@ +import * as TanStackStart from '@tanstack/solid-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/solid-start-plugin/tests/createMiddleware/test-files/createMiddlewareValidator.tsx b/packages/solid-start-plugin/tests/createMiddleware/test-files/createMiddlewareValidator.tsx new file mode 100644 index 0000000000..62ea02b36a --- /dev/null +++ b/packages/solid-start-plugin/tests/createMiddleware/test-files/createMiddlewareValidator.tsx @@ -0,0 +1,8 @@ +import { createMiddleware } from '@tanstack/solid-start' +import { z } from 'zod' + +export const withUseServer = createMiddleware({ + id: 'test', +}) + .validator(z.number()) + .server(({ input }) => input + 1) diff --git a/packages/solid-start-plugin/tests/createServerFn/createServerFn.test.ts b/packages/solid-start-plugin/tests/createServerFn/createServerFn.test.ts new file mode 100644 index 0000000000..8d30fee5ad --- /dev/null +++ b/packages/solid-start-plugin/tests/createServerFn/createServerFn.test.ts @@ -0,0 +1,176 @@ +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, + dce: false, + }) + + 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/solid-start' + createServerFn()`, + root: './test-files', + filename: 'no-fn.ts', + dce: false, + }) + }).toThrowError() + }) + + test('should be assigned to a variable', () => { + expect(() => { + compileStartOutput({ + env: 'client', + code: ` + import { createServerFn } from '@tanstack/solid-start' + createServerFn().handler(async () => {})`, + root: './test-files', + filename: 'no-fn.ts', + dce: false, + }) + }).toThrowError() + }) + + test('should work with identifiers of functions', () => { + const code = ` + import { createServerFn } from '@tanstack/solid-start' + const myFunc = () => { + return 'hello from the server' + } + const myServerFn = createServerFn().handler(myFunc)` + + const compiledResultClient = compileStartOutput({ + root: '/test', + filename: 'test.ts', + code, + env: 'client', + dce: false, + }) + + const compiledResultServer = compileStartOutput({ + root: '/test', + filename: 'test.ts', + code, + env: 'server', + dce: false, + }) + + expect(compiledResultClient.code).toMatchInlineSnapshot(` + "import { createServerFn } from '@tanstack/solid-start'; + const myServerFn = createServerFn().handler((opts, signal) => { + "use server"; + + return myServerFn.__executeServer(opts, signal); + });" + `) + + expect(compiledResultServer.code).toMatchInlineSnapshot(` + "import { createServerFn } from '@tanstack/solid-start'; + const myFunc = () => { + return 'hello from the server'; + }; + const myServerFn = createServerFn().handler((opts, signal) => { + "use server"; + + return myServerFn.__executeServer(opts, signal); + }, myFunc);" + `) + }) + + test('should use dce by default', () => { + const code = ` + import { createServerFn } from '@tanstack/solid-start' + const exportedVar = 'exported' + export const exportedFn = createServerFn().handler(async () => { + return exportedVar + }) + const nonExportedVar = 'non-exported' + const nonExportedFn = createServerFn().handler(async () => { + return nonExportedVar + })` + + // Client + const compiledResult = compileStartOutput({ + root: '/test', + filename: 'test.ts', + code, + env: 'client', + dce: true, + }) + + expect(compiledResult.code).toMatchInlineSnapshot(` + "import { createServerFn } from '@tanstack/solid-start'; + export const exportedFn = createServerFn().handler((opts, signal) => { + "use server"; + + return exportedFn.__executeServer(opts, signal); + }); + const nonExportedFn = createServerFn().handler((opts, signal) => { + "use server"; + + return nonExportedFn.__executeServer(opts, signal); + });" + `) + + // Server + const compiledResultServer = compileStartOutput({ + root: '/test', + filename: 'test.ts', + code, + env: 'server', + dce: true, + }) + + expect(compiledResultServer.code).toMatchInlineSnapshot(` + "import { createServerFn } from '@tanstack/solid-start'; + const exportedVar = 'exported'; + export const exportedFn = createServerFn().handler((opts, signal) => { + "use server"; + + return exportedFn.__executeServer(opts, signal); + }, async () => { + return exportedVar; + }); + const nonExportedVar = 'non-exported'; + const nonExportedFn = createServerFn().handler((opts, signal) => { + "use server"; + + return nonExportedFn.__executeServer(opts, signal); + }, async () => { + return nonExportedVar; + });" + `) + }) +}) diff --git a/packages/solid-start-plugin/tests/createServerFn/snapshots/client/createServerFnDestructured.tsx b/packages/solid-start-plugin/tests/createServerFn/snapshots/client/createServerFnDestructured.tsx new file mode 100644 index 0000000000..d8105165f0 --- /dev/null +++ b/packages/solid-start-plugin/tests/createServerFn/snapshots/client/createServerFnDestructured.tsx @@ -0,0 +1,56 @@ +import { createServerFn } from '@tanstack/solid-start'; +import { z } from 'zod'; +export const withUseServer = createServerFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withUseServer.__executeServer(opts, signal); +}); +export const withArrowFunction = createServerFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withArrowFunction.__executeServer(opts, signal); +}); +export const withArrowFunctionAndFunction = createServerFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withArrowFunctionAndFunction.__executeServer(opts, signal); +}); +export const withoutUseServer = createServerFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withoutUseServer.__executeServer(opts, signal); +}); +export const withVariable = createServerFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withVariable.__executeServer(opts, signal); +}); +function zodValidator(schema: TSchema, fn: (input: z.output) => TResult) { + return async (input: unknown) => { + return fn(schema.parse(input)); + }; +} +export const withZodValidator = createServerFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withZodValidator.__executeServer(opts, signal); +}); +export const withValidatorFn = createServerFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withValidatorFn.__executeServer(opts, signal); +}); \ No newline at end of file diff --git a/packages/solid-start-plugin/tests/createServerFn/snapshots/client/createServerFnDestructuredRename.tsx b/packages/solid-start-plugin/tests/createServerFn/snapshots/client/createServerFnDestructuredRename.tsx new file mode 100644 index 0000000000..e7d1010563 --- /dev/null +++ b/packages/solid-start-plugin/tests/createServerFn/snapshots/client/createServerFnDestructuredRename.tsx @@ -0,0 +1,35 @@ +import { createServerFn as serverFn } from '@tanstack/solid-start'; +import { z } from 'zod'; +export const withUseServer = serverFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withUseServer.__executeServer(opts, signal); +}); +export const withoutUseServer = serverFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withoutUseServer.__executeServer(opts, signal); +}); +export const withVariable = serverFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withVariable.__executeServer(opts, signal); +}); +function zodValidator(schema: TSchema, fn: (input: z.output) => TResult) { + return async (input: unknown) => { + return fn(schema.parse(input)); + }; +} +export const withZodValidator = serverFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withZodValidator.__executeServer(opts, signal); +}); \ No newline at end of file diff --git a/packages/solid-start-plugin/tests/createServerFn/snapshots/client/createServerFnStarImport.tsx b/packages/solid-start-plugin/tests/createServerFn/snapshots/client/createServerFnStarImport.tsx new file mode 100644 index 0000000000..e2735ea5d1 --- /dev/null +++ b/packages/solid-start-plugin/tests/createServerFn/snapshots/client/createServerFnStarImport.tsx @@ -0,0 +1,35 @@ +import * as TanStackStart from '@tanstack/solid-start'; +import { z } from 'zod'; +export const withUseServer = TanStackStart.createServerFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withUseServer.__executeServer(opts, signal); +}); +export const withoutUseServer = TanStackStart.createServerFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withoutUseServer.__executeServer(opts, signal); +}); +export const withVariable = TanStackStart.createServerFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withVariable.__executeServer(opts, signal); +}); +function zodValidator(schema: TSchema, fn: (input: z.output) => TResult) { + return async (input: unknown) => { + return fn(schema.parse(input)); + }; +} +export const withZodValidator = TanStackStart.createServerFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withZodValidator.__executeServer(opts, signal); +}); \ No newline at end of file diff --git a/packages/solid-start-plugin/tests/createServerFn/snapshots/client/createServerFnValidator.tsx b/packages/solid-start-plugin/tests/createServerFn/snapshots/client/createServerFnValidator.tsx new file mode 100644 index 0000000000..f43f06ee88 --- /dev/null +++ b/packages/solid-start-plugin/tests/createServerFn/snapshots/client/createServerFnValidator.tsx @@ -0,0 +1,9 @@ +import { createServerFn } from '@tanstack/solid-start'; +import { z } from 'zod'; +export const withUseServer = createServerFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withUseServer.__executeServer(opts, signal); +}); \ No newline at end of file diff --git a/packages/solid-start-plugin/tests/createServerFn/snapshots/server/createServerFnDestructured.tsx b/packages/solid-start-plugin/tests/createServerFn/snapshots/server/createServerFnDestructured.tsx new file mode 100644 index 0000000000..673c4e90fe --- /dev/null +++ b/packages/solid-start-plugin/tests/createServerFn/snapshots/server/createServerFnDestructured.tsx @@ -0,0 +1,77 @@ +import { createServerFn } from '@tanstack/solid-start'; +import { z } from 'zod'; +export const withUseServer = createServerFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withUseServer.__executeServer(opts, signal); +}, async function () { + console.info('Fetching posts...'); + await new Promise(r => setTimeout(r, 500)); + return axios.get>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10)); +}); +export const withArrowFunction = createServerFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withArrowFunction.__executeServer(opts, signal); +}, async () => null); +export const withArrowFunctionAndFunction = createServerFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withArrowFunctionAndFunction.__executeServer(opts, signal); +}, async () => test()); +export const withoutUseServer = createServerFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withoutUseServer.__executeServer(opts, signal); +}, async () => { + console.info('Fetching posts...'); + await new Promise(r => setTimeout(r, 500)); + return axios.get>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10)); +}); +export const withVariable = createServerFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withVariable.__executeServer(opts, signal); +}, abstractedFunction); +async function abstractedFunction() { + console.info('Fetching posts...'); + await new Promise(r => setTimeout(r, 500)); + return axios.get>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10)); +} +function zodValidator(schema: TSchema, fn: (input: z.output) => TResult) { + return async (input: unknown) => { + return fn(schema.parse(input)); + }; +} +export const withZodValidator = createServerFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withZodValidator.__executeServer(opts, signal); +}, zodValidator(z.number(), input => { + return { + 'you gave': input + }; +})); +export const withValidatorFn = createServerFn({ + method: 'GET' +}).validator(z.number()).handler((opts, signal) => { + "use server"; + + return withValidatorFn.__executeServer(opts, signal); +}, async ({ + input +}) => { + return null; +}); \ No newline at end of file diff --git a/packages/solid-start-plugin/tests/createServerFn/snapshots/server/createServerFnDestructuredRename.tsx b/packages/solid-start-plugin/tests/createServerFn/snapshots/server/createServerFnDestructuredRename.tsx new file mode 100644 index 0000000000..d9902417f5 --- /dev/null +++ b/packages/solid-start-plugin/tests/createServerFn/snapshots/server/createServerFnDestructuredRename.tsx @@ -0,0 +1,52 @@ +import { createServerFn as serverFn } from '@tanstack/solid-start'; +import { z } from 'zod'; +export const withUseServer = serverFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withUseServer.__executeServer(opts, signal); +}, async function () { + console.info('Fetching posts...'); + await new Promise(r => setTimeout(r, 500)); + return axios.get>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10)); +}); +export const withoutUseServer = serverFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withoutUseServer.__executeServer(opts, signal); +}, async () => { + console.info('Fetching posts...'); + await new Promise(r => setTimeout(r, 500)); + return axios.get>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10)); +}); +export const withVariable = serverFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withVariable.__executeServer(opts, signal); +}, abstractedFunction); +async function abstractedFunction() { + console.info('Fetching posts...'); + await new Promise(r => setTimeout(r, 500)); + return axios.get>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10)); +} +function zodValidator(schema: TSchema, fn: (input: z.output) => TResult) { + return async (input: unknown) => { + return fn(schema.parse(input)); + }; +} +export const withZodValidator = serverFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withZodValidator.__executeServer(opts, signal); +}, zodValidator(z.number(), input => { + return { + 'you gave': input + }; +})); \ No newline at end of file diff --git a/packages/solid-start-plugin/tests/createServerFn/snapshots/server/createServerFnStarImport.tsx b/packages/solid-start-plugin/tests/createServerFn/snapshots/server/createServerFnStarImport.tsx new file mode 100644 index 0000000000..83964ecc5c --- /dev/null +++ b/packages/solid-start-plugin/tests/createServerFn/snapshots/server/createServerFnStarImport.tsx @@ -0,0 +1,54 @@ +import * as TanStackStart from '@tanstack/solid-start'; +import { z } from 'zod'; +export const withUseServer = TanStackStart.createServerFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withUseServer.__executeServer(opts, signal); +}, async function () { + 'use server'; + + console.info('Fetching posts...'); + await new Promise(r => setTimeout(r, 500)); + return axios.get>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10)); +}); +export const withoutUseServer = TanStackStart.createServerFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withoutUseServer.__executeServer(opts, signal); +}, async () => { + console.info('Fetching posts...'); + await new Promise(r => setTimeout(r, 500)); + return axios.get>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10)); +}); +export const withVariable = TanStackStart.createServerFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withVariable.__executeServer(opts, signal); +}, abstractedFunction); +async function abstractedFunction() { + console.info('Fetching posts...'); + await new Promise(r => setTimeout(r, 500)); + return axios.get>('https://jsonplaceholder.typicode.com/posts').then(r => r.data.slice(0, 10)); +} +function zodValidator(schema: TSchema, fn: (input: z.output) => TResult) { + return async (input: unknown) => { + return fn(schema.parse(input)); + }; +} +export const withZodValidator = TanStackStart.createServerFn({ + method: 'GET' +}).handler((opts, signal) => { + "use server"; + + return withZodValidator.__executeServer(opts, signal); +}, zodValidator(z.number(), input => { + return { + 'you gave': input + }; +})); \ No newline at end of file diff --git a/packages/solid-start-plugin/tests/createServerFn/snapshots/server/createServerFnValidator.tsx b/packages/solid-start-plugin/tests/createServerFn/snapshots/server/createServerFnValidator.tsx new file mode 100644 index 0000000000..364f8354fa --- /dev/null +++ b/packages/solid-start-plugin/tests/createServerFn/snapshots/server/createServerFnValidator.tsx @@ -0,0 +1,11 @@ +import { createServerFn } from '@tanstack/solid-start'; +import { z } from 'zod'; +export const withUseServer = createServerFn({ + method: 'GET' +}).validator(z.number()).handler((opts, signal) => { + "use server"; + + return withUseServer.__executeServer(opts, signal); +}, ({ + input +}) => input + 1); \ No newline at end of file diff --git a/packages/solid-start-plugin/tests/createServerFn/test-files/createServerFnDestructured.tsx b/packages/solid-start-plugin/tests/createServerFn/test-files/createServerFnDestructured.tsx new file mode 100644 index 0000000000..fd7f48510d --- /dev/null +++ b/packages/solid-start-plugin/tests/createServerFn/test-files/createServerFnDestructured.tsx @@ -0,0 +1,67 @@ +import { createServerFn } from '@tanstack/solid-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/solid-start-plugin/tests/createServerFn/test-files/createServerFnDestructuredRename.tsx b/packages/solid-start-plugin/tests/createServerFn/test-files/createServerFnDestructuredRename.tsx new file mode 100644 index 0000000000..823339b3ac --- /dev/null +++ b/packages/solid-start-plugin/tests/createServerFn/test-files/createServerFnDestructuredRename.tsx @@ -0,0 +1,51 @@ +import { createServerFn as serverFn } from '@tanstack/solid-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/solid-start-plugin/tests/createServerFn/test-files/createServerFnStarImport.tsx b/packages/solid-start-plugin/tests/createServerFn/test-files/createServerFnStarImport.tsx new file mode 100644 index 0000000000..d4ecddb304 --- /dev/null +++ b/packages/solid-start-plugin/tests/createServerFn/test-files/createServerFnStarImport.tsx @@ -0,0 +1,52 @@ +import * as TanStackStart from '@tanstack/solid-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/solid-start-plugin/tests/createServerFn/test-files/createServerFnValidator.tsx b/packages/solid-start-plugin/tests/createServerFn/test-files/createServerFnValidator.tsx new file mode 100644 index 0000000000..0d535eb369 --- /dev/null +++ b/packages/solid-start-plugin/tests/createServerFn/test-files/createServerFnValidator.tsx @@ -0,0 +1,8 @@ +import { createServerFn } from '@tanstack/solid-start' +import { z } from 'zod' + +export const withUseServer = createServerFn({ + method: 'GET', +}) + .validator(z.number()) + .handler(({ input }) => input + 1) diff --git a/packages/solid-start-plugin/tests/envOnly/envOnly.test.ts b/packages/solid-start-plugin/tests/envOnly/envOnly.test.ts new file mode 100644 index 0000000000..00c9786170 --- /dev/null +++ b/packages/solid-start-plugin/tests/envOnly/envOnly.test.ts @@ -0,0 +1,61 @@ +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, + dce: false, + }) + + await expect(compiledResult.code).toMatchFileSnapshot( + `./snapshots/${env}/${filename}`, + ) + }, + ) + }) + test('should error if implementation not provided', () => { + expect(() => { + compileStartOutput({ + env: 'client', + code: ` + import { clientOnly } from '@tanstack/solid-start' + const fn = clientOnly()`, + root: './test-files', + filename: 'no-fn.ts', + dce: false, + }) + }).toThrowError() + expect(() => { + compileStartOutput({ + env: 'server', + code: ` + import { serverOnly } from '@tanstack/solid-start' + const fn = serverOnly()`, + root: './test-files', + filename: 'no-fn.ts', + dce: false, + }) + }).toThrowError() + }) +}) diff --git a/packages/solid-start-plugin/tests/envOnly/snapshots/client/envOnlyDestructured.tsx b/packages/solid-start-plugin/tests/envOnly/snapshots/client/envOnlyDestructured.tsx new file mode 100644 index 0000000000..8080d677e6 --- /dev/null +++ b/packages/solid-start-plugin/tests/envOnly/snapshots/client/envOnlyDestructured.tsx @@ -0,0 +1,5 @@ +import { serverOnly, clientOnly } from '@tanstack/solid-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/solid-start-plugin/tests/envOnly/snapshots/client/envOnlyDestructuredRename.tsx b/packages/solid-start-plugin/tests/envOnly/snapshots/client/envOnlyDestructuredRename.tsx new file mode 100644 index 0000000000..c3e67e045b --- /dev/null +++ b/packages/solid-start-plugin/tests/envOnly/snapshots/client/envOnlyDestructuredRename.tsx @@ -0,0 +1,5 @@ +import { serverOnly as serverFn, clientOnly as clientFn } from '@tanstack/solid-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/solid-start-plugin/tests/envOnly/snapshots/client/envOnlyStarImport.tsx b/packages/solid-start-plugin/tests/envOnly/snapshots/client/envOnlyStarImport.tsx new file mode 100644 index 0000000000..d860ab6d21 --- /dev/null +++ b/packages/solid-start-plugin/tests/envOnly/snapshots/client/envOnlyStarImport.tsx @@ -0,0 +1,5 @@ +import * as TanstackStart from '@tanstack/solid-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/solid-start-plugin/tests/envOnly/snapshots/server/envOnlyDestructured.tsx b/packages/solid-start-plugin/tests/envOnly/snapshots/server/envOnlyDestructured.tsx new file mode 100644 index 0000000000..35bf2907cc --- /dev/null +++ b/packages/solid-start-plugin/tests/envOnly/snapshots/server/envOnlyDestructured.tsx @@ -0,0 +1,5 @@ +import { serverOnly, clientOnly } from '@tanstack/solid-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/solid-start-plugin/tests/envOnly/snapshots/server/envOnlyDestructuredRename.tsx b/packages/solid-start-plugin/tests/envOnly/snapshots/server/envOnlyDestructuredRename.tsx new file mode 100644 index 0000000000..31c1c2a23b --- /dev/null +++ b/packages/solid-start-plugin/tests/envOnly/snapshots/server/envOnlyDestructuredRename.tsx @@ -0,0 +1,5 @@ +import { serverOnly as serverFn, clientOnly as clientFn } from '@tanstack/solid-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/solid-start-plugin/tests/envOnly/snapshots/server/envOnlyStarImport.tsx b/packages/solid-start-plugin/tests/envOnly/snapshots/server/envOnlyStarImport.tsx new file mode 100644 index 0000000000..318b47dd64 --- /dev/null +++ b/packages/solid-start-plugin/tests/envOnly/snapshots/server/envOnlyStarImport.tsx @@ -0,0 +1,5 @@ +import * as TanstackStart from '@tanstack/solid-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/solid-start-plugin/tests/envOnly/test-files/envOnlyDestructured.tsx b/packages/solid-start-plugin/tests/envOnly/test-files/envOnlyDestructured.tsx new file mode 100644 index 0000000000..c4513f737d --- /dev/null +++ b/packages/solid-start-plugin/tests/envOnly/test-files/envOnlyDestructured.tsx @@ -0,0 +1,5 @@ +import { serverOnly, clientOnly } from '@tanstack/solid-start' + +const serverFunc = serverOnly(() => 'server') + +const clientFunc = clientOnly(() => 'client') diff --git a/packages/solid-start-plugin/tests/envOnly/test-files/envOnlyDestructuredRename.tsx b/packages/solid-start-plugin/tests/envOnly/test-files/envOnlyDestructuredRename.tsx new file mode 100644 index 0000000000..4b48ba3b58 --- /dev/null +++ b/packages/solid-start-plugin/tests/envOnly/test-files/envOnlyDestructuredRename.tsx @@ -0,0 +1,8 @@ +import { + serverOnly as serverFn, + clientOnly as clientFn, +} from '@tanstack/solid-start' + +const serverFunc = serverFn(() => 'server') + +const clientFunc = clientFn(() => 'client') diff --git a/packages/solid-start-plugin/tests/envOnly/test-files/envOnlyStarImport.tsx b/packages/solid-start-plugin/tests/envOnly/test-files/envOnlyStarImport.tsx new file mode 100644 index 0000000000..adeab52616 --- /dev/null +++ b/packages/solid-start-plugin/tests/envOnly/test-files/envOnlyStarImport.tsx @@ -0,0 +1,5 @@ +import * as TanstackStart from '@tanstack/solid-start' + +const serverFunc = TanstackStart.serverOnly(() => 'server') + +const clientFunc = TanstackStart.clientOnly(() => 'client') diff --git a/packages/solid-start-plugin/tsconfig.json b/packages/solid-start-plugin/tsconfig.json new file mode 100644 index 0000000000..37d21ef6ca --- /dev/null +++ b/packages/solid-start-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/solid-start-plugin/vite.config.ts b/packages/solid-start-plugin/vite.config.ts new file mode 100644 index 0000000000..5389f0f739 --- /dev/null +++ b/packages/solid-start-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/pnpm-lock.yaml b/pnpm-lock.yaml index ef18ab1bc7..219435d236 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5788,12 +5788,12 @@ importers: '@tanstack/solid-router': specifier: workspace:^ version: link:../solid-router + '@tanstack/solid-start-plugin': + specifier: workspace:^ + version: link:../solid-start-plugin '@tanstack/solid-start-server-functions-handler': specifier: workspace:^ version: link:../solid-start-server-functions-handler - '@tanstack/start-plugin': - specifier: workspace:* - version: link:../start-plugin import-meta-resolve: specifier: ^4.1.0 version: 4.1.0 @@ -5819,6 +5819,55 @@ importers: specifier: ^3.24.1 version: 3.24.1 + packages/solid-start-plugin: + dependencies: + '@babel/code-frame': + specifier: 7.26.2 + version: 7.26.2 + '@babel/core': + specifier: ^7.26.8 + version: 7.26.8 + '@babel/plugin-syntax-jsx': + specifier: ^7.25.9 + version: 7.25.9(@babel/core@7.26.8) + '@babel/plugin-syntax-typescript': + specifier: ^7.25.9 + version: 7.25.9(@babel/core@7.26.8) + '@babel/template': + specifier: ^7.26.8 + version: 7.26.8 + '@babel/traverse': + specifier: ^7.26.8 + version: 7.26.8 + '@babel/types': + specifier: ^7.26.8 + version: 7.26.8 + '@tanstack/router-utils': + specifier: workspace:* + version: link:../router-utils + babel-dead-code-elimination: + specifier: ^1.0.9 + version: 1.0.9 + tiny-invariant: + specifier: ^1.3.3 + version: 1.3.3 + vite: + specifier: 6.1.0 + version: 6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) + devDependencies: + '@types/babel__code-frame': + specifier: ^7.0.6 + version: 7.0.6 + '@types/babel__core': + specifier: ^7.20.5 + version: 7.20.5 + '@types/babel__template': + specifier: ^7.4.4 + version: 7.4.4 + '@types/babel__traverse': + specifier: ^7.20.6 + version: 7.20.6 + packages/solid-start-router-manifest: dependencies: '@tanstack/router-core': From 31cf3291599394f6a1aed74ba7d90b4d1280b0de Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Tue, 25 Feb 2025 22:16:28 +0100 Subject: [PATCH 050/124] import solid-start-plugin --- packages/solid-start-config/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/solid-start-config/src/index.ts b/packages/solid-start-config/src/index.ts index 876f0a322b..0453e2e0ab 100644 --- a/packages/solid-start-config/src/index.ts +++ b/packages/solid-start-config/src/index.ts @@ -11,7 +11,7 @@ import { config } from 'vinxi/plugins/config' // // @ts-expect-error // import { serverComponents } from '@vinxi/server-components/plugin' import { createTanStackServerFnPlugin } from '@tanstack/server-functions-plugin' -import { createTanStackStartPlugin } from '@tanstack/start-plugin' +import { createTanStackStartPlugin } from '@tanstack/solid-start-plugin' import { createFetch } from 'ofetch' import { createNitro } from 'nitropack' import { tanstackStartVinxiFileRouter } from './vinxi-file-router.js' From 83b0cb9d3f12417229a1b1fae575329b50163a76 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Tue, 25 Feb 2025 22:39:25 +0100 Subject: [PATCH 051/124] lock --- pnpm-lock.yaml | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 219435d236..74b7e09cef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1881,6 +1881,61 @@ importers: specifier: ^2.11.2 version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + e2e/solid-start/basic: + dependencies: + '@tanstack/solid-router': + specifier: workspace:^ + version: link:../../../packages/solid-router + '@tanstack/solid-start': + specifier: workspace:^ + version: link:../../../packages/solid-start + redaxios: + specifier: ^0.5.1 + version: 0.5.1 + solid-js: + specifier: ^1.0.0 + version: 1.9.4 + tailwind-merge: + specifier: ^2.6.0 + version: 2.6.0 + vinxi: + specifier: 0.5.3 + version: 0.5.3(@types/node@22.13.4)(db0@0.2.3)(ioredis@5.4.2)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0) + zod: + specifier: ^3.24.1 + version: 3.24.1 + devDependencies: + '@playwright/test': + specifier: ^1.50.1 + version: 1.50.1 + '@tanstack/router-e2e-utils': + specifier: workspace:^ + version: link:../../e2e-utils + '@types/node': + specifier: ^22.10.2 + version: 22.13.4 + autoprefixer: + specifier: ^10.4.20 + version: 10.4.20(postcss@8.5.1) + combinate: + specifier: ^1.1.11 + version: 1.1.11 + postcss: + specifier: ^8.5.1 + version: 8.5.1 + tailwindcss: + specifier: ^3.4.17 + version: 3.4.17 + typescript: + specifier: ^5.7.2 + version: 5.7.3 + vite-plugin-solid: + specifier: ^2.11.2 + version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.4)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + examples/react/authenticated-routes: dependencies: '@tanstack/react-router': From b2787790809721967f48de4434d185cd42c25608 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Tue, 25 Feb 2025 22:43:37 +0100 Subject: [PATCH 052/124] revert change --- packages/react-start-plugin/src/index.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/react-start-plugin/src/index.ts b/packages/react-start-plugin/src/index.ts index 4f27b30f1a..74d1e50ffb 100644 --- a/packages/react-start-plugin/src/index.ts +++ b/packages/react-start-plugin/src/index.ts @@ -110,6 +110,18 @@ export function TanStackStartServerFnsAndMiddleware(opts: { return null } + if (code.includes('@react-refresh')) { + throw new Error( + `We detected that the '@vitejs/plugin-react' was passed before '@tanstack/react-start-plugin'. Please make sure that '@tanstack/router-vite-plugin' is passed before '@vitejs/plugin-react' and try again: +e.g. +plugins: [ + TanStackStartVite(), // Place this before viteReact() + viteReact(), +] +`, + ) + } + if (debug) console.info(`${opts.env} Compiling Start: `, id) const compiled = compileStartOutput({ From 7b1978cf32317d3889a5c50bf2f1dc25e4638536 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Tue, 25 Feb 2025 22:44:08 +0100 Subject: [PATCH 053/124] revert change --- packages/react-start-plugin/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-start-plugin/src/index.ts b/packages/react-start-plugin/src/index.ts index 74d1e50ffb..0909c923f9 100644 --- a/packages/react-start-plugin/src/index.ts +++ b/packages/react-start-plugin/src/index.ts @@ -114,6 +114,7 @@ export function TanStackStartServerFnsAndMiddleware(opts: { throw new Error( `We detected that the '@vitejs/plugin-react' was passed before '@tanstack/react-start-plugin'. Please make sure that '@tanstack/router-vite-plugin' is passed before '@vitejs/plugin-react' and try again: e.g. + plugins: [ TanStackStartVite(), // Place this before viteReact() viteReact(), From bea3b28bf0647e32bfabe62b37aa8a5b2818a345 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Tue, 25 Feb 2025 22:46:36 +0100 Subject: [PATCH 054/124] revert change react-refresh --- .../src/core/router-code-splitter-plugin.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 7a5df622d2..7c1ecc17a8 100644 --- a/packages/router-plugin/src/core/router-code-splitter-plugin.ts +++ b/packages/router-plugin/src/core/router-code-splitter-plugin.ts @@ -55,7 +55,14 @@ type BannedBeforeExternalPlugin = { frameworks: Array } -const bannedBeforeExternalPlugins: Array = [] +const bannedBeforeExternalPlugins: Array = [ + { + identifier: '@react-refresh', + pkg: '@vitejs/plugin-react', + usage: 'viteReact()', + frameworks: ['vite'], + }, +] class FoundPluginInBeforeCode extends Error { constructor(externalPlugin: BannedBeforeExternalPlugin, framework: string) { From 049a8f769c7501a2308ba12fd9d0e1406d343f5c Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Tue, 25 Feb 2025 17:36:26 -0600 Subject: [PATCH 055/124] Fix streaming and hydration --- e2e/solid-start/basic/app/client.tsx | 6 ++---- e2e/solid-start/basic/app/routes/__root.tsx | 15 +++++++++------ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/e2e/solid-start/basic/app/client.tsx b/e2e/solid-start/basic/app/client.tsx index c0189b46fd..4f55fc0a56 100644 --- a/e2e/solid-start/basic/app/client.tsx +++ b/e2e/solid-start/basic/app/client.tsx @@ -1,10 +1,8 @@ /// -import { StartClient } from '@tanstack/solid-start' import { createRouter } from './router' import { hydrate } from 'solid-js/web' +import { RouterProvider } from '@tanstack/solid-router' const router = createRouter() -const appDiv = document.getElementById('app')! - -hydrate(() => , appDiv) +hydrate(() => , document) diff --git a/e2e/solid-start/basic/app/routes/__root.tsx b/e2e/solid-start/basic/app/routes/__root.tsx index 2993bc40e2..79b2355640 100644 --- a/e2e/solid-start/basic/app/routes/__root.tsx +++ b/e2e/solid-start/basic/app/routes/__root.tsx @@ -74,11 +74,14 @@ function RootComponent() { function RootDocument({ children }: { children: Solid.JSX.Element }) { return ( - - <> + + + + + +
- - +
- - + + ) } From f7be3749c67f39c7a452d5b8996af6bb6a06ab36 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2025 23:37:31 +0000 Subject: [PATCH 056/124] ci: apply automated fixes --- e2e/solid-start/basic/app/routes/__root.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/e2e/solid-start/basic/app/routes/__root.tsx b/e2e/solid-start/basic/app/routes/__root.tsx index 79b2355640..04a68fbd6a 100644 --- a/e2e/solid-start/basic/app/routes/__root.tsx +++ b/e2e/solid-start/basic/app/routes/__root.tsx @@ -81,7 +81,6 @@ function RootDocument({ children }: { children: Solid.JSX.Element }) {
-
Date: Wed, 26 Feb 2025 04:34:58 +0100 Subject: [PATCH 057/124] add timeout for redirect tests --- e2e/solid-start/basic/tests/redirect.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/e2e/solid-start/basic/tests/redirect.spec.ts b/e2e/solid-start/basic/tests/redirect.spec.ts index 1809b531df..1ae32c9f77 100644 --- a/e2e/solid-start/basic/tests/redirect.spec.ts +++ b/e2e/solid-start/basic/tests/redirect.spec.ts @@ -184,6 +184,9 @@ test.describe('redirects', () => { await page.goto( `/redirect/${target}/serverFn/via-useServerFn${reloadDocument ? '?reloadDocument=true' : ''}`, ) + + await page.waitForTimeout(3000); + const button = page.getByTestId('redirect-on-click') let fullPageLoad = false From a03a7ad287fb1f082973f41d7a39a5fe6032ccd4 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 26 Feb 2025 03:35:59 +0000 Subject: [PATCH 058/124] ci: apply automated fixes --- e2e/solid-start/basic/tests/redirect.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/solid-start/basic/tests/redirect.spec.ts b/e2e/solid-start/basic/tests/redirect.spec.ts index 1ae32c9f77..a75be8501f 100644 --- a/e2e/solid-start/basic/tests/redirect.spec.ts +++ b/e2e/solid-start/basic/tests/redirect.spec.ts @@ -185,7 +185,7 @@ test.describe('redirects', () => { `/redirect/${target}/serverFn/via-useServerFn${reloadDocument ? '?reloadDocument=true' : ''}`, ) - await page.waitForTimeout(3000); + await page.waitForTimeout(3000) const button = page.getByTestId('redirect-on-click') From 9abcf82c7fc3d1de2a59fd6ae9e825cfead3bedb Mon Sep 17 00:00:00 2001 From: Iha Shin Date: Thu, 27 Feb 2025 02:48:32 +0900 Subject: [PATCH 059/124] fix: correctly render and hydrate app shell --- e2e/solid-start/basic/app/client.tsx | 6 +- e2e/solid-start/basic/app/routes/__root.tsx | 164 ++++++++---------- .../solid-start-client/src/StartClient.tsx | 19 +- .../solid-start-server/src/StartServer.tsx | 30 +++- .../src/defaultRenderHandler.tsx | 2 +- .../src/defaultStreamHandler.tsx | 68 ++------ .../src/transformStreamWithRouter.ts | 10 -- packages/solid-start-server/vite.config.ts | 5 +- 8 files changed, 138 insertions(+), 166 deletions(-) diff --git a/e2e/solid-start/basic/app/client.tsx b/e2e/solid-start/basic/app/client.tsx index 4f55fc0a56..ba0f02fac0 100644 --- a/e2e/solid-start/basic/app/client.tsx +++ b/e2e/solid-start/basic/app/client.tsx @@ -1,8 +1,8 @@ /// -import { createRouter } from './router' import { hydrate } from 'solid-js/web' -import { RouterProvider } from '@tanstack/solid-router' +import { StartClient } from '@tanstack/solid-start' +import { createRouter } from './router' const router = createRouter() -hydrate(() => , document) +hydrate(() => , document.body) diff --git a/e2e/solid-start/basic/app/routes/__root.tsx b/e2e/solid-start/basic/app/routes/__root.tsx index 04a68fbd6a..7d1b479e26 100644 --- a/e2e/solid-start/basic/app/routes/__root.tsx +++ b/e2e/solid-start/basic/app/routes/__root.tsx @@ -1,14 +1,9 @@ import { - HeadContent, Link, Outlet, - Scripts, createRootRoute, } from '@tanstack/solid-router' -import * as Solid from 'solid-js' -import { Hydration, HydrationScript, NoHydration } from 'solid-js/web' - import { NotFound } from '~/components/NotFound' import appCss from '~/styles/app.css?url' import { seo } from '~/utils/seo' @@ -56,9 +51,77 @@ export const Route = createRootRoute({ function RootComponent() { return ( - + <> +
+ + Home + {' '} + + Posts + {' '} + + Users + {' '} + + Layout + {' '} + + Scripts + {' '} + + Deferred + {' '} + + redirect + {' '} + + This Route Does Not Exist + +
-
+ ) } @@ -71,90 +134,3 @@ function RootComponent() { // default: res.TanStackRouterDevtools, // })), // ) - -function RootDocument({ children }: { children: Solid.JSX.Element }) { - return ( - - - - - - -
-
- - Home - {' '} - - Posts - {' '} - - Users - {' '} - - Layout - {' '} - - Scripts - {' '} - - Deferred - {' '} - - redirect - {' '} - - This Route Does Not Exist - -
- - {children} - - -
- - - ) -} diff --git a/packages/solid-start-client/src/StartClient.tsx b/packages/solid-start-client/src/StartClient.tsx index 7bb49c2e8b..86d13fa7aa 100644 --- a/packages/solid-start-client/src/StartClient.tsx +++ b/packages/solid-start-client/src/StartClient.tsx @@ -1,9 +1,12 @@ import { Await, RouterProvider } from '@tanstack/solid-router' import { hydrate } from './ssr-client' import type { AnyRouter } from '@tanstack/solid-router' +import type { JSXElement } from 'solid-js' let hydrationPromise: Promise>> | undefined +const Dummy = (props: { children?: JSXElement }) => <>{props.children} + export function StartClient(props: { router: AnyRouter }) { if (!hydrationPromise) { if (!props.router.state.matches.length) { @@ -15,7 +18,21 @@ export function StartClient(props: { router: AnyRouter }) { return ( } + children={() => ( + + + ( + + {props.children} + + + )} + /> + + + )} /> ) } diff --git a/packages/solid-start-server/src/StartServer.tsx b/packages/solid-start-server/src/StartServer.tsx index 8f1d54b6b7..0077ba6998 100644 --- a/packages/solid-start-server/src/StartServer.tsx +++ b/packages/solid-start-server/src/StartServer.tsx @@ -1,8 +1,34 @@ -import { RouterProvider } from '@tanstack/solid-router' +import { HeadContent, RouterProvider, Scripts } from '@tanstack/solid-router' +import { Hydration, HydrationScript, NoHydration, ssr } from 'solid-js/web' import type { AnyRouter } from '@tanstack/solid-router' +const docType = ssr('') + export function StartServer(props: { router: TRouter }) { - return + return ( + + {docType as any} + + + + + + + ( + + + {props.children} + + + )} + /> + + + + + ) } diff --git a/packages/solid-start-server/src/defaultRenderHandler.tsx b/packages/solid-start-server/src/defaultRenderHandler.tsx index 0bbd9854a8..47049fe757 100644 --- a/packages/solid-start-server/src/defaultRenderHandler.tsx +++ b/packages/solid-start-server/src/defaultRenderHandler.tsx @@ -10,7 +10,7 @@ export const defaultRenderHandler = defineHandlerCallback( router.serverSsr!.injectedHtml, ).then((htmls) => htmls.join('')) html = html.replace(``, `${injectedHtml}`) - return new Response(`${html}`, { + return new Response(html, { status: router.state.statusCode, headers: responseHeaders, }) diff --git a/packages/solid-start-server/src/defaultStreamHandler.tsx b/packages/solid-start-server/src/defaultStreamHandler.tsx index e11de7ee96..71eace0003 100644 --- a/packages/solid-start-server/src/defaultStreamHandler.tsx +++ b/packages/solid-start-server/src/defaultStreamHandler.tsx @@ -1,71 +1,31 @@ -import { PassThrough } from 'node:stream' import { isbot } from 'isbot' import * as Solid from 'solid-js/web' import { StartServer } from './StartServer' -import { - transformPipeableStreamWithRouter, - transformReadableStreamWithRouter, -} from './transformStreamWithRouter' +import { transformReadableStreamWithRouter } from './transformStreamWithRouter' import { defineHandlerCallback } from './handlerCallback' import type { ReadableStream } from 'node:stream/web' export const defaultStreamHandler = defineHandlerCallback( - ({ request, router, responseHeaders }) => { - if (typeof Solid.renderToStream === 'function') { - const stream = Solid.renderToStream(() => ) + async ({ request, router, responseHeaders }) => { + const { writable, readable } = new TransformStream() - const { writable, readable } = new TransformStream() - stream.pipeTo(writable) + const stream = Solid.renderToStream(() => ) - const responseStream = transformReadableStreamWithRouter( - router, - readable as unknown as ReadableStream, - ) - return new Response(responseStream as any, { - status: router.state.statusCode, - headers: responseHeaders, - }) + if (isbot(request.headers.get('User-Agent'))) { + await stream } + stream.pipeTo(writable) - if (typeof Solid.renderToStream === 'function') { - const reactAppPassthrough = new PassThrough() - - try { - const pipeable = Solid.renderToStream( - () => , - { - ...(isbot(request.headers.get('User-Agent')) - ? { - onCompleteAll() { - pipeable.pipe(reactAppPassthrough) - }, - } - : { - onCompleteShell() { - pipeable.pipe(reactAppPassthrough) - }, - }), - }, - ) - } catch (e) { - console.error('Error in renderToPipeableStream:', e) - } - - const responseStream = transformPipeableStreamWithRouter( - router, - reactAppPassthrough, - ) - return new Response(responseStream as any, { - status: router.state.statusCode, - headers: responseHeaders, - }) - } - - throw new Error( - 'No renderToReadableStream or renderToPipeableStream found in react-dom/server. Ensure you are using a version of react-dom that supports streaming.', + const responseStream = transformReadableStreamWithRouter( + router, + readable as unknown as ReadableStream, ) + return new Response(responseStream as any, { + status: router.state.statusCode, + headers: responseHeaders, + }) }, ) diff --git a/packages/solid-start-server/src/transformStreamWithRouter.ts b/packages/solid-start-server/src/transformStreamWithRouter.ts index 2c88b0517f..10b18c0ad4 100644 --- a/packages/solid-start-server/src/transformStreamWithRouter.ts +++ b/packages/solid-start-server/src/transformStreamWithRouter.ts @@ -1,5 +1,4 @@ import { ReadableStream } from 'node:stream/web' -import { Readable } from 'node:stream' import { createControlledPromise } from '@tanstack/solid-router' import type { AnyRouter } from '@tanstack/solid-router' @@ -10,15 +9,6 @@ export function transformReadableStreamWithRouter( return transformStreamWithRouter(router, routerStream) } -export function transformPipeableStreamWithRouter( - router: AnyRouter, - routerStream: Readable, -) { - return Readable.fromWeb( - transformStreamWithRouter(router, Readable.toWeb(routerStream)), - ) -} - // regex pattern for matching closing body and html tags const patternBodyStart = /()/ diff --git a/packages/solid-start-server/vite.config.ts b/packages/solid-start-server/vite.config.ts index db10017130..c0a7454291 100644 --- a/packages/solid-start-server/vite.config.ts +++ b/packages/solid-start-server/vite.config.ts @@ -6,7 +6,10 @@ import minifyScriptPlugin from './vite-minify-plugin' import type { ViteUserConfig } from 'vitest/config' const config = defineConfig({ - plugins: [minifyScriptPlugin(), solid()] as ViteUserConfig['plugins'], + plugins: [ + minifyScriptPlugin(), + solid({ solid: { generate: 'ssr' } }), + ] as ViteUserConfig['plugins'], test: { name: packageJson.name, watch: false, From ac311d4c49d4862bd7f6eb39a4145903eba7e14e Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Thu, 27 Feb 2025 10:49:24 +0100 Subject: [PATCH 060/124] remove 3s timeout from redirect test --- e2e/solid-start/basic/tests/redirect.spec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/e2e/solid-start/basic/tests/redirect.spec.ts b/e2e/solid-start/basic/tests/redirect.spec.ts index a75be8501f..4f4cb73b1c 100644 --- a/e2e/solid-start/basic/tests/redirect.spec.ts +++ b/e2e/solid-start/basic/tests/redirect.spec.ts @@ -185,8 +185,6 @@ test.describe('redirects', () => { `/redirect/${target}/serverFn/via-useServerFn${reloadDocument ? '?reloadDocument=true' : ''}`, ) - await page.waitForTimeout(3000) - const button = page.getByTestId('redirect-on-click') let fullPageLoad = false From e01e02230beb84a26497ab57959f945f8b5fc458 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Thu, 27 Feb 2025 11:04:19 +0100 Subject: [PATCH 061/124] add publish --- scripts/publish.js | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/scripts/publish.js b/scripts/publish.js index db8db849a4..d8396a4f2f 100644 --- a/scripts/publish.js +++ b/scripts/publish.js @@ -84,6 +84,50 @@ await publish({ name: '@tanstack/create-start', packageDir: 'packages/create-start', }, + { + name: '@tanstack/solid-start', + packageDir: 'packages/solid-start', + }, + { + name: '@tanstack/solid-start-plugin', + packageDir: 'packages/solid-start-plugin', + }, + { + name: '@tanstack/solid-start-client', + packageDir: 'packages/solid-start-client', + }, + { + name: '@tanstack/solid-start-server', + packageDir: 'packages/solid-start-server', + }, + { + name: '@tanstack/solid-start-config', + packageDir: 'packages/solid-start-config', + }, + { + name: '@tanstack/solid-start-api-routes', + packageDir: 'packages/solid-start-api-routes', + }, + { + name: '@tanstack/solid-start-server-functions-fetcher', + packageDir: 'packages/solid-start-server-functions-fetcher', + }, + { + name: '@tanstack/solid-start-server-functions-handler', + packageDir: 'packages/solid-start-server-functions-handler', + }, + { + name: '@tanstack/solid-start-server-functions-client', + packageDir: 'packages/solid-start-server-functions-client', + }, + { + name: '@tanstack/solid-start-server-functions-ssr', + packageDir: 'packages/solid-start-server-functions-ssr', + }, + { + name: '@tanstack/solid-start-router-manifest', + packageDir: 'packages/solid-start-router-manifest', + }, { name: '@tanstack/react-start', packageDir: 'packages/react-start', From 638a1cd653dcfe3c6155f232c0ba6642c77e9bd5 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Thu, 27 Feb 2025 11:05:32 +0100 Subject: [PATCH 062/124] lockfile --- pnpm-lock.yaml | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 74b7e09cef..c4c6d8c69e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4781,6 +4781,52 @@ importers: specifier: ^2.11.2 version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + examples/solid/start-bare: + dependencies: + '@tanstack/solid-router': + specifier: workspace:^ + version: link:../../../packages/solid-router + '@tanstack/solid-start': + specifier: workspace:^ + version: link:../../../packages/solid-start + redaxios: + specifier: ^0.5.1 + version: 0.5.1 + solid-js: + specifier: ^1.0.0 + version: 1.9.5 + tailwind-merge: + specifier: ^2.6.0 + version: 2.6.0 + vinxi: + specifier: 0.5.3 + version: 0.5.3(@types/node@22.13.4)(db0@0.2.3)(ioredis@5.4.2)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0) + zod: + specifier: ^3.24.1 + version: 3.24.1 + devDependencies: + '@tailwindcss/vite': + specifier: ^4.0.8 + version: 4.0.8(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + '@types/node': + specifier: ^22.10.2 + version: 22.13.4 + combinate: + specifier: ^1.1.11 + version: 1.1.11 + tailwindcss: + specifier: ^4.0.0 + version: 4.0.8 + typescript: + specifier: ^5.7.2 + version: 5.7.3 + vite-plugin-solid: + specifier: ^2.11.2 + version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + packages/arktype-adapter: devDependencies: '@tanstack/react-router': From e1b1c33ea34c9349cb2cf284fb91aef72676af0b Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 27 Feb 2025 10:06:44 +0000 Subject: [PATCH 063/124] ci: apply automated fixes --- e2e/solid-start/basic/app/routes/__root.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/e2e/solid-start/basic/app/routes/__root.tsx b/e2e/solid-start/basic/app/routes/__root.tsx index 7d1b479e26..33fca76bdc 100644 --- a/e2e/solid-start/basic/app/routes/__root.tsx +++ b/e2e/solid-start/basic/app/routes/__root.tsx @@ -1,8 +1,4 @@ -import { - Link, - Outlet, - createRootRoute, -} from '@tanstack/solid-router' +import { Link, Outlet, createRootRoute } from '@tanstack/solid-router' import { NotFound } from '~/components/NotFound' import appCss from '~/styles/app.css?url' From cc543a2a82cee9c67d0a20f35ba377ca83952e25 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Thu, 27 Feb 2025 14:45:07 +0100 Subject: [PATCH 064/124] lint --- packages/solid-start-client/src/renderRSC.tsx | 2 +- packages/solid-start-router-manifest/src/index.ts | 2 +- packages/solid-start-server/src/ssr-server.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/solid-start-client/src/renderRSC.tsx b/packages/solid-start-client/src/renderRSC.tsx index deddfa3484..7c534ebf3a 100644 --- a/packages/solid-start-client/src/renderRSC.tsx +++ b/packages/solid-start-client/src/renderRSC.tsx @@ -2,8 +2,8 @@ // // @ts-expect-error // import * as reactDom from '@vinxi/react-server-dom/client' // import { isValidElement } from 'solid-js' -import * as Solid from 'solid-js' import invariant from 'tiny-invariant' +import type * as Solid from 'solid-js' export function renderRsc(input: any): Solid.JSX.Element { // TODO: isValidElement diff --git a/packages/solid-start-router-manifest/src/index.ts b/packages/solid-start-router-manifest/src/index.ts index b1565c0ed7..9a41e3f788 100644 --- a/packages/solid-start-router-manifest/src/index.ts +++ b/packages/solid-start-router-manifest/src/index.ts @@ -21,7 +21,7 @@ export function getFullRouterManifest() { rootRoute.assets = rootRoute.assets || [] - let script = '' + const script = '' // Always fake that HMR is ready if (process.env.NODE_ENV === 'development') { const CLIENT_BASE = sanitizeBase(process.env.TSS_CLIENT_BASE || '') diff --git a/packages/solid-start-server/src/ssr-server.ts b/packages/solid-start-server/src/ssr-server.ts index af996f7822..87525ae8a7 100644 --- a/packages/solid-start-server/src/ssr-server.ts +++ b/packages/solid-start-server/src/ssr-server.ts @@ -16,7 +16,7 @@ import type { SsrMatch, } from '@tanstack/solid-start-client' import type { AnyRouteMatch, AnyRouter } from '@tanstack/solid-router' -import type { Manifest, DeferredPromise } from '@tanstack/router-core' +import type { DeferredPromise, Manifest } from '@tanstack/router-core' export type ServerExtractedEntry = | ServerExtractedStream From a4d2e81b695f3badfc31a5d261908afb21c84a0a Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Thu, 27 Feb 2025 18:16:18 +0100 Subject: [PATCH 065/124] add e2e/solid-start/webbsite --- e2e/solid-start/basic/app/routes/__root.tsx | 10 - e2e/solid-start/website/.gitignore | 22 ++ e2e/solid-start/website/.prettierignore | 4 + e2e/solid-start/website/app.config.ts | 12 + e2e/solid-start/website/app/client.tsx | 8 + .../app/components/DefaultCatchBoundary.tsx | 53 +++ .../website/app/components/NotFound.tsx | 25 ++ e2e/solid-start/website/app/routeTree.gen.ts | 370 ++++++++++++++++++ e2e/solid-start/website/app/router.tsx | 23 ++ ...t.$version.docs.framework.$framework.$.tsx | 44 +++ ...n.docs.framework.$framework.examples.$.tsx | 32 ++ ...ersion.docs.framework.$framework.index.tsx | 15 + ...ect.$version.docs.framework.$framework.tsx | 123 ++++++ .../routes/$project.$version.docs.index.tsx | 14 + .../website/app/routes/$project.index.tsx | 13 + e2e/solid-start/website/app/routes/__root.tsx | 60 +++ .../_library.$project.$version.index.tsx | 23 ++ .../website/app/routes/_library.$project.tsx | 15 + .../website/app/routes/_library.index.tsx | 13 + .../website/app/routes/_library.tsx | 62 +++ .../website/app/server/document.tsx | 55 +++ .../website/app/server/projects.tsx | 33 ++ e2e/solid-start/website/app/ssr.tsx | 13 + e2e/solid-start/website/app/styles/app.css | 22 ++ e2e/solid-start/website/app/utils/seo.ts | 36 ++ e2e/solid-start/website/package.json | 33 ++ e2e/solid-start/website/playwright.config.ts | 34 ++ e2e/solid-start/website/postcss.config.mjs | 6 + .../website/public/android-chrome-192x192.png | Bin 0 -> 29964 bytes .../website/public/android-chrome-512x512.png | Bin 0 -> 109271 bytes .../website/public/apple-touch-icon.png | Bin 0 -> 27246 bytes .../website/public/favicon-16x16.png | Bin 0 -> 832 bytes .../website/public/favicon-32x32.png | Bin 0 -> 2115 bytes e2e/solid-start/website/public/favicon.ico | Bin 0 -> 15406 bytes e2e/solid-start/website/public/favicon.png | Bin 0 -> 1507 bytes .../website/public/site.webmanifest | 19 + e2e/solid-start/website/tailwind.config.mjs | 4 + e2e/solid-start/website/tests/app.spec.ts | 19 + e2e/solid-start/website/tsconfig.json | 23 ++ pnpm-lock.yaml | 77 ++++ 40 files changed, 1305 insertions(+), 10 deletions(-) create mode 100644 e2e/solid-start/website/.gitignore create mode 100644 e2e/solid-start/website/.prettierignore create mode 100644 e2e/solid-start/website/app.config.ts create mode 100644 e2e/solid-start/website/app/client.tsx create mode 100644 e2e/solid-start/website/app/components/DefaultCatchBoundary.tsx create mode 100644 e2e/solid-start/website/app/components/NotFound.tsx create mode 100644 e2e/solid-start/website/app/routeTree.gen.ts create mode 100644 e2e/solid-start/website/app/router.tsx create mode 100644 e2e/solid-start/website/app/routes/$project.$version.docs.framework.$framework.$.tsx create mode 100644 e2e/solid-start/website/app/routes/$project.$version.docs.framework.$framework.examples.$.tsx create mode 100644 e2e/solid-start/website/app/routes/$project.$version.docs.framework.$framework.index.tsx create mode 100644 e2e/solid-start/website/app/routes/$project.$version.docs.framework.$framework.tsx create mode 100644 e2e/solid-start/website/app/routes/$project.$version.docs.index.tsx create mode 100644 e2e/solid-start/website/app/routes/$project.index.tsx create mode 100644 e2e/solid-start/website/app/routes/__root.tsx create mode 100644 e2e/solid-start/website/app/routes/_library.$project.$version.index.tsx create mode 100644 e2e/solid-start/website/app/routes/_library.$project.tsx create mode 100644 e2e/solid-start/website/app/routes/_library.index.tsx create mode 100644 e2e/solid-start/website/app/routes/_library.tsx create mode 100644 e2e/solid-start/website/app/server/document.tsx create mode 100644 e2e/solid-start/website/app/server/projects.tsx create mode 100644 e2e/solid-start/website/app/ssr.tsx create mode 100644 e2e/solid-start/website/app/styles/app.css create mode 100644 e2e/solid-start/website/app/utils/seo.ts create mode 100644 e2e/solid-start/website/package.json create mode 100644 e2e/solid-start/website/playwright.config.ts create mode 100644 e2e/solid-start/website/postcss.config.mjs create mode 100644 e2e/solid-start/website/public/android-chrome-192x192.png create mode 100644 e2e/solid-start/website/public/android-chrome-512x512.png create mode 100644 e2e/solid-start/website/public/apple-touch-icon.png create mode 100644 e2e/solid-start/website/public/favicon-16x16.png create mode 100644 e2e/solid-start/website/public/favicon-32x32.png create mode 100644 e2e/solid-start/website/public/favicon.ico create mode 100644 e2e/solid-start/website/public/favicon.png create mode 100644 e2e/solid-start/website/public/site.webmanifest create mode 100644 e2e/solid-start/website/tailwind.config.mjs create mode 100644 e2e/solid-start/website/tests/app.spec.ts create mode 100644 e2e/solid-start/website/tsconfig.json diff --git a/e2e/solid-start/basic/app/routes/__root.tsx b/e2e/solid-start/basic/app/routes/__root.tsx index 33fca76bdc..5f92885c6b 100644 --- a/e2e/solid-start/basic/app/routes/__root.tsx +++ b/e2e/solid-start/basic/app/routes/__root.tsx @@ -120,13 +120,3 @@ function RootComponent() { ) } - -// const RouterDevtools = -// process.env.NODE_ENV === 'production' -// ? () => null // Render nothing in production -// : React.lazy(() => -// // Lazy load in development -// import('@tanstack/router-devtools').then((res) => ({ -// default: res.TanStackRouterDevtools, -// })), -// ) diff --git a/e2e/solid-start/website/.gitignore b/e2e/solid-start/website/.gitignore new file mode 100644 index 0000000000..be342025da --- /dev/null +++ b/e2e/solid-start/website/.gitignore @@ -0,0 +1,22 @@ +node_modules +package-lock.json +yarn.lock + +.DS_Store +.cache +.env +.vercel +.output +.vinxi + +/build/ +/api/ +/server/build +/public/build +.vinxi +# Sentry Config File +.env.sentry-build-plugin +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/solid-start/website/.prettierignore b/e2e/solid-start/website/.prettierignore new file mode 100644 index 0000000000..2be5eaa6ec --- /dev/null +++ b/e2e/solid-start/website/.prettierignore @@ -0,0 +1,4 @@ +**/build +**/public +pnpm-lock.yaml +routeTree.gen.ts \ No newline at end of file diff --git a/e2e/solid-start/website/app.config.ts b/e2e/solid-start/website/app.config.ts new file mode 100644 index 0000000000..5c531d7e3d --- /dev/null +++ b/e2e/solid-start/website/app.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from '@tanstack/solid-start/config' +import tsConfigPaths from 'vite-tsconfig-paths' + +export default defineConfig({ + vite: { + plugins: [ + tsConfigPaths({ + projects: ['./tsconfig.json'], + }), + ], + }, +}) diff --git a/e2e/solid-start/website/app/client.tsx b/e2e/solid-start/website/app/client.tsx new file mode 100644 index 0000000000..ba0f02fac0 --- /dev/null +++ b/e2e/solid-start/website/app/client.tsx @@ -0,0 +1,8 @@ +/// +import { hydrate } from 'solid-js/web' +import { StartClient } from '@tanstack/solid-start' +import { createRouter } from './router' + +const router = createRouter() + +hydrate(() => , document.body) diff --git a/e2e/solid-start/website/app/components/DefaultCatchBoundary.tsx b/e2e/solid-start/website/app/components/DefaultCatchBoundary.tsx new file mode 100644 index 0000000000..32aed20e67 --- /dev/null +++ b/e2e/solid-start/website/app/components/DefaultCatchBoundary.tsx @@ -0,0 +1,53 @@ +import { + ErrorComponent, + Link, + rootRouteId, + useMatch, + useRouter, +} from '@tanstack/solid-router' +import type { ErrorComponentProps } from '@tanstack/solid-router' + +export function DefaultCatchBoundary({ error }: ErrorComponentProps) { + const router = useRouter() + const isRoot = useMatch({ + strict: false, + select: (state) => state.id === rootRouteId, + }) + + console.error(error) + + return ( +
+ +
+ + {isRoot() ? ( + + Home + + ) : ( + { + e.preventDefault() + window.history.back() + }} + > + Go Back + + )} +
+
+ ) +} diff --git a/e2e/solid-start/website/app/components/NotFound.tsx b/e2e/solid-start/website/app/components/NotFound.tsx new file mode 100644 index 0000000000..ca4c1960fa --- /dev/null +++ b/e2e/solid-start/website/app/components/NotFound.tsx @@ -0,0 +1,25 @@ +import { Link } from '@tanstack/solid-router' + +export function NotFound({ children }: { children?: any }) { + return ( +
+
+ {children ||

The page you are looking for does not exist.

} +
+

+ + + Start Over + +

+
+ ) +} diff --git a/e2e/solid-start/website/app/routeTree.gen.ts b/e2e/solid-start/website/app/routeTree.gen.ts new file mode 100644 index 0000000000..32515b924b --- /dev/null +++ b/e2e/solid-start/website/app/routeTree.gen.ts @@ -0,0 +1,370 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +// Import Routes + +import { Route as rootRoute } from './routes/__root' +import { Route as LibraryImport } from './routes/_library' +import { Route as LibraryIndexImport } from './routes/_library.index' +import { Route as ProjectIndexImport } from './routes/$project.index' +import { Route as LibraryProjectImport } from './routes/_library.$project' +import { Route as LibraryProjectVersionIndexImport } from './routes/_library.$project.$version.index' +import { Route as ProjectVersionDocsIndexImport } from './routes/$project.$version.docs.index' +import { Route as ProjectVersionDocsFrameworkFrameworkImport } from './routes/$project.$version.docs.framework.$framework' +import { Route as ProjectVersionDocsFrameworkFrameworkIndexImport } from './routes/$project.$version.docs.framework.$framework.index' +import { Route as ProjectVersionDocsFrameworkFrameworkSplatImport } from './routes/$project.$version.docs.framework.$framework.$' +import { Route as ProjectVersionDocsFrameworkFrameworkExamplesSplatImport } from './routes/$project.$version.docs.framework.$framework.examples.$' + +// Create/Update Routes + +const LibraryRoute = LibraryImport.update({ + id: '/_library', + getParentRoute: () => rootRoute, +} as any) + +const LibraryIndexRoute = LibraryIndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => LibraryRoute, +} as any) + +const ProjectIndexRoute = ProjectIndexImport.update({ + id: '/$project/', + path: '/$project/', + getParentRoute: () => rootRoute, +} as any) + +const LibraryProjectRoute = LibraryProjectImport.update({ + id: '/$project', + path: '/$project', + getParentRoute: () => LibraryRoute, +} as any) + +const LibraryProjectVersionIndexRoute = LibraryProjectVersionIndexImport.update( + { + id: '/$version/', + path: '/$version/', + getParentRoute: () => LibraryProjectRoute, + } as any, +) + +const ProjectVersionDocsIndexRoute = ProjectVersionDocsIndexImport.update({ + id: '/$project/$version/docs/', + path: '/$project/$version/docs/', + getParentRoute: () => rootRoute, +} as any) + +const ProjectVersionDocsFrameworkFrameworkRoute = + ProjectVersionDocsFrameworkFrameworkImport.update({ + id: '/$project/$version/docs/framework/$framework', + path: '/$project/$version/docs/framework/$framework', + getParentRoute: () => rootRoute, + } as any) + +const ProjectVersionDocsFrameworkFrameworkIndexRoute = + ProjectVersionDocsFrameworkFrameworkIndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => ProjectVersionDocsFrameworkFrameworkRoute, + } as any) + +const ProjectVersionDocsFrameworkFrameworkSplatRoute = + ProjectVersionDocsFrameworkFrameworkSplatImport.update({ + id: '/$', + path: '/$', + getParentRoute: () => ProjectVersionDocsFrameworkFrameworkRoute, + } as any) + +const ProjectVersionDocsFrameworkFrameworkExamplesSplatRoute = + ProjectVersionDocsFrameworkFrameworkExamplesSplatImport.update({ + id: '/examples/$', + path: '/examples/$', + getParentRoute: () => ProjectVersionDocsFrameworkFrameworkRoute, + } as any) + +// Populate the FileRoutesByPath interface + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/_library': { + id: '/_library' + path: '' + fullPath: '' + preLoaderRoute: typeof LibraryImport + parentRoute: typeof rootRoute + } + '/_library/$project': { + id: '/_library/$project' + path: '/$project' + fullPath: '/$project' + preLoaderRoute: typeof LibraryProjectImport + parentRoute: typeof LibraryImport + } + '/$project/': { + id: '/$project/' + path: '/$project' + fullPath: '/$project' + preLoaderRoute: typeof ProjectIndexImport + parentRoute: typeof rootRoute + } + '/_library/': { + id: '/_library/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof LibraryIndexImport + parentRoute: typeof LibraryImport + } + '/$project/$version/docs/': { + id: '/$project/$version/docs/' + path: '/$project/$version/docs' + fullPath: '/$project/$version/docs' + preLoaderRoute: typeof ProjectVersionDocsIndexImport + parentRoute: typeof rootRoute + } + '/_library/$project/$version/': { + id: '/_library/$project/$version/' + path: '/$version' + fullPath: '/$project/$version' + preLoaderRoute: typeof LibraryProjectVersionIndexImport + parentRoute: typeof LibraryProjectImport + } + '/$project/$version/docs/framework/$framework': { + id: '/$project/$version/docs/framework/$framework' + path: '/$project/$version/docs/framework/$framework' + fullPath: '/$project/$version/docs/framework/$framework' + preLoaderRoute: typeof ProjectVersionDocsFrameworkFrameworkImport + parentRoute: typeof rootRoute + } + '/$project/$version/docs/framework/$framework/$': { + id: '/$project/$version/docs/framework/$framework/$' + path: '/$' + fullPath: '/$project/$version/docs/framework/$framework/$' + preLoaderRoute: typeof ProjectVersionDocsFrameworkFrameworkSplatImport + parentRoute: typeof ProjectVersionDocsFrameworkFrameworkImport + } + '/$project/$version/docs/framework/$framework/': { + id: '/$project/$version/docs/framework/$framework/' + path: '/' + fullPath: '/$project/$version/docs/framework/$framework/' + preLoaderRoute: typeof ProjectVersionDocsFrameworkFrameworkIndexImport + parentRoute: typeof ProjectVersionDocsFrameworkFrameworkImport + } + '/$project/$version/docs/framework/$framework/examples/$': { + id: '/$project/$version/docs/framework/$framework/examples/$' + path: '/examples/$' + fullPath: '/$project/$version/docs/framework/$framework/examples/$' + preLoaderRoute: typeof ProjectVersionDocsFrameworkFrameworkExamplesSplatImport + parentRoute: typeof ProjectVersionDocsFrameworkFrameworkImport + } + } +} + +// Create and export the route tree + +interface LibraryProjectRouteChildren { + LibraryProjectVersionIndexRoute: typeof LibraryProjectVersionIndexRoute +} + +const LibraryProjectRouteChildren: LibraryProjectRouteChildren = { + LibraryProjectVersionIndexRoute: LibraryProjectVersionIndexRoute, +} + +const LibraryProjectRouteWithChildren = LibraryProjectRoute._addFileChildren( + LibraryProjectRouteChildren, +) + +interface LibraryRouteChildren { + LibraryProjectRoute: typeof LibraryProjectRouteWithChildren + LibraryIndexRoute: typeof LibraryIndexRoute +} + +const LibraryRouteChildren: LibraryRouteChildren = { + LibraryProjectRoute: LibraryProjectRouteWithChildren, + LibraryIndexRoute: LibraryIndexRoute, +} + +const LibraryRouteWithChildren = + LibraryRoute._addFileChildren(LibraryRouteChildren) + +interface ProjectVersionDocsFrameworkFrameworkRouteChildren { + ProjectVersionDocsFrameworkFrameworkSplatRoute: typeof ProjectVersionDocsFrameworkFrameworkSplatRoute + ProjectVersionDocsFrameworkFrameworkIndexRoute: typeof ProjectVersionDocsFrameworkFrameworkIndexRoute + ProjectVersionDocsFrameworkFrameworkExamplesSplatRoute: typeof ProjectVersionDocsFrameworkFrameworkExamplesSplatRoute +} + +const ProjectVersionDocsFrameworkFrameworkRouteChildren: ProjectVersionDocsFrameworkFrameworkRouteChildren = + { + ProjectVersionDocsFrameworkFrameworkSplatRoute: + ProjectVersionDocsFrameworkFrameworkSplatRoute, + ProjectVersionDocsFrameworkFrameworkIndexRoute: + ProjectVersionDocsFrameworkFrameworkIndexRoute, + ProjectVersionDocsFrameworkFrameworkExamplesSplatRoute: + ProjectVersionDocsFrameworkFrameworkExamplesSplatRoute, + } + +const ProjectVersionDocsFrameworkFrameworkRouteWithChildren = + ProjectVersionDocsFrameworkFrameworkRoute._addFileChildren( + ProjectVersionDocsFrameworkFrameworkRouteChildren, + ) + +export interface FileRoutesByFullPath { + '': typeof LibraryRouteWithChildren + '/$project': typeof ProjectIndexRoute + '/': typeof LibraryIndexRoute + '/$project/$version/docs': typeof ProjectVersionDocsIndexRoute + '/$project/$version': typeof LibraryProjectVersionIndexRoute + '/$project/$version/docs/framework/$framework': typeof ProjectVersionDocsFrameworkFrameworkRouteWithChildren + '/$project/$version/docs/framework/$framework/$': typeof ProjectVersionDocsFrameworkFrameworkSplatRoute + '/$project/$version/docs/framework/$framework/': typeof ProjectVersionDocsFrameworkFrameworkIndexRoute + '/$project/$version/docs/framework/$framework/examples/$': typeof ProjectVersionDocsFrameworkFrameworkExamplesSplatRoute +} + +export interface FileRoutesByTo { + '/$project': typeof ProjectIndexRoute + '/': typeof LibraryIndexRoute + '/$project/$version/docs': typeof ProjectVersionDocsIndexRoute + '/$project/$version': typeof LibraryProjectVersionIndexRoute + '/$project/$version/docs/framework/$framework/$': typeof ProjectVersionDocsFrameworkFrameworkSplatRoute + '/$project/$version/docs/framework/$framework': typeof ProjectVersionDocsFrameworkFrameworkIndexRoute + '/$project/$version/docs/framework/$framework/examples/$': typeof ProjectVersionDocsFrameworkFrameworkExamplesSplatRoute +} + +export interface FileRoutesById { + __root__: typeof rootRoute + '/_library': typeof LibraryRouteWithChildren + '/_library/$project': typeof LibraryProjectRouteWithChildren + '/$project/': typeof ProjectIndexRoute + '/_library/': typeof LibraryIndexRoute + '/$project/$version/docs/': typeof ProjectVersionDocsIndexRoute + '/_library/$project/$version/': typeof LibraryProjectVersionIndexRoute + '/$project/$version/docs/framework/$framework': typeof ProjectVersionDocsFrameworkFrameworkRouteWithChildren + '/$project/$version/docs/framework/$framework/$': typeof ProjectVersionDocsFrameworkFrameworkSplatRoute + '/$project/$version/docs/framework/$framework/': typeof ProjectVersionDocsFrameworkFrameworkIndexRoute + '/$project/$version/docs/framework/$framework/examples/$': typeof ProjectVersionDocsFrameworkFrameworkExamplesSplatRoute +} + +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '' + | '/$project' + | '/' + | '/$project/$version/docs' + | '/$project/$version' + | '/$project/$version/docs/framework/$framework' + | '/$project/$version/docs/framework/$framework/$' + | '/$project/$version/docs/framework/$framework/' + | '/$project/$version/docs/framework/$framework/examples/$' + fileRoutesByTo: FileRoutesByTo + to: + | '/$project' + | '/' + | '/$project/$version/docs' + | '/$project/$version' + | '/$project/$version/docs/framework/$framework/$' + | '/$project/$version/docs/framework/$framework' + | '/$project/$version/docs/framework/$framework/examples/$' + id: + | '__root__' + | '/_library' + | '/_library/$project' + | '/$project/' + | '/_library/' + | '/$project/$version/docs/' + | '/_library/$project/$version/' + | '/$project/$version/docs/framework/$framework' + | '/$project/$version/docs/framework/$framework/$' + | '/$project/$version/docs/framework/$framework/' + | '/$project/$version/docs/framework/$framework/examples/$' + fileRoutesById: FileRoutesById +} + +export interface RootRouteChildren { + LibraryRoute: typeof LibraryRouteWithChildren + ProjectIndexRoute: typeof ProjectIndexRoute + ProjectVersionDocsIndexRoute: typeof ProjectVersionDocsIndexRoute + ProjectVersionDocsFrameworkFrameworkRoute: typeof ProjectVersionDocsFrameworkFrameworkRouteWithChildren +} + +const rootRouteChildren: RootRouteChildren = { + LibraryRoute: LibraryRouteWithChildren, + ProjectIndexRoute: ProjectIndexRoute, + ProjectVersionDocsIndexRoute: ProjectVersionDocsIndexRoute, + ProjectVersionDocsFrameworkFrameworkRoute: + ProjectVersionDocsFrameworkFrameworkRouteWithChildren, +} + +export const routeTree = rootRoute + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +/* ROUTE_MANIFEST_START +{ + "routes": { + "__root__": { + "filePath": "__root.tsx", + "children": [ + "/_library", + "/$project/", + "/$project/$version/docs/", + "/$project/$version/docs/framework/$framework" + ] + }, + "/_library": { + "filePath": "_library.tsx", + "children": [ + "/_library/$project", + "/_library/" + ] + }, + "/_library/$project": { + "filePath": "_library.$project.tsx", + "parent": "/_library", + "children": [ + "/_library/$project/$version/" + ] + }, + "/$project/": { + "filePath": "$project.index.tsx" + }, + "/_library/": { + "filePath": "_library.index.tsx", + "parent": "/_library" + }, + "/$project/$version/docs/": { + "filePath": "$project.$version.docs.index.tsx" + }, + "/_library/$project/$version/": { + "filePath": "_library.$project.$version.index.tsx", + "parent": "/_library/$project" + }, + "/$project/$version/docs/framework/$framework": { + "filePath": "$project.$version.docs.framework.$framework.tsx", + "children": [ + "/$project/$version/docs/framework/$framework/$", + "/$project/$version/docs/framework/$framework/", + "/$project/$version/docs/framework/$framework/examples/$" + ] + }, + "/$project/$version/docs/framework/$framework/$": { + "filePath": "$project.$version.docs.framework.$framework.$.tsx", + "parent": "/$project/$version/docs/framework/$framework" + }, + "/$project/$version/docs/framework/$framework/": { + "filePath": "$project.$version.docs.framework.$framework.index.tsx", + "parent": "/$project/$version/docs/framework/$framework" + }, + "/$project/$version/docs/framework/$framework/examples/$": { + "filePath": "$project.$version.docs.framework.$framework.examples.$.tsx", + "parent": "/$project/$version/docs/framework/$framework" + } + } +} +ROUTE_MANIFEST_END */ diff --git a/e2e/solid-start/website/app/router.tsx b/e2e/solid-start/website/app/router.tsx new file mode 100644 index 0000000000..cf079db611 --- /dev/null +++ b/e2e/solid-start/website/app/router.tsx @@ -0,0 +1,23 @@ +import { createRouter as createTanStackRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' +import { DefaultCatchBoundary } from './components/DefaultCatchBoundary' +import { NotFound } from './components/NotFound' + +export function createRouter() { + const router = createTanStackRouter({ + routeTree, + scrollRestoration: true, + defaultPreload: 'intent', + defaultStaleTime: 5000, + defaultErrorComponent: DefaultCatchBoundary, + defaultNotFoundComponent: () => , + }) + + return router +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/e2e/solid-start/website/app/routes/$project.$version.docs.framework.$framework.$.tsx b/e2e/solid-start/website/app/routes/$project.$version.docs.framework.$framework.$.tsx new file mode 100644 index 0000000000..1df208a1c4 --- /dev/null +++ b/e2e/solid-start/website/app/routes/$project.$version.docs.framework.$framework.$.tsx @@ -0,0 +1,44 @@ +import { ErrorComponent, createFileRoute } from '@tanstack/solid-router' +import type { ErrorComponentProps } from '@tanstack/solid-router' +import { NotFound } from '~/components/NotFound' +import { getDocument } from '~/server/document' +import { capitalize, seo } from '~/utils/seo' + +export const Route = createFileRoute( + '/$project/$version/docs/framework/$framework/$', +)({ + loader: ({ params: { _splat } }) => + getDocument({ + data: _splat!, + }), + head: ({ loaderData, params }) => ({ + meta: seo({ + title: `${loaderData?.title || 'Project'} | TanStack ${capitalize(params.project)} ${capitalize(params.framework)}`, + }), + }), + errorComponent: PostErrorComponent, + component: Page, + notFoundComponent: () => { + return Document not found + }, +}) + +function PostErrorComponent({ error }: ErrorComponentProps) { + return +} + +function Page() { + const post = Route.useLoaderData() + + return ( +
+

+ {post().title} +

+
{post().content}
+
+ ) +} diff --git a/e2e/solid-start/website/app/routes/$project.$version.docs.framework.$framework.examples.$.tsx b/e2e/solid-start/website/app/routes/$project.$version.docs.framework.$framework.examples.$.tsx new file mode 100644 index 0000000000..53e0c46c1b --- /dev/null +++ b/e2e/solid-start/website/app/routes/$project.$version.docs.framework.$framework.examples.$.tsx @@ -0,0 +1,32 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { NotFound } from '~/components/NotFound' +import { capitalize, seo } from '~/utils/seo' + +export const Route = createFileRoute( + '/$project/$version/docs/framework/$framework/examples/$', +)({ + head: ({ params }) => ({ + meta: seo({ + title: `${capitalize(params._splat || '')} Example | TanStack ${capitalize(params.project)} ${capitalize(params.framework)}`, + }), + }), + component: Page, + notFoundComponent: () => { + return Example not found + }, +}) + +function Page() { + const params = Route.useParams() + + return ( +
+

+ {params()._splat} example +

+
+ ) +} diff --git a/e2e/solid-start/website/app/routes/$project.$version.docs.framework.$framework.index.tsx b/e2e/solid-start/website/app/routes/$project.$version.docs.framework.$framework.index.tsx new file mode 100644 index 0000000000..3f6e4af4ef --- /dev/null +++ b/e2e/solid-start/website/app/routes/$project.$version.docs.framework.$framework.index.tsx @@ -0,0 +1,15 @@ +import { createFileRoute, redirect } from '@tanstack/solid-router' + +export const Route = createFileRoute( + '/$project/$version/docs/framework/$framework/', +)({ + loader: () => { + throw redirect({ + from: '/$project/$version/docs/framework/$framework/', + to: '/$project/$version/docs/framework/$framework/$', + params: { + _splat: 'overview', + }, + }) + }, +}) diff --git a/e2e/solid-start/website/app/routes/$project.$version.docs.framework.$framework.tsx b/e2e/solid-start/website/app/routes/$project.$version.docs.framework.$framework.tsx new file mode 100644 index 0000000000..62d59befae --- /dev/null +++ b/e2e/solid-start/website/app/routes/$project.$version.docs.framework.$framework.tsx @@ -0,0 +1,123 @@ +import { + Link, + Outlet, + createFileRoute, + useLocation, +} from '@tanstack/solid-router' +import { getDocumentHeads } from '~/server/document' +import { getProject } from '~/server/projects' + +export const Route = createFileRoute( + '/$project/$version/docs/framework/$framework', +)({ + loader: async ({ params: { project } }) => { + const library = await getProject({ data: project }) + const documents = await getDocumentHeads() + return { + library, + documents, + } + }, + component: Page, +}) + +function Page() { + const project = Route.useLoaderData({ select: (s) => s.library }) + const documents = Route.useLoaderData({ select: (s) => s.documents }) + const pathname = useLocation({ select: (s) => s.pathname }) + + return ( +
+ +
+

+ {pathname()} +

+ +
+
+ ) +} diff --git a/e2e/solid-start/website/app/routes/$project.$version.docs.index.tsx b/e2e/solid-start/website/app/routes/$project.$version.docs.index.tsx new file mode 100644 index 0000000000..ff049e373c --- /dev/null +++ b/e2e/solid-start/website/app/routes/$project.$version.docs.index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute, redirect } from '@tanstack/solid-router' + +export const Route = createFileRoute('/$project/$version/docs/')({ + loader: () => { + throw redirect({ + from: '/$project/$version/docs', + to: '/$project/$version/docs/framework/$framework/$', + params: { + framework: 'solid', + _splat: 'overview', + }, + }) + }, +}) diff --git a/e2e/solid-start/website/app/routes/$project.index.tsx b/e2e/solid-start/website/app/routes/$project.index.tsx new file mode 100644 index 0000000000..6e250ac011 --- /dev/null +++ b/e2e/solid-start/website/app/routes/$project.index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute, redirect } from '@tanstack/solid-router' + +export const Route = createFileRoute('/$project/')({ + loader: ({ params }) => { + throw redirect({ + to: '/$project/$version', + params: { + project: params.project, + version: 'latest', + }, + }) + }, +}) diff --git a/e2e/solid-start/website/app/routes/__root.tsx b/e2e/solid-start/website/app/routes/__root.tsx new file mode 100644 index 0000000000..8c9a241c64 --- /dev/null +++ b/e2e/solid-start/website/app/routes/__root.tsx @@ -0,0 +1,60 @@ +import { + Outlet, + createRootRoute, +} from '@tanstack/solid-router' +import { NotFound } from '~/components/NotFound' +import appCss from '~/styles/app.css?url' +import { seo } from '~/utils/seo' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { + charset: 'utf-8', + }, + { + name: 'viewport', + content: 'width=device-width, initial-scale=1', + }, + ...seo({ + title: 'TanStack Website', + description: `TanStack projects are type-safe!!!`, + }), + ], + links: [ + { rel: 'stylesheet', href: appCss }, + { + rel: 'apple-touch-icon', + sizes: '180x180', + href: '/apple-touch-icon.png', + }, + { + rel: 'icon', + type: 'image/png', + sizes: '32x32', + href: '/favicon-32x32.png', + }, + { + rel: 'icon', + type: 'image/png', + sizes: '16x16', + href: '/favicon-16x16.png', + }, + { rel: 'manifest', href: '/site.webmanifest', color: '#fffff' }, + { rel: 'icon', href: '/favicon.ico' }, + ], + }), + errorComponent: (props) => { + return

{props.error.stack}

+ }, + notFoundComponent: () => , + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + ) +} diff --git a/e2e/solid-start/website/app/routes/_library.$project.$version.index.tsx b/e2e/solid-start/website/app/routes/_library.$project.$version.index.tsx new file mode 100644 index 0000000000..201e85f8e9 --- /dev/null +++ b/e2e/solid-start/website/app/routes/_library.$project.$version.index.tsx @@ -0,0 +1,23 @@ +import { Link, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_library/$project/$version/')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return ( +
+

+ {params().project} landing page +

+

version: {params().version}

+

+ + Get started with our documentation. + +

+
+ ) +} diff --git a/e2e/solid-start/website/app/routes/_library.$project.tsx b/e2e/solid-start/website/app/routes/_library.$project.tsx new file mode 100644 index 0000000000..6fdcb8e530 --- /dev/null +++ b/e2e/solid-start/website/app/routes/_library.$project.tsx @@ -0,0 +1,15 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { getProject } from '~/server/projects' +import { seo } from '~/utils/seo' + +export const Route = createFileRoute('/_library/$project')({ + loader: ({ params: { project } }) => getProject({ data: project }), + head: ({ loaderData }) => ({ + meta: seo({ title: `TanStack ${loaderData?.name || 'Project'}` }), + }), + component: () => ( +
+ +
+ ), +}) diff --git a/e2e/solid-start/website/app/routes/_library.index.tsx b/e2e/solid-start/website/app/routes/_library.index.tsx new file mode 100644 index 0000000000..25bf776480 --- /dev/null +++ b/e2e/solid-start/website/app/routes/_library.index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_library/')({ + component: Home, +}) + +function Home() { + return ( +
+

Website Landing Page

+
+ ) +} diff --git a/e2e/solid-start/website/app/routes/_library.tsx b/e2e/solid-start/website/app/routes/_library.tsx new file mode 100644 index 0000000000..c25ae5cfe1 --- /dev/null +++ b/e2e/solid-start/website/app/routes/_library.tsx @@ -0,0 +1,62 @@ +import { + Link, + Outlet, + createFileRoute, + useLocation, +} from '@tanstack/solid-router' +import { getProjects } from '~/server/projects' + +export const Route = createFileRoute('/_library')({ + loader: async () => { + const projects = await getProjects() + return { + libraries: projects, + } + }, + component: Layout, +}) + +function Layout() { + const loaderData = Route.useLoaderData() + const pathname = useLocation({ select: (s) => s.pathname }) + return ( +
+ +
+

+ {pathname()} +

+ +
+
+ ) +} diff --git a/e2e/solid-start/website/app/server/document.tsx b/e2e/solid-start/website/app/server/document.tsx new file mode 100644 index 0000000000..0090c3b1b1 --- /dev/null +++ b/e2e/solid-start/website/app/server/document.tsx @@ -0,0 +1,55 @@ +import { notFound } from '@tanstack/solid-router' +import { createServerFn } from '@tanstack/solid-start' + +const documents: Array<{ id: string; title: string; content: string }> = [ + { + id: 'overview', + title: 'Overview', + content: 'This is the content of the overview document', + }, + { + id: 'getting-started', + title: 'Getting Started', + content: 'To get started, you need to do the following...', + }, + { + id: 'installation', + title: 'Installation', + content: 'To install this package, run the following command...', + }, + { + id: 'ref/useQueryFunction', + title: 'useQuery Reference', + content: 'The useQuery function is used to...', + }, + { + id: 'ref/useMutationFunction', + title: 'useMutation Reference', + content: 'The useMutation function is used to...', + }, +] + +export const getDocumentHeads = createServerFn({ method: 'GET' }).handler( + async () => { + await new Promise((resolve) => setTimeout(resolve, 200)) + + return documents.map(({ id, title }) => ({ + id, + title, + })) + }, +) + +export const getDocument = createServerFn({ method: 'GET' }) + .validator((id: string) => id) + .handler(async ({ data: id }) => { + await new Promise((resolve) => setTimeout(resolve, 200)) + + const document = documents.find((doc) => doc.id === id) + + if (!document) { + throw notFound() + } + + return document + }) diff --git a/e2e/solid-start/website/app/server/projects.tsx b/e2e/solid-start/website/app/server/projects.tsx new file mode 100644 index 0000000000..a66de2b2ca --- /dev/null +++ b/e2e/solid-start/website/app/server/projects.tsx @@ -0,0 +1,33 @@ +import { createServerFn } from '@tanstack/solid-start' +import { notFound } from '@tanstack/solid-router' +import { capitalize } from '~/utils/seo' + +const projects = ['router', 'table', 'query', 'form', 'ranger'] + +export const getProjects = createServerFn({ method: 'GET' }).handler( + async () => { + await new Promise((resolve) => setTimeout(resolve, 200)) + + return projects + }, +) + +export const getProject = createServerFn({ method: 'GET' }) + .validator((project: string) => project) + .handler(async (ctx) => { + await new Promise((resolve) => setTimeout(resolve, 200)) + + const selectedProject = projects.find((p) => p === ctx.data.toLowerCase()) + + if (!selectedProject) { + throw notFound() + } + + return { + id: selectedProject, + name: capitalize(selectedProject), + versions: ['latest', 'v2', 'v1'], + frameworks: ['solid', 'react', 'vue', 'solidjs', 'svelte'], + examples: ['basic', 'kitchen-sink'], + } + }) diff --git a/e2e/solid-start/website/app/ssr.tsx b/e2e/solid-start/website/app/ssr.tsx new file mode 100644 index 0000000000..ebd14c8120 --- /dev/null +++ b/e2e/solid-start/website/app/ssr.tsx @@ -0,0 +1,13 @@ +/// +import { + createStartHandler, + defaultStreamHandler, +} from '@tanstack/solid-start/server' +import { getRouterManifest } from '@tanstack/solid-start/router-manifest' + +import { createRouter } from './router' + +export default createStartHandler({ + createRouter, + getRouterManifest, +})(defaultStreamHandler) diff --git a/e2e/solid-start/website/app/styles/app.css b/e2e/solid-start/website/app/styles/app.css new file mode 100644 index 0000000000..c53c870665 --- /dev/null +++ b/e2e/solid-start/website/app/styles/app.css @@ -0,0 +1,22 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + html { + color-scheme: light dark; + } + + * { + @apply border-gray-200 dark:border-gray-800; + } + + html, + body { + @apply text-gray-900 bg-gray-50 dark:bg-gray-950 dark:text-gray-200; + } + + .using-mouse * { + outline: none !important; + } +} diff --git a/e2e/solid-start/website/app/utils/seo.ts b/e2e/solid-start/website/app/utils/seo.ts new file mode 100644 index 0000000000..82cf2aec07 --- /dev/null +++ b/e2e/solid-start/website/app/utils/seo.ts @@ -0,0 +1,36 @@ +export const seo = ({ + title, + description, + keywords, + image, +}: { + title: string + description?: string + image?: string + keywords?: string +}) => { + const tags = [ + { title }, + { name: 'description', content: description }, + { name: 'keywords', content: keywords }, + { name: 'twitter:title', content: title }, + { name: 'twitter:description', content: description }, + { name: 'twitter:creator', content: '@tannerlinsley' }, + { name: 'twitter:site', content: '@tannerlinsley' }, + { name: 'og:type', content: 'website' }, + { name: 'og:title', content: title }, + { name: 'og:description', content: description }, + ...(image + ? [ + { name: 'twitter:image', content: image }, + { name: 'twitter:card', content: 'summary_large_image' }, + { name: 'og:image', content: image }, + ] + : []), + ] + + return tags +} + +export const capitalize = (str: string) => + str.charAt(0).toUpperCase() + str.slice(1) diff --git a/e2e/solid-start/website/package.json b/e2e/solid-start/website/package.json new file mode 100644 index 0000000000..fc60a05ff8 --- /dev/null +++ b/e2e/solid-start/website/package.json @@ -0,0 +1,33 @@ +{ + "name": "tanstack-solid-start-e2e-website", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev": "vinxi dev --port 3000", + "dev:e2e": "vinxi dev", + "build": "vinxi build && tsc --noEmit", + "start": "vinxi start", + "test:e2e": "playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/solid-router": "workspace:^", + "@tanstack/solid-start": "workspace:^", + "solid-js": "^1.0.0", + "redaxios": "^0.5.1", + "tailwind-merge": "^2.6.0", + "vinxi": "0.5.3", + "zod": "^3.24.1" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@tanstack/router-e2e-utils": "workspace:^", + "@types/node": "^22.10.2", + "vite-plugin-solid": "^2.11.6", + "postcss": "^8.5.1", + "autoprefixer": "^10.4.20", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.2", + "vite-tsconfig-paths": "^5.1.4" + } +} diff --git a/e2e/solid-start/website/playwright.config.ts b/e2e/solid-start/website/playwright.config.ts new file mode 100644 index 0000000000..bb77d0cf70 --- /dev/null +++ b/e2e/solid-start/website/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test' +import { derivePort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const PORT = derivePort(packageJson.name) +const baseURL = `http://localhost:${PORT}` +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + webServer: { + command: `VITE_SERVER_PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm start --port ${PORT}`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/solid-start/website/postcss.config.mjs b/e2e/solid-start/website/postcss.config.mjs new file mode 100644 index 0000000000..2e7af2b7f1 --- /dev/null +++ b/e2e/solid-start/website/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/e2e/solid-start/website/public/android-chrome-192x192.png b/e2e/solid-start/website/public/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..09c8324f8c6781bc90fcf5dd38e0702bd5f171f3 GIT binary patch literal 29964 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 b=FG literal 0 HcmV?d00001 diff --git a/e2e/solid-start/website/public/android-chrome-512x512.png b/e2e/solid-start/website/public/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..11d626ea3d00fddd52861bf0af5554a92fc30d69 GIT binary patch literal 109271 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 Ef@mE(_W%F@ literal 0 HcmV?d00001 diff --git a/e2e/solid-start/website/public/apple-touch-icon.png b/e2e/solid-start/website/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5a9423cc02c40ea066d2a061cdcdf621c0718cb1 GIT binary patch literal 27246 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⪼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 N002ovPDHLkV1k;v<;Va4 literal 0 HcmV?d00001 diff --git a/e2e/solid-start/website/public/favicon-16x16.png b/e2e/solid-start/website/public/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..e3389b00443d602a249b48ae0e21e333971c9c1e GIT binary patch literal 832 zcmV-G1Hb%Px%`bk7VR5(v%lW%BSRTRK~=OuZ4FYhI3vvy^4t8t~UDHGBS#44K%hSiDal!9Nj zeb^RZ;3&cb72F3w6s4;uUA7>Cf>1=Z56;a$8wZ=NKHtq z33GDV=_|V0E(QPTEB1=edhG{{=-+C??RQi7*e9J7YBnZR+PppIKjA8EV+}Yt`zH<7 zGR%O5D3|Ef)99hUXu19=Cxaki@6toZIe&u-X0(oDFRl|x6P7Bwc^mB(i9|gOY)Jh6DaPkmt z8Wr!@TP1|RPlYRlJe^eRB0K(gn91{(=zi{HrkpHIflw8&q%;+U!V&dFPYDQAWJh@< zwcklN9VEX?vG;}75GqCepKC25M^xV%BcR%A8c!)S5k4P0$g}HS#lA6zy`~K@9&EX4 zOrvr%_1U2dp~gg6G*;&`Jag*?jZTJv+$S`*#)zme_SFWOCm*F4U&hJihqxAu;8z}x zWv7!u>YKRa3PUJQs55wC)i4QvFOAV0jW$?1cnUJJ7Vl?z_ge{9q!zpfCJxd8vJ>-ql@0&`Bttn1~4;s>8tax%hr(^q#A_*wqAv( zh(T&>hJD?OhiP$sxAzkd1W1^EHV1WL3xM&6$*HhG@|8}OjPpG{+!;GC>w;ha0000< KMNUMnLSTZGXp5cz literal 0 HcmV?d00001 diff --git a/e2e/solid-start/website/public/favicon-32x32.png b/e2e/solid-start/website/public/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..900c77d444c9b31e6144b6ac0bb1ab7bb7ca767f GIT binary patch literal 2115 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 literal 0 HcmV?d00001 diff --git a/e2e/solid-start/website/public/favicon.ico b/e2e/solid-start/website/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1a1751676f7e22811b1070572093996c93c87617 GIT binary patch literal 15406 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 literal 0 HcmV?d00001 diff --git a/e2e/solid-start/website/public/favicon.png b/e2e/solid-start/website/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..1e77bc06091ade4496525a09d8900675afcf03f0 GIT binary patch literal 1507 zcmV<91swW`P)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91AfN*P1ONa40RR91AOHXW0IY^$^8f$^O-V#SR9Fd>S3Qp$MHK9ro!#4$ zEP@L_hX|b@f=!*_42h6mKu7{)4)_U*>1>0bCkUj;Z1X!7 zHe(Ew^Oi(|bW3J~xu+)XbtFF?4>!7TH$>(D_atUQVEj(8fGvYu2NF33#JZX>)(Vj8 zIi@z>Glt?6t~;Lf(|C8F>;WF^8F<^s7Scr!sZc01uB?HMHoL5+FZ>B(g+r-)?Sn)#3Zal#?G@GAwO5U27MpGOlC2+_saA)rl zP-<@-n~;PQOlm|Hi<+W;NdR;5+=zADzM&?!+CPD36=cGwHy6!D^vPEHG?rO`K>G|M z3FposX{yT132wuw1OR3Um_5JoKB#6?!QgBupIT;?YIr;WcpmuCE>S75mZid+ens#E zGPuYjiG0UNNVWu=f!Id^?9)34)eIpu-`j_~W0iAQzK(}XYc_!;87Tk~?4tq|h=2(! zuq0HCiNK)@+ocCKR3q1REdUju>HdYxd>JX@%oOibg+J~D+}rhz54D!NfC{h-OYk{M zkzmFtdrL@nL0bm8nF@pob1CeLC>12ef#in-Bzv2!wi)Iuwq24)`AH}|0QNQ^f$KHv z?5PBPo1*#GAuAk+Poe`?UJ>mP`@~d4a(103j0lwUx@_+$#B&VC%7r>#2$HIiD`KO8L|s3Yp%M}BT0;NJDzZtPnx=4%enhU zhW*pNN0t`^4%5MKAR+}=^Q?QeqQ`>bbK zf+-ji$Uz8V0?LpX@kh`k%DL)GCA2=@SJNKg56Wh>>pr=7{1PmHqG|~=AdLV3002ov JPDHLkV1ivgp)>#h literal 0 HcmV?d00001 diff --git a/e2e/solid-start/website/public/site.webmanifest b/e2e/solid-start/website/public/site.webmanifest new file mode 100644 index 0000000000..fa99de77db --- /dev/null +++ b/e2e/solid-start/website/public/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/e2e/solid-start/website/tailwind.config.mjs b/e2e/solid-start/website/tailwind.config.mjs new file mode 100644 index 0000000000..07c3598bac --- /dev/null +++ b/e2e/solid-start/website/tailwind.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./app/**/*.{js,jsx,ts,tsx}'], +} diff --git a/e2e/solid-start/website/tests/app.spec.ts b/e2e/solid-start/website/tests/app.spec.ts new file mode 100644 index 0000000000..7282086ad4 --- /dev/null +++ b/e2e/solid-start/website/tests/app.spec.ts @@ -0,0 +1,19 @@ +import { expect, test } from '@playwright/test' + +const routeTestId = 'selected-route-label' + +test('resolves to the latest version on load of a project like "/router"', async ({ + page, +}) => { + await page.goto('/router') + + await expect(page.getByTestId(routeTestId)).toContainText('/router/latest') +}) + +test('resolves to the overview docs page', async ({ page }) => { + await page.goto('/router/latest/docs') + + await expect(page.getByTestId(routeTestId)).toContainText( + '/router/latest/docs/framework/solid/overview', + ) +}) diff --git a/e2e/solid-start/website/tsconfig.json b/e2e/solid-start/website/tsconfig.json new file mode 100644 index 0000000000..86fe6d2cf5 --- /dev/null +++ b/e2e/solid-start/website/tsconfig.json @@ -0,0 +1,23 @@ +{ + "include": ["**/*.ts", "**/*.tsx"], + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "isolatedModules": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "target": "ES2022", + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, + "noEmit": true + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4c6d8c69e..3ae7c73624 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1936,6 +1936,58 @@ importers: specifier: ^5.1.4 version: 5.1.4(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + e2e/solid-start/website: + dependencies: + '@tanstack/solid-router': + specifier: workspace:^ + version: link:../../../packages/solid-router + '@tanstack/solid-start': + specifier: workspace:^ + version: link:../../../packages/solid-start + redaxios: + specifier: ^0.5.1 + version: 0.5.1 + solid-js: + specifier: ^1.0.0 + version: 1.9.5 + tailwind-merge: + specifier: ^2.6.0 + version: 2.6.0 + vinxi: + specifier: 0.5.3 + version: 0.5.3(@types/node@22.13.4)(db0@0.2.3)(ioredis@5.4.2)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0) + zod: + specifier: ^3.24.1 + version: 3.24.1 + devDependencies: + '@playwright/test': + specifier: ^1.50.1 + version: 1.50.1 + '@tanstack/router-e2e-utils': + specifier: workspace:^ + version: link:../../e2e-utils + '@types/node': + specifier: ^22.10.2 + version: 22.13.4 + autoprefixer: + specifier: ^10.4.20 + version: 10.4.20(postcss@8.5.1) + postcss: + specifier: ^8.5.1 + version: 8.5.1 + tailwindcss: + specifier: ^3.4.17 + version: 3.4.17 + typescript: + specifier: ^5.7.2 + version: 5.7.3 + vite-plugin-solid: + specifier: ^2.11.6 + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + examples/react/authenticated-routes: dependencies: '@tanstack/react-router': @@ -13647,6 +13699,16 @@ packages: '@testing-library/jest-dom': optional: true + vite-plugin-solid@2.11.6: + resolution: {integrity: sha512-Sl5CTqJTGyEeOsmdH6BOgalIZlwH3t4/y0RQuFLMGnvWMBvxb4+lq7x3BSiAw6etf0QexfNJW7HSOO/Qf7pigg==} + peerDependencies: + '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.* + solid-js: ^1.7.2 + vite: 6.1.0 + peerDependenciesMeta: + '@testing-library/jest-dom': + optional: true + vite-tsconfig-paths@5.1.4: resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} peerDependencies: @@ -21880,6 +21942,21 @@ snapshots: transitivePeerDependencies: - supports-color + vite-plugin-solid@2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)): + dependencies: + '@babel/core': 7.26.8 + '@types/babel__core': 7.20.5 + babel-preset-solid: 1.9.3(@babel/core@7.26.8) + merge-anything: 5.1.7 + solid-js: 1.9.5 + solid-refresh: 0.6.3(solid-js@1.9.5) + vite: 6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) + vitefu: 1.0.5(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + optionalDependencies: + '@testing-library/jest-dom': 6.6.3 + transitivePeerDependencies: + - supports-color + vite-tsconfig-paths@5.1.4(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)): dependencies: debug: 4.4.0(supports-color@9.4.0) From e663e2ed455834797b2669e50719ab1776b1fdb0 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 27 Feb 2025 17:17:25 +0000 Subject: [PATCH 066/124] ci: apply automated fixes --- e2e/solid-start/website/app/routes/__root.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/e2e/solid-start/website/app/routes/__root.tsx b/e2e/solid-start/website/app/routes/__root.tsx index 8c9a241c64..8c3ced26f1 100644 --- a/e2e/solid-start/website/app/routes/__root.tsx +++ b/e2e/solid-start/website/app/routes/__root.tsx @@ -1,7 +1,4 @@ -import { - Outlet, - createRootRoute, -} from '@tanstack/solid-router' +import { Outlet, createRootRoute } from '@tanstack/solid-router' import { NotFound } from '~/components/NotFound' import appCss from '~/styles/app.css?url' import { seo } from '~/utils/seo' From ec0448fe9863d8dcbb5942423d41209d3bf71a52 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Thu, 27 Feb 2025 18:26:57 +0100 Subject: [PATCH 067/124] add server-functions --- e2e/solid-start/server-functions/.gitignore | 22 + .../server-functions/.prettierignore | 4 + .../server-functions/app.config.ts | 12 + .../server-functions/app/client.tsx | 8 + .../app/components/DefaultCatchBoundary.tsx | 53 +++ .../app/components/NotFound.tsx | 25 + .../server-functions/app/routeTree.gen.ts | 430 ++++++++++++++++++ .../server-functions/app/router.tsx | 22 + .../server-functions/app/routes/__root.tsx | 40 ++ .../app/routes/abort-signal.tsx | 85 ++++ .../app/routes/consistent.tsx | 123 +++++ .../app/routes/cookies/index.tsx | 19 + .../app/routes/cookies/set.tsx | 66 +++ .../app/routes/dead-code-preserve.tsx | 60 +++ .../server-functions/app/routes/env-only.tsx | 76 ++++ .../server-functions/app/routes/headers.tsx | 68 +++ .../server-functions/app/routes/index.tsx | 75 +++ .../app/routes/isomorphic-fns.tsx | 71 +++ .../server-functions/app/routes/multipart.tsx | 107 +++++ .../app/routes/return-null.tsx | 68 +++ .../app/routes/serialize-form-data.tsx | 84 ++++ .../server-functions/app/routes/status.tsx | 30 ++ .../app/routes/submit-post-formdata.tsx | 60 +++ e2e/solid-start/server-functions/app/ssr.tsx | 12 + .../server-functions/app/styles/app.css | 22 + e2e/solid-start/server-functions/package.json | 36 ++ .../server-functions/playwright.config.ts | 35 ++ .../server-functions/postcss.config.mjs | 6 + .../server-functions/public/favicon.ico | Bin 0 -> 15406 bytes .../server-functions/public/favicon.png | Bin 0 -> 1507 bytes .../server-functions/tailwind.config.mjs | 4 + .../server-functions/tests/fixture.ts | 28 ++ .../tests/server-functions.spec.ts | 323 +++++++++++++ .../server-functions/tsconfig.json | 23 + pnpm-lock.yaml | 61 +++ 35 files changed, 2158 insertions(+) create mode 100644 e2e/solid-start/server-functions/.gitignore create mode 100644 e2e/solid-start/server-functions/.prettierignore create mode 100644 e2e/solid-start/server-functions/app.config.ts create mode 100644 e2e/solid-start/server-functions/app/client.tsx create mode 100644 e2e/solid-start/server-functions/app/components/DefaultCatchBoundary.tsx create mode 100644 e2e/solid-start/server-functions/app/components/NotFound.tsx create mode 100644 e2e/solid-start/server-functions/app/routeTree.gen.ts create mode 100644 e2e/solid-start/server-functions/app/router.tsx create mode 100644 e2e/solid-start/server-functions/app/routes/__root.tsx create mode 100644 e2e/solid-start/server-functions/app/routes/abort-signal.tsx create mode 100644 e2e/solid-start/server-functions/app/routes/consistent.tsx create mode 100644 e2e/solid-start/server-functions/app/routes/cookies/index.tsx create mode 100644 e2e/solid-start/server-functions/app/routes/cookies/set.tsx create mode 100644 e2e/solid-start/server-functions/app/routes/dead-code-preserve.tsx create mode 100644 e2e/solid-start/server-functions/app/routes/env-only.tsx create mode 100644 e2e/solid-start/server-functions/app/routes/headers.tsx create mode 100644 e2e/solid-start/server-functions/app/routes/index.tsx create mode 100644 e2e/solid-start/server-functions/app/routes/isomorphic-fns.tsx create mode 100644 e2e/solid-start/server-functions/app/routes/multipart.tsx create mode 100644 e2e/solid-start/server-functions/app/routes/return-null.tsx create mode 100644 e2e/solid-start/server-functions/app/routes/serialize-form-data.tsx create mode 100644 e2e/solid-start/server-functions/app/routes/status.tsx create mode 100644 e2e/solid-start/server-functions/app/routes/submit-post-formdata.tsx create mode 100644 e2e/solid-start/server-functions/app/ssr.tsx create mode 100644 e2e/solid-start/server-functions/app/styles/app.css create mode 100644 e2e/solid-start/server-functions/package.json create mode 100644 e2e/solid-start/server-functions/playwright.config.ts create mode 100644 e2e/solid-start/server-functions/postcss.config.mjs create mode 100644 e2e/solid-start/server-functions/public/favicon.ico create mode 100644 e2e/solid-start/server-functions/public/favicon.png create mode 100644 e2e/solid-start/server-functions/tailwind.config.mjs create mode 100644 e2e/solid-start/server-functions/tests/fixture.ts create mode 100644 e2e/solid-start/server-functions/tests/server-functions.spec.ts create mode 100644 e2e/solid-start/server-functions/tsconfig.json diff --git a/e2e/solid-start/server-functions/.gitignore b/e2e/solid-start/server-functions/.gitignore new file mode 100644 index 0000000000..be342025da --- /dev/null +++ b/e2e/solid-start/server-functions/.gitignore @@ -0,0 +1,22 @@ +node_modules +package-lock.json +yarn.lock + +.DS_Store +.cache +.env +.vercel +.output +.vinxi + +/build/ +/api/ +/server/build +/public/build +.vinxi +# Sentry Config File +.env.sentry-build-plugin +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/solid-start/server-functions/.prettierignore b/e2e/solid-start/server-functions/.prettierignore new file mode 100644 index 0000000000..2be5eaa6ec --- /dev/null +++ b/e2e/solid-start/server-functions/.prettierignore @@ -0,0 +1,4 @@ +**/build +**/public +pnpm-lock.yaml +routeTree.gen.ts \ No newline at end of file diff --git a/e2e/solid-start/server-functions/app.config.ts b/e2e/solid-start/server-functions/app.config.ts new file mode 100644 index 0000000000..5c531d7e3d --- /dev/null +++ b/e2e/solid-start/server-functions/app.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from '@tanstack/solid-start/config' +import tsConfigPaths from 'vite-tsconfig-paths' + +export default defineConfig({ + vite: { + plugins: [ + tsConfigPaths({ + projects: ['./tsconfig.json'], + }), + ], + }, +}) diff --git a/e2e/solid-start/server-functions/app/client.tsx b/e2e/solid-start/server-functions/app/client.tsx new file mode 100644 index 0000000000..ba0f02fac0 --- /dev/null +++ b/e2e/solid-start/server-functions/app/client.tsx @@ -0,0 +1,8 @@ +/// +import { hydrate } from 'solid-js/web' +import { StartClient } from '@tanstack/solid-start' +import { createRouter } from './router' + +const router = createRouter() + +hydrate(() => , document.body) diff --git a/e2e/solid-start/server-functions/app/components/DefaultCatchBoundary.tsx b/e2e/solid-start/server-functions/app/components/DefaultCatchBoundary.tsx new file mode 100644 index 0000000000..e15888ddb4 --- /dev/null +++ b/e2e/solid-start/server-functions/app/components/DefaultCatchBoundary.tsx @@ -0,0 +1,53 @@ +import { + ErrorComponent, + Link, + rootRouteId, + useMatch, + useRouter, +} from '@tanstack/solid-router' +import type { ErrorComponentProps } from '@tanstack/solid-router' + +export function DefaultCatchBoundary({ error }: ErrorComponentProps) { + const router = useRouter() + const isRoot = useMatch({ + strict: false, + select: (state) => state.id === rootRouteId, + }) + + console.error(error) + + return ( +
+ +
+ + {isRoot ? ( + + Home + + ) : ( + { + e.preventDefault() + window.history.back() + }} + > + Go Back + + )} +
+
+ ) +} diff --git a/e2e/solid-start/server-functions/app/components/NotFound.tsx b/e2e/solid-start/server-functions/app/components/NotFound.tsx new file mode 100644 index 0000000000..eb0a968d39 --- /dev/null +++ b/e2e/solid-start/server-functions/app/components/NotFound.tsx @@ -0,0 +1,25 @@ +import { Link } from '@tanstack/solid-router' + +export function NotFound({ children }: { children?: any }) { + return ( +
+
+ {children ||

The page you are looking for does not exist.

} +
+

+ + + Start Over + +

+
+ ) +} diff --git a/e2e/solid-start/server-functions/app/routeTree.gen.ts b/e2e/solid-start/server-functions/app/routeTree.gen.ts new file mode 100644 index 0000000000..423eef5f56 --- /dev/null +++ b/e2e/solid-start/server-functions/app/routeTree.gen.ts @@ -0,0 +1,430 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +// Import Routes + +import { Route as rootRoute } from './routes/__root' +import { Route as SubmitPostFormdataImport } from './routes/submit-post-formdata' +import { Route as StatusImport } from './routes/status' +import { Route as SerializeFormDataImport } from './routes/serialize-form-data' +import { Route as ReturnNullImport } from './routes/return-null' +import { Route as MultipartImport } from './routes/multipart' +import { Route as IsomorphicFnsImport } from './routes/isomorphic-fns' +import { Route as HeadersImport } from './routes/headers' +import { Route as EnvOnlyImport } from './routes/env-only' +import { Route as DeadCodePreserveImport } from './routes/dead-code-preserve' +import { Route as ConsistentImport } from './routes/consistent' +import { Route as AbortSignalImport } from './routes/abort-signal' +import { Route as IndexImport } from './routes/index' +import { Route as CookiesIndexImport } from './routes/cookies/index' +import { Route as CookiesSetImport } from './routes/cookies/set' + +// Create/Update Routes + +const SubmitPostFormdataRoute = SubmitPostFormdataImport.update({ + id: '/submit-post-formdata', + path: '/submit-post-formdata', + getParentRoute: () => rootRoute, +} as any) + +const StatusRoute = StatusImport.update({ + id: '/status', + path: '/status', + getParentRoute: () => rootRoute, +} as any) + +const SerializeFormDataRoute = SerializeFormDataImport.update({ + id: '/serialize-form-data', + path: '/serialize-form-data', + getParentRoute: () => rootRoute, +} as any) + +const ReturnNullRoute = ReturnNullImport.update({ + id: '/return-null', + path: '/return-null', + getParentRoute: () => rootRoute, +} as any) + +const MultipartRoute = MultipartImport.update({ + id: '/multipart', + path: '/multipart', + getParentRoute: () => rootRoute, +} as any) + +const IsomorphicFnsRoute = IsomorphicFnsImport.update({ + id: '/isomorphic-fns', + path: '/isomorphic-fns', + getParentRoute: () => rootRoute, +} as any) + +const HeadersRoute = HeadersImport.update({ + id: '/headers', + path: '/headers', + getParentRoute: () => rootRoute, +} as any) + +const EnvOnlyRoute = EnvOnlyImport.update({ + id: '/env-only', + path: '/env-only', + getParentRoute: () => rootRoute, +} as any) + +const DeadCodePreserveRoute = DeadCodePreserveImport.update({ + id: '/dead-code-preserve', + path: '/dead-code-preserve', + getParentRoute: () => rootRoute, +} as any) + +const ConsistentRoute = ConsistentImport.update({ + id: '/consistent', + path: '/consistent', + getParentRoute: () => rootRoute, +} as any) + +const AbortSignalRoute = AbortSignalImport.update({ + id: '/abort-signal', + path: '/abort-signal', + getParentRoute: () => rootRoute, +} as any) + +const IndexRoute = IndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRoute, +} as any) + +const CookiesIndexRoute = CookiesIndexImport.update({ + id: '/cookies/', + path: '/cookies/', + getParentRoute: () => rootRoute, +} as any) + +const CookiesSetRoute = CookiesSetImport.update({ + id: '/cookies/set', + path: '/cookies/set', + getParentRoute: () => rootRoute, +} as any) + +// Populate the FileRoutesByPath interface + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexImport + parentRoute: typeof rootRoute + } + '/abort-signal': { + id: '/abort-signal' + path: '/abort-signal' + fullPath: '/abort-signal' + preLoaderRoute: typeof AbortSignalImport + parentRoute: typeof rootRoute + } + '/consistent': { + id: '/consistent' + path: '/consistent' + fullPath: '/consistent' + preLoaderRoute: typeof ConsistentImport + parentRoute: typeof rootRoute + } + '/dead-code-preserve': { + id: '/dead-code-preserve' + path: '/dead-code-preserve' + fullPath: '/dead-code-preserve' + preLoaderRoute: typeof DeadCodePreserveImport + parentRoute: typeof rootRoute + } + '/env-only': { + id: '/env-only' + path: '/env-only' + fullPath: '/env-only' + preLoaderRoute: typeof EnvOnlyImport + parentRoute: typeof rootRoute + } + '/headers': { + id: '/headers' + path: '/headers' + fullPath: '/headers' + preLoaderRoute: typeof HeadersImport + parentRoute: typeof rootRoute + } + '/isomorphic-fns': { + id: '/isomorphic-fns' + path: '/isomorphic-fns' + fullPath: '/isomorphic-fns' + preLoaderRoute: typeof IsomorphicFnsImport + parentRoute: typeof rootRoute + } + '/multipart': { + id: '/multipart' + path: '/multipart' + fullPath: '/multipart' + preLoaderRoute: typeof MultipartImport + parentRoute: typeof rootRoute + } + '/return-null': { + id: '/return-null' + path: '/return-null' + fullPath: '/return-null' + preLoaderRoute: typeof ReturnNullImport + parentRoute: typeof rootRoute + } + '/serialize-form-data': { + id: '/serialize-form-data' + path: '/serialize-form-data' + fullPath: '/serialize-form-data' + preLoaderRoute: typeof SerializeFormDataImport + parentRoute: typeof rootRoute + } + '/status': { + id: '/status' + path: '/status' + fullPath: '/status' + preLoaderRoute: typeof StatusImport + parentRoute: typeof rootRoute + } + '/submit-post-formdata': { + id: '/submit-post-formdata' + path: '/submit-post-formdata' + fullPath: '/submit-post-formdata' + preLoaderRoute: typeof SubmitPostFormdataImport + parentRoute: typeof rootRoute + } + '/cookies/set': { + id: '/cookies/set' + path: '/cookies/set' + fullPath: '/cookies/set' + preLoaderRoute: typeof CookiesSetImport + parentRoute: typeof rootRoute + } + '/cookies/': { + id: '/cookies/' + path: '/cookies' + fullPath: '/cookies' + preLoaderRoute: typeof CookiesIndexImport + parentRoute: typeof rootRoute + } + } +} + +// Create and export the route tree + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/abort-signal': typeof AbortSignalRoute + '/consistent': typeof ConsistentRoute + '/dead-code-preserve': typeof DeadCodePreserveRoute + '/env-only': typeof EnvOnlyRoute + '/headers': typeof HeadersRoute + '/isomorphic-fns': typeof IsomorphicFnsRoute + '/multipart': typeof MultipartRoute + '/return-null': typeof ReturnNullRoute + '/serialize-form-data': typeof SerializeFormDataRoute + '/status': typeof StatusRoute + '/submit-post-formdata': typeof SubmitPostFormdataRoute + '/cookies/set': typeof CookiesSetRoute + '/cookies': typeof CookiesIndexRoute +} + +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/abort-signal': typeof AbortSignalRoute + '/consistent': typeof ConsistentRoute + '/dead-code-preserve': typeof DeadCodePreserveRoute + '/env-only': typeof EnvOnlyRoute + '/headers': typeof HeadersRoute + '/isomorphic-fns': typeof IsomorphicFnsRoute + '/multipart': typeof MultipartRoute + '/return-null': typeof ReturnNullRoute + '/serialize-form-data': typeof SerializeFormDataRoute + '/status': typeof StatusRoute + '/submit-post-formdata': typeof SubmitPostFormdataRoute + '/cookies/set': typeof CookiesSetRoute + '/cookies': typeof CookiesIndexRoute +} + +export interface FileRoutesById { + __root__: typeof rootRoute + '/': typeof IndexRoute + '/abort-signal': typeof AbortSignalRoute + '/consistent': typeof ConsistentRoute + '/dead-code-preserve': typeof DeadCodePreserveRoute + '/env-only': typeof EnvOnlyRoute + '/headers': typeof HeadersRoute + '/isomorphic-fns': typeof IsomorphicFnsRoute + '/multipart': typeof MultipartRoute + '/return-null': typeof ReturnNullRoute + '/serialize-form-data': typeof SerializeFormDataRoute + '/status': typeof StatusRoute + '/submit-post-formdata': typeof SubmitPostFormdataRoute + '/cookies/set': typeof CookiesSetRoute + '/cookies/': typeof CookiesIndexRoute +} + +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/abort-signal' + | '/consistent' + | '/dead-code-preserve' + | '/env-only' + | '/headers' + | '/isomorphic-fns' + | '/multipart' + | '/return-null' + | '/serialize-form-data' + | '/status' + | '/submit-post-formdata' + | '/cookies/set' + | '/cookies' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/abort-signal' + | '/consistent' + | '/dead-code-preserve' + | '/env-only' + | '/headers' + | '/isomorphic-fns' + | '/multipart' + | '/return-null' + | '/serialize-form-data' + | '/status' + | '/submit-post-formdata' + | '/cookies/set' + | '/cookies' + id: + | '__root__' + | '/' + | '/abort-signal' + | '/consistent' + | '/dead-code-preserve' + | '/env-only' + | '/headers' + | '/isomorphic-fns' + | '/multipart' + | '/return-null' + | '/serialize-form-data' + | '/status' + | '/submit-post-formdata' + | '/cookies/set' + | '/cookies/' + fileRoutesById: FileRoutesById +} + +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AbortSignalRoute: typeof AbortSignalRoute + ConsistentRoute: typeof ConsistentRoute + DeadCodePreserveRoute: typeof DeadCodePreserveRoute + EnvOnlyRoute: typeof EnvOnlyRoute + HeadersRoute: typeof HeadersRoute + IsomorphicFnsRoute: typeof IsomorphicFnsRoute + MultipartRoute: typeof MultipartRoute + ReturnNullRoute: typeof ReturnNullRoute + SerializeFormDataRoute: typeof SerializeFormDataRoute + StatusRoute: typeof StatusRoute + SubmitPostFormdataRoute: typeof SubmitPostFormdataRoute + CookiesSetRoute: typeof CookiesSetRoute + CookiesIndexRoute: typeof CookiesIndexRoute +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + AbortSignalRoute: AbortSignalRoute, + ConsistentRoute: ConsistentRoute, + DeadCodePreserveRoute: DeadCodePreserveRoute, + EnvOnlyRoute: EnvOnlyRoute, + HeadersRoute: HeadersRoute, + IsomorphicFnsRoute: IsomorphicFnsRoute, + MultipartRoute: MultipartRoute, + ReturnNullRoute: ReturnNullRoute, + SerializeFormDataRoute: SerializeFormDataRoute, + StatusRoute: StatusRoute, + SubmitPostFormdataRoute: SubmitPostFormdataRoute, + CookiesSetRoute: CookiesSetRoute, + CookiesIndexRoute: CookiesIndexRoute, +} + +export const routeTree = rootRoute + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +/* ROUTE_MANIFEST_START +{ + "routes": { + "__root__": { + "filePath": "__root.tsx", + "children": [ + "/", + "/abort-signal", + "/consistent", + "/dead-code-preserve", + "/env-only", + "/headers", + "/isomorphic-fns", + "/multipart", + "/return-null", + "/serialize-form-data", + "/status", + "/submit-post-formdata", + "/cookies/set", + "/cookies/" + ] + }, + "/": { + "filePath": "index.tsx" + }, + "/abort-signal": { + "filePath": "abort-signal.tsx" + }, + "/consistent": { + "filePath": "consistent.tsx" + }, + "/dead-code-preserve": { + "filePath": "dead-code-preserve.tsx" + }, + "/env-only": { + "filePath": "env-only.tsx" + }, + "/headers": { + "filePath": "headers.tsx" + }, + "/isomorphic-fns": { + "filePath": "isomorphic-fns.tsx" + }, + "/multipart": { + "filePath": "multipart.tsx" + }, + "/return-null": { + "filePath": "return-null.tsx" + }, + "/serialize-form-data": { + "filePath": "serialize-form-data.tsx" + }, + "/status": { + "filePath": "status.tsx" + }, + "/submit-post-formdata": { + "filePath": "submit-post-formdata.tsx" + }, + "/cookies/set": { + "filePath": "cookies/set.tsx" + }, + "/cookies/": { + "filePath": "cookies/index.tsx" + } + } +} +ROUTE_MANIFEST_END */ diff --git a/e2e/solid-start/server-functions/app/router.tsx b/e2e/solid-start/server-functions/app/router.tsx new file mode 100644 index 0000000000..c45bed4758 --- /dev/null +++ b/e2e/solid-start/server-functions/app/router.tsx @@ -0,0 +1,22 @@ +import { createRouter as createTanStackRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' +import { DefaultCatchBoundary } from './components/DefaultCatchBoundary' +import { NotFound } from './components/NotFound' + +export function createRouter() { + const router = createTanStackRouter({ + routeTree, + defaultPreload: 'intent', + defaultErrorComponent: DefaultCatchBoundary, + defaultNotFoundComponent: () => , + scrollRestoration: true, + }) + + return router +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/e2e/solid-start/server-functions/app/routes/__root.tsx b/e2e/solid-start/server-functions/app/routes/__root.tsx new file mode 100644 index 0000000000..b4c7c82e02 --- /dev/null +++ b/e2e/solid-start/server-functions/app/routes/__root.tsx @@ -0,0 +1,40 @@ +import * as Solid from 'react' +import { + HeadContent, + Link, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' + +import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary' +import { NotFound } from '~/components/NotFound' +import appCss from '~/styles/app.css?url' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { + charSet: 'utf-8', + }, + { + name: 'viewport', + content: 'width=device-width, initial-scale=1', + }, + ], + links: [{ rel: 'stylesheet', href: appCss }], + }), + errorComponent: (props) => { + return

{props.error.stack}

+ }, + notFoundComponent: () => , + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + ) +} diff --git a/e2e/solid-start/server-functions/app/routes/abort-signal.tsx b/e2e/solid-start/server-functions/app/routes/abort-signal.tsx new file mode 100644 index 0000000000..b7ca65a093 --- /dev/null +++ b/e2e/solid-start/server-functions/app/routes/abort-signal.tsx @@ -0,0 +1,85 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { createServerFn } from '@tanstack/solid-start' +import * as Solid from 'solid-js' + +export const Route = createFileRoute('/abort-signal')({ + component: RouteComponent, +}) + +const abortableServerFn = createServerFn().handler( + async ({ context, signal }) => { + console.log('server function started', { context, signal }) + return new Promise((resolve, reject) => { + if (signal.aborted) { + return reject(new Error('Aborted before start')) + } + const timerId = setTimeout(() => { + console.log('server function finished') + resolve('server function result') + }, 1000) + const onAbort = () => { + clearTimeout(timerId) + console.log('server function aborted') + reject(new Error('Aborted')) + } + signal.addEventListener('abort', onAbort, { once: true }) + }) + }, +) + +function RouteComponent() { + const [errorMessage, setErrorMessage] = Solid.createSignal( + undefined, + ) + const [result, setResult] = Solid.createSignal(undefined) + + const reset = () => { + setErrorMessage(undefined) + setResult(undefined) + } + return ( +
+ +
+ +
+ result:

{result() ?? '$undefined'}

+
+
+ message:{' '} +

{errorMessage() ?? '$undefined'}

+
+
+ ) +} diff --git a/e2e/solid-start/server-functions/app/routes/consistent.tsx b/e2e/solid-start/server-functions/app/routes/consistent.tsx new file mode 100644 index 0000000000..1438741462 --- /dev/null +++ b/e2e/solid-start/server-functions/app/routes/consistent.tsx @@ -0,0 +1,123 @@ +import { createFileRoute } from '@tanstack/solid-router' +import * as Solid from 'solid-js' +import { createServerFn } from '@tanstack/solid-start' + +/** + * This checks whether the returned payloads from a + * server function are the same, regardless of whether the server function is + * called directly from the client or from within the server function. + * @link https://github.com/TanStack/router/issues/1866 + * @link https://github.com/TanStack/router/issues/2481 + */ + +export const Route = createFileRoute('/consistent')({ + component: ConsistentServerFnCalls, + loader: async () => { + const data = await cons_serverGetFn1({ data: { username: 'TEST' } }) + console.log('cons_serverGetFn1', data) + return { data } + }, +}) + +const cons_getFn1 = createServerFn() + .validator((d: { username: string }) => d) + .handler(async ({ data }) => { + return { payload: data } + }) + +const cons_serverGetFn1 = createServerFn() + .validator((d: { username: string }) => d) + .handler(({ data }) => { + return cons_getFn1({ data }) + }) + +const cons_postFn1 = createServerFn({ method: 'POST' }) + .validator((d: { username: string }) => d) + .handler(async ({ data }) => { + return { payload: data } + }) + +const cons_serverPostFn1 = createServerFn({ method: 'POST' }) + .validator((d: { username: string }) => d) + .handler(({ data }) => { + return cons_postFn1({ data }) + }) + +function ConsistentServerFnCalls() { + const [getServerResult, setGetServerResult] = Solid.createSignal({}) + const [getDirectResult, setGetDirectResult] = Solid.createSignal({}) + + const [postServerResult, setPostServerResult] = Solid.createSignal({}) + const [postDirectResult, setPostDirectResult] = Solid.createSignal({}) + + return ( +
+

Consistent Server Fn GET Calls

+

+ This component checks whether the returned payloads from server function + are the same, regardless of whether the server function is called + directly from the client or from within the server function. +

+
+ It should return{' '} + +
+            {JSON.stringify({ payload: { username: 'TEST' } })}
+          
+
+
+

+ {`GET: cons_getFn1 called from server cons_serverGetFn1 returns`} +
+ + {JSON.stringify(getServerResult)} + +

+

+ {`GET: cons_getFn1 called directly returns`} +
+ + {JSON.stringify(getDirectResult)} + +

+

+ {`POST: cons_postFn1 called from cons_serverPostFn1 returns`} +
+ + {JSON.stringify(postServerResult)} + +

+

+ {`POST: cons_postFn1 called directly returns`} +
+ + {JSON.stringify(postDirectResult)} + +

+ +
+ ) +} diff --git a/e2e/solid-start/server-functions/app/routes/cookies/index.tsx b/e2e/solid-start/server-functions/app/routes/cookies/index.tsx new file mode 100644 index 0000000000..2001a7df9c --- /dev/null +++ b/e2e/solid-start/server-functions/app/routes/cookies/index.tsx @@ -0,0 +1,19 @@ +import { Link, createFileRoute } from '@tanstack/solid-router' +import { z } from 'zod' + +const cookieSchema = z + .object({ value: z.string() }) + .catch(() => ({ value: `CLIENT-${Date.now()}` })) +export const Route = createFileRoute('/cookies/')({ + validateSearch: cookieSchema, + component: RouteComponent, +}) + +function RouteComponent() { + const search = Route.useSearch() + return ( + + got to route that sets the cookies with {JSON.stringify(search)} + + ) +} diff --git a/e2e/solid-start/server-functions/app/routes/cookies/set.tsx b/e2e/solid-start/server-functions/app/routes/cookies/set.tsx new file mode 100644 index 0000000000..5a5e282bd8 --- /dev/null +++ b/e2e/solid-start/server-functions/app/routes/cookies/set.tsx @@ -0,0 +1,66 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { createServerFn } from '@tanstack/solid-start' +import { setCookie } from '@tanstack/solid-start/server' +import { z } from 'zod' +import Cookies from 'js-cookie' +import React, { useEffect } from 'react' + +const cookieSchema = z.object({ value: z.string() }) + +export const Route = createFileRoute('/cookies/set')({ + validateSearch: cookieSchema, + loaderDeps: ({ search }) => search, + loader: async ({ deps }) => { + await setCookieServerFn1({ data: deps }) + await setCookieServerFn2({ data: deps }) + }, + component: RouteComponent, +}) + +export const setCookieServerFn1 = createServerFn() + .validator(cookieSchema) + .handler(({ data }) => { + setCookie(`cookie-1-${data.value}`, data.value) + setCookie(`cookie-2-${data.value}`, data.value) + }) + +export const setCookieServerFn2 = createServerFn() + .validator(cookieSchema) + .handler(({ data }) => { + setCookie(`cookie-3-${data.value}`, data.value) + setCookie(`cookie-4-${data.value}`, data.value) + }) + +function RouteComponent() { + const { value: expectedCookieValue } = Route.useSearch() + const [cookiesFromDocument, setCookiesFromDocument] = Solid.createSignal< + Record | undefined + >(undefined) + useEffect(() => { + const tempCookies: Record = {} + for (let i = 1; i <= 4; i++) { + const key = `cookie-${i}-${expectedCookieValue}` + tempCookies[key] = Cookies.get(key) + } + setCookiesFromDocument(tempCookies) + }, []) + return ( +
+

cookies result

+ + + + + + + {Object.entries(cookiesFromDocument || {}).map(([key, value]) => ( + + + + + ))} + +
cookievalue
{key}{value}
+
+ ) +} diff --git a/e2e/solid-start/server-functions/app/routes/dead-code-preserve.tsx b/e2e/solid-start/server-functions/app/routes/dead-code-preserve.tsx new file mode 100644 index 0000000000..b870c693d4 --- /dev/null +++ b/e2e/solid-start/server-functions/app/routes/dead-code-preserve.tsx @@ -0,0 +1,60 @@ +import * as fs from 'node:fs' +import { createServerFn } from '@tanstack/solid-start' +import { getRequestHeader } from '@tanstack/solid-start/server' +import { useState } from 'react' +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/dead-code-preserve')({ + component: RouteComponent, +}) + +// by using this we make sure DCE still works - this errors when imported on the client + +const filePath = 'count-effect.txt' + +async function readCount() { + return parseInt( + await fs.promises.readFile(filePath, 'utf-8').catch(() => '0'), + ) +} + +async function updateCount() { + const count = await readCount() + await fs.promises.writeFile(filePath, `${count + 1}`) + return true +} + +const writeFileServerFn = createServerFn().handler(async () => { + // eslint-disable-next-line unused-imports/no-unused-vars + const test = await updateCount() + return getRequestHeader('X-Test') +}) + +const readFileServerFn = createServerFn().handler(async () => { + const data = await readCount() + return data +}) + +function RouteComponent() { + const [serverFnOutput, setServerFnOutput] = useState() + return ( +
+

Dead code test

+

+ This server function writes to a file as a side effect, then reads it. +

+ +

Server output

+
{serverFnOutput}
+
+ ) +} diff --git a/e2e/solid-start/server-functions/app/routes/env-only.tsx b/e2e/solid-start/server-functions/app/routes/env-only.tsx new file mode 100644 index 0000000000..8abaf7b6b8 --- /dev/null +++ b/e2e/solid-start/server-functions/app/routes/env-only.tsx @@ -0,0 +1,76 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { clientOnly, createServerFn, serverOnly } from '@tanstack/solid-start' +import { useState } from 'react' + +const serverEcho = serverOnly((input: string) => 'server got: ' + input) +const clientEcho = clientOnly((input: string) => 'client got: ' + input) + +const testOnServer = createServerFn().handler(() => { + const serverOnServer = serverEcho('hello') + let clientOnServer: string + try { + clientOnServer = clientEcho('hello') + } catch (e) { + clientOnServer = + 'clientEcho threw an error: ' + + (e instanceof Error ? e.message : String(e)) + } + return { serverOnServer, clientOnServer } +}) + +export const Route = createFileRoute('/env-only')({ + component: RouteComponent, +}) + +function RouteComponent() { + const [results, setResults] = useState>>() + + async function handleClick() { + const { serverOnServer, clientOnServer } = await testOnServer() + const clientOnClient = clientEcho('hello') + let serverOnClient: string + try { + serverOnClient = serverEcho('hello') + } catch (e) { + serverOnClient = + 'serverEcho threw an error: ' + + (e instanceof Error ? e.message : String(e)) + } + setResults({ + serverOnServer, + clientOnServer, + clientOnClient, + serverOnClient, + }) + } + + const { serverOnServer, clientOnServer, clientOnClient, serverOnClient } = + results || {} + + return ( +
+ + {!!results && ( +
+

+ serverEcho +

+ When we called the function on the server: +
{serverOnServer}
+ When we called the function on the client: +
{serverOnClient}
+
+

+ clientEcho +

+ When we called the function on the server: +
{clientOnServer}
+ When we called the function on the client: +
{clientOnClient}
+
+ )} +
+ ) +} diff --git a/e2e/solid-start/server-functions/app/routes/headers.tsx b/e2e/solid-start/server-functions/app/routes/headers.tsx new file mode 100644 index 0000000000..94ef5a05c1 --- /dev/null +++ b/e2e/solid-start/server-functions/app/routes/headers.tsx @@ -0,0 +1,68 @@ +import { createFileRoute } from '@tanstack/solid-router' +import * as Solid from 'solid-js' +import { createServerFn } from '@tanstack/solid-start' +import { getHeaders, setHeader } from '@tanstack/solid-start/server' +import type { HTTPHeaderName } from '@tanstack/solid-start/server' + +export const Route = createFileRoute('/headers')({ + loader: async () => { + return { + testHeaders: await getTestHeaders(), + } + }, + component: () => { + const loaderData = Route.useLoaderData() + return + }, +}) + +export const getTestHeaders = createServerFn().handler(() => { + setHeader('x-test-header', 'test-value') + + return { + serverHeaders: getHeaders(), + headers: getHeaders(), + } +}) + +type TestHeadersResult = { + headers?: Partial> + serverHeaders?: Partial> +} + +function ResponseHeaders({ + initialTestHeaders, +}: { + initialTestHeaders: TestHeadersResult +}) { + const [testHeadersResult, setTestHeadersResult] = + Solid.createSignal(initialTestHeaders) + + return ( +
+

Headers Test

+
{ + evt.preventDefault() + getTestHeaders().then(setTestHeadersResult) + }} + > + +
+
+

Headers:

+
+          {JSON.stringify(testHeadersResult().headers, null, 2)}
+        
+
+
+ ) +} diff --git a/e2e/solid-start/server-functions/app/routes/index.tsx b/e2e/solid-start/server-functions/app/routes/index.tsx new file mode 100644 index 0000000000..75eae15e9d --- /dev/null +++ b/e2e/solid-start/server-functions/app/routes/index.tsx @@ -0,0 +1,75 @@ +import { Link, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return ( +
+

Server functions E2E tests

+
    +
  • + + Consistent server function returns both on client and server for GET + and POST calls + +
  • +
  • + + submitting multipart/form-data as server function input + +
  • +
  • + + Server function can return null for GET and POST calls + +
  • +
  • + + Server function can correctly send and receive FormData + +
  • +
  • + + server function can correctly send and receive headers + +
  • +
  • + + Direct POST submitting FormData to a Server function returns the + correct message + +
  • +
  • + + invoking a server function with custom response status code + +
  • +
  • + + isomorphic functions can have different implementations on client + and server + +
  • +
  • + + env-only functions can only be called on the server or client + respectively + +
  • +
  • + server function sets cookies +
  • +
  • + + dead code elimation only affects code after transformation + +
  • +
  • + aborting a server function call +
  • +
+
+ ) +} diff --git a/e2e/solid-start/server-functions/app/routes/isomorphic-fns.tsx b/e2e/solid-start/server-functions/app/routes/isomorphic-fns.tsx new file mode 100644 index 0000000000..43b4e3820d --- /dev/null +++ b/e2e/solid-start/server-functions/app/routes/isomorphic-fns.tsx @@ -0,0 +1,71 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { createIsomorphicFn, createServerFn } from '@tanstack/solid-start' +import { useState } from 'react' + +const getEnv = createIsomorphicFn() + .server(() => 'server') + .client(() => 'client') + +const getServerEnv = createServerFn().handler(() => getEnv()) + +const getEcho = createIsomorphicFn() + .server((input: string) => 'server received ' + input) + .client((input) => 'client received ' + input) + +const getServerEcho = createServerFn() + .validator((input: string) => input) + .handler(({ data }) => getEcho(data)) + +export const Route = createFileRoute('/isomorphic-fns')({ + component: RouteComponent, + loader() { + return { + envOnLoad: getEnv(), + } + }, +}) + +function RouteComponent() { + const loaderData = Route.useLoaderData() + const [results, setResults] = useState>>() + async function handleClick() { + const envOnClick = getEnv() + const echo = getEcho('hello') + const [serverEnv, serverEcho] = await Promise.all([ + getServerEnv(), + getServerEcho({ data: 'hello' }), + ]) + setResults({ envOnClick, echo, serverEnv, serverEcho }) + } + const { envOnClick, echo, serverEnv, serverEcho } = results || {} + return ( +
+ + {!!results && ( +
+

+ getEnv +

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

+ echo +

+ When we called the function on the server it returned: +
+            {JSON.stringify(serverEcho)}
+          
+ When we called the function on the client it returned: +
{JSON.stringify(echo)}
+
+ )} +
+ ) +} diff --git a/e2e/solid-start/server-functions/app/routes/multipart.tsx b/e2e/solid-start/server-functions/app/routes/multipart.tsx new file mode 100644 index 0000000000..78f6e40668 --- /dev/null +++ b/e2e/solid-start/server-functions/app/routes/multipart.tsx @@ -0,0 +1,107 @@ +import { createFileRoute } from '@tanstack/solid-router' +import * as Solid from 'solid-js' +import { createServerFn } from '@tanstack/solid-start' + +export const Route = createFileRoute('/multipart')({ + component: MultipartServerFnCall, +}) + +const multipartFormDataServerFn = createServerFn({ method: 'POST' }) + .validator((x: unknown) => { + if (!(x instanceof FormData)) { + throw new Error('Invalid form data') + } + + const value = x.get('input_field') + const file = x.get('input_file') + + if (typeof value !== 'string') { + throw new Error('Submitted value is not a string') + } + + if (!(file instanceof File)) { + throw new Error('File is required') + } + + return { + submittedValue: value, + file, + } + }) + .handler(async ({ data }) => { + const contents = await data.file.text() + return { + value: data.submittedValue, + file: { + name: data.file.name, + size: data.file.size, + contents: contents, + }, + } + }) + +function MultipartServerFnCall() { + const formRef: HTMLFormElement | null = null + const [multipartResult, setMultipartResult] = Solid.createSignal({}) + + const handleSubmit = (e: any) => { + e.preventDefault() + + if (!formRef) { + return + } + + const formData = new FormData(formRef) + multipartFormDataServerFn({ data: formData }).then(setMultipartResult) + } + + return ( +
+

Multipart Server Fn POST Call

+
+ It should return{' '} + +
+            {JSON.stringify({
+              value: 'test field value',
+              file: { name: 'my_file.txt', size: 9, contents: 'test data' },
+            })}
+          
+
+
+
+ + + + +
+
+
+          {JSON.stringify(multipartResult)}
+        
+
+
+ ) +} diff --git a/e2e/solid-start/server-functions/app/routes/return-null.tsx b/e2e/solid-start/server-functions/app/routes/return-null.tsx new file mode 100644 index 0000000000..2e789d2eef --- /dev/null +++ b/e2e/solid-start/server-functions/app/routes/return-null.tsx @@ -0,0 +1,68 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { createServerFn } from '@tanstack/solid-start' +import * as Solid from 'solid-js' + +/** + * This checks whether the server function can + * return null without throwing an error or returning something else. + * @link https://github.com/TanStack/router/issues/2776 + */ + +export const Route = createFileRoute('/return-null')({ + component: AllowServerFnReturnNull, +}) + +const $allow_return_null_getFn = createServerFn().handler(async () => { + return null +}) +const $allow_return_null_postFn = createServerFn({ method: 'POST' }).handler( + async () => { + return null + }, +) + +function AllowServerFnReturnNull() { + const [getServerResult, setGetServerResult] = Solid.createSignal('-') + const [postServerResult, setPostServerResult] = Solid.createSignal('-') + + 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/solid-start/server-functions/app/routes/serialize-form-data.tsx b/e2e/solid-start/server-functions/app/routes/serialize-form-data.tsx new file mode 100644 index 0000000000..62b1fb30c4 --- /dev/null +++ b/e2e/solid-start/server-functions/app/routes/serialize-form-data.tsx @@ -0,0 +1,84 @@ +import * as Solid from 'solid-js' +import { createFileRoute } from '@tanstack/solid-router' +import { createServerFn } from '@tanstack/solid-start' + +export const Route = createFileRoute('/serialize-form-data')({ + component: SerializeFormDataFnCall, +}) + +const testValues = { + name: 'Sean', + age: 25, + pet1: 'dog', + pet2: 'cat', + __adder: 1, +} + +export const greetUser = createServerFn() + .validator((data: FormData) => { + if (!(data instanceof FormData)) { + throw new Error('Invalid! FormData is required') + } + const name = data.get('name') + const age = data.get('age') + const pets = data.getAll('pet') + + if (!name || !age || pets.length === 0) { + throw new Error('Name, age and pets are required') + } + + return { + name: name.toString(), + age: parseInt(age.toString(), 10), + pets: pets.map((pet) => pet.toString()), + } + }) + .handler(({ data: { name, age, pets } }) => { + return `Hello, ${name}! You are ${age + testValues.__adder} years old, and your favorite pets are ${pets.join(',')}.` + }) + +export function SerializeFormDataFnCall() { + const [formDataResult, setFormDataResult] = Solid.createSignal({}) + + return ( +
+

Serialize FormData Fn POST Call

+
+ It should return{' '} + +
+            Hello, {testValues.name}! You are{' '}
+            {testValues.age + testValues.__adder} years old, and your favorite{' '}
+            pets are {testValues.pet1},{testValues.pet2}.
+          
+
+
+
{ + evt.preventDefault() + const data = new FormData(evt.currentTarget) + greetUser({ data }).then(setFormDataResult) + }} + > + + + + + +
+
+
+          {JSON.stringify(formDataResult)}
+        
+
+
+ ) +} diff --git a/e2e/solid-start/server-functions/app/routes/status.tsx b/e2e/solid-start/server-functions/app/routes/status.tsx new file mode 100644 index 0000000000..22b45bb28f --- /dev/null +++ b/e2e/solid-start/server-functions/app/routes/status.tsx @@ -0,0 +1,30 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { createServerFn, useServerFn } from '@tanstack/solid-start' +import { setResponseStatus } from '@tanstack/solid-start/server' + +const helloFn = createServerFn().handler(() => { + setResponseStatus(225, `hello`) + return { + hello: 'world', + } +}) + +export const Route = createFileRoute('/status')({ + component: StatusComponent, +}) + +function StatusComponent() { + const hello = useServerFn(helloFn) + + return ( +
+ +
+ ) +} diff --git a/e2e/solid-start/server-functions/app/routes/submit-post-formdata.tsx b/e2e/solid-start/server-functions/app/routes/submit-post-formdata.tsx new file mode 100644 index 0000000000..d9b8ed5588 --- /dev/null +++ b/e2e/solid-start/server-functions/app/routes/submit-post-formdata.tsx @@ -0,0 +1,60 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { createServerFn } from '@tanstack/solid-start' + +export const Route = createFileRoute('/submit-post-formdata')({ + component: SubmitPostFormDataFn, +}) + +const testValues = { + name: 'Sean', +} + +export const greetUser = createServerFn({ method: 'POST' }) + .validator((data: FormData) => { + if (!(data instanceof FormData)) { + throw new Error('Invalid! FormData is required') + } + const name = data.get('name') + + if (!name) { + throw new Error('Name is required') + } + + return { + name: name.toString(), + } + }) + .handler(({ data: { name } }) => { + return new Response(`Hello, ${name}!`) + }) + +function SubmitPostFormDataFn() { + return ( +
+

Submit POST FormData Fn Call

+
+ It should return navigate and return{' '} + +
+            Hello, {testValues.name}!
+          
+
+
+
+ + +
+
+ ) +} diff --git a/e2e/solid-start/server-functions/app/ssr.tsx b/e2e/solid-start/server-functions/app/ssr.tsx new file mode 100644 index 0000000000..6d10bea05f --- /dev/null +++ b/e2e/solid-start/server-functions/app/ssr.tsx @@ -0,0 +1,12 @@ +import { + createStartHandler, + defaultStreamHandler, +} from '@tanstack/solid-start/server' +import { getRouterManifest } from '@tanstack/solid-start/router-manifest' + +import { createRouter } from './router' + +export default createStartHandler({ + createRouter, + getRouterManifest, +})(defaultStreamHandler) diff --git a/e2e/solid-start/server-functions/app/styles/app.css b/e2e/solid-start/server-functions/app/styles/app.css new file mode 100644 index 0000000000..c53c870665 --- /dev/null +++ b/e2e/solid-start/server-functions/app/styles/app.css @@ -0,0 +1,22 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + html { + color-scheme: light dark; + } + + * { + @apply border-gray-200 dark:border-gray-800; + } + + html, + body { + @apply text-gray-900 bg-gray-50 dark:bg-gray-950 dark:text-gray-200; + } + + .using-mouse * { + outline: none !important; + } +} diff --git a/e2e/solid-start/server-functions/package.json b/e2e/solid-start/server-functions/package.json new file mode 100644 index 0000000000..34207420fb --- /dev/null +++ b/e2e/solid-start/server-functions/package.json @@ -0,0 +1,36 @@ +{ + "name": "tanstack-solid-start-e2e-server-functions", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev": "vinxi dev --port 3000", + "dev:e2e": "vinxi dev", + "build": "vinxi build && tsc --noEmit", + "start": "vinxi start", + "test:e2e": "playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/solid-router": "workspace:^", + "@tanstack/solid-start": "workspace:^", + "js-cookie": "^3.0.5", + "solid-js": "^1.0.0", + "redaxios": "^0.5.1", + "tailwind-merge": "^2.6.0", + "vinxi": "0.5.3", + "zod": "^3.24.1" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@tanstack/router-e2e-utils": "workspace:^", + "@types/js-cookie": "^3.0.6", + "@types/node": "^22.10.2", + "vite-plugin-solid": "^2.11.6", + "autoprefixer": "^10.4.20", + "combinate": "^1.1.11", + "postcss": "^8.5.1", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.2", + "vite-tsconfig-paths": "^5.1.4" + } +} diff --git a/e2e/solid-start/server-functions/playwright.config.ts b/e2e/solid-start/server-functions/playwright.config.ts new file mode 100644 index 0000000000..409765549f --- /dev/null +++ b/e2e/solid-start/server-functions/playwright.config.ts @@ -0,0 +1,35 @@ +import { defineConfig, devices } from '@playwright/test' +import { derivePort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +export const PORT = derivePort(packageJson.name) +const baseURL = `http://localhost:${PORT}` + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + webServer: { + command: `VITE_SERVER_PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm start --port ${PORT}`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/solid-start/server-functions/postcss.config.mjs b/e2e/solid-start/server-functions/postcss.config.mjs new file mode 100644 index 0000000000..2e7af2b7f1 --- /dev/null +++ b/e2e/solid-start/server-functions/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/e2e/solid-start/server-functions/public/favicon.ico b/e2e/solid-start/server-functions/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1a1751676f7e22811b1070572093996c93c87617 GIT binary patch literal 15406 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 literal 0 HcmV?d00001 diff --git a/e2e/solid-start/server-functions/public/favicon.png b/e2e/solid-start/server-functions/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..1e77bc06091ade4496525a09d8900675afcf03f0 GIT binary patch literal 1507 zcmV<91swW`P)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91AfN*P1ONa40RR91AOHXW0IY^$^8f$^O-V#SR9Fd>S3Qp$MHK9ro!#4$ zEP@L_hX|b@f=!*_42h6mKu7{)4)_U*>1>0bCkUj;Z1X!7 zHe(Ew^Oi(|bW3J~xu+)XbtFF?4>!7TH$>(D_atUQVEj(8fGvYu2NF33#JZX>)(Vj8 zIi@z>Glt?6t~;Lf(|C8F>;WF^8F<^s7Scr!sZc01uB?HMHoL5+FZ>B(g+r-)?Sn)#3Zal#?G@GAwO5U27MpGOlC2+_saA)rl zP-<@-n~;PQOlm|Hi<+W;NdR;5+=zADzM&?!+CPD36=cGwHy6!D^vPEHG?rO`K>G|M z3FposX{yT132wuw1OR3Um_5JoKB#6?!QgBupIT;?YIr;WcpmuCE>S75mZid+ens#E zGPuYjiG0UNNVWu=f!Id^?9)34)eIpu-`j_~W0iAQzK(}XYc_!;87Tk~?4tq|h=2(! zuq0HCiNK)@+ocCKR3q1REdUju>HdYxd>JX@%oOibg+J~D+}rhz54D!NfC{h-OYk{M zkzmFtdrL@nL0bm8nF@pob1CeLC>12ef#in-Bzv2!wi)Iuwq24)`AH}|0QNQ^f$KHv z?5PBPo1*#GAuAk+Poe`?UJ>mP`@~d4a(103j0lwUx@_+$#B&VC%7r>#2$HIiD`KO8L|s3Yp%M}BT0;NJDzZtPnx=4%enhU zhW*pNN0t`^4%5MKAR+}=^Q?QeqQ`>bbK zf+-ji$Uz8V0?LpX@kh`k%DL)GCA2=@SJNKg56Wh>>pr=7{1PmHqG|~=AdLV3002ov JPDHLkV1ivgp)>#h literal 0 HcmV?d00001 diff --git a/e2e/solid-start/server-functions/tailwind.config.mjs b/e2e/solid-start/server-functions/tailwind.config.mjs new file mode 100644 index 0000000000..07c3598bac --- /dev/null +++ b/e2e/solid-start/server-functions/tailwind.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./app/**/*.{js,jsx,ts,tsx}'], +} diff --git a/e2e/solid-start/server-functions/tests/fixture.ts b/e2e/solid-start/server-functions/tests/fixture.ts new file mode 100644 index 0000000000..abb7b1d564 --- /dev/null +++ b/e2e/solid-start/server-functions/tests/fixture.ts @@ -0,0 +1,28 @@ +import { test as base, expect } from '@playwright/test' + +export interface TestFixtureOptions { + whitelistErrors: Array +} +export const test = base.extend({ + whitelistErrors: [[], { option: true }], + page: async ({ page, whitelistErrors }, use) => { + const errorMessages: Array = [] + page.on('console', (m) => { + if (m.type() === 'error') { + const text = m.text() + for (const whitelistError of whitelistErrors) { + if ( + (typeof whitelistError === 'string' && + text.includes(whitelistError)) || + (whitelistError instanceof RegExp && whitelistError.test(text)) + ) { + return + } + } + errorMessages.push(text) + } + }) + await use(page) + expect(errorMessages).toEqual([]) + }, +}) diff --git a/e2e/solid-start/server-functions/tests/server-functions.spec.ts b/e2e/solid-start/server-functions/tests/server-functions.spec.ts new file mode 100644 index 0000000000..29c36bc6e1 --- /dev/null +++ b/e2e/solid-start/server-functions/tests/server-functions.spec.ts @@ -0,0 +1,323 @@ +import * as fs from 'node:fs' +import { expect, test } from '@playwright/test' +import { PORT } from '../playwright.config' +import type { Page } from '@playwright/test' + +test('invoking a server function with custom response status code', async ({ + page, +}) => { + await page.goto('/status') + + await page.waitForLoadState('networkidle') + + const requestPromise = new Promise((resolve) => { + page.on('response', async (response) => { + expect(response.status()).toBe(225) + expect(response.statusText()).toBe('hello') + expect(response.headers()['content-type']).toBe('application/json') + expect(await response.json()).toEqual( + expect.objectContaining({ + result: { hello: 'world' }, + context: {}, + }), + ) + resolve() + }) + }) + await page.getByTestId('invoke-server-fn').click() + await requestPromise +}) + +test('Consistent server function returns both on client and server for GET and POST calls', async ({ + page, +}) => { + await page.goto('/consistent') + + await page.waitForLoadState('networkidle') + const expected = + (await page + .getByTestId('expected-consistent-server-fns-result') + .textContent()) || '' + expect(expected).not.toBe('') + + await page.getByTestId('test-consistent-server-fn-calls-btn').click() + await page.waitForLoadState('networkidle') + + // GET calls + await expect(page.getByTestId('cons_serverGetFn1-response')).toContainText( + expected, + ) + await expect(page.getByTestId('cons_getFn1-response')).toContainText(expected) + + // POST calls + await expect(page.getByTestId('cons_serverPostFn1-response')).toContainText( + expected, + ) + await expect(page.getByTestId('cons_postFn1-response')).toContainText( + expected, + ) +}) + +test('submitting multipart/form-data as server function input', async ({ + page, +}) => { + await page.goto('/multipart') + + await page.waitForLoadState('networkidle') + const expected = + (await page + .getByTestId('expected-multipart-server-fn-result') + .textContent()) || '' + expect(expected).not.toBe('') + + const fileChooserPromise = page.waitForEvent('filechooser') + await page.getByTestId('multipart-form-file-input').click() + const fileChooser = await fileChooserPromise + await fileChooser.setFiles({ + name: 'my_file.txt', + mimeType: 'text/plain', + buffer: Buffer.from('test data', 'utf-8'), + }) + await page.getByText('Submit (onClick)').click() + await page.waitForLoadState('networkidle') + + await expect(page.getByTestId('multipart-form-response')).toContainText( + expected, + ) +}) + +test('isomorphic functions can have different implementations on client and server', async ({ + page, +}) => { + await page.goto('/isomorphic-fns') + + await page.waitForLoadState('networkidle') + + await page.getByTestId('test-isomorphic-results-btn').click() + await page.waitForLoadState('networkidle') + + await expect(page.getByTestId('server-result')).toContainText('server') + await expect(page.getByTestId('client-result')).toContainText('client') + await expect(page.getByTestId('ssr-result')).toContainText('server') + + await expect(page.getByTestId('server-echo-result')).toContainText( + 'server received hello', + ) + await expect(page.getByTestId('client-echo-result')).toContainText( + 'client received hello', + ) +}) + +test('env-only functions can only be called on the server or client respectively', async ({ + page, +}) => { + await page.goto('/env-only') + + await page.waitForLoadState('networkidle') + + await page.getByTestId('test-env-only-results-btn').click() + await page.waitForLoadState('networkidle') + + await expect(page.getByTestId('server-on-server')).toContainText( + 'server got: hello', + ) + await expect(page.getByTestId('server-on-client')).toContainText( + 'serverEcho threw an error: serverOnly() functions can only be called on the server!', + ) + + await expect(page.getByTestId('client-on-server')).toContainText( + 'clientEcho threw an error: clientOnly() functions can only be called on the client!', + ) + await expect(page.getByTestId('client-on-client')).toContainText( + 'client got: hello', + ) +}) + +test('Server function can return null for GET and POST calls', async ({ + page, +}) => { + await page.goto('/return-null') + + 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)) +}) + +test('Server function can correctly send and receive FormData', async ({ + page, +}) => { + await page.goto('/serialize-form-data') + + await page.waitForLoadState('networkidle') + const expected = + (await page + .getByTestId('expected-serialize-formdata-server-fn-result') + .textContent()) || '' + expect(expected).not.toBe('') + + await page.getByTestId('test-serialize-formdata-fn-calls-btn').click() + await page.waitForLoadState('networkidle') + + await expect( + page.getByTestId('serialize-formdata-form-response'), + ).toContainText(expected) +}) + +test('server function can correctly send and receive headers', async ({ + page, +}) => { + await page.goto('/headers') + + await page.waitForLoadState('networkidle') + // console.log(await page.getByTestId('test-headers-result').textContent()) + await expect(page.getByTestId('test-headers-result')).toContainText(`{ + "accept": "application/json", + "accept-encoding": "gzip, deflate, br, zstd", + "accept-language": "en-US", + "connection": "keep-alive", + "content-type": "application/json", + "host": "localhost:${PORT}", + "sec-ch-ua": "\\"Not(A:Brand\\";v=\\"99\\", \\"HeadlessChrome\\";v=\\"133\\", \\"Chromium\\";v=\\"133\\"", + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": "\\"Windows\\"", + "sec-fetch-dest": "document", + "sec-fetch-mode": "navigate", + "sec-fetch-site": "none", + "sec-fetch-user": "?1", + "upgrade-insecure-requests": "1", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Safari/537.36" +}`) + + await page.getByTestId('test-headers-btn').click() + await page.waitForLoadState('networkidle') + + await expect(page.getByTestId('test-headers-result')).toContainText(`{ + "host": "localhost:${PORT}", + "connection": "keep-alive", + "sec-ch-ua-platform": "\\"Windows\\"", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Safari/537.36", + "accept": "application/json", + "sec-ch-ua": "\\"Not(A:Brand\\";v=\\"99\\", \\"HeadlessChrome\\";v=\\"133\\", \\"Chromium\\";v=\\"133\\"", + "content-type": "application/json", + "sec-ch-ua-mobile": "?0", + "accept-language": "en-US", + "sec-fetch-site": "same-origin", + "sec-fetch-mode": "cors", + "sec-fetch-dest": "empty", + "referer": "http://localhost:${PORT}/headers", + "accept-encoding": "gzip, deflate, br, zstd" +}`) +}) + +test('Direct POST submitting FormData to a Server function returns the correct message', async ({ + page, +}) => { + await page.goto('/submit-post-formdata') + + await page.waitForLoadState('networkidle') + + const expected = + (await page + .getByTestId('expected-submit-post-formdata-server-fn-result') + .textContent()) || '' + expect(expected).not.toBe('') + + await page.getByTestId('test-submit-post-formdata-fn-calls-btn').click() + await page.waitForLoadState('networkidle') + + const result = await page.innerText('body') + expect(result).toBe(expected) +}) + +test("server function's dead code is preserved if already there", async ({ + page, +}) => { + await page.goto('/dead-code-preserve') + + await page.waitForLoadState('networkidle') + await page.getByTestId('test-dead-code-fn-call-btn').click() + await page.waitForLoadState('networkidle') + + await expect(page.getByTestId('dead-code-fn-call-response')).toContainText( + '1', + ) + + await fs.promises.rm('count-effect.txt') +}) + +test.describe('server function sets cookies', () => { + async function runCookieTest(page: Page, expectedCookieValue: string) { + for (let i = 1; i <= 4; i++) { + const key = `cookie-${i}-${expectedCookieValue}` + + const actualValue = await page.getByTestId(key).textContent() + expect(actualValue).toBe(expectedCookieValue) + } + } + test('SSR', async ({ page }) => { + const expectedCookieValue = `SSR-${Date.now()}` + await page.goto(`/cookies/set?value=${expectedCookieValue}`) + await runCookieTest(page, expectedCookieValue) + }) + + test('client side navigation', async ({ page }) => { + const expectedCookieValue = `CLIENT-${Date.now()}` + await page.goto(`/cookies?value=${expectedCookieValue}`) + await page.getByTestId('link-to-set').click() + await runCookieTest(page, expectedCookieValue) + }) +}) + +test.describe('aborting a server function call', () => { + test('without aborting', async ({ page }) => { + await page.goto('/abort-signal') + + await page.waitForLoadState('networkidle') + + await page.getByTestId('run-without-abort-btn').click() + await page.waitForLoadState('networkidle') + await page.waitForSelector( + '[data-testid="result"]:has-text("server function result")', + ) + await page.waitForSelector( + '[data-testid="errorMessage"]:has-text("$undefined")', + ) + + const result = (await page.getByTestId('result').textContent()) || '' + expect(result).toBe('server function result') + + const errorMessage = + (await page.getByTestId('errorMessage').textContent()) || '' + expect(errorMessage).toBe('$undefined') + }) + + test('aborting', async ({ page }) => { + await page.goto('/abort-signal') + + await page.waitForLoadState('networkidle') + + await page.getByTestId('run-with-abort-btn').click() + await page.waitForLoadState('networkidle') + await page.waitForSelector('[data-testid="result"]:has-text("$undefined")') + await page.waitForSelector( + '[data-testid="errorMessage"]:has-text("aborted")', + ) + + const result = (await page.getByTestId('result').textContent()) || '' + expect(result).toBe('$undefined') + + const errorMessage = + (await page.getByTestId('errorMessage').textContent()) || '' + expect(errorMessage).toContain('abort') + }) +}) diff --git a/e2e/solid-start/server-functions/tsconfig.json b/e2e/solid-start/server-functions/tsconfig.json new file mode 100644 index 0000000000..7340ddd786 --- /dev/null +++ b/e2e/solid-start/server-functions/tsconfig.json @@ -0,0 +1,23 @@ +{ + "include": ["**/*.ts", "**/*.tsx", "public/script*.js"], + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "isolatedModules": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "target": "ES2022", + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": "", + "paths": { + "~/*": ["app/*"] + }, + "noEmit": true + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3ae7c73624..b2d97dc522 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1936,6 +1936,67 @@ importers: specifier: ^5.1.4 version: 5.1.4(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + e2e/solid-start/server-functions: + dependencies: + '@tanstack/solid-router': + specifier: workspace:^ + version: link:../../../packages/solid-router + '@tanstack/solid-start': + specifier: workspace:^ + version: link:../../../packages/solid-start + js-cookie: + specifier: ^3.0.5 + version: 3.0.5 + redaxios: + specifier: ^0.5.1 + version: 0.5.1 + solid-js: + specifier: ^1.0.0 + version: 1.9.5 + tailwind-merge: + specifier: ^2.6.0 + version: 2.6.0 + vinxi: + specifier: 0.5.3 + version: 0.5.3(@types/node@22.13.4)(db0@0.2.3)(ioredis@5.4.2)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0) + zod: + specifier: ^3.24.1 + version: 3.24.1 + devDependencies: + '@playwright/test': + specifier: ^1.50.1 + version: 1.50.1 + '@tanstack/router-e2e-utils': + specifier: workspace:^ + version: link:../../e2e-utils + '@types/js-cookie': + specifier: ^3.0.6 + version: 3.0.6 + '@types/node': + specifier: ^22.10.2 + version: 22.13.4 + autoprefixer: + specifier: ^10.4.20 + version: 10.4.20(postcss@8.5.1) + combinate: + specifier: ^1.1.11 + version: 1.1.11 + postcss: + specifier: ^8.5.1 + version: 8.5.1 + tailwindcss: + specifier: ^3.4.17 + version: 3.4.17 + typescript: + specifier: ^5.7.2 + version: 5.7.3 + vite-plugin-solid: + specifier: ^2.11.6 + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + e2e/solid-start/website: dependencies: '@tanstack/solid-router': From fbda8178ce00683e6cdb7337b1e10d8fff96634c Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 27 Feb 2025 17:28:29 +0000 Subject: [PATCH 068/124] ci: apply automated fixes --- .../server-functions/app/routes/abort-signal.tsx | 6 +++--- .../server-functions/app/routes/isomorphic-fns.tsx | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/e2e/solid-start/server-functions/app/routes/abort-signal.tsx b/e2e/solid-start/server-functions/app/routes/abort-signal.tsx index b7ca65a093..752346264e 100644 --- a/e2e/solid-start/server-functions/app/routes/abort-signal.tsx +++ b/e2e/solid-start/server-functions/app/routes/abort-signal.tsx @@ -28,9 +28,9 @@ const abortableServerFn = createServerFn().handler( ) function RouteComponent() { - const [errorMessage, setErrorMessage] = Solid.createSignal( - undefined, - ) + const [errorMessage, setErrorMessage] = Solid.createSignal< + string | undefined + >(undefined) const [result, setResult] = Solid.createSignal(undefined) const reset = () => { diff --git a/e2e/solid-start/server-functions/app/routes/isomorphic-fns.tsx b/e2e/solid-start/server-functions/app/routes/isomorphic-fns.tsx index 43b4e3820d..c81c14a574 100644 --- a/e2e/solid-start/server-functions/app/routes/isomorphic-fns.tsx +++ b/e2e/solid-start/server-functions/app/routes/isomorphic-fns.tsx @@ -53,7 +53,9 @@ function RouteComponent() { When we called the function on the client it returned:
{JSON.stringify(envOnClick)}
When we called the function during SSR it returned: -
{JSON.stringify(loaderData().envOnLoad)}
+
+            {JSON.stringify(loaderData().envOnLoad)}
+          

When we called the function on the server it returned:
-            {JSON.stringify(serverEcho)}
+            {JSON.stringify(results()?.serverEcho)}
           
When we called the function on the client it returned: -
{JSON.stringify(echo)}
+
{JSON.stringify(results()?.echo)}

echo From 6d46d7fabe262f28e93a9b9b5c0e54581284f2e4 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Thu, 27 Feb 2025 18:30:18 +0100 Subject: [PATCH 069/124] work on server functions --- .../app/components/DefaultCatchBoundary.tsx | 2 +- .../server-functions/app/routes/cookies/set.tsx | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/e2e/solid-start/server-functions/app/components/DefaultCatchBoundary.tsx b/e2e/solid-start/server-functions/app/components/DefaultCatchBoundary.tsx index e15888ddb4..32aed20e67 100644 --- a/e2e/solid-start/server-functions/app/components/DefaultCatchBoundary.tsx +++ b/e2e/solid-start/server-functions/app/components/DefaultCatchBoundary.tsx @@ -28,7 +28,7 @@ export function DefaultCatchBoundary({ error }: ErrorComponentProps) { > Try Again - {isRoot ? ( + {isRoot() ? ( | undefined >(undefined) - useEffect(() => { + Solid.createEffect(() => { const tempCookies: Record = {} for (let i = 1; i <= 4; i++) { - const key = `cookie-${i}-${expectedCookieValue}` + const key = `cookie-${i}-${search().value}` tempCookies[key] = Cookies.get(key) } setCookiesFromDocument(tempCookies) @@ -53,8 +53,8 @@ function RouteComponent() { cookie value - {Object.entries(cookiesFromDocument || {}).map(([key, value]) => ( - + {Object.entries(cookiesFromDocument() || {}).map(([key, value]) => ( + {key} {value} From d6d3a53afe4735778ae611db85226c06255b7172 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Thu, 27 Feb 2025 18:32:59 +0100 Subject: [PATCH 070/124] cleanup --- e2e/solid-start/server-functions/app/routes/__root.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/e2e/solid-start/server-functions/app/routes/__root.tsx b/e2e/solid-start/server-functions/app/routes/__root.tsx index b4c7c82e02..d25a8953b5 100644 --- a/e2e/solid-start/server-functions/app/routes/__root.tsx +++ b/e2e/solid-start/server-functions/app/routes/__root.tsx @@ -1,11 +1,4 @@ -import * as Solid from 'react' -import { - HeadContent, - Link, - Outlet, - Scripts, - createRootRoute, -} from '@tanstack/solid-router' +import { Outlet, createRootRoute } from '@tanstack/solid-router' import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary' import { NotFound } from '~/components/NotFound' From 04ea5b1ee2a4932f9439218b1240f42f82fe22ec Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Thu, 27 Feb 2025 18:37:42 +0100 Subject: [PATCH 071/124] add basic-tsr-config --- e2e/solid-start/basic-tsr-config/.gitignore | 24 +++++ .../basic-tsr-config/.prettierignore | 4 + e2e/solid-start/basic-tsr-config/README.md | 72 +++++++++++++++ .../basic-tsr-config/app.config.ts | 8 ++ e2e/solid-start/basic-tsr-config/package.json | 24 +++++ .../basic-tsr-config/playwright.config.ts | 34 +++++++ .../basic-tsr-config/src/app/client.tsx | 8 ++ .../basic-tsr-config/src/app/routeTree.gen.ts | 88 +++++++++++++++++++ .../basic-tsr-config/src/app/router.tsx | 17 ++++ .../src/app/routes/__root.tsx | 33 +++++++ .../basic-tsr-config/src/app/routes/index.tsx | 36 ++++++++ .../basic-tsr-config/src/app/ssr.tsx | 13 +++ .../basic-tsr-config/tests/app.spec.ts | 9 ++ .../basic-tsr-config/tsconfig.json | 23 +++++ pnpm-lock.yaml | 25 ++++++ 15 files changed, 418 insertions(+) create mode 100644 e2e/solid-start/basic-tsr-config/.gitignore create mode 100644 e2e/solid-start/basic-tsr-config/.prettierignore create mode 100644 e2e/solid-start/basic-tsr-config/README.md create mode 100644 e2e/solid-start/basic-tsr-config/app.config.ts create mode 100644 e2e/solid-start/basic-tsr-config/package.json create mode 100644 e2e/solid-start/basic-tsr-config/playwright.config.ts create mode 100644 e2e/solid-start/basic-tsr-config/src/app/client.tsx create mode 100644 e2e/solid-start/basic-tsr-config/src/app/routeTree.gen.ts create mode 100644 e2e/solid-start/basic-tsr-config/src/app/router.tsx create mode 100644 e2e/solid-start/basic-tsr-config/src/app/routes/__root.tsx create mode 100644 e2e/solid-start/basic-tsr-config/src/app/routes/index.tsx create mode 100644 e2e/solid-start/basic-tsr-config/src/app/ssr.tsx create mode 100644 e2e/solid-start/basic-tsr-config/tests/app.spec.ts create mode 100644 e2e/solid-start/basic-tsr-config/tsconfig.json diff --git a/e2e/solid-start/basic-tsr-config/.gitignore b/e2e/solid-start/basic-tsr-config/.gitignore new file mode 100644 index 0000000000..2b76174be5 --- /dev/null +++ b/e2e/solid-start/basic-tsr-config/.gitignore @@ -0,0 +1,24 @@ +node_modules +package-lock.json +yarn.lock + +.DS_Store +.cache +.env +.vercel +.output +.vinxi + +/build/ +/api/ +/server/build +/public/build +.vinxi +# Sentry Config File +.env.sentry-build-plugin +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ + +count.txt diff --git a/e2e/solid-start/basic-tsr-config/.prettierignore b/e2e/solid-start/basic-tsr-config/.prettierignore new file mode 100644 index 0000000000..2be5eaa6ec --- /dev/null +++ b/e2e/solid-start/basic-tsr-config/.prettierignore @@ -0,0 +1,4 @@ +**/build +**/public +pnpm-lock.yaml +routeTree.gen.ts \ No newline at end of file diff --git a/e2e/solid-start/basic-tsr-config/README.md b/e2e/solid-start/basic-tsr-config/README.md new file mode 100644 index 0000000000..90cba4aac1 --- /dev/null +++ b/e2e/solid-start/basic-tsr-config/README.md @@ -0,0 +1,72 @@ +# Welcome to TanStack.com! + +This site is built with TanStack Router! + +- [TanStack Router Docs](https://tanstack.com/router) + +It's deployed automagically with Netlify! + +- [Netlify](https://netlify.com/) + +## Development + +From your terminal: + +```sh +pnpm install +pnpm dev +``` + +This starts your app in development mode, rebuilding assets on file changes. + +## Editing and previewing the docs of TanStack projects locally + +The documentations for all TanStack projects except for `React Charts` are hosted on [https://tanstack.com](https://tanstack.com), powered by this TanStack Router app. +In production, the markdown doc pages are fetched from the GitHub repos of the projects, but in development they are read from the local file system. + +Follow these steps if you want to edit the doc pages of a project (in these steps we'll assume it's [`TanStack/form`](https://github.com/tanstack/form)) and preview them locally : + +1. Create a new directory called `tanstack`. + +```sh +mkdir tanstack +``` + +2. Enter the directory and clone this repo and the repo of the project there. + +```sh +cd tanstack +git clone git@github.com:TanStack/tanstack.com.git +git clone git@github.com:TanStack/form.git +``` + +> [!NOTE] +> Your `tanstack` directory should look like this: +> +> ``` +> tanstack/ +> | +> +-- form/ +> | +> +-- tanstack.com/ +> ``` + +> [!WARNING] +> Make sure the name of the directory in your local file system matches the name of the project's repo. For example, `tanstack/form` must be cloned into `form` (this is the default) instead of `some-other-name`, because that way, the doc pages won't be found. + +3. Enter the `tanstack/tanstack.com` directory, install the dependencies and run the app in dev mode: + +```sh +cd tanstack.com +pnpm i +# The app will run on https://localhost:3000 by default +pnpm dev +``` + +4. Now you can visit http://localhost:3000/form/latest/docs/overview in the browser and see the changes you make in `tanstack/form/docs`. + +> [!NOTE] +> The updated pages need to be manually reloaded in the browser. + +> [!WARNING] +> You will need to update the `docs/config.json` file (in the project's repo) if you add a new doc page! diff --git a/e2e/solid-start/basic-tsr-config/app.config.ts b/e2e/solid-start/basic-tsr-config/app.config.ts new file mode 100644 index 0000000000..d798414e98 --- /dev/null +++ b/e2e/solid-start/basic-tsr-config/app.config.ts @@ -0,0 +1,8 @@ +// app.config.ts +import { defineConfig } from '@tanstack/solid-start/config' + +export default defineConfig({ + tsr: { + appDirectory: './src/app', + }, +}) diff --git a/e2e/solid-start/basic-tsr-config/package.json b/e2e/solid-start/basic-tsr-config/package.json new file mode 100644 index 0000000000..46ad24a933 --- /dev/null +++ b/e2e/solid-start/basic-tsr-config/package.json @@ -0,0 +1,24 @@ +{ + "name": "tanstack-start-e2e-basic-tsr-config", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev": "vinxi dev --port 3000", + "dev:e2e": "vinxi dev", + "build": "vinxi build && tsc --noEmit", + "start": "vinxi start", + "test:e2e": "playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/solid-router": "workspace:^", + "@tanstack/solid-start": "workspace:^", + "solid-js": "^1.0.0", + "vinxi": "0.5.3" + }, + "devDependencies": { + "@tanstack/router-e2e-utils": "workspace:^", + "@types/node": "^22.10.2", + "typescript": "^5.7.2" + } +} diff --git a/e2e/solid-start/basic-tsr-config/playwright.config.ts b/e2e/solid-start/basic-tsr-config/playwright.config.ts new file mode 100644 index 0000000000..bb77d0cf70 --- /dev/null +++ b/e2e/solid-start/basic-tsr-config/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test' +import { derivePort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const PORT = derivePort(packageJson.name) +const baseURL = `http://localhost:${PORT}` +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + webServer: { + command: `VITE_SERVER_PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm start --port ${PORT}`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/solid-start/basic-tsr-config/src/app/client.tsx b/e2e/solid-start/basic-tsr-config/src/app/client.tsx new file mode 100644 index 0000000000..ba0f02fac0 --- /dev/null +++ b/e2e/solid-start/basic-tsr-config/src/app/client.tsx @@ -0,0 +1,8 @@ +/// +import { hydrate } from 'solid-js/web' +import { StartClient } from '@tanstack/solid-start' +import { createRouter } from './router' + +const router = createRouter() + +hydrate(() => , document.body) diff --git a/e2e/solid-start/basic-tsr-config/src/app/routeTree.gen.ts b/e2e/solid-start/basic-tsr-config/src/app/routeTree.gen.ts new file mode 100644 index 0000000000..c260510053 --- /dev/null +++ b/e2e/solid-start/basic-tsr-config/src/app/routeTree.gen.ts @@ -0,0 +1,88 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +// Import Routes + +import { Route as rootRoute } from './routes/__root' +import { Route as IndexImport } from './routes/index' + +// Create/Update Routes + +const IndexRoute = IndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRoute, +} as any) + +// Populate the FileRoutesByPath interface + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexImport + parentRoute: typeof rootRoute + } + } +} + +// Create and export the route tree + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute +} + +export interface FileRoutesByTo { + '/': typeof IndexRoute +} + +export interface FileRoutesById { + __root__: typeof rootRoute + '/': typeof IndexRoute +} + +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' + fileRoutesByTo: FileRoutesByTo + to: '/' + id: '__root__' | '/' + fileRoutesById: FileRoutesById +} + +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, +} + +export const routeTree = rootRoute + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +/* ROUTE_MANIFEST_START +{ + "routes": { + "__root__": { + "filePath": "__root.tsx", + "children": [ + "/" + ] + }, + "/": { + "filePath": "index.tsx" + } + } +} +ROUTE_MANIFEST_END */ diff --git a/e2e/solid-start/basic-tsr-config/src/app/router.tsx b/e2e/solid-start/basic-tsr-config/src/app/router.tsx new file mode 100644 index 0000000000..e230377cb1 --- /dev/null +++ b/e2e/solid-start/basic-tsr-config/src/app/router.tsx @@ -0,0 +1,17 @@ +import { createRouter as createTanStackRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createRouter() { + const router = createTanStackRouter({ + routeTree, + scrollRestoration: true, + }) + + return router +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/e2e/solid-start/basic-tsr-config/src/app/routes/__root.tsx b/e2e/solid-start/basic-tsr-config/src/app/routes/__root.tsx new file mode 100644 index 0000000000..cd9abed1a0 --- /dev/null +++ b/e2e/solid-start/basic-tsr-config/src/app/routes/__root.tsx @@ -0,0 +1,33 @@ +import * as Solid from 'solid-js' +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' + +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 ( + <> + + + ) +} diff --git a/e2e/solid-start/basic-tsr-config/src/app/routes/index.tsx b/e2e/solid-start/basic-tsr-config/src/app/routes/index.tsx new file mode 100644 index 0000000000..c406aad351 --- /dev/null +++ b/e2e/solid-start/basic-tsr-config/src/app/routes/index.tsx @@ -0,0 +1,36 @@ +import { createFileRoute, useRouter } from '@tanstack/solid-router' +import { createServerFn } from '@tanstack/solid-start' + +let count = 0 + +const getCount = createServerFn({ method: 'GET' }).handler(() => { + return count +}) + +const updateCount = createServerFn({ method: 'POST' }) + .validator((addBy: number) => addBy) + .handler(({ data: addBy }) => { + count += addBy + }) + +export const Route = createFileRoute('/')({ + component: Home, + loader: async () => await getCount(), +}) + +function Home() { + const router = useRouter() + const state = Route.useLoaderData() + + return ( + + ) +} diff --git a/e2e/solid-start/basic-tsr-config/src/app/ssr.tsx b/e2e/solid-start/basic-tsr-config/src/app/ssr.tsx new file mode 100644 index 0000000000..ebd14c8120 --- /dev/null +++ b/e2e/solid-start/basic-tsr-config/src/app/ssr.tsx @@ -0,0 +1,13 @@ +/// +import { + createStartHandler, + defaultStreamHandler, +} from '@tanstack/solid-start/server' +import { getRouterManifest } from '@tanstack/solid-start/router-manifest' + +import { createRouter } from './router' + +export default createStartHandler({ + createRouter, + getRouterManifest, +})(defaultStreamHandler) diff --git a/e2e/solid-start/basic-tsr-config/tests/app.spec.ts b/e2e/solid-start/basic-tsr-config/tests/app.spec.ts new file mode 100644 index 0000000000..7cb82cdff2 --- /dev/null +++ b/e2e/solid-start/basic-tsr-config/tests/app.spec.ts @@ -0,0 +1,9 @@ +import { expect, test } from '@playwright/test' + +test('opening the app', async ({ page }) => { + await page.goto('/') + + await expect(page.getByText('Add 1 to 0?')).toBeTruthy() + await page.click('button') + await expect(page.getByText('Add 1 to 1?')).toBeTruthy() +}) diff --git a/e2e/solid-start/basic-tsr-config/tsconfig.json b/e2e/solid-start/basic-tsr-config/tsconfig.json new file mode 100644 index 0000000000..86fe6d2cf5 --- /dev/null +++ b/e2e/solid-start/basic-tsr-config/tsconfig.json @@ -0,0 +1,23 @@ +{ + "include": ["**/*.ts", "**/*.tsx"], + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "isolatedModules": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "target": "ES2022", + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, + "noEmit": true + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b2d97dc522..cc499a9ca1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1936,6 +1936,31 @@ importers: specifier: ^5.1.4 version: 5.1.4(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + e2e/solid-start/basic-tsr-config: + dependencies: + '@tanstack/solid-router': + specifier: workspace:^ + version: link:../../../packages/solid-router + '@tanstack/solid-start': + specifier: workspace:^ + version: link:../../../packages/solid-start + solid-js: + specifier: ^1.0.0 + version: 1.9.5 + vinxi: + specifier: 0.5.3 + version: 0.5.3(@types/node@22.13.4)(db0@0.2.3)(ioredis@5.4.2)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0) + devDependencies: + '@tanstack/router-e2e-utils': + specifier: workspace:^ + version: link:../../e2e-utils + '@types/node': + specifier: ^22.10.2 + version: 22.13.4 + typescript: + specifier: ^5.7.2 + version: 5.7.3 + e2e/solid-start/server-functions: dependencies: '@tanstack/solid-router': From 910ffaf133944b62ea9bd928cf1a0fa17fdd1e6d Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Thu, 27 Feb 2025 18:46:01 +0100 Subject: [PATCH 072/124] scroll retoration --- e2e/solid-start/scroll-restoration/.gitignore | 22 +++ .../scroll-restoration/.prettierignore | 4 + .../scroll-restoration/app.config.ts | 12 ++ e2e/solid-start/scroll-restoration/app/api.ts | 6 + .../scroll-restoration/app/client.tsx | 8 + .../app/components/DefaultCatchBoundary.tsx | 53 ++++++ .../app/components/NotFound.tsx | 25 +++ .../scroll-restoration/app/routeTree.gen.ts | 162 ++++++++++++++++++ .../scroll-restoration/app/router.tsx | 22 +++ .../app/routes/(tests)/normal-page.tsx | 17 ++ .../app/routes/(tests)/with-loader.tsx | 21 +++ .../app/routes/(tests)/with-search.tsx | 19 ++ .../app/routes/-components/scroll-block.tsx | 16 ++ .../scroll-restoration/app/routes/__root.tsx | 105 ++++++++++++ .../scroll-restoration/app/routes/index.tsx | 35 ++++ .../scroll-restoration/app/ssr.tsx | 12 ++ .../scroll-restoration/app/styles/app.css | 22 +++ .../scroll-restoration/app/utils/posts.tsx | 37 ++++ .../scroll-restoration/app/utils/seo.ts | 33 ++++ .../scroll-restoration/app/utils/users.tsx | 10 ++ .../scroll-restoration/package.json | 35 ++++ .../scroll-restoration/playwright.config.ts | 35 ++++ .../scroll-restoration/postcss.config.mjs | 6 + .../public/android-chrome-192x192.png | Bin 0 -> 29964 bytes .../public/android-chrome-512x512.png | Bin 0 -> 109271 bytes .../public/apple-touch-icon.png | Bin 0 -> 27246 bytes .../public/favicon-16x16.png | Bin 0 -> 832 bytes .../public/favicon-32x32.png | Bin 0 -> 2115 bytes .../scroll-restoration/public/favicon.ico | Bin 0 -> 15406 bytes .../scroll-restoration/public/favicon.png | Bin 0 -> 1507 bytes .../scroll-restoration/public/script.js | 2 + .../scroll-restoration/public/script2.js | 2 + .../public/site.webmanifest | 19 ++ .../scroll-restoration/tailwind.config.mjs | 4 + .../scroll-restoration/tests/app.spec.ts | 47 +++++ .../scroll-restoration/tsconfig.json | 23 +++ pnpm-lock.yaml | 58 +++++++ 37 files changed, 872 insertions(+) create mode 100644 e2e/solid-start/scroll-restoration/.gitignore create mode 100644 e2e/solid-start/scroll-restoration/.prettierignore create mode 100644 e2e/solid-start/scroll-restoration/app.config.ts create mode 100644 e2e/solid-start/scroll-restoration/app/api.ts create mode 100644 e2e/solid-start/scroll-restoration/app/client.tsx create mode 100644 e2e/solid-start/scroll-restoration/app/components/DefaultCatchBoundary.tsx create mode 100644 e2e/solid-start/scroll-restoration/app/components/NotFound.tsx create mode 100644 e2e/solid-start/scroll-restoration/app/routeTree.gen.ts create mode 100644 e2e/solid-start/scroll-restoration/app/router.tsx create mode 100644 e2e/solid-start/scroll-restoration/app/routes/(tests)/normal-page.tsx create mode 100644 e2e/solid-start/scroll-restoration/app/routes/(tests)/with-loader.tsx create mode 100644 e2e/solid-start/scroll-restoration/app/routes/(tests)/with-search.tsx create mode 100644 e2e/solid-start/scroll-restoration/app/routes/-components/scroll-block.tsx create mode 100644 e2e/solid-start/scroll-restoration/app/routes/__root.tsx create mode 100644 e2e/solid-start/scroll-restoration/app/routes/index.tsx create mode 100644 e2e/solid-start/scroll-restoration/app/ssr.tsx create mode 100644 e2e/solid-start/scroll-restoration/app/styles/app.css create mode 100644 e2e/solid-start/scroll-restoration/app/utils/posts.tsx create mode 100644 e2e/solid-start/scroll-restoration/app/utils/seo.ts create mode 100644 e2e/solid-start/scroll-restoration/app/utils/users.tsx create mode 100644 e2e/solid-start/scroll-restoration/package.json create mode 100644 e2e/solid-start/scroll-restoration/playwright.config.ts create mode 100644 e2e/solid-start/scroll-restoration/postcss.config.mjs create mode 100644 e2e/solid-start/scroll-restoration/public/android-chrome-192x192.png create mode 100644 e2e/solid-start/scroll-restoration/public/android-chrome-512x512.png create mode 100644 e2e/solid-start/scroll-restoration/public/apple-touch-icon.png create mode 100644 e2e/solid-start/scroll-restoration/public/favicon-16x16.png create mode 100644 e2e/solid-start/scroll-restoration/public/favicon-32x32.png create mode 100644 e2e/solid-start/scroll-restoration/public/favicon.ico create mode 100644 e2e/solid-start/scroll-restoration/public/favicon.png create mode 100644 e2e/solid-start/scroll-restoration/public/script.js create mode 100644 e2e/solid-start/scroll-restoration/public/script2.js create mode 100644 e2e/solid-start/scroll-restoration/public/site.webmanifest create mode 100644 e2e/solid-start/scroll-restoration/tailwind.config.mjs create mode 100644 e2e/solid-start/scroll-restoration/tests/app.spec.ts create mode 100644 e2e/solid-start/scroll-restoration/tsconfig.json diff --git a/e2e/solid-start/scroll-restoration/.gitignore b/e2e/solid-start/scroll-restoration/.gitignore new file mode 100644 index 0000000000..be342025da --- /dev/null +++ b/e2e/solid-start/scroll-restoration/.gitignore @@ -0,0 +1,22 @@ +node_modules +package-lock.json +yarn.lock + +.DS_Store +.cache +.env +.vercel +.output +.vinxi + +/build/ +/api/ +/server/build +/public/build +.vinxi +# Sentry Config File +.env.sentry-build-plugin +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/solid-start/scroll-restoration/.prettierignore b/e2e/solid-start/scroll-restoration/.prettierignore new file mode 100644 index 0000000000..2be5eaa6ec --- /dev/null +++ b/e2e/solid-start/scroll-restoration/.prettierignore @@ -0,0 +1,4 @@ +**/build +**/public +pnpm-lock.yaml +routeTree.gen.ts \ No newline at end of file diff --git a/e2e/solid-start/scroll-restoration/app.config.ts b/e2e/solid-start/scroll-restoration/app.config.ts new file mode 100644 index 0000000000..5c531d7e3d --- /dev/null +++ b/e2e/solid-start/scroll-restoration/app.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from '@tanstack/solid-start/config' +import tsConfigPaths from 'vite-tsconfig-paths' + +export default defineConfig({ + vite: { + plugins: [ + tsConfigPaths({ + projects: ['./tsconfig.json'], + }), + ], + }, +}) diff --git a/e2e/solid-start/scroll-restoration/app/api.ts b/e2e/solid-start/scroll-restoration/app/api.ts new file mode 100644 index 0000000000..ed511bcd26 --- /dev/null +++ b/e2e/solid-start/scroll-restoration/app/api.ts @@ -0,0 +1,6 @@ +import { + createStartAPIHandler, + defaultAPIFileRouteHandler, +} from '@tanstack/solid-start/api' + +export default createStartAPIHandler(defaultAPIFileRouteHandler) diff --git a/e2e/solid-start/scroll-restoration/app/client.tsx b/e2e/solid-start/scroll-restoration/app/client.tsx new file mode 100644 index 0000000000..ba0f02fac0 --- /dev/null +++ b/e2e/solid-start/scroll-restoration/app/client.tsx @@ -0,0 +1,8 @@ +/// +import { hydrate } from 'solid-js/web' +import { StartClient } from '@tanstack/solid-start' +import { createRouter } from './router' + +const router = createRouter() + +hydrate(() => , document.body) diff --git a/e2e/solid-start/scroll-restoration/app/components/DefaultCatchBoundary.tsx b/e2e/solid-start/scroll-restoration/app/components/DefaultCatchBoundary.tsx new file mode 100644 index 0000000000..32aed20e67 --- /dev/null +++ b/e2e/solid-start/scroll-restoration/app/components/DefaultCatchBoundary.tsx @@ -0,0 +1,53 @@ +import { + ErrorComponent, + Link, + rootRouteId, + useMatch, + useRouter, +} from '@tanstack/solid-router' +import type { ErrorComponentProps } from '@tanstack/solid-router' + +export function DefaultCatchBoundary({ error }: ErrorComponentProps) { + const router = useRouter() + const isRoot = useMatch({ + strict: false, + select: (state) => state.id === rootRouteId, + }) + + console.error(error) + + return ( +
+ +
+ + {isRoot() ? ( + + Home + + ) : ( + { + e.preventDefault() + window.history.back() + }} + > + Go Back + + )} +
+
+ ) +} diff --git a/e2e/solid-start/scroll-restoration/app/components/NotFound.tsx b/e2e/solid-start/scroll-restoration/app/components/NotFound.tsx new file mode 100644 index 0000000000..ca4c1960fa --- /dev/null +++ b/e2e/solid-start/scroll-restoration/app/components/NotFound.tsx @@ -0,0 +1,25 @@ +import { Link } from '@tanstack/solid-router' + +export function NotFound({ children }: { children?: any }) { + return ( +
+
+ {children ||

The page you are looking for does not exist.

} +
+

+ + + Start Over + +

+
+ ) +} diff --git a/e2e/solid-start/scroll-restoration/app/routeTree.gen.ts b/e2e/solid-start/scroll-restoration/app/routeTree.gen.ts new file mode 100644 index 0000000000..d5cbca5b25 --- /dev/null +++ b/e2e/solid-start/scroll-restoration/app/routeTree.gen.ts @@ -0,0 +1,162 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +// Import Routes + +import { Route as rootRoute } from './routes/__root' +import { Route as IndexImport } from './routes/index' +import { Route as testsWithSearchImport } from './routes/(tests)/with-search' +import { Route as testsWithLoaderImport } from './routes/(tests)/with-loader' +import { Route as testsNormalPageImport } from './routes/(tests)/normal-page' + +// Create/Update Routes + +const IndexRoute = IndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRoute, +} as any) + +const testsWithSearchRoute = testsWithSearchImport.update({ + id: '/(tests)/with-search', + path: '/with-search', + getParentRoute: () => rootRoute, +} as any) + +const testsWithLoaderRoute = testsWithLoaderImport.update({ + id: '/(tests)/with-loader', + path: '/with-loader', + getParentRoute: () => rootRoute, +} as any) + +const testsNormalPageRoute = testsNormalPageImport.update({ + id: '/(tests)/normal-page', + path: '/normal-page', + getParentRoute: () => rootRoute, +} as any) + +// Populate the FileRoutesByPath interface + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexImport + parentRoute: typeof rootRoute + } + '/(tests)/normal-page': { + id: '/(tests)/normal-page' + path: '/normal-page' + fullPath: '/normal-page' + preLoaderRoute: typeof testsNormalPageImport + parentRoute: typeof rootRoute + } + '/(tests)/with-loader': { + id: '/(tests)/with-loader' + path: '/with-loader' + fullPath: '/with-loader' + preLoaderRoute: typeof testsWithLoaderImport + parentRoute: typeof rootRoute + } + '/(tests)/with-search': { + id: '/(tests)/with-search' + path: '/with-search' + fullPath: '/with-search' + preLoaderRoute: typeof testsWithSearchImport + parentRoute: typeof rootRoute + } + } +} + +// Create and export the route tree + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/normal-page': typeof testsNormalPageRoute + '/with-loader': typeof testsWithLoaderRoute + '/with-search': typeof testsWithSearchRoute +} + +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/normal-page': typeof testsNormalPageRoute + '/with-loader': typeof testsWithLoaderRoute + '/with-search': typeof testsWithSearchRoute +} + +export interface FileRoutesById { + __root__: typeof rootRoute + '/': typeof IndexRoute + '/(tests)/normal-page': typeof testsNormalPageRoute + '/(tests)/with-loader': typeof testsWithLoaderRoute + '/(tests)/with-search': typeof testsWithSearchRoute +} + +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/normal-page' | '/with-loader' | '/with-search' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/normal-page' | '/with-loader' | '/with-search' + id: + | '__root__' + | '/' + | '/(tests)/normal-page' + | '/(tests)/with-loader' + | '/(tests)/with-search' + fileRoutesById: FileRoutesById +} + +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + testsNormalPageRoute: typeof testsNormalPageRoute + testsWithLoaderRoute: typeof testsWithLoaderRoute + testsWithSearchRoute: typeof testsWithSearchRoute +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + testsNormalPageRoute: testsNormalPageRoute, + testsWithLoaderRoute: testsWithLoaderRoute, + testsWithSearchRoute: testsWithSearchRoute, +} + +export const routeTree = rootRoute + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +/* ROUTE_MANIFEST_START +{ + "routes": { + "__root__": { + "filePath": "__root.tsx", + "children": [ + "/", + "/(tests)/normal-page", + "/(tests)/with-loader", + "/(tests)/with-search" + ] + }, + "/": { + "filePath": "index.tsx" + }, + "/(tests)/normal-page": { + "filePath": "(tests)/normal-page.tsx" + }, + "/(tests)/with-loader": { + "filePath": "(tests)/with-loader.tsx" + }, + "/(tests)/with-search": { + "filePath": "(tests)/with-search.tsx" + } + } +} +ROUTE_MANIFEST_END */ diff --git a/e2e/solid-start/scroll-restoration/app/router.tsx b/e2e/solid-start/scroll-restoration/app/router.tsx new file mode 100644 index 0000000000..b0449d7478 --- /dev/null +++ b/e2e/solid-start/scroll-restoration/app/router.tsx @@ -0,0 +1,22 @@ +import { createRouter as createTanStackRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' +import { DefaultCatchBoundary } from './components/DefaultCatchBoundary' +import { NotFound } from './components/NotFound' + +export function createRouter() { + const router = createTanStackRouter({ + routeTree, + scrollRestoration: true, + defaultPreload: 'intent', + defaultErrorComponent: DefaultCatchBoundary, + defaultNotFoundComponent: () => , + }) + + return router +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/e2e/solid-start/scroll-restoration/app/routes/(tests)/normal-page.tsx b/e2e/solid-start/scroll-restoration/app/routes/(tests)/normal-page.tsx new file mode 100644 index 0000000000..76d32719ef --- /dev/null +++ b/e2e/solid-start/scroll-restoration/app/routes/(tests)/normal-page.tsx @@ -0,0 +1,17 @@ +import * as Solid from 'solid-js' +import { createFileRoute } from '@tanstack/solid-router' +import { ScrollBlock } from '../-components/scroll-block' + +export const Route = createFileRoute('/(tests)/normal-page')({ + component: Component, +}) + +function Component() { + return ( +
+

normal-page

+
+ +
+ ) +} diff --git a/e2e/solid-start/scroll-restoration/app/routes/(tests)/with-loader.tsx b/e2e/solid-start/scroll-restoration/app/routes/(tests)/with-loader.tsx new file mode 100644 index 0000000000..aae7611b51 --- /dev/null +++ b/e2e/solid-start/scroll-restoration/app/routes/(tests)/with-loader.tsx @@ -0,0 +1,21 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { ScrollBlock } from '../-components/scroll-block' +import { sleep } from '~/utils/posts' + +export const Route = createFileRoute('/(tests)/with-loader')({ + loader: async () => { + await sleep(1000) + return { foo: 'bar' } + }, + component: Component, +}) + +function Component() { + return ( +
+

lazy-with-loader-page

+
+ +
+ ) +} diff --git a/e2e/solid-start/scroll-restoration/app/routes/(tests)/with-search.tsx b/e2e/solid-start/scroll-restoration/app/routes/(tests)/with-search.tsx new file mode 100644 index 0000000000..5d43b7f9bb --- /dev/null +++ b/e2e/solid-start/scroll-restoration/app/routes/(tests)/with-search.tsx @@ -0,0 +1,19 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { z } from 'zod' +import { zodValidator } from '@tanstack/zod-adapter' +import { ScrollBlock } from '../-components/scroll-block' + +export const Route = createFileRoute('/(tests)/with-search')({ + validateSearch: zodValidator(z.object({ where: z.string() })), + component: Component, +}) + +function Component() { + return ( +
+

page-with-search

+
+ +
+ ) +} diff --git a/e2e/solid-start/scroll-restoration/app/routes/-components/scroll-block.tsx b/e2e/solid-start/scroll-restoration/app/routes/-components/scroll-block.tsx new file mode 100644 index 0000000000..0539293f92 --- /dev/null +++ b/e2e/solid-start/scroll-restoration/app/routes/-components/scroll-block.tsx @@ -0,0 +1,16 @@ +export const atTheTopId = 'at-the-top' +export const atTheBottomId = 'at-the-bottom' + +export function ScrollBlock({ number = 100 }: { number?: number }) { + return ( + <> +
+ {Array.from({ length: number }).map((_, i) => ( +
{i}
+ ))} +
+ At the bottom +
+ + ) +} diff --git a/e2e/solid-start/scroll-restoration/app/routes/__root.tsx b/e2e/solid-start/scroll-restoration/app/routes/__root.tsx new file mode 100644 index 0000000000..389b8025bd --- /dev/null +++ b/e2e/solid-start/scroll-restoration/app/routes/__root.tsx @@ -0,0 +1,105 @@ +import * as Solid from 'solid-js' +import { + Link, + Outlet, + createRootRoute, + linkOptions, + HeadContent, + Scripts, +} from '@tanstack/solid-router' +import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary' +import { NotFound } from '~/components/NotFound' +import appCss from '~/styles/app.css?url' +import { seo } from '~/utils/seo' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { + charset: 'utf-8', + }, + { + name: 'viewport', + content: 'width=device-width, initial-scale=1', + }, + ...seo({ + title: + 'TanStack Start | Type-Safe, Client-First, Full-Stack React Framework', + description: `TanStack Start is a type-safe, client-first, full-stack React framework. `, + }), + ], + links: [ + { rel: 'stylesheet', href: appCss }, + { + rel: 'apple-touch-icon', + sizes: '180x180', + href: '/apple-touch-icon.png', + }, + { + rel: 'icon', + type: 'image/png', + sizes: '32x32', + href: '/favicon-32x32.png', + }, + { + rel: 'icon', + type: 'image/png', + sizes: '16x16', + href: '/favicon-16x16.png', + }, + { rel: 'manifest', href: '/site.webmanifest', color: '#fffff' }, + { rel: 'icon', href: '/favicon.ico' }, + ], + }), + errorComponent: (props) => { + return

{props.error.stack}

+ }, + notFoundComponent: () => , + component: RootComponent, +}) + +function RootComponent() { + return ( + <> +

)}
From 3a7d6629ff624be1bdc6756eb45de5e1b7ef632d Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Thu, 27 Feb 2025 22:55:42 +0100 Subject: [PATCH 089/124] fix env-only test --- .../server-functions/app/routes/env-only.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/e2e/solid-start/server-functions/app/routes/env-only.tsx b/e2e/solid-start/server-functions/app/routes/env-only.tsx index d563fae5c8..df8f08eed2 100644 --- a/e2e/solid-start/server-functions/app/routes/env-only.tsx +++ b/e2e/solid-start/server-functions/app/routes/env-only.tsx @@ -44,9 +44,6 @@ function RouteComponent() { }) } - const { serverOnServer, clientOnServer, clientOnClient, serverOnClient } = - results() || {} - return (
)}
From 99f2ad692eb6cc0810568125d9d363c7c39dcff9 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Thu, 27 Feb 2025 22:58:08 +0100 Subject: [PATCH 090/124] fix serialize form test --- .../server-functions/app/routes/serialize-form-data.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/solid-start/server-functions/app/routes/serialize-form-data.tsx b/e2e/solid-start/server-functions/app/routes/serialize-form-data.tsx index d72bada712..487d91e86d 100644 --- a/e2e/solid-start/server-functions/app/routes/serialize-form-data.tsx +++ b/e2e/solid-start/server-functions/app/routes/serialize-form-data.tsx @@ -72,7 +72,7 @@ export function SerializeFormDataFnCall() {
-          {JSON.stringify(formDataResult)}
+          {JSON.stringify(formDataResult())}
         
From e9724de037edaa974a4b9fd87b5b604a99ff5479 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Thu, 27 Feb 2025 23:04:24 +0100 Subject: [PATCH 091/124] fix ref --- e2e/solid-start/server-functions/app/routes/multipart.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/solid-start/server-functions/app/routes/multipart.tsx b/e2e/solid-start/server-functions/app/routes/multipart.tsx index fe77828af2..e04b20e1b4 100644 --- a/e2e/solid-start/server-functions/app/routes/multipart.tsx +++ b/e2e/solid-start/server-functions/app/routes/multipart.tsx @@ -74,7 +74,7 @@ function MultipartServerFnCall() { action={multipartFormDataServerFn.url} method="post" enctype="multipart/form-data" - ref={formRef!} + ref={()=>formRef} data-testid="multipart-form" > From 12ab5e07c9db8eb42a8dbb04fa502b0e7e9e29c7 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Thu, 27 Feb 2025 23:19:13 +0100 Subject: [PATCH 092/124] fix multipart test --- e2e/solid-start/server-functions/app/routes/multipart.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/solid-start/server-functions/app/routes/multipart.tsx b/e2e/solid-start/server-functions/app/routes/multipart.tsx index e04b20e1b4..07fc47ea7c 100644 --- a/e2e/solid-start/server-functions/app/routes/multipart.tsx +++ b/e2e/solid-start/server-functions/app/routes/multipart.tsx @@ -41,7 +41,7 @@ const multipartFormDataServerFn = createServerFn({ method: 'POST' }) }) function MultipartServerFnCall() { - const formRef: HTMLFormElement | null = null + let formRef: HTMLFormElement | undefined; const [multipartResult, setMultipartResult] = Solid.createSignal({}) const handleSubmit = (e: any) => { @@ -74,7 +74,7 @@ function MultipartServerFnCall() { action={multipartFormDataServerFn.url} method="post" enctype="multipart/form-data" - ref={()=>formRef} + ref={formRef} data-testid="multipart-form" > From 3e25ae5a8e8216a64a90305d3e79aa95cba4b2d1 Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Thu, 27 Feb 2025 16:26:18 -0600 Subject: [PATCH 093/124] fix solid router test --- packages/solid-start-server/src/StartServer.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/solid-start-server/src/StartServer.tsx b/packages/solid-start-server/src/StartServer.tsx index 0077ba6998..fc5f979aa0 100644 --- a/packages/solid-start-server/src/StartServer.tsx +++ b/packages/solid-start-server/src/StartServer.tsx @@ -1,4 +1,8 @@ -import { HeadContent, RouterProvider, Scripts } from '@tanstack/solid-router' +import { + RouterProvider, + Scripts, + ServerHeadContent, +} from '@tanstack/solid-router' import { Hydration, HydrationScript, NoHydration, ssr } from 'solid-js/web' import type { AnyRouter } from '@tanstack/solid-router' @@ -20,7 +24,7 @@ export function StartServer(props: { router={props.router} InnerWrap={(props) => ( - + {props.children} From 5640f600a96f2a169cd83afff0f463db90c1431f Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Thu, 27 Feb 2025 16:50:33 -0600 Subject: [PATCH 094/124] fix scroll restoration test --- e2e/solid-start/scroll-restoration/tests/app.spec.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/e2e/solid-start/scroll-restoration/tests/app.spec.ts b/e2e/solid-start/scroll-restoration/tests/app.spec.ts index 9a9752432b..413a68593e 100644 --- a/e2e/solid-start/scroll-restoration/tests/app.spec.ts +++ b/e2e/solid-start/scroll-restoration/tests/app.spec.ts @@ -1,5 +1,4 @@ import { expect, test } from '@playwright/test' -import { linkOptions } from '@tanstack/solid-router' test('Smoke - Renders home', async ({ page }) => { await page.goto('/') @@ -10,9 +9,9 @@ test('Smoke - Renders home', async ({ page }) => { // Test for scroll related stuff ;[ - linkOptions({ to: '/normal-page' }), - linkOptions({ to: '/with-loader' }), - linkOptions({ to: '/with-search', search: { where: 'footer' } }), + { to: '/normal-page' }, + { to: '/with-loader' }, + { to: '/with-search', search: { where: 'footer' } }, ].forEach((options) => { test(`On navigate to ${options.to} (from the header), scroll should be at top`, async ({ page, From 14b80a1c9f13fc461a3742dd62c98e175581adec Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Fri, 28 Feb 2025 00:15:09 +0100 Subject: [PATCH 095/124] align with react-start-client --- .../solid-start-client/src/ssr-client.tsx | 79 ++++++++++++------- pnpm-lock.yaml | 25 ------ 2 files changed, 52 insertions(+), 52 deletions(-) diff --git a/packages/solid-start-client/src/ssr-client.tsx b/packages/solid-start-client/src/ssr-client.tsx index b84fc9f919..d9bc984d59 100644 --- a/packages/solid-start-client/src/ssr-client.tsx +++ b/packages/solid-start-client/src/ssr-client.tsx @@ -9,7 +9,11 @@ import type { MakeRouteMatch, } from '@tanstack/solid-router' -import type { DeferredPromiseState, Manifest } from '@tanstack/router-core' +import type { + DeferredPromiseState, + Manifest, + RouteContextOptions, +} from '@tanstack/router-core' declare global { interface Window { @@ -122,13 +126,9 @@ export function hydrate(router: AnyRouter) { return router.loadRouteChunk(route) }), ) + // Right after hydration and before the first render, we need to rehydrate each match + // First step is to reyhdrate loaderData and __beforeLoadContext matches.forEach((match) => { - const route = router.looseRoutesById[match.routeId]! - - // Right after hydration and before the first render, we need to rehydrate each match - // This includes rehydrating the loaderData and also using the beforeLoadContext - // to reconstruct any context that was serialized on the server - const dehydratedMatch = window.__TSR_SSR__!.matches.find( (d) => d.id === match.id, ) @@ -136,20 +136,11 @@ export function hydrate(router: AnyRouter) { if (dehydratedMatch) { Object.assign(match, dehydratedMatch) - const parentMatch = matches[match.index - 1] - const parentContext = parentMatch?.context ?? router.options.context ?? {} - // Handle beforeLoadContext if (dehydratedMatch.__beforeLoadContext) { match.__beforeLoadContext = router.ssr!.serializer.parse( dehydratedMatch.__beforeLoadContext, ) as any - - match.context = { - ...parentContext, - ...match.__routeContext, - ...match.__beforeLoadContext, - } } // Handle loaderData @@ -175,6 +166,51 @@ export function hydrate(router: AnyRouter) { }) } + return match + }) + + router.__store.setState((s) => { + return { + ...s, + matches, + } + }) + + // Allow the user to handle custom hydration data + router.options.hydrate?.(dehydratedData) + + // now that all necessary data is hydrated: + // 1) fully reconstruct the route context + // 2) execute `head()` and `scripts()` for each match + router.state.matches.forEach((match) => { + const route = router.looseRoutesById[match.routeId]! + + const parentMatch = router.state.matches[match.index - 1] + const parentContext = parentMatch?.context ?? router.options.context ?? {} + + // `context()` was already executed by `matchRoutes`, however route context was not yet fully reconstructed + // so run it again and merge route context + const contextFnContext: RouteContextOptions = { + deps: match.loaderDeps, + params: match.params, + context: parentContext, + location: router.state.location, + navigate: (opts: any) => + router.navigate({ ...opts, _fromLocation: router.state.location }), + buildLocation: router.buildLocation, + cause: match.cause, + abortController: match.abortController, + preload: false, + matches, + } + match.__routeContext = route.options.context?.(contextFnContext) ?? {} + + match.context = { + ...parentContext, + ...match.__routeContext, + ...match.__beforeLoadContext, + } + const assetContext = { matches: router.state.matches, match, @@ -189,19 +225,8 @@ export function hydrate(router: AnyRouter) { match.links = headFnContent?.links match.headScripts = headFnContent?.scripts match.scripts = scripts - - return match }) - router.__store.setState((s) => { - return { - ...s, - matches, - } - }) - - // Allow the user to handle custom hydration data - router.options.hydrate?.(dehydratedData) return routeChunkPromise } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40bfecb9a1..a80b0e979f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1991,31 +1991,6 @@ importers: specifier: ^5.1.4 version: 5.1.4(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) - e2e/solid-start/basic-tsr-config: - dependencies: - '@tanstack/solid-router': - specifier: workspace:^ - version: link:../../../packages/solid-router - '@tanstack/solid-start': - specifier: workspace:^ - version: link:../../../packages/solid-start - solid-js: - specifier: ^1.0.0 - version: 1.9.5 - vinxi: - specifier: 0.5.3 - version: 0.5.3(@types/node@22.13.4)(db0@0.2.3)(ioredis@5.4.2)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0) - devDependencies: - '@tanstack/router-e2e-utils': - specifier: workspace:^ - version: link:../../e2e-utils - '@types/node': - specifier: ^22.10.2 - version: 22.13.4 - typescript: - specifier: ^5.7.2 - version: 5.7.3 - e2e/solid-start/scroll-restoration: dependencies: '@tanstack/solid-router': From e63f2afc8a557109f07345431bd8515a942c22f9 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Fri, 28 Feb 2025 00:51:00 +0100 Subject: [PATCH 096/124] add server function raw response --- .../server-functions/app/routeTree.gen.ts | 26 ++ .../app/routes/consistent.tsx | 10 +- .../server-functions/app/routes/index.tsx | 3 + .../app/routes/raw-response.tsx | 46 +++ .../app/routes/submit-post-formdata.tsx | 2 +- .../tests/server-functions.spec.ts | 14 + .../src/createMiddleware.ts | 173 +++++++--- .../solid-start-client/src/createServerFn.ts | 326 +++++++++++++----- .../src/tests/createServerFn.test-d.tsx | 239 +++++++++---- .../src/index.ts | 10 +- .../src/index.ts | 8 +- 11 files changed, 647 insertions(+), 210 deletions(-) create mode 100644 e2e/solid-start/server-functions/app/routes/raw-response.tsx diff --git a/e2e/solid-start/server-functions/app/routeTree.gen.ts b/e2e/solid-start/server-functions/app/routeTree.gen.ts index 423eef5f56..4a34fccd1c 100644 --- a/e2e/solid-start/server-functions/app/routeTree.gen.ts +++ b/e2e/solid-start/server-functions/app/routeTree.gen.ts @@ -15,6 +15,7 @@ import { Route as SubmitPostFormdataImport } from './routes/submit-post-formdata import { Route as StatusImport } from './routes/status' import { Route as SerializeFormDataImport } from './routes/serialize-form-data' import { Route as ReturnNullImport } from './routes/return-null' +import { Route as RawResponseImport } from './routes/raw-response' import { Route as MultipartImport } from './routes/multipart' import { Route as IsomorphicFnsImport } from './routes/isomorphic-fns' import { Route as HeadersImport } from './routes/headers' @@ -52,6 +53,12 @@ const ReturnNullRoute = ReturnNullImport.update({ getParentRoute: () => rootRoute, } as any) +const RawResponseRoute = RawResponseImport.update({ + id: '/raw-response', + path: '/raw-response', + getParentRoute: () => rootRoute, +} as any) + const MultipartRoute = MultipartImport.update({ id: '/multipart', path: '/multipart', @@ -172,6 +179,13 @@ declare module '@tanstack/solid-router' { preLoaderRoute: typeof MultipartImport parentRoute: typeof rootRoute } + '/raw-response': { + id: '/raw-response' + path: '/raw-response' + fullPath: '/raw-response' + preLoaderRoute: typeof RawResponseImport + parentRoute: typeof rootRoute + } '/return-null': { id: '/return-null' path: '/return-null' @@ -228,6 +242,7 @@ export interface FileRoutesByFullPath { '/headers': typeof HeadersRoute '/isomorphic-fns': typeof IsomorphicFnsRoute '/multipart': typeof MultipartRoute + '/raw-response': typeof RawResponseRoute '/return-null': typeof ReturnNullRoute '/serialize-form-data': typeof SerializeFormDataRoute '/status': typeof StatusRoute @@ -245,6 +260,7 @@ export interface FileRoutesByTo { '/headers': typeof HeadersRoute '/isomorphic-fns': typeof IsomorphicFnsRoute '/multipart': typeof MultipartRoute + '/raw-response': typeof RawResponseRoute '/return-null': typeof ReturnNullRoute '/serialize-form-data': typeof SerializeFormDataRoute '/status': typeof StatusRoute @@ -263,6 +279,7 @@ export interface FileRoutesById { '/headers': typeof HeadersRoute '/isomorphic-fns': typeof IsomorphicFnsRoute '/multipart': typeof MultipartRoute + '/raw-response': typeof RawResponseRoute '/return-null': typeof ReturnNullRoute '/serialize-form-data': typeof SerializeFormDataRoute '/status': typeof StatusRoute @@ -282,6 +299,7 @@ export interface FileRouteTypes { | '/headers' | '/isomorphic-fns' | '/multipart' + | '/raw-response' | '/return-null' | '/serialize-form-data' | '/status' @@ -298,6 +316,7 @@ export interface FileRouteTypes { | '/headers' | '/isomorphic-fns' | '/multipart' + | '/raw-response' | '/return-null' | '/serialize-form-data' | '/status' @@ -314,6 +333,7 @@ export interface FileRouteTypes { | '/headers' | '/isomorphic-fns' | '/multipart' + | '/raw-response' | '/return-null' | '/serialize-form-data' | '/status' @@ -332,6 +352,7 @@ export interface RootRouteChildren { HeadersRoute: typeof HeadersRoute IsomorphicFnsRoute: typeof IsomorphicFnsRoute MultipartRoute: typeof MultipartRoute + RawResponseRoute: typeof RawResponseRoute ReturnNullRoute: typeof ReturnNullRoute SerializeFormDataRoute: typeof SerializeFormDataRoute StatusRoute: typeof StatusRoute @@ -349,6 +370,7 @@ const rootRouteChildren: RootRouteChildren = { HeadersRoute: HeadersRoute, IsomorphicFnsRoute: IsomorphicFnsRoute, MultipartRoute: MultipartRoute, + RawResponseRoute: RawResponseRoute, ReturnNullRoute: ReturnNullRoute, SerializeFormDataRoute: SerializeFormDataRoute, StatusRoute: StatusRoute, @@ -375,6 +397,7 @@ export const routeTree = rootRoute "/headers", "/isomorphic-fns", "/multipart", + "/raw-response", "/return-null", "/serialize-form-data", "/status", @@ -407,6 +430,9 @@ export const routeTree = rootRoute "/multipart": { "filePath": "multipart.tsx" }, + "/raw-response": { + "filePath": "raw-response.tsx" + }, "/return-null": { "filePath": "return-null.tsx" }, diff --git a/e2e/solid-start/server-functions/app/routes/consistent.tsx b/e2e/solid-start/server-functions/app/routes/consistent.tsx index ed65258da0..9e4c1b3b08 100644 --- a/e2e/solid-start/server-functions/app/routes/consistent.tsx +++ b/e2e/solid-start/server-functions/app/routes/consistent.tsx @@ -21,19 +21,19 @@ export const Route = createFileRoute('/consistent')({ const cons_getFn1 = createServerFn() .validator((d: { username: string }) => d) - .handler(async ({ data }) => { + .handler(({ data }) => { return { payload: data } }) const cons_serverGetFn1 = createServerFn() .validator((d: { username: string }) => d) - .handler(({ data }) => { + .handler(async ({ data }) => { return cons_getFn1({ data }) }) const cons_postFn1 = createServerFn({ method: 'POST' }) .validator((d: { username: string }) => d) - .handler(async ({ data }) => { + .handler(({ data }) => { return { payload: data } }) @@ -111,9 +111,7 @@ function ConsistentServerFnCalls() { ) cons_postFn1({ data: { username: 'TEST' } }).then(setPostDirectResult) - cons_postFn1({ data: { username: 'TEST' }, fullResponse: true }).then( - setPostDirectResult, - ) + cons_postFn1({ data: { username: 'TEST' } }).then(setPostDirectResult) }} > Test Consistent server function responses diff --git a/e2e/solid-start/server-functions/app/routes/index.tsx b/e2e/solid-start/server-functions/app/routes/index.tsx index 75eae15e9d..bf28fb8b00 100644 --- a/e2e/solid-start/server-functions/app/routes/index.tsx +++ b/e2e/solid-start/server-functions/app/routes/index.tsx @@ -69,6 +69,9 @@ function Home() {
  • aborting a server function call
  • +
  • + server function returns raw response +
  • ) diff --git a/e2e/solid-start/server-functions/app/routes/raw-response.tsx b/e2e/solid-start/server-functions/app/routes/raw-response.tsx new file mode 100644 index 0000000000..c408dba4b4 --- /dev/null +++ b/e2e/solid-start/server-functions/app/routes/raw-response.tsx @@ -0,0 +1,46 @@ +import * as Solid from 'solid-js' +import { createFileRoute } from '@tanstack/react-router' +import { createServerFn } from '@tanstack/react-start' + +export const Route = createFileRoute('/raw-response')({ + component: RouteComponent, +}) + +const expectedValue = 'Hello from a server function!' +export const rawResponseFn = createServerFn({ response: 'raw' }).handler(() => { + return new Response(expectedValue) +}) + +function RouteComponent() { + const [formDataResult, setFormDataResult] = Solid.createSignal({}) + + return ( +
    +

    Raw Response

    +
    + It should return{' '} + +
    {expectedValue}
    +
    +
    + + + +
    +
    {JSON.stringify(formDataResult())}
    +
    +
    + ) +} diff --git a/e2e/solid-start/server-functions/app/routes/submit-post-formdata.tsx b/e2e/solid-start/server-functions/app/routes/submit-post-formdata.tsx index d9b8ed5588..3fb6ea356f 100644 --- a/e2e/solid-start/server-functions/app/routes/submit-post-formdata.tsx +++ b/e2e/solid-start/server-functions/app/routes/submit-post-formdata.tsx @@ -9,7 +9,7 @@ const testValues = { name: 'Sean', } -export const greetUser = createServerFn({ method: 'POST' }) +export const greetUser = createServerFn({ method: 'POST', response: 'raw' }) .validator((data: FormData) => { if (!(data instanceof FormData)) { throw new Error('Invalid! FormData is required') diff --git a/e2e/solid-start/server-functions/tests/server-functions.spec.ts b/e2e/solid-start/server-functions/tests/server-functions.spec.ts index 29c36bc6e1..ad211b9075 100644 --- a/e2e/solid-start/server-functions/tests/server-functions.spec.ts +++ b/e2e/solid-start/server-functions/tests/server-functions.spec.ts @@ -321,3 +321,17 @@ test.describe('aborting a server function call', () => { expect(errorMessage).toContain('abort') }) }) + +test('raw response', async ({ page }) => { + await page.goto('/raw-response') + + await page.waitForLoadState('networkidle') + + const expectedValue = (await page.getByTestId('expected').textContent()) || '' + expect(expectedValue).not.toBe('') + + await page.getByTestId('button').click() + await page.waitForLoadState('networkidle') + + await expect(page.getByTestId('response')).toContainText(expectedValue) +}) \ No newline at end of file diff --git a/packages/solid-start-client/src/createMiddleware.ts b/packages/solid-start-client/src/createMiddleware.ts index 34c4920597..5c57402e9a 100644 --- a/packages/solid-start-client/src/createMiddleware.ts +++ b/packages/solid-start-client/src/createMiddleware.ts @@ -1,6 +1,7 @@ import type { ConstrainValidator, Method, + ServerFnResponseType, ServerFnTypeOrTypeFn, } from './createServerFn' import type { @@ -131,6 +132,7 @@ export interface MiddlewareOptions< in out TValidator, in out TServerContext, in out TClientContext, + in out TServerFnResponseType extends ServerFnResponseType, > { validateClient?: boolean middleware?: TMiddlewares @@ -139,14 +141,16 @@ export interface MiddlewareOptions< TMiddlewares, TValidator, TServerContext, - TClientContext + TClientContext, + TServerFnResponseType > server?: MiddlewareServerFn< TMiddlewares, TValidator, TServerContext, unknown, - unknown + unknown, + TServerFnResponseType > } @@ -169,10 +173,12 @@ export interface MiddlewareServerFnOptions< in out TMiddlewares, in out TValidator, in out TServerSendContext, + in out TServerFnResponseType, > { data: Expand> context: Expand> next: MiddlewareServerNextFn + response: TServerFnResponseType method: Method filename: string functionId: string @@ -185,11 +191,13 @@ export type MiddlewareServerFn< TServerSendContext, TNewServerContext, TSendContext, + TServerFnResponseType extends ServerFnResponseType, > = ( options: MiddlewareServerFnOptions< TMiddlewares, TValidator, - TServerSendContext + TServerSendContext, + TServerFnResponseType >, ) => MiddlewareServerFnResult< TMiddlewares, @@ -233,16 +241,23 @@ export type MiddlewareClientNextFn = < export interface MiddlewareClientFnOptions< in out TMiddlewares, in out TValidator, + in out TServerFnResponseType extends ServerFnResponseType, > { data: Expand> context: Expand> sendContext: Expand> method: Method + response: TServerFnResponseType signal: AbortSignal next: MiddlewareClientNextFn filename: string functionId: string - type: ServerFnTypeOrTypeFn + type: ServerFnTypeOrTypeFn< + Method, + TServerFnResponseType, + TMiddlewares, + TValidator + > } export type MiddlewareClientFn< @@ -250,8 +265,13 @@ export type MiddlewareClientFn< TValidator, TSendContext, TClientContext, + TServerFnResponseType extends ServerFnResponseType, > = ( - options: MiddlewareClientFnOptions, + options: MiddlewareClientFnOptions< + TMiddlewares, + TValidator, + TServerFnResponseType + >, ) => MiddlewareClientFnResult export type MiddlewareClientFnResult< @@ -290,7 +310,15 @@ export type ClientResultWithContext< headers: HeadersInit } -export type AnyMiddleware = MiddlewareWithTypes +export type AnyMiddleware = MiddlewareWithTypes< + any, + any, + any, + any, + any, + any, + any +> export interface MiddlewareTypes< in out TMiddlewares, @@ -341,6 +369,7 @@ export interface MiddlewareWithTypes< TServerSendContext, TClientContext, TClientSendContext, + TServerFnResponseType extends ServerFnResponseType, > { _types: MiddlewareTypes< TMiddlewares, @@ -354,26 +383,44 @@ export interface MiddlewareWithTypes< TMiddlewares, TValidator, TServerContext, - TClientContext + TClientContext, + TServerFnResponseType > } -export interface MiddlewareAfterValidator - extends MiddlewareWithTypes< +export interface MiddlewareAfterValidator< + TMiddlewares, + TValidator, + TServerFnResponseType extends ServerFnResponseType, +> extends MiddlewareWithTypes< TMiddlewares, TValidator, undefined, undefined, undefined, - undefined + undefined, + ServerFnResponseType >, - MiddlewareServer, - MiddlewareClient {} + MiddlewareServer< + TMiddlewares, + TValidator, + undefined, + undefined, + TServerFnResponseType + >, + MiddlewareClient {} -export interface MiddlewareValidator { +export interface MiddlewareValidator< + TMiddlewares, + TServerFnResponseType extends ServerFnResponseType, +> { validator: ( input: ConstrainValidator, - ) => MiddlewareAfterValidator + ) => MiddlewareAfterValidator< + TMiddlewares, + TNewValidator, + TServerFnResponseType + > } export interface MiddlewareAfterServer< @@ -383,13 +430,15 @@ export interface MiddlewareAfterServer< TServerSendContext, TClientContext, TClientSendContext, + TServerFnResponseType extends ServerFnResponseType, > extends MiddlewareWithTypes< TMiddlewares, TValidator, TServerContext, TServerSendContext, TClientContext, - TClientSendContext + TClientSendContext, + TServerFnResponseType > {} export interface MiddlewareServer< @@ -397,6 +446,7 @@ export interface MiddlewareServer< TValidator, TServerSendContext, TClientContext, + TServerFnResponseType extends ServerFnResponseType, > { server: ( server: MiddlewareServerFn< @@ -404,7 +454,8 @@ export interface MiddlewareServer< TValidator, TServerSendContext, TNewServerContext, - TSendContext + TSendContext, + TServerFnResponseType >, ) => MiddlewareAfterServer< TMiddlewares, @@ -412,7 +463,8 @@ export interface MiddlewareServer< TNewServerContext, TServerSendContext, TClientContext, - TSendContext + TSendContext, + ServerFnResponseType > } @@ -421,71 +473,97 @@ export interface MiddlewareAfterClient< TValidator, TServerSendContext, TClientContext, + TServerFnResponseType extends ServerFnResponseType, > extends MiddlewareWithTypes< TMiddlewares, TValidator, undefined, TServerSendContext, TClientContext, - undefined + undefined, + TServerFnResponseType >, MiddlewareServer< TMiddlewares, TValidator, TServerSendContext, - TClientContext + TClientContext, + TServerFnResponseType > {} -export interface MiddlewareClient { + export interface MiddlewareClient< + TMiddlewares, + TValidator, + TServerFnResponseType extends ServerFnResponseType, + > { client: ( client: MiddlewareClientFn< TMiddlewares, TValidator, TSendServerContext, - TNewClientContext + TNewClientContext, + TServerFnResponseType >, ) => MiddlewareAfterClient< TMiddlewares, TValidator, TSendServerContext, - TNewClientContext + TNewClientContext, + ServerFnResponseType > } -export interface MiddlewareAfterMiddleware - extends MiddlewareWithTypes< +export interface MiddlewareAfterMiddleware< + TMiddlewares, + TServerFnResponseType extends ServerFnResponseType, +> extends MiddlewareWithTypes< TMiddlewares, undefined, undefined, undefined, undefined, - undefined + undefined, + TServerFnResponseType >, - MiddlewareServer, - MiddlewareClient, - MiddlewareValidator {} - -export interface Middleware extends MiddlewareAfterMiddleware { - middleware: ( - middlewares: Constrain>, - ) => MiddlewareAfterMiddleware -} + MiddlewareServer< + TMiddlewares, + undefined, + undefined, + undefined, + TServerFnResponseType + >, + MiddlewareClient, + MiddlewareValidator {} + + export interface Middleware + extends MiddlewareAfterMiddleware { + middleware: ( + middlewares: Constrain>, + ) => MiddlewareAfterMiddleware + } -export function createMiddleware( - options?: { - validateClient?: boolean - }, - __opts?: MiddlewareOptions, -): Middleware { - // const resolvedOptions = (__opts || options) as MiddlewareOptions< - const resolvedOptions = - __opts || - ((options || {}) as MiddlewareOptions< + export function createMiddleware( + options?: { + validateClient?: boolean + }, + __opts?: MiddlewareOptions< unknown, undefined, undefined, - undefined - >) + undefined, + ServerFnResponseType + >, + ): Middleware { + // const resolvedOptions = (__opts || options) as MiddlewareOptions< + const resolvedOptions = + __opts || + ((options || {}) as MiddlewareOptions< + unknown, + undefined, + undefined, + undefined, + ServerFnResponseType + >) return { options: resolvedOptions as any, @@ -513,5 +591,4 @@ export function createMiddleware( Object.assign(resolvedOptions, { server }), ) as any }, - } as unknown as Middleware -} + } as unknown as Middleware} diff --git a/packages/solid-start-client/src/createServerFn.ts b/packages/solid-start-client/src/createServerFn.ts index c685af88fc..efc570590e 100644 --- a/packages/solid-start-client/src/createServerFn.ts +++ b/packages/solid-start-client/src/createServerFn.ts @@ -4,6 +4,7 @@ import { isNotFound, isRedirect } from '@tanstack/solid-router' import { mergeHeaders } from './headers' import { globalMiddleware } from './registerGlobalMiddleware' import { startSerializer } from './serializer' +import type { Readable } from 'node:stream' import type { AnyValidator, Constrain, @@ -31,20 +32,37 @@ export interface JsonResponse extends Response { export type CompiledFetcherFnOptions = { method: Method data: unknown + response?: ServerFnResponseType headers?: HeadersInit signal?: AbortSignal context?: any } -export type Fetcher = +export type Fetcher< + TMiddlewares, + TValidator, + TResponse, + TServerFnResponseType extends ServerFnResponseType, +> = undefined extends IntersectAllValidatorInputs - ? OptionalFetcher - : RequiredFetcher + ? OptionalFetcher< + TMiddlewares, + TValidator, + TResponse, + TServerFnResponseType + > + : RequiredFetcher< + TMiddlewares, + TValidator, + TResponse, + TServerFnResponseType + > export interface FetcherBase { url: string __executeServer: (opts: { method: Method + response?: ServerFnResponseType data: unknown headers?: HeadersInit context?: any @@ -55,51 +73,50 @@ export interface FetcherBase { export type FetchResult< TMiddlewares, TResponse, - TFullResponse extends boolean, -> = false extends TFullResponse - ? Promise> - : Promise> - -export interface OptionalFetcher - extends FetcherBase { - ( - options?: OptionalFetcherDataOptions< - TMiddlewares, - TValidator, - TFullResponse - >, - ): FetchResult + TServerFnResponseType extends ServerFnResponseType, +> = TServerFnResponseType extends 'raw' + ? Promise + : TServerFnResponseType extends 'full' + ? Promise> + : Promise> + +export interface OptionalFetcher< + TMiddlewares, + TValidator, + TResponse, + TServerFnResponseType extends ServerFnResponseType, +> extends FetcherBase { + ( + options?: OptionalFetcherDataOptions, + ): FetchResult } -export interface RequiredFetcher - extends FetcherBase { - ( - opts: RequiredFetcherDataOptions, - ): FetchResult +export interface RequiredFetcher< + TMiddlewares, + TValidator, + TResponse, + TServerFnResponseType extends ServerFnResponseType, +> extends FetcherBase { + ( + opts: RequiredFetcherDataOptions, + ): FetchResult } -export type FetcherBaseOptions = { +export type FetcherBaseOptions = { headers?: HeadersInit type?: ServerFnType signal?: AbortSignal - fullResponse?: TFullResponse } export type ServerFnType = 'static' | 'dynamic' -export interface OptionalFetcherDataOptions< - TMiddlewares, - TValidator, - TFullResponse extends boolean, -> extends FetcherBaseOptions { +export interface OptionalFetcherDataOptions + extends FetcherBaseOptions { data?: Expand> } -export interface RequiredFetcherDataOptions< - TMiddlewares, - TValidator, - TFullResponse extends boolean, -> extends FetcherBaseOptions { +export interface RequiredFetcherDataOptions + extends FetcherBaseOptions { data: Expand> } @@ -119,39 +136,78 @@ export type RscStream = { } export type Method = 'GET' | 'POST' +export type ServerFnResponseType = 'data' | 'full' | 'raw' -export type ServerFn = ( - ctx: ServerFnCtx, -) => Promise> | SerializerStringify +// see https://h3.unjs.io/guide/event-handler#responses-types +export type RawResponse = Response | ReadableStream | Readable | null | string -export interface ServerFnCtx { +export type ServerFnReturnType< + TServerFnResponseType extends ServerFnResponseType, + TResponse, +> = TServerFnResponseType extends 'raw' + ? RawResponse | Promise + : Promise> | SerializerStringify +export type ServerFn< + TMethod, + TServerFnResponseType extends ServerFnResponseType, + TMiddlewares, + TValidator, + TResponse, +> = ( + ctx: ServerFnCtx, +) => ServerFnReturnType + +export interface ServerFnCtx< + TMethod, + TServerFnResponseType extends ServerFnResponseType, + TMiddlewares, + TValidator, +> { method: TMethod + response: TServerFnResponseType data: Expand> context: Expand> signal: AbortSignal } -export type CompiledFetcherFn = { +export type CompiledFetcherFn< + TResponse, + TServerFnResponseType extends ServerFnResponseType, +> = { ( - opts: CompiledFetcherFnOptions & ServerFnBaseOptions, + opts: CompiledFetcherFnOptions & + ServerFnBaseOptions, ): Promise url: string } type ServerFnBaseOptions< TMethod extends Method = 'GET', + TServerFnResponseType extends ServerFnResponseType = 'data', TResponse = unknown, TMiddlewares = unknown, TInput = unknown, > = { method: TMethod + response?: TServerFnResponseType validateClient?: boolean middleware?: Constrain> validator?: ConstrainValidator - extractedFn?: CompiledFetcherFn - serverFn?: ServerFn + extractedFn?: CompiledFetcherFn + serverFn?: ServerFn< + TMethod, + TServerFnResponseType, + TMiddlewares, + TInput, + TResponse + > functionId: string - type: ServerFnTypeOrTypeFn + type: ServerFnTypeOrTypeFn< + TMethod, + TServerFnResponseType, + TMiddlewares, + AnyValidator + > } export type ValidatorSerializerStringify = Validator< @@ -166,78 +222,142 @@ export type ConstrainValidator = unknown extends TValidator ? TValidator : Constrain> -export interface ServerFnMiddleware { +export interface ServerFnMiddleware< + TMethod extends Method, + TServerFnResponseType extends ServerFnResponseType, + TValidator, +> { middleware: ( middlewares: Constrain>, - ) => ServerFnAfterMiddleware + ) => ServerFnAfterMiddleware< + TMethod, + TServerFnResponseType, + TNewMiddlewares, + TValidator + > } export interface ServerFnAfterMiddleware< TMethod extends Method, + TServerFnResponseType extends ServerFnResponseType, TMiddlewares, TValidator, -> extends ServerFnValidator, - ServerFnTyper, - ServerFnHandler {} +> extends ServerFnValidator, + ServerFnTyper, + ServerFnHandler {} -export type ValidatorFn = ( +export type ValidatorFn< + TMethod extends Method, + TServerFnResponseType extends ServerFnResponseType, + TMiddlewares, +> = ( validator: ConstrainValidator, -) => ServerFnAfterValidator +) => ServerFnAfterValidator< + TMethod, + TServerFnResponseType, + TMiddlewares, + TValidator +> -export interface ServerFnValidator { - validator: ValidatorFn +export interface ServerFnValidator< + TMethod extends Method, + TServerFnResponseType extends ServerFnResponseType, + TMiddlewares, +> { + validator: ValidatorFn } export interface ServerFnAfterValidator< TMethod extends Method, + TServerFnResponseType extends ServerFnResponseType, TMiddlewares, TValidator, -> extends ServerFnMiddleware, - ServerFnTyper, - ServerFnHandler {} +> extends ServerFnMiddleware, + ServerFnTyper, + ServerFnHandler {} // Typer export interface ServerFnTyper< TMethod extends Method, + TServerFnResponseType extends ServerFnResponseType, TMiddlewares, TValidator, > { type: ( - typer: ServerFnTypeOrTypeFn, - ) => ServerFnAfterTyper + typer: ServerFnTypeOrTypeFn< + TMethod, + TServerFnResponseType, + TMiddlewares, + TValidator + >, + ) => ServerFnAfterTyper< + TMethod, + TServerFnResponseType, + TMiddlewares, + TValidator + > } export type ServerFnTypeOrTypeFn< TMethod extends Method, + TServerFnResponseType extends ServerFnResponseType, TMiddlewares, TValidator, > = | ServerFnType - | ((ctx: ServerFnCtx) => ServerFnType) + | (( + ctx: ServerFnCtx< + TMethod, + TServerFnResponseType, + TMiddlewares, + TValidator + >, + ) => ServerFnType) export interface ServerFnAfterTyper< TMethod extends Method, + TServerFnResponseType extends ServerFnResponseType, TMiddlewares, TValidator, -> extends ServerFnHandler {} +> extends ServerFnHandler< + TMethod, + TServerFnResponseType, + TMiddlewares, + TValidator + > {} // Handler export interface ServerFnHandler< TMethod extends Method, + TServerFnResponseType extends ServerFnResponseType, TMiddlewares, TValidator, > { handler: ( - fn?: ServerFn, - ) => Fetcher + fn?: ServerFn< + TMethod, + TServerFnResponseType, + TMiddlewares, + TValidator, + TNewResponse + >, + ) => Fetcher } -export interface ServerFnBuilder - extends ServerFnMiddleware, - ServerFnValidator, - ServerFnTyper, - ServerFnHandler { - options: ServerFnBaseOptions +export interface ServerFnBuilder< + TMethod extends Method = 'GET', + TServerFnResponseType extends ServerFnResponseType = 'data', +> extends ServerFnMiddleware, + ServerFnValidator, + ServerFnTyper, + ServerFnHandler { + options: ServerFnBaseOptions< + TMethod, + TServerFnResponseType, + unknown, + undefined, + undefined + > } type StaticCachedResult = { @@ -379,18 +499,27 @@ setServerFnStaticCache(() => { export function createServerFn< TMethod extends Method, + TServerFnResponseType extends ServerFnResponseType = 'data', TResponse = unknown, TMiddlewares = undefined, TValidator = undefined, >( options?: { method?: TMethod + response?: TServerFnResponseType type?: ServerFnType }, - __opts?: ServerFnBaseOptions, -): ServerFnBuilder { + __opts?: ServerFnBaseOptions< + TMethod, + TServerFnResponseType, + TResponse, + TMiddlewares, + TValidator + >, +): ServerFnBuilder { const resolvedOptions = (__opts || options || {}) as ServerFnBaseOptions< TMethod, + ServerFnResponseType, TResponse, TMiddlewares, TValidator @@ -403,30 +532,45 @@ export function createServerFn< return { options: resolvedOptions as any, middleware: (middleware) => { - return createServerFn( - undefined, - Object.assign(resolvedOptions, { middleware }), - ) as any + return createServerFn< + TMethod, + ServerFnResponseType, + TResponse, + TMiddlewares, + TValidator + >(undefined, Object.assign(resolvedOptions, { middleware })) as any }, validator: (validator) => { - return createServerFn( - undefined, - Object.assign(resolvedOptions, { validator }), - ) as any + return createServerFn< + TMethod, + ServerFnResponseType, + TResponse, + TMiddlewares, + TValidator + >(undefined, Object.assign(resolvedOptions, { validator })) as any }, type: (type) => { - return createServerFn( - undefined, - Object.assign(resolvedOptions, { type }), - ) as any + return createServerFn< + TMethod, + ServerFnResponseType, + TResponse, + TMiddlewares, + TValidator + >(undefined, Object.assign(resolvedOptions, { type })) as any }, handler: (...args) => { // This function signature changes due to AST transformations // in the babel plugin. We need to cast it to the correct // function signature post-transformation const [extractedFn, serverFn] = args as unknown as [ - CompiledFetcherFn, - ServerFn, + CompiledFetcherFn, + ServerFn< + TMethod, + TServerFnResponseType, + TMiddlewares, + TValidator, + TResponse + >, ] // Keep the original function around so we can use it @@ -455,6 +599,9 @@ export function createServerFn< signal: opts?.signal, context: {}, }).then((d) => { + if (resolvedOptions.response === 'full') { + return d + } if (d.error) throw d.error return d.result }) @@ -589,19 +736,20 @@ function flattenMiddlewares( export type MiddlewareOptions = { method: Method + response?: ServerFnResponseType data: any headers?: HeadersInit signal?: AbortSignal sendContext?: any context?: any - type: ServerFnTypeOrTypeFn + type: ServerFnTypeOrTypeFn functionId: string } export type MiddlewareResult = MiddlewareOptions & { result?: unknown error?: unknown - type: ServerFnTypeOrTypeFn + type: ServerFnTypeOrTypeFn } export type NextFn = (ctx: MiddlewareResult) => Promise @@ -634,7 +782,11 @@ const applyMiddleware = async ( }, headers: mergeHeaders(ctx.headers, userCtx.headers), result: - userCtx.result !== undefined ? userCtx.result : (ctx as any).result, + userCtx.result !== undefined + ? userCtx.result + : ctx.response === 'raw' + ? userCtx + : (ctx as any).result, error: userCtx.error ?? (ctx as any).error, }) }) as any, @@ -729,7 +881,7 @@ async function executeMiddleware( } function serverFnBaseToMiddleware( - options: ServerFnBaseOptions, + options: ServerFnBaseOptions, ): AnyMiddleware { return { _types: undefined!, diff --git a/packages/solid-start-client/src/tests/createServerFn.test-d.tsx b/packages/solid-start-client/src/tests/createServerFn.test-d.tsx index 981b48b461..2bf9986cf6 100644 --- a/packages/solid-start-client/src/tests/createServerFn.test-d.tsx +++ b/packages/solid-start-client/src/tests/createServerFn.test-d.tsx @@ -1,4 +1,4 @@ -import { expectTypeOf, test } from 'vitest' +import { describe, expectTypeOf, test } from 'vitest' import { createServerFn } from '../createServerFn' import { createMiddleware } from '../createMiddleware' import type { Constrain, Validator } from '@tanstack/router-core' @@ -20,16 +20,17 @@ test('createServerFn without middleware', () => { context: undefined data: undefined signal: AbortSignal + response: 'data' }>() }) }) test('createServerFn with validator', () => { - const fnAfterValidator = createServerFn({ method: 'GET' }).validator( - (input: { input: string }) => ({ - a: input.input, - }), - ) + const fnAfterValidator = createServerFn({ + method: 'GET', + }).validator((input: { input: string }) => ({ + a: input.input, + })) expectTypeOf(fnAfterValidator).toHaveProperty('handler') expectTypeOf(fnAfterValidator).toHaveProperty('middleware') @@ -43,6 +44,7 @@ test('createServerFn with validator', () => { a: string } signal: AbortSignal + response: 'data' }>() }) @@ -50,11 +52,10 @@ test('createServerFn with validator', () => { data: { input: string } headers?: HeadersInit type?: 'static' | 'dynamic' - fullResponse?: boolean signal?: AbortSignal }>() - expectTypeOf(fn).returns.resolves.toEqualTypeOf() + expectTypeOf>().resolves.toEqualTypeOf() }) test('createServerFn with middleware and context', () => { @@ -105,11 +106,12 @@ test('createServerFn with middleware and context', () => { } data: undefined signal: AbortSignal + response: 'data' }>() }) }) -test('createServerFn with middleware and validator', () => { +describe('createServerFn with middleware and validator', () => { const middleware1 = createMiddleware().validator( (input: { readonly inputA: 'inputA' }) => ({ @@ -126,60 +128,96 @@ test('createServerFn with middleware and validator', () => { const middleware3 = createMiddleware().middleware([middleware1, middleware2]) - const fn = createServerFn({ method: 'GET' }) - .middleware([middleware3]) - .validator( - (input: { readonly inputC: 'inputC' }) => - ({ - outputC: 'outputC', - }) as const, - ) - .handler((options) => { - expectTypeOf(options).toEqualTypeOf<{ - method: 'GET' - context: undefined - data: { - readonly outputA: 'outputA' - readonly outputB: 'outputB' - readonly outputC: 'outputC' - } - signal: AbortSignal - }>() + test(`response: 'data'`, () => { + const fn = createServerFn({ method: 'GET', response: 'data' }) + .middleware([middleware3]) + .validator( + (input: { readonly inputC: 'inputC' }) => + ({ + outputC: 'outputC', + }) as const, + ) + .handler((options) => { + expectTypeOf(options).toEqualTypeOf<{ + method: 'GET' + context: undefined + data: { + readonly outputA: 'outputA' + readonly outputB: 'outputB' + readonly outputC: 'outputC' + } + signal: AbortSignal + response: 'data' + }>() + + return 'some-data' as const + }) + + expectTypeOf(fn).parameter(0).toEqualTypeOf<{ + data: { + readonly inputA: 'inputA' + readonly inputB: 'inputB' + readonly inputC: 'inputC' + } + headers?: HeadersInit + type?: 'static' | 'dynamic' + signal?: AbortSignal + }>() - return 'data' as const - }) + expectTypeOf(fn).returns.resolves.toEqualTypeOf<'some-data'>() + expectTypeOf(() => + fn({ + data: { inputA: 'inputA', inputB: 'inputB', inputC: 'inputC' }, + }), + ).returns.resolves.toEqualTypeOf<'some-data'>() + }) - expectTypeOf(fn).parameter(0).toEqualTypeOf<{ - data: { - readonly inputA: 'inputA' - readonly inputB: 'inputB' - readonly inputC: 'inputC' - } - headers?: HeadersInit - type?: 'static' | 'dynamic' - fullResponse?: boolean - signal?: AbortSignal - }>() + test(`response: 'full'`, () => { + const fn = createServerFn({ method: 'GET', response: 'full' }) + .middleware([middleware3]) + .validator( + (input: { readonly inputC: 'inputC' }) => + ({ + outputC: 'outputC', + }) as const, + ) + .handler((options) => { + expectTypeOf(options).toEqualTypeOf<{ + method: 'GET' + context: undefined + data: { + readonly outputA: 'outputA' + readonly outputB: 'outputB' + readonly outputC: 'outputC' + } + signal: AbortSignal + response: 'full' + }>() + + return 'some-data' as const + }) + + expectTypeOf(fn).parameter(0).toEqualTypeOf<{ + data: { + readonly inputA: 'inputA' + readonly inputB: 'inputB' + readonly inputC: 'inputC' + } + headers?: HeadersInit + type?: 'static' | 'dynamic' + signal?: AbortSignal + }>() - expectTypeOf(fn).returns.resolves.toEqualTypeOf<'data'>() - - expectTypeOf(() => - fn({ - fullResponse: false, - data: { inputA: 'inputA', inputB: 'inputB', inputC: 'inputC' }, - }), - ).returns.resolves.toEqualTypeOf<'data'>() - - expectTypeOf(() => - fn({ - fullResponse: true, - data: { inputA: 'inputA', inputB: 'inputB', inputC: 'inputC' }, - }), - ).returns.resolves.toEqualTypeOf<{ - result: 'data' - context: undefined - error: unknown - }>() + expectTypeOf(() => + fn({ + data: { inputA: 'inputA', inputB: 'inputB', inputC: 'inputC' }, + }), + ).returns.resolves.toEqualTypeOf<{ + result: 'some-data' + context: undefined + error: unknown + }>() + }) }) test('createServerFn overrides properties', () => { @@ -255,6 +293,7 @@ test('createServerFn where validator is a primitive', () => { context: undefined data: 'c' signal: AbortSignal + response: 'data' }>() }) }) @@ -268,6 +307,7 @@ test('createServerFn where validator is optional if object is optional', () => { context: undefined data: 'c' | undefined signal: AbortSignal + response: 'data' }>() }) @@ -276,13 +316,12 @@ test('createServerFn where validator is optional if object is optional', () => { data?: 'c' | undefined headers?: HeadersInit type?: 'static' | 'dynamic' - fullResponse?: boolean signal?: AbortSignal } | undefined >() - expectTypeOf(fn).returns.resolves.toEqualTypeOf() + expectTypeOf>().resolves.toEqualTypeOf() }) test('createServerFn where data is optional if there is no validator', () => { @@ -292,6 +331,7 @@ test('createServerFn where data is optional if there is no validator', () => { context: undefined data: undefined signal: AbortSignal + response: 'data' }>() }) @@ -300,13 +340,12 @@ test('createServerFn where data is optional if there is no validator', () => { data?: undefined headers?: HeadersInit type?: 'static' | 'dynamic' - fullResponse?: boolean signal?: AbortSignal } | undefined >() - expectTypeOf(fn).returns.resolves.toEqualTypeOf() + expectTypeOf>().resolves.toEqualTypeOf() }) test('createServerFn returns Date', () => { @@ -388,3 +427,75 @@ test('createServerFn can validate FormData', () => { > >() }) + +describe('response', () => { + describe('data', () => { + test(`response: 'data' is passed into handler without response being set`, () => { + createServerFn().handler((options) => { + expectTypeOf(options.response).toEqualTypeOf<'data'>() + }) + }) + + test(`response: 'data' is passed into handler with explicit response: 'data'`, () => { + createServerFn({ response: 'data' }).handler((options) => { + expectTypeOf(options.response).toEqualTypeOf<'data'>() + }) + }) + }) + describe('full', () => { + test(`response: 'full' is passed into handler`, () => { + createServerFn({ response: 'full' }).handler((options) => { + expectTypeOf(options.response).toEqualTypeOf<'full'>() + }) + }) + }) + + describe('raw', () => { + test(`response: 'raw' is passed into handler`, () => { + createServerFn({ response: 'raw' }).handler((options) => { + expectTypeOf(options.response).toEqualTypeOf<'raw'>() + return null + }) + }) + }) + test(`client receives Response when Response is returned`, () => { + const fn = createServerFn({ response: 'raw' }).handler(() => { + return new Response('Hello World') + }) + + expectTypeOf(fn()).toEqualTypeOf>() + }) + + test(`client receives Response when ReadableStream is returned`, () => { + const fn = createServerFn({ response: 'raw' }).handler(() => { + return new ReadableStream() + }) + + expectTypeOf(fn()).toEqualTypeOf>() + }) + + test(`client receives Response when string is returned`, () => { + const fn = createServerFn({ response: 'raw' }).handler(() => { + return 'hello' + }) + + expectTypeOf(fn()).toEqualTypeOf>() + }) +}) + +test('createServerFn can be used as a mutation function', () => { + const serverFn = createServerFn() + .validator((data: number) => data) + .handler(() => 'foo') + + type MutationFunction = ( + variables: TVariables, + ) => Promise + + // simplifeid "clone" of @tansctack/react-query's useMutation + const useMutation = ( + fn: MutationFunction, + ) => {} + + useMutation(serverFn) +}) \ No newline at end of file diff --git a/packages/solid-start-server-functions-fetcher/src/index.ts b/packages/solid-start-server-functions-fetcher/src/index.ts index 655f91dc33..ff34d0fce5 100644 --- a/packages/solid-start-server-functions-fetcher/src/index.ts +++ b/packages/solid-start-server-functions-fetcher/src/index.ts @@ -13,7 +13,7 @@ export async function serverFnFetcher( // If createServerFn was used to wrap the fetcher, // We need to handle the arguments differently if (isPlainObject(_first) && _first.method) { - const first = _first as MiddlewareClientFnOptions & { + const first = _first as MiddlewareClientFnOptions & { headers: HeadersInit } const type = first.data instanceof FormData ? 'formData' : 'payload' @@ -56,6 +56,10 @@ export async function serverFnFetcher( url += `?createServerFn` } + if (first.response === 'raw') { + url += `&raw` + } + const handlerResponse = await handler(url, { method: first.method, headers, @@ -110,7 +114,9 @@ export async function serverFnFetcher( } } -function getFetcherRequestOptions(opts: MiddlewareClientFnOptions) { +function getFetcherRequestOptions( + opts: MiddlewareClientFnOptions, +) { if (opts.method === 'POST') { if (opts.data instanceof FormData) { opts.data.set('__TSR_CONTEXT', startSerializer.stringify(opts.context)) diff --git a/packages/solid-start-server-functions-handler/src/index.ts b/packages/solid-start-server-functions-handler/src/index.ts index 3cfd145e85..c80b7a0f37 100644 --- a/packages/solid-start-server-functions-handler/src/index.ts +++ b/packages/solid-start-server-functions-handler/src/index.ts @@ -75,6 +75,7 @@ async function handleServerRequest({ } const isCreateServerFn = 'createServerFn' in search + const isRaw = 'raw' in search if (typeof serverFnId !== 'string') { throw new Error('Invalid server action param for serverFnId: ' + serverFnId) @@ -179,8 +180,8 @@ async function handleServerRequest({ // Any time we get a Response back, we should just // return it immediately. - if (result instanceof Response) { - return result + if (result.result instanceof Response) { + return result.result } // If this is a non createServerFn request, we need to @@ -276,6 +277,9 @@ async function handleServerRequest({ })() event.node.req.removeListener('close', abort) + if (isRaw) { + return response + } if (process.env.NODE_ENV === 'development') console.info(`ServerFn Response: ${response.status}`) From ff1db2f51655fd05ac4e6752bb5a9b1dfcb2a9fa Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Fri, 28 Feb 2025 00:52:32 +0100 Subject: [PATCH 097/124] fix import --- e2e/solid-start/server-functions/app/routes/raw-response.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/solid-start/server-functions/app/routes/raw-response.tsx b/e2e/solid-start/server-functions/app/routes/raw-response.tsx index c408dba4b4..d0ff217944 100644 --- a/e2e/solid-start/server-functions/app/routes/raw-response.tsx +++ b/e2e/solid-start/server-functions/app/routes/raw-response.tsx @@ -1,6 +1,6 @@ import * as Solid from 'solid-js' -import { createFileRoute } from '@tanstack/react-router' -import { createServerFn } from '@tanstack/react-start' +import { createFileRoute } from '@tanstack/solid-router' +import { createServerFn } from '@tanstack/solid-start' export const Route = createFileRoute('/raw-response')({ component: RouteComponent, From 66154b10eb8459f52091c38c04a05edcb7138e73 Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Thu, 27 Feb 2025 18:03:51 -0600 Subject: [PATCH 098/124] fix server function tests --- e2e/solid-start/server-functions/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/solid-start/server-functions/package.json b/e2e/solid-start/server-functions/package.json index 34207420fb..4e9d908851 100644 --- a/e2e/solid-start/server-functions/package.json +++ b/e2e/solid-start/server-functions/package.json @@ -14,7 +14,7 @@ "@tanstack/solid-router": "workspace:^", "@tanstack/solid-start": "workspace:^", "js-cookie": "^3.0.5", - "solid-js": "^1.0.0", + "solid-js": "1.9.4", "redaxios": "^0.5.1", "tailwind-merge": "^2.6.0", "vinxi": "0.5.3", From bc74b98871ae49564656b8f00440153eb4e4f3a6 Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Thu, 27 Feb 2025 18:05:45 -0600 Subject: [PATCH 099/124] fix server function tests --- pnpm-lock.yaml | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a80b0e979f..243eee3226 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2064,8 +2064,8 @@ importers: specifier: ^0.5.1 version: 0.5.1 solid-js: - specifier: ^1.0.0 - version: 1.9.5 + specifier: 1.9.4 + version: 1.9.4 tailwind-merge: specifier: ^2.6.0 version: 2.6.0 @@ -2105,7 +2105,7 @@ importers: version: 5.7.3 vite-plugin-solid: specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.4)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) vite-tsconfig-paths: specifier: ^5.1.4 version: 5.1.4(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) @@ -22116,6 +22116,21 @@ snapshots: transitivePeerDependencies: - supports-color + vite-plugin-solid@2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.4)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)): + dependencies: + '@babel/core': 7.26.8 + '@types/babel__core': 7.20.5 + babel-preset-solid: 1.9.3(@babel/core@7.26.8) + merge-anything: 5.1.7 + solid-js: 1.9.4 + solid-refresh: 0.6.3(solid-js@1.9.4) + vite: 6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) + vitefu: 1.0.5(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + optionalDependencies: + '@testing-library/jest-dom': 6.6.3 + transitivePeerDependencies: + - supports-color + vite-plugin-solid@2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)): dependencies: '@babel/core': 7.26.8 From 2fbafed92919413742594962f633efb632a5c251 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Fri, 28 Feb 2025 01:16:54 +0100 Subject: [PATCH 100/124] format --- .../app/routes/isomorphic-fns.tsx | 14 +++- .../server-functions/app/routes/multipart.tsx | 2 +- .../tests/server-functions.spec.ts | 2 +- .../src/createMiddleware.ts | 73 ++++++++++--------- .../src/tests/createServerFn.test-d.tsx | 2 +- 5 files changed, 50 insertions(+), 43 deletions(-) diff --git a/e2e/solid-start/server-functions/app/routes/isomorphic-fns.tsx b/e2e/solid-start/server-functions/app/routes/isomorphic-fns.tsx index 53410ef0cf..3327b38af7 100644 --- a/e2e/solid-start/server-functions/app/routes/isomorphic-fns.tsx +++ b/e2e/solid-start/server-functions/app/routes/isomorphic-fns.tsx @@ -37,7 +37,7 @@ function RouteComponent() { ]) setResults({ envOnClick, echo, serverEnv, serverEcho }) } - + return (
    )}
    diff --git a/e2e/solid-start/server-functions/app/routes/multipart.tsx b/e2e/solid-start/server-functions/app/routes/multipart.tsx index 07fc47ea7c..5e959da7ae 100644 --- a/e2e/solid-start/server-functions/app/routes/multipart.tsx +++ b/e2e/solid-start/server-functions/app/routes/multipart.tsx @@ -41,7 +41,7 @@ const multipartFormDataServerFn = createServerFn({ method: 'POST' }) }) function MultipartServerFnCall() { - let formRef: HTMLFormElement | undefined; + let formRef: HTMLFormElement | undefined const [multipartResult, setMultipartResult] = Solid.createSignal({}) const handleSubmit = (e: any) => { diff --git a/e2e/solid-start/server-functions/tests/server-functions.spec.ts b/e2e/solid-start/server-functions/tests/server-functions.spec.ts index ad211b9075..e0decd0ddf 100644 --- a/e2e/solid-start/server-functions/tests/server-functions.spec.ts +++ b/e2e/solid-start/server-functions/tests/server-functions.spec.ts @@ -334,4 +334,4 @@ test('raw response', async ({ page }) => { await page.waitForLoadState('networkidle') await expect(page.getByTestId('response')).toContainText(expectedValue) -}) \ No newline at end of file +}) diff --git a/packages/solid-start-client/src/createMiddleware.ts b/packages/solid-start-client/src/createMiddleware.ts index 5c57402e9a..654fe78271 100644 --- a/packages/solid-start-client/src/createMiddleware.ts +++ b/packages/solid-start-client/src/createMiddleware.ts @@ -402,13 +402,13 @@ export interface MiddlewareAfterValidator< ServerFnResponseType >, MiddlewareServer< - TMiddlewares, - TValidator, - undefined, - undefined, - TServerFnResponseType - >, - MiddlewareClient {} + TMiddlewares, + TValidator, + undefined, + undefined, + TServerFnResponseType + >, + MiddlewareClient {} export interface MiddlewareValidator< TMiddlewares, @@ -491,11 +491,11 @@ export interface MiddlewareAfterClient< TServerFnResponseType > {} - export interface MiddlewareClient< - TMiddlewares, - TValidator, - TServerFnResponseType extends ServerFnResponseType, - > { +export interface MiddlewareClient< + TMiddlewares, + TValidator, + TServerFnResponseType extends ServerFnResponseType, +> { client: ( client: MiddlewareClientFn< TMiddlewares, @@ -535,35 +535,35 @@ export interface MiddlewareAfterMiddleware< MiddlewareClient, MiddlewareValidator {} - export interface Middleware - extends MiddlewareAfterMiddleware { - middleware: ( - middlewares: Constrain>, - ) => MiddlewareAfterMiddleware - } +export interface Middleware + extends MiddlewareAfterMiddleware { + middleware: ( + middlewares: Constrain>, + ) => MiddlewareAfterMiddleware +} - export function createMiddleware( - options?: { - validateClient?: boolean - }, - __opts?: MiddlewareOptions< +export function createMiddleware( + options?: { + validateClient?: boolean + }, + __opts?: MiddlewareOptions< + unknown, + undefined, + undefined, + undefined, + ServerFnResponseType + >, +): Middleware { + // const resolvedOptions = (__opts || options) as MiddlewareOptions< + const resolvedOptions = + __opts || + ((options || {}) as MiddlewareOptions< unknown, undefined, undefined, undefined, ServerFnResponseType - >, - ): Middleware { - // const resolvedOptions = (__opts || options) as MiddlewareOptions< - const resolvedOptions = - __opts || - ((options || {}) as MiddlewareOptions< - unknown, - undefined, - undefined, - undefined, - ServerFnResponseType - >) + >) return { options: resolvedOptions as any, @@ -591,4 +591,5 @@ export interface MiddlewareAfterMiddleware< Object.assign(resolvedOptions, { server }), ) as any }, - } as unknown as Middleware} + } as unknown as Middleware +} diff --git a/packages/solid-start-client/src/tests/createServerFn.test-d.tsx b/packages/solid-start-client/src/tests/createServerFn.test-d.tsx index 2bf9986cf6..372c0a456b 100644 --- a/packages/solid-start-client/src/tests/createServerFn.test-d.tsx +++ b/packages/solid-start-client/src/tests/createServerFn.test-d.tsx @@ -498,4 +498,4 @@ test('createServerFn can be used as a mutation function', () => { ) => {} useMutation(serverFn) -}) \ No newline at end of file +}) From 9690a3055257c409e8446ae9f01327948b3e7828 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Fri, 28 Feb 2025 14:22:41 +0100 Subject: [PATCH 101/124] align with #3605 --- packages/solid-start-client/src/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/solid-start-client/src/index.tsx b/packages/solid-start-client/src/index.tsx index 28b3ff5dd6..bc97adc78e 100644 --- a/packages/solid-start-client/src/index.tsx +++ b/packages/solid-start-client/src/index.tsx @@ -19,6 +19,7 @@ export { type FetcherBaseOptions, type ServerFn, type ServerFnCtx, + type ServerFnResponseType, } from './createServerFn' export { createMiddleware, From fe93868eab10229f52bc3302b88560ff072b4e1b Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Fri, 28 Feb 2025 15:46:38 +0100 Subject: [PATCH 102/124] comment out start packages from publish.js --- scripts/publish.js | 88 +++++++++++++++++++++++----------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/scripts/publish.js b/scripts/publish.js index d8396a4f2f..d8ae8585b7 100644 --- a/scripts/publish.js +++ b/scripts/publish.js @@ -84,50 +84,50 @@ await publish({ name: '@tanstack/create-start', packageDir: 'packages/create-start', }, - { - name: '@tanstack/solid-start', - packageDir: 'packages/solid-start', - }, - { - name: '@tanstack/solid-start-plugin', - packageDir: 'packages/solid-start-plugin', - }, - { - name: '@tanstack/solid-start-client', - packageDir: 'packages/solid-start-client', - }, - { - name: '@tanstack/solid-start-server', - packageDir: 'packages/solid-start-server', - }, - { - name: '@tanstack/solid-start-config', - packageDir: 'packages/solid-start-config', - }, - { - name: '@tanstack/solid-start-api-routes', - packageDir: 'packages/solid-start-api-routes', - }, - { - name: '@tanstack/solid-start-server-functions-fetcher', - packageDir: 'packages/solid-start-server-functions-fetcher', - }, - { - name: '@tanstack/solid-start-server-functions-handler', - packageDir: 'packages/solid-start-server-functions-handler', - }, - { - name: '@tanstack/solid-start-server-functions-client', - packageDir: 'packages/solid-start-server-functions-client', - }, - { - name: '@tanstack/solid-start-server-functions-ssr', - packageDir: 'packages/solid-start-server-functions-ssr', - }, - { - name: '@tanstack/solid-start-router-manifest', - packageDir: 'packages/solid-start-router-manifest', - }, + // { + // name: '@tanstack/solid-start', + // packageDir: 'packages/solid-start', + // }, + // { + // name: '@tanstack/solid-start-plugin', + // packageDir: 'packages/solid-start-plugin', + // }, + // { + // name: '@tanstack/solid-start-client', + // packageDir: 'packages/solid-start-client', + // }, + // { + // name: '@tanstack/solid-start-server', + // packageDir: 'packages/solid-start-server', + // }, + // { + // name: '@tanstack/solid-start-config', + // packageDir: 'packages/solid-start-config', + // }, + // { + // name: '@tanstack/solid-start-api-routes', + // packageDir: 'packages/solid-start-api-routes', + // }, + // { + // name: '@tanstack/solid-start-server-functions-fetcher', + // packageDir: 'packages/solid-start-server-functions-fetcher', + // }, + // { + // name: '@tanstack/solid-start-server-functions-handler', + // packageDir: 'packages/solid-start-server-functions-handler', + // }, + // { + // name: '@tanstack/solid-start-server-functions-client', + // packageDir: 'packages/solid-start-server-functions-client', + // }, + // { + // name: '@tanstack/solid-start-server-functions-ssr', + // packageDir: 'packages/solid-start-server-functions-ssr', + // }, + // { + // name: '@tanstack/solid-start-router-manifest', + // packageDir: 'packages/solid-start-router-manifest', + // }, { name: '@tanstack/react-start', packageDir: 'packages/react-start', From 64921f2ae9cec056998d13c1ad954eeb8a60ef27 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Fri, 28 Feb 2025 18:41:50 +0100 Subject: [PATCH 103/124] add e2e tests for updating meta tags in client side navigation --- e2e/react-start/basic/app/routes/posts.tsx | 7 +++++++ e2e/react-start/basic/tests/meta.spec.ts | 10 ++++++++++ e2e/solid-start/basic/app/routes/posts.tsx | 7 +++++++ e2e/solid-start/basic/tests/meta.spec.ts | 10 ++++++++++ 4 files changed, 34 insertions(+) create mode 100644 e2e/react-start/basic/tests/meta.spec.ts create mode 100644 e2e/solid-start/basic/tests/meta.spec.ts diff --git a/e2e/react-start/basic/app/routes/posts.tsx b/e2e/react-start/basic/app/routes/posts.tsx index 5a67375c84..173bfdeb4c 100644 --- a/e2e/react-start/basic/app/routes/posts.tsx +++ b/e2e/react-start/basic/app/routes/posts.tsx @@ -3,6 +3,13 @@ import { Link, Outlet, createFileRoute } from '@tanstack/react-router' import { fetchPosts } from '~/utils/posts' export const Route = createFileRoute('/posts')({ + head: () => ({ + meta: [{ + title: + 'Posts page', + }, + ] + }), loader: async () => fetchPosts(), component: PostsComponent, }) diff --git a/e2e/react-start/basic/tests/meta.spec.ts b/e2e/react-start/basic/tests/meta.spec.ts new file mode 100644 index 0000000000..ad30a043cd --- /dev/null +++ b/e2e/react-start/basic/tests/meta.spec.ts @@ -0,0 +1,10 @@ + +import { expect, test } from '@playwright/test' + +test('Should change title on client side navigation', async ({ page }) => { + await page.goto('/') + + await page.getByRole('link', { name: 'Posts' }).click() + + await expect(page).toHaveTitle('Posts page') +}) \ No newline at end of file diff --git a/e2e/solid-start/basic/app/routes/posts.tsx b/e2e/solid-start/basic/app/routes/posts.tsx index 90d87d8a28..e23de12e23 100644 --- a/e2e/solid-start/basic/app/routes/posts.tsx +++ b/e2e/solid-start/basic/app/routes/posts.tsx @@ -4,6 +4,13 @@ import { For } from 'solid-js' import { fetchPosts } from '~/utils/posts' export const Route = createFileRoute('/posts')({ + head: () => ({ + meta: [{ + title: + 'Posts page', + }, + ] + }), loader: async () => fetchPosts(), component: PostsComponent, }) diff --git a/e2e/solid-start/basic/tests/meta.spec.ts b/e2e/solid-start/basic/tests/meta.spec.ts new file mode 100644 index 0000000000..ad30a043cd --- /dev/null +++ b/e2e/solid-start/basic/tests/meta.spec.ts @@ -0,0 +1,10 @@ + +import { expect, test } from '@playwright/test' + +test('Should change title on client side navigation', async ({ page }) => { + await page.goto('/') + + await page.getByRole('link', { name: 'Posts' }).click() + + await expect(page).toHaveTitle('Posts page') +}) \ No newline at end of file From d782a63b4797a9ce193d94fa419ba641e57517db Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 28 Feb 2025 17:43:05 +0000 Subject: [PATCH 104/124] ci: apply automated fixes --- e2e/react-start/basic/app/routes/posts.tsx | 8 ++++---- e2e/react-start/basic/tests/meta.spec.ts | 5 ++--- e2e/solid-start/basic/app/routes/posts.tsx | 8 ++++---- e2e/solid-start/basic/tests/meta.spec.ts | 5 ++--- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/e2e/react-start/basic/app/routes/posts.tsx b/e2e/react-start/basic/app/routes/posts.tsx index 173bfdeb4c..0f69c18341 100644 --- a/e2e/react-start/basic/app/routes/posts.tsx +++ b/e2e/react-start/basic/app/routes/posts.tsx @@ -4,11 +4,11 @@ import { fetchPosts } from '~/utils/posts' export const Route = createFileRoute('/posts')({ head: () => ({ - meta: [{ - title: - 'Posts page', + meta: [ + { + title: 'Posts page', }, - ] + ], }), loader: async () => fetchPosts(), component: PostsComponent, diff --git a/e2e/react-start/basic/tests/meta.spec.ts b/e2e/react-start/basic/tests/meta.spec.ts index ad30a043cd..80032d058f 100644 --- a/e2e/react-start/basic/tests/meta.spec.ts +++ b/e2e/react-start/basic/tests/meta.spec.ts @@ -1,10 +1,9 @@ - import { expect, test } from '@playwright/test' test('Should change title on client side navigation', async ({ page }) => { await page.goto('/') await page.getByRole('link', { name: 'Posts' }).click() - + await expect(page).toHaveTitle('Posts page') -}) \ No newline at end of file +}) diff --git a/e2e/solid-start/basic/app/routes/posts.tsx b/e2e/solid-start/basic/app/routes/posts.tsx index e23de12e23..0e94cd4d2c 100644 --- a/e2e/solid-start/basic/app/routes/posts.tsx +++ b/e2e/solid-start/basic/app/routes/posts.tsx @@ -5,11 +5,11 @@ import { fetchPosts } from '~/utils/posts' export const Route = createFileRoute('/posts')({ head: () => ({ - meta: [{ - title: - 'Posts page', + meta: [ + { + title: 'Posts page', }, - ] + ], }), loader: async () => fetchPosts(), component: PostsComponent, diff --git a/e2e/solid-start/basic/tests/meta.spec.ts b/e2e/solid-start/basic/tests/meta.spec.ts index ad30a043cd..80032d058f 100644 --- a/e2e/solid-start/basic/tests/meta.spec.ts +++ b/e2e/solid-start/basic/tests/meta.spec.ts @@ -1,10 +1,9 @@ - import { expect, test } from '@playwright/test' test('Should change title on client side navigation', async ({ page }) => { await page.goto('/') await page.getByRole('link', { name: 'Posts' }).click() - + await expect(page).toHaveTitle('Posts page') -}) \ No newline at end of file +}) From 2953a471587ea45437a729fd9b712c3e57bdc563 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Fri, 28 Feb 2025 18:47:11 +0100 Subject: [PATCH 105/124] add to nav test --- e2e/react-start/basic/tests/meta.spec.ts | 9 --------- e2e/react-start/basic/tests/navigation.spec.ts | 16 ++++++++++++++++ e2e/solid-start/basic/tests/meta.spec.ts | 9 --------- e2e/solid-start/basic/tests/navigation.spec.ts | 8 ++++++++ 4 files changed, 24 insertions(+), 18 deletions(-) delete mode 100644 e2e/react-start/basic/tests/meta.spec.ts delete mode 100644 e2e/solid-start/basic/tests/meta.spec.ts diff --git a/e2e/react-start/basic/tests/meta.spec.ts b/e2e/react-start/basic/tests/meta.spec.ts deleted file mode 100644 index 80032d058f..0000000000 --- a/e2e/react-start/basic/tests/meta.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { expect, test } from '@playwright/test' - -test('Should change title on client side navigation', async ({ page }) => { - await page.goto('/') - - await page.getByRole('link', { name: 'Posts' }).click() - - await expect(page).toHaveTitle('Posts page') -}) diff --git a/e2e/react-start/basic/tests/navigation.spec.ts b/e2e/react-start/basic/tests/navigation.spec.ts index ffa4ddb595..4a8b27532d 100644 --- a/e2e/react-start/basic/tests/navigation.spec.ts +++ b/e2e/react-start/basic/tests/navigation.spec.ts @@ -45,3 +45,19 @@ test('Navigating to a not-found route', async ({ page }) => { await page.getByRole('link', { name: 'Start Over' }).click() await expect(page.getByRole('heading')).toContainText('Welcome Home!') }) + +test('Should change title on client side navigation', async ({ page }) => { + await page.goto('/') + + await page.getByRole('link', { name: 'Posts' }).click() + + await expect(page).toHaveTitle('Posts page') +}) + +test('Should change title on client side navigation', async ({ page }) => { + await page.goto('/') + + await page.getByRole('link', { name: 'Posts' }).click() + + await expect(page).toHaveTitle('Posts page') +}) diff --git a/e2e/solid-start/basic/tests/meta.spec.ts b/e2e/solid-start/basic/tests/meta.spec.ts deleted file mode 100644 index 80032d058f..0000000000 --- a/e2e/solid-start/basic/tests/meta.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { expect, test } from '@playwright/test' - -test('Should change title on client side navigation', async ({ page }) => { - await page.goto('/') - - await page.getByRole('link', { name: 'Posts' }).click() - - await expect(page).toHaveTitle('Posts page') -}) diff --git a/e2e/solid-start/basic/tests/navigation.spec.ts b/e2e/solid-start/basic/tests/navigation.spec.ts index ffa4ddb595..fe64e44f79 100644 --- a/e2e/solid-start/basic/tests/navigation.spec.ts +++ b/e2e/solid-start/basic/tests/navigation.spec.ts @@ -45,3 +45,11 @@ test('Navigating to a not-found route', async ({ page }) => { await page.getByRole('link', { name: 'Start Over' }).click() await expect(page.getByRole('heading')).toContainText('Welcome Home!') }) + +test('Should change title on client side navigation', async ({ page }) => { + await page.goto('/') + + await page.getByRole('link', { name: 'Posts' }).click() + + await expect(page).toHaveTitle('Posts page') +}) From 1409a851d6f11b904228340b17eeef589ca493b7 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 28 Feb 2025 17:50:05 +0000 Subject: [PATCH 106/124] ci: apply automated fixes --- e2e/solid-start/basic/tests/navigation.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/solid-start/basic/tests/navigation.spec.ts b/e2e/solid-start/basic/tests/navigation.spec.ts index fe64e44f79..2c61b6fc01 100644 --- a/e2e/solid-start/basic/tests/navigation.spec.ts +++ b/e2e/solid-start/basic/tests/navigation.spec.ts @@ -50,6 +50,6 @@ test('Should change title on client side navigation', async ({ page }) => { await page.goto('/') await page.getByRole('link', { name: 'Posts' }).click() - + await expect(page).toHaveTitle('Posts page') }) From d9ed5d4ccf2b3382dd2cb0ef8a9d2cee6c7b04c6 Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Fri, 28 Feb 2025 19:50:43 -0600 Subject: [PATCH 107/124] Get @solidjs/meta working --- packages/solid-start-client/src/StartClient.tsx | 7 +++++-- packages/solid-start-server/package.json | 5 +++-- packages/solid-start-server/src/StartServer.tsx | 9 ++++++--- pnpm-lock.yaml | 6 ++++++ 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/solid-start-client/src/StartClient.tsx b/packages/solid-start-client/src/StartClient.tsx index 86d13fa7aa..4336e15915 100644 --- a/packages/solid-start-client/src/StartClient.tsx +++ b/packages/solid-start-client/src/StartClient.tsx @@ -1,4 +1,4 @@ -import { Await, RouterProvider } from '@tanstack/solid-router' +import { Await, HeadContent, RouterProvider } from '@tanstack/solid-router' import { hydrate } from './ssr-client' import type { AnyRouter } from '@tanstack/solid-router' import type { JSXElement } from 'solid-js' @@ -25,7 +25,10 @@ export function StartClient(props: { router: AnyRouter }) { router={props.router} InnerWrap={(props) => ( - {props.children} + + + {props.children} + )} diff --git a/packages/solid-start-server/package.json b/packages/solid-start-server/package.json index c2fb17b205..858f9ccbfa 100644 --- a/packages/solid-start-server/package.json +++ b/packages/solid-start-server/package.json @@ -62,6 +62,7 @@ "node": ">=12" }, "dependencies": { + "@solidjs/meta": "^0.29.4", "@tanstack/history": "workspace:^", "@tanstack/router-core": "workspace:^", "@tanstack/solid-router": "workspace:^", @@ -73,11 +74,11 @@ "unctx": "^2.4.1" }, "devDependencies": { - "vite-plugin-solid": "^2.11.2", "@types/jsesc": "^3.0.3", "esbuild": "^0.25.0", "solid-js": "^1.0.0", - "typescript": "^5.7.2" + "typescript": "^5.7.2", + "vite-plugin-solid": "^2.11.2" }, "peerDependencies": { "solid-js": "^1.0.0" diff --git a/packages/solid-start-server/src/StartServer.tsx b/packages/solid-start-server/src/StartServer.tsx index fc5f979aa0..b6ad972745 100644 --- a/packages/solid-start-server/src/StartServer.tsx +++ b/packages/solid-start-server/src/StartServer.tsx @@ -5,6 +5,7 @@ import { } from '@tanstack/solid-router' import { Hydration, HydrationScript, NoHydration, ssr } from 'solid-js/web' import type { AnyRouter } from '@tanstack/solid-router' +import { MetaProvider, Title } from '@solidjs/meta' const docType = ssr('') @@ -24,9 +25,11 @@ export function StartServer(props: { router={props.router} InnerWrap={(props) => ( - - {props.children} - + + + {props.children} + + )} /> diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 243eee3226..b9eebad6cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1883,6 +1883,9 @@ importers: e2e/solid-start/basic: dependencies: + '@solidjs/meta': + specifier: ^0.29.4 + version: 0.29.4(solid-js@1.9.4) '@tanstack/solid-router': specifier: workspace:^ version: link:../../../packages/solid-router @@ -6213,6 +6216,9 @@ importers: packages/solid-start-server: dependencies: + '@solidjs/meta': + specifier: ^0.29.4 + version: 0.29.4(solid-js@1.9.4) '@tanstack/history': specifier: workspace:* version: link:../history From 98baae42f3f3ba080795a8ce934faf95bc476421 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Sat, 1 Mar 2025 02:51:33 +0100 Subject: [PATCH 108/124] revert changes to react --- e2e/react-start/basic/app/routes/posts.tsx | 7 ------- e2e/react-start/basic/tests/navigation.spec.ts | 18 +----------------- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/e2e/react-start/basic/app/routes/posts.tsx b/e2e/react-start/basic/app/routes/posts.tsx index 0f69c18341..5a67375c84 100644 --- a/e2e/react-start/basic/app/routes/posts.tsx +++ b/e2e/react-start/basic/app/routes/posts.tsx @@ -3,13 +3,6 @@ import { Link, Outlet, createFileRoute } from '@tanstack/react-router' import { fetchPosts } from '~/utils/posts' export const Route = createFileRoute('/posts')({ - head: () => ({ - meta: [ - { - title: 'Posts page', - }, - ], - }), loader: async () => fetchPosts(), component: PostsComponent, }) diff --git a/e2e/react-start/basic/tests/navigation.spec.ts b/e2e/react-start/basic/tests/navigation.spec.ts index 4a8b27532d..f333b5fdd5 100644 --- a/e2e/react-start/basic/tests/navigation.spec.ts +++ b/e2e/react-start/basic/tests/navigation.spec.ts @@ -44,20 +44,4 @@ test('Navigating to a not-found route', async ({ page }) => { await page.getByRole('link', { name: 'This Route Does Not Exist' }).click() await page.getByRole('link', { name: 'Start Over' }).click() await expect(page.getByRole('heading')).toContainText('Welcome Home!') -}) - -test('Should change title on client side navigation', async ({ page }) => { - await page.goto('/') - - await page.getByRole('link', { name: 'Posts' }).click() - - await expect(page).toHaveTitle('Posts page') -}) - -test('Should change title on client side navigation', async ({ page }) => { - await page.goto('/') - - await page.getByRole('link', { name: 'Posts' }).click() - - await expect(page).toHaveTitle('Posts page') -}) +}) \ No newline at end of file From 445aeac9acd630cb6aca86767a5424272973eafc Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Sat, 1 Mar 2025 02:52:27 +0100 Subject: [PATCH 109/124] newline --- e2e/react-start/basic/tests/navigation.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/react-start/basic/tests/navigation.spec.ts b/e2e/react-start/basic/tests/navigation.spec.ts index f333b5fdd5..ffa4ddb595 100644 --- a/e2e/react-start/basic/tests/navigation.spec.ts +++ b/e2e/react-start/basic/tests/navigation.spec.ts @@ -44,4 +44,4 @@ test('Navigating to a not-found route', async ({ page }) => { await page.getByRole('link', { name: 'This Route Does Not Exist' }).click() await page.getByRole('link', { name: 'Start Over' }).click() await expect(page.getByRole('heading')).toContainText('Welcome Home!') -}) \ No newline at end of file +}) From b3712fea91273351d5ad068e6e6957e99d8512b9 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Sat, 1 Mar 2025 02:55:20 +0100 Subject: [PATCH 110/124] fix lockfile --- pnpm-lock.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9eebad6cc..b121a28ad8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1883,9 +1883,6 @@ importers: e2e/solid-start/basic: dependencies: - '@solidjs/meta': - specifier: ^0.29.4 - version: 0.29.4(solid-js@1.9.4) '@tanstack/solid-router': specifier: workspace:^ version: link:../../../packages/solid-router From fac83a0b91f21eaf4e5149de56c57709df46d374 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Sat, 1 Mar 2025 02:58:54 +0100 Subject: [PATCH 111/124] skip solid-router nav test --- e2e/solid-router/basic-file-based/tests/app.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/solid-router/basic-file-based/tests/app.spec.ts b/e2e/solid-router/basic-file-based/tests/app.spec.ts index 767ebe0468..485e6ab30c 100644 --- a/e2e/solid-router/basic-file-based/tests/app.spec.ts +++ b/e2e/solid-router/basic-file-based/tests/app.spec.ts @@ -254,7 +254,7 @@ async function structuralSharingTest(page: Page, enabled: boolean) { await checkSearch({ bar: 'b2', foo: 'f2' }) } -test('Should change title on client side navigation', async ({ page }) => { +test.skip('Should change title on client side navigation', async ({ page }) => { await page.goto('/') await page.getByRole('link', { name: 'Posts' }).click() From 6f7e4453e85782c340d855def87f9e64fc44f2fa Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Sat, 1 Mar 2025 03:11:55 +0100 Subject: [PATCH 112/124] format --- packages/solid-start-server/src/StartServer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/solid-start-server/src/StartServer.tsx b/packages/solid-start-server/src/StartServer.tsx index b6ad972745..569c744cd4 100644 --- a/packages/solid-start-server/src/StartServer.tsx +++ b/packages/solid-start-server/src/StartServer.tsx @@ -4,8 +4,8 @@ import { ServerHeadContent, } from '@tanstack/solid-router' import { Hydration, HydrationScript, NoHydration, ssr } from 'solid-js/web' +import { MetaProvider } from '@solidjs/meta' import type { AnyRouter } from '@tanstack/solid-router' -import { MetaProvider, Title } from '@solidjs/meta' const docType = ssr('') From f190c7af2bc8346e0475aaaaa0214a451528eaea Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Sat, 1 Mar 2025 03:19:12 +0100 Subject: [PATCH 113/124] fix client navigation head tag updates for solid-router --- e2e/solid-router/basic-file-based/tests/app.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/solid-router/basic-file-based/tests/app.spec.ts b/e2e/solid-router/basic-file-based/tests/app.spec.ts index 485e6ab30c..a65ae1e5e3 100644 --- a/e2e/solid-router/basic-file-based/tests/app.spec.ts +++ b/e2e/solid-router/basic-file-based/tests/app.spec.ts @@ -254,7 +254,7 @@ async function structuralSharingTest(page: Page, enabled: boolean) { await checkSearch({ bar: 'b2', foo: 'f2' }) } -test.skip('Should change title on client side navigation', async ({ page }) => { +test.only('Should change title on client side navigation', async ({ page }) => { await page.goto('/') await page.getByRole('link', { name: 'Posts' }).click() From 0aa9487735cc37518992ed68ffc2c8ba2bb4dd04 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Sat, 1 Mar 2025 03:19:47 +0100 Subject: [PATCH 114/124] remove .only --- e2e/solid-router/basic-file-based/tests/app.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/solid-router/basic-file-based/tests/app.spec.ts b/e2e/solid-router/basic-file-based/tests/app.spec.ts index a65ae1e5e3..767ebe0468 100644 --- a/e2e/solid-router/basic-file-based/tests/app.spec.ts +++ b/e2e/solid-router/basic-file-based/tests/app.spec.ts @@ -254,7 +254,7 @@ async function structuralSharingTest(page: Page, enabled: boolean) { await checkSearch({ bar: 'b2', foo: 'f2' }) } -test.only('Should change title on client side navigation', async ({ page }) => { +test('Should change title on client side navigation', async ({ page }) => { await page.goto('/') await page.getByRole('link', { name: 'Posts' }).click() From 17611e0f23ae85b1cd19db4470e96d3335fcfe0d Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Sat, 1 Mar 2025 04:27:36 +0100 Subject: [PATCH 115/124] move ServerHeadContent to solid-start-server --- .../solid-start-server/src/StartServer.tsx | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/solid-start-server/src/StartServer.tsx b/packages/solid-start-server/src/StartServer.tsx index 569c744cd4..1527789ed5 100644 --- a/packages/solid-start-server/src/StartServer.tsx +++ b/packages/solid-start-server/src/StartServer.tsx @@ -1,12 +1,27 @@ import { + Asset, RouterProvider, Scripts, - ServerHeadContent, + useTags, } from '@tanstack/solid-router' -import { Hydration, HydrationScript, NoHydration, ssr } from 'solid-js/web' +import { Hydration, HydrationScript, NoHydration, ssr, useAssets } from 'solid-js/web' import { MetaProvider } from '@solidjs/meta' import type { AnyRouter } from '@tanstack/solid-router' +export function ServerHeadContent() { + const tags = useTags() + useAssets(() => { + return ( + + {tags().map((tag) => ( + + ))} + + ) + }) + return null +} + const docType = ssr('') export function StartServer(props: { From ab1a9999eee4fc6103d220d41b853b035d71ac11 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 1 Mar 2025 03:28:44 +0000 Subject: [PATCH 116/124] ci: apply automated fixes --- packages/solid-start-server/src/StartServer.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/solid-start-server/src/StartServer.tsx b/packages/solid-start-server/src/StartServer.tsx index 1527789ed5..93d2a452d3 100644 --- a/packages/solid-start-server/src/StartServer.tsx +++ b/packages/solid-start-server/src/StartServer.tsx @@ -1,10 +1,11 @@ +import { Asset, RouterProvider, Scripts, useTags } from '@tanstack/solid-router' import { - Asset, - RouterProvider, - Scripts, - useTags, -} from '@tanstack/solid-router' -import { Hydration, HydrationScript, NoHydration, ssr, useAssets } from 'solid-js/web' + Hydration, + HydrationScript, + NoHydration, + ssr, + useAssets, +} from 'solid-js/web' import { MetaProvider } from '@solidjs/meta' import type { AnyRouter } from '@tanstack/solid-router' From 0a2eac5cff2dd36df921b662aa2658810f24222f Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Sat, 1 Mar 2025 04:33:01 +0100 Subject: [PATCH 117/124] remove basic auth --- e2e/solid-start/basic-auth/.env | 7 - e2e/solid-start/basic-auth/.gitignore | 22 -- e2e/solid-start/basic-auth/.prettierignore | 4 - e2e/solid-start/basic-auth/app.config.ts | 12 - e2e/solid-start/basic-auth/app/client.tsx | 8 - .../basic-auth/app/components/Auth.tsx | 58 ---- .../app/components/DefaultCatchBoundary.tsx | 53 ---- .../basic-auth/app/components/Login.tsx | 71 ----- .../basic-auth/app/components/NotFound.tsx | 25 -- .../basic-auth/app/hooks/useMutation.ts | 41 --- .../basic-auth/app/routeTree.gen.ts | 290 ------------------ e2e/solid-start/basic-auth/app/router.tsx | 22 -- .../basic-auth/app/routes/__root.tsx | 120 -------- .../basic-auth/app/routes/_authed.tsx | 61 ---- .../app/routes/_authed/posts.$postId.tsx | 29 -- .../app/routes/_authed/posts.index.tsx | 9 - .../basic-auth/app/routes/_authed/posts.tsx | 39 --- .../basic-auth/app/routes/index.tsx | 13 - .../basic-auth/app/routes/login.tsx | 11 - .../basic-auth/app/routes/logout.tsx | 19 -- .../basic-auth/app/routes/signup.tsx | 100 ------ e2e/solid-start/basic-auth/app/ssr.tsx | 12 - e2e/solid-start/basic-auth/app/styles/app.css | 22 -- e2e/solid-start/basic-auth/app/utils/posts.ts | 37 --- .../basic-auth/app/utils/prisma.ts | 16 - e2e/solid-start/basic-auth/app/utils/seo.ts | 33 -- .../basic-auth/app/utils/session.ts | 13 - e2e/solid-start/basic-auth/package.json | 35 --- .../basic-auth/playwright.config.ts | 34 -- e2e/solid-start/basic-auth/postcss.config.mjs | 6 - e2e/solid-start/basic-auth/prisma/dev.db | Bin 24576 -> 0 bytes .../20240811183753_init/migration.sql | 8 - .../prisma/migrations/migration_lock.toml | 3 - .../basic-auth/prisma/schema.prisma | 16 - .../public/android-chrome-192x192.png | Bin 29964 -> 0 bytes .../public/android-chrome-512x512.png | Bin 109271 -> 0 bytes .../basic-auth/public/apple-touch-icon.png | Bin 27246 -> 0 bytes .../basic-auth/public/favicon-16x16.png | Bin 832 -> 0 bytes .../basic-auth/public/favicon-32x32.png | Bin 2115 -> 0 bytes e2e/solid-start/basic-auth/public/favicon.ico | Bin 15406 -> 0 bytes e2e/solid-start/basic-auth/public/favicon.png | Bin 1507 -> 0 bytes .../basic-auth/public/site.webmanifest | 19 -- .../basic-auth/tailwind.config.mjs | 4 - e2e/solid-start/basic-auth/tests/app.spec.ts | 60 ---- .../basic-auth/tests/mock-db-setup.test.ts | 21 -- .../basic-auth/tests/mock-db-teardown.test.ts | 21 -- e2e/solid-start/basic-auth/tsconfig.json | 23 -- 47 files changed, 1397 deletions(-) delete mode 100644 e2e/solid-start/basic-auth/.env delete mode 100644 e2e/solid-start/basic-auth/.gitignore delete mode 100644 e2e/solid-start/basic-auth/.prettierignore delete mode 100644 e2e/solid-start/basic-auth/app.config.ts delete mode 100644 e2e/solid-start/basic-auth/app/client.tsx delete mode 100644 e2e/solid-start/basic-auth/app/components/Auth.tsx delete mode 100644 e2e/solid-start/basic-auth/app/components/DefaultCatchBoundary.tsx delete mode 100644 e2e/solid-start/basic-auth/app/components/Login.tsx delete mode 100644 e2e/solid-start/basic-auth/app/components/NotFound.tsx delete mode 100644 e2e/solid-start/basic-auth/app/hooks/useMutation.ts delete mode 100644 e2e/solid-start/basic-auth/app/routeTree.gen.ts delete mode 100644 e2e/solid-start/basic-auth/app/router.tsx delete mode 100644 e2e/solid-start/basic-auth/app/routes/__root.tsx delete mode 100644 e2e/solid-start/basic-auth/app/routes/_authed.tsx delete mode 100644 e2e/solid-start/basic-auth/app/routes/_authed/posts.$postId.tsx delete mode 100644 e2e/solid-start/basic-auth/app/routes/_authed/posts.index.tsx delete mode 100644 e2e/solid-start/basic-auth/app/routes/_authed/posts.tsx delete mode 100644 e2e/solid-start/basic-auth/app/routes/index.tsx delete mode 100644 e2e/solid-start/basic-auth/app/routes/login.tsx delete mode 100644 e2e/solid-start/basic-auth/app/routes/logout.tsx delete mode 100644 e2e/solid-start/basic-auth/app/routes/signup.tsx delete mode 100644 e2e/solid-start/basic-auth/app/ssr.tsx delete mode 100644 e2e/solid-start/basic-auth/app/styles/app.css delete mode 100644 e2e/solid-start/basic-auth/app/utils/posts.ts delete mode 100644 e2e/solid-start/basic-auth/app/utils/prisma.ts delete mode 100644 e2e/solid-start/basic-auth/app/utils/seo.ts delete mode 100644 e2e/solid-start/basic-auth/app/utils/session.ts delete mode 100644 e2e/solid-start/basic-auth/package.json delete mode 100644 e2e/solid-start/basic-auth/playwright.config.ts delete mode 100644 e2e/solid-start/basic-auth/postcss.config.mjs delete mode 100644 e2e/solid-start/basic-auth/prisma/dev.db delete mode 100644 e2e/solid-start/basic-auth/prisma/migrations/20240811183753_init/migration.sql delete mode 100644 e2e/solid-start/basic-auth/prisma/migrations/migration_lock.toml delete mode 100644 e2e/solid-start/basic-auth/prisma/schema.prisma delete mode 100644 e2e/solid-start/basic-auth/public/android-chrome-192x192.png delete mode 100644 e2e/solid-start/basic-auth/public/android-chrome-512x512.png delete mode 100644 e2e/solid-start/basic-auth/public/apple-touch-icon.png delete mode 100644 e2e/solid-start/basic-auth/public/favicon-16x16.png delete mode 100644 e2e/solid-start/basic-auth/public/favicon-32x32.png delete mode 100644 e2e/solid-start/basic-auth/public/favicon.ico delete mode 100644 e2e/solid-start/basic-auth/public/favicon.png delete mode 100644 e2e/solid-start/basic-auth/public/site.webmanifest delete mode 100644 e2e/solid-start/basic-auth/tailwind.config.mjs delete mode 100644 e2e/solid-start/basic-auth/tests/app.spec.ts delete mode 100644 e2e/solid-start/basic-auth/tests/mock-db-setup.test.ts delete mode 100644 e2e/solid-start/basic-auth/tests/mock-db-teardown.test.ts delete mode 100644 e2e/solid-start/basic-auth/tsconfig.json diff --git a/e2e/solid-start/basic-auth/.env b/e2e/solid-start/basic-auth/.env deleted file mode 100644 index c498ab59bf..0000000000 --- a/e2e/solid-start/basic-auth/.env +++ /dev/null @@ -1,7 +0,0 @@ -# Environment variables declared in this file are automatically made available to Prisma. -# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema - -# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. -# See the documentation for all the connection string options: https://pris.ly/d/connection-strings - -DATABASE_URL="file:./dev.db" \ No newline at end of file diff --git a/e2e/solid-start/basic-auth/.gitignore b/e2e/solid-start/basic-auth/.gitignore deleted file mode 100644 index b15fed94e2..0000000000 --- a/e2e/solid-start/basic-auth/.gitignore +++ /dev/null @@ -1,22 +0,0 @@ -node_modules -package-lock.json -yarn.lock - -!.env -.DS_Store -.cache -.vercel -.output -.vinxi - -/build/ -/api/ -/server/build -/public/build -.vinxi -# Sentry Config File -.env.sentry-build-plugin -/test-results/ -/playwright-report/ -/blob-report/ -/playwright/.cache/ diff --git a/e2e/solid-start/basic-auth/.prettierignore b/e2e/solid-start/basic-auth/.prettierignore deleted file mode 100644 index 2be5eaa6ec..0000000000 --- a/e2e/solid-start/basic-auth/.prettierignore +++ /dev/null @@ -1,4 +0,0 @@ -**/build -**/public -pnpm-lock.yaml -routeTree.gen.ts \ No newline at end of file diff --git a/e2e/solid-start/basic-auth/app.config.ts b/e2e/solid-start/basic-auth/app.config.ts deleted file mode 100644 index 5c531d7e3d..0000000000 --- a/e2e/solid-start/basic-auth/app.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from '@tanstack/solid-start/config' -import tsConfigPaths from 'vite-tsconfig-paths' - -export default defineConfig({ - vite: { - plugins: [ - tsConfigPaths({ - projects: ['./tsconfig.json'], - }), - ], - }, -}) diff --git a/e2e/solid-start/basic-auth/app/client.tsx b/e2e/solid-start/basic-auth/app/client.tsx deleted file mode 100644 index ba0f02fac0..0000000000 --- a/e2e/solid-start/basic-auth/app/client.tsx +++ /dev/null @@ -1,8 +0,0 @@ -/// -import { hydrate } from 'solid-js/web' -import { StartClient } from '@tanstack/solid-start' -import { createRouter } from './router' - -const router = createRouter() - -hydrate(() => , document.body) diff --git a/e2e/solid-start/basic-auth/app/components/Auth.tsx b/e2e/solid-start/basic-auth/app/components/Auth.tsx deleted file mode 100644 index fa1767edf3..0000000000 --- a/e2e/solid-start/basic-auth/app/components/Auth.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import * as Solid from 'solid-js' -export function Auth({ - actionText, - onSubmit, - status, - afterSubmit, -}: { - actionText: string - onSubmit: (e: any) => void - status: 'pending' | 'idle' | 'success' | 'error' - afterSubmit?: Solid.JSX.Element -}) { - return ( -
    -
    -

    {actionText}

    -
    { - e.preventDefault() - onSubmit(e) - }} - class="space-y-4" - > -
    - - -
    -
    - - -
    - - {afterSubmit ? afterSubmit : null} -
    -
    -
    - ) -} diff --git a/e2e/solid-start/basic-auth/app/components/DefaultCatchBoundary.tsx b/e2e/solid-start/basic-auth/app/components/DefaultCatchBoundary.tsx deleted file mode 100644 index 32aed20e67..0000000000 --- a/e2e/solid-start/basic-auth/app/components/DefaultCatchBoundary.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { - ErrorComponent, - Link, - rootRouteId, - useMatch, - useRouter, -} from '@tanstack/solid-router' -import type { ErrorComponentProps } from '@tanstack/solid-router' - -export function DefaultCatchBoundary({ error }: ErrorComponentProps) { - const router = useRouter() - const isRoot = useMatch({ - strict: false, - select: (state) => state.id === rootRouteId, - }) - - console.error(error) - - return ( -
    - -
    - - {isRoot() ? ( - - Home - - ) : ( - { - e.preventDefault() - window.history.back() - }} - > - Go Back - - )} -
    -
    - ) -} diff --git a/e2e/solid-start/basic-auth/app/components/Login.tsx b/e2e/solid-start/basic-auth/app/components/Login.tsx deleted file mode 100644 index b750f96718..0000000000 --- a/e2e/solid-start/basic-auth/app/components/Login.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { useRouter } from '@tanstack/solid-router' -import { useServerFn } from '@tanstack/solid-start' -import { useMutation } from '../hooks/useMutation' -import { loginFn } from '../routes/_authed' -import { Auth } from './Auth' -import { signupFn } from '~/routes/signup' - -export function Login() { - const router = useRouter() - - const loginMutation = useMutation({ - fn: loginFn, - onSuccess: async (ctx) => { - if (!ctx.data?.error) { - await router.invalidate() - router.navigate({ to: '/' }) - return - } - }, - }) - - const signupMutation = useMutation({ - fn: useServerFn(signupFn), - }) - - return ( - { - const formData = new FormData(e.target as HTMLFormElement) - - loginMutation.mutate({ - data: { - email: formData.get('email') as string, - password: formData.get('password') as string, - }, - }) - }} - afterSubmit={ - loginMutation.data() ? ( - <> -
    {loginMutation.data().message}
    - {loginMutation.data().userNotFound ? ( -
    - -
    - ) : null} - - ) : null - } - /> - ) -} diff --git a/e2e/solid-start/basic-auth/app/components/NotFound.tsx b/e2e/solid-start/basic-auth/app/components/NotFound.tsx deleted file mode 100644 index ca4c1960fa..0000000000 --- a/e2e/solid-start/basic-auth/app/components/NotFound.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Link } from '@tanstack/solid-router' - -export function NotFound({ children }: { children?: any }) { - return ( -
    -
    - {children ||

    The page you are looking for does not exist.

    } -
    -

    - - - Start Over - -

    -
    - ) -} diff --git a/e2e/solid-start/basic-auth/app/hooks/useMutation.ts b/e2e/solid-start/basic-auth/app/hooks/useMutation.ts deleted file mode 100644 index 605f40ab3d..0000000000 --- a/e2e/solid-start/basic-auth/app/hooks/useMutation.ts +++ /dev/null @@ -1,41 +0,0 @@ -import * as Solid from 'solid-js' - -export function useMutation(opts: { - fn: (variables: TVariables) => Promise - onSuccess?: (ctx: { data: TData }) => void | Promise -}) { - const [submittedAt, setSubmittedAt] = Solid.createSignal() - const [variables, setVariables] = Solid.createSignal() - const [error, setError] = Solid.createSignal() - const [data, setData] = Solid.createSignal() - const [status, setStatus] = Solid.createSignal< - 'idle' | 'pending' | 'success' | 'error' - >('idle') - - const mutate = async (variables: TVariables): Promise => { - setStatus('pending') - setSubmittedAt(Date.now()) - setVariables(variables) - // - try { - const data = await opts.fn(variables) - await opts.onSuccess?.({ data }) - setStatus('success') - setError(undefined) - setData(data) - return data - } catch (err: any) { - setStatus('error') - setError(err) - } - } - - return { - status, - variables, - submittedAt, - mutate, - error, - data, - } -} diff --git a/e2e/solid-start/basic-auth/app/routeTree.gen.ts b/e2e/solid-start/basic-auth/app/routeTree.gen.ts deleted file mode 100644 index 502a1476a9..0000000000 --- a/e2e/solid-start/basic-auth/app/routeTree.gen.ts +++ /dev/null @@ -1,290 +0,0 @@ -/* eslint-disable */ - -// @ts-nocheck - -// noinspection JSUnusedGlobalSymbols - -// This file was automatically generated by TanStack Router. -// You should NOT make any changes in this file as it will be overwritten. -// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. - -// Import Routes - -import { Route as rootRoute } from './routes/__root' -import { Route as SignupImport } from './routes/signup' -import { Route as LogoutImport } from './routes/logout' -import { Route as LoginImport } from './routes/login' -import { Route as AuthedImport } from './routes/_authed' -import { Route as IndexImport } from './routes/index' -import { Route as AuthedPostsImport } from './routes/_authed/posts' -import { Route as AuthedPostsIndexImport } from './routes/_authed/posts.index' -import { Route as AuthedPostsPostIdImport } from './routes/_authed/posts.$postId' - -// Create/Update Routes - -const SignupRoute = SignupImport.update({ - id: '/signup', - path: '/signup', - getParentRoute: () => rootRoute, -} as any) - -const LogoutRoute = LogoutImport.update({ - id: '/logout', - path: '/logout', - getParentRoute: () => rootRoute, -} as any) - -const LoginRoute = LoginImport.update({ - id: '/login', - path: '/login', - getParentRoute: () => rootRoute, -} as any) - -const AuthedRoute = AuthedImport.update({ - id: '/_authed', - getParentRoute: () => rootRoute, -} as any) - -const IndexRoute = IndexImport.update({ - id: '/', - path: '/', - getParentRoute: () => rootRoute, -} as any) - -const AuthedPostsRoute = AuthedPostsImport.update({ - id: '/posts', - path: '/posts', - getParentRoute: () => AuthedRoute, -} as any) - -const AuthedPostsIndexRoute = AuthedPostsIndexImport.update({ - id: '/', - path: '/', - getParentRoute: () => AuthedPostsRoute, -} as any) - -const AuthedPostsPostIdRoute = AuthedPostsPostIdImport.update({ - id: '/$postId', - path: '/$postId', - getParentRoute: () => AuthedPostsRoute, -} as any) - -// Populate the FileRoutesByPath interface - -declare module '@tanstack/solid-router' { - interface FileRoutesByPath { - '/': { - id: '/' - path: '/' - fullPath: '/' - preLoaderRoute: typeof IndexImport - parentRoute: typeof rootRoute - } - '/_authed': { - id: '/_authed' - path: '' - fullPath: '' - preLoaderRoute: typeof AuthedImport - parentRoute: typeof rootRoute - } - '/login': { - id: '/login' - path: '/login' - fullPath: '/login' - preLoaderRoute: typeof LoginImport - parentRoute: typeof rootRoute - } - '/logout': { - id: '/logout' - path: '/logout' - fullPath: '/logout' - preLoaderRoute: typeof LogoutImport - parentRoute: typeof rootRoute - } - '/signup': { - id: '/signup' - path: '/signup' - fullPath: '/signup' - preLoaderRoute: typeof SignupImport - parentRoute: typeof rootRoute - } - '/_authed/posts': { - id: '/_authed/posts' - path: '/posts' - fullPath: '/posts' - preLoaderRoute: typeof AuthedPostsImport - parentRoute: typeof AuthedImport - } - '/_authed/posts/$postId': { - id: '/_authed/posts/$postId' - path: '/$postId' - fullPath: '/posts/$postId' - preLoaderRoute: typeof AuthedPostsPostIdImport - parentRoute: typeof AuthedPostsImport - } - '/_authed/posts/': { - id: '/_authed/posts/' - path: '/' - fullPath: '/posts/' - preLoaderRoute: typeof AuthedPostsIndexImport - parentRoute: typeof AuthedPostsImport - } - } -} - -// Create and export the route tree - -interface AuthedPostsRouteChildren { - AuthedPostsPostIdRoute: typeof AuthedPostsPostIdRoute - AuthedPostsIndexRoute: typeof AuthedPostsIndexRoute -} - -const AuthedPostsRouteChildren: AuthedPostsRouteChildren = { - AuthedPostsPostIdRoute: AuthedPostsPostIdRoute, - AuthedPostsIndexRoute: AuthedPostsIndexRoute, -} - -const AuthedPostsRouteWithChildren = AuthedPostsRoute._addFileChildren( - AuthedPostsRouteChildren, -) - -interface AuthedRouteChildren { - AuthedPostsRoute: typeof AuthedPostsRouteWithChildren -} - -const AuthedRouteChildren: AuthedRouteChildren = { - AuthedPostsRoute: AuthedPostsRouteWithChildren, -} - -const AuthedRouteWithChildren = - AuthedRoute._addFileChildren(AuthedRouteChildren) - -export interface FileRoutesByFullPath { - '/': typeof IndexRoute - '': typeof AuthedRouteWithChildren - '/login': typeof LoginRoute - '/logout': typeof LogoutRoute - '/signup': typeof SignupRoute - '/posts': typeof AuthedPostsRouteWithChildren - '/posts/$postId': typeof AuthedPostsPostIdRoute - '/posts/': typeof AuthedPostsIndexRoute -} - -export interface FileRoutesByTo { - '/': typeof IndexRoute - '': typeof AuthedRouteWithChildren - '/login': typeof LoginRoute - '/logout': typeof LogoutRoute - '/signup': typeof SignupRoute - '/posts/$postId': typeof AuthedPostsPostIdRoute - '/posts': typeof AuthedPostsIndexRoute -} - -export interface FileRoutesById { - __root__: typeof rootRoute - '/': typeof IndexRoute - '/_authed': typeof AuthedRouteWithChildren - '/login': typeof LoginRoute - '/logout': typeof LogoutRoute - '/signup': typeof SignupRoute - '/_authed/posts': typeof AuthedPostsRouteWithChildren - '/_authed/posts/$postId': typeof AuthedPostsPostIdRoute - '/_authed/posts/': typeof AuthedPostsIndexRoute -} - -export interface FileRouteTypes { - fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: - | '/' - | '' - | '/login' - | '/logout' - | '/signup' - | '/posts' - | '/posts/$postId' - | '/posts/' - fileRoutesByTo: FileRoutesByTo - to: '/' | '' | '/login' | '/logout' | '/signup' | '/posts/$postId' | '/posts' - id: - | '__root__' - | '/' - | '/_authed' - | '/login' - | '/logout' - | '/signup' - | '/_authed/posts' - | '/_authed/posts/$postId' - | '/_authed/posts/' - fileRoutesById: FileRoutesById -} - -export interface RootRouteChildren { - IndexRoute: typeof IndexRoute - AuthedRoute: typeof AuthedRouteWithChildren - LoginRoute: typeof LoginRoute - LogoutRoute: typeof LogoutRoute - SignupRoute: typeof SignupRoute -} - -const rootRouteChildren: RootRouteChildren = { - IndexRoute: IndexRoute, - AuthedRoute: AuthedRouteWithChildren, - LoginRoute: LoginRoute, - LogoutRoute: LogoutRoute, - SignupRoute: SignupRoute, -} - -export const routeTree = rootRoute - ._addFileChildren(rootRouteChildren) - ._addFileTypes() - -/* ROUTE_MANIFEST_START -{ - "routes": { - "__root__": { - "filePath": "__root.tsx", - "children": [ - "/", - "/_authed", - "/login", - "/logout", - "/signup" - ] - }, - "/": { - "filePath": "index.tsx" - }, - "/_authed": { - "filePath": "_authed.tsx", - "children": [ - "/_authed/posts" - ] - }, - "/login": { - "filePath": "login.tsx" - }, - "/logout": { - "filePath": "logout.tsx" - }, - "/signup": { - "filePath": "signup.tsx" - }, - "/_authed/posts": { - "filePath": "_authed/posts.tsx", - "parent": "/_authed", - "children": [ - "/_authed/posts/$postId", - "/_authed/posts/" - ] - }, - "/_authed/posts/$postId": { - "filePath": "_authed/posts.$postId.tsx", - "parent": "/_authed/posts" - }, - "/_authed/posts/": { - "filePath": "_authed/posts.index.tsx", - "parent": "/_authed/posts" - } - } -} -ROUTE_MANIFEST_END */ diff --git a/e2e/solid-start/basic-auth/app/router.tsx b/e2e/solid-start/basic-auth/app/router.tsx deleted file mode 100644 index c45bed4758..0000000000 --- a/e2e/solid-start/basic-auth/app/router.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { createRouter as createTanStackRouter } from '@tanstack/solid-router' -import { routeTree } from './routeTree.gen' -import { DefaultCatchBoundary } from './components/DefaultCatchBoundary' -import { NotFound } from './components/NotFound' - -export function createRouter() { - const router = createTanStackRouter({ - routeTree, - defaultPreload: 'intent', - defaultErrorComponent: DefaultCatchBoundary, - defaultNotFoundComponent: () => , - scrollRestoration: true, - }) - - return router -} - -declare module '@tanstack/solid-router' { - interface Register { - router: ReturnType - } -} diff --git a/e2e/solid-start/basic-auth/app/routes/__root.tsx b/e2e/solid-start/basic-auth/app/routes/__root.tsx deleted file mode 100644 index 5995c7ef55..0000000000 --- a/e2e/solid-start/basic-auth/app/routes/__root.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { - HeadContent, - Link, - Outlet, - Scripts, - createRootRoute, -} from '@tanstack/solid-router' -import { createServerFn } from '@tanstack/solid-start' -import * as Solid from 'solid-js' - -import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary.js' -import { NotFound } from '~/components/NotFound.js' -import appCss from '~/styles/app.css?url' -import { seo } from '~/utils/seo.js' -import { useAppSession } from '~/utils/session.js' - -const fetchUser = createServerFn({ method: 'GET' }).handler(async () => { - // We need to auth on the server so we have access to secure cookies - const session = await useAppSession() - - if (!session.data.userEmail) { - return null - } - - return { - email: session.data.userEmail, - } -}) - -export const Route = createRootRoute({ - head: () => ({ - meta: [ - { - charset: 'utf-8', - }, - { - name: 'viewport', - content: 'width=device-width, initial-scale=1', - }, - ...seo({ - title: - 'TanStack Start | Type-Safe, Client-First, Full-Stack React Framework', - description: `TanStack Start is a type-safe, client-first, full-stack React framework. `, - }), - ], - links: [ - { rel: 'stylesheet', href: appCss }, - { - rel: 'apple-touch-icon', - sizes: '180x180', - href: '/apple-touch-icon.png', - }, - { - rel: 'icon', - type: 'image/png', - sizes: '32x32', - href: '/favicon-32x32.png', - }, - { - rel: 'icon', - type: 'image/png', - sizes: '16x16', - href: '/favicon-16x16.png', - }, - { rel: 'manifest', href: '/site.webmanifest', color: '#fffff' }, - { rel: 'icon', href: '/favicon.ico' }, - ], - }), - beforeLoad: async () => { - const user = await fetchUser() - - return { - user, - } - }, - errorComponent: (props) => { - return

    {props.error.stack}

    - }, - notFoundComponent: () => , - component: RootComponent, -}) - -function RootComponent() { - const routeContext = Route.useRouteContext() - return ( - <> -
    - - Home - {' '} - - Posts - -
    - {routeContext().user ? ( - <> - {routeContext().user?.email} - Logout - - ) : ( - Login - )} -
    -
    -
    - - - ) -} diff --git a/e2e/solid-start/basic-auth/app/routes/_authed.tsx b/e2e/solid-start/basic-auth/app/routes/_authed.tsx deleted file mode 100644 index a42aac212a..0000000000 --- a/e2e/solid-start/basic-auth/app/routes/_authed.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { createFileRoute } from '@tanstack/solid-router' -import { createServerFn } from '@tanstack/solid-start' - -import { hashPassword, prismaClient } from '~/utils/prisma' -import { Login } from '~/components/Login' -import { useAppSession } from '~/utils/session' - -export const loginFn = createServerFn({ - method: 'POST', -}) - .validator((payload: { email: string; password: string }) => payload) - .handler(async ({ data }) => { - // Find the user - const user = await prismaClient.user.findUnique({ - where: { - email: data.email, - }, - }) - - // Check if the user exists - if (!user) { - return { - error: true, - userNotFound: true, - message: 'User not found', - } - } - - // Check if the password is correct - const hashedPassword = await hashPassword(data.password) - - if (user.password !== hashedPassword) { - return { - error: true, - message: 'Incorrect password', - } - } - - // Create a session - const session = await useAppSession() - - // Store the user's email in the session - await session.update({ - userEmail: user.email, - }) - }) - -export const Route = createFileRoute('/_authed')({ - beforeLoad: ({ context }) => { - if (!context.user) { - throw new Error('Not authenticated') - } - }, - errorComponent: ({ error }) => { - if (error.message === 'Not authenticated') { - return - } - - throw error - }, -}) diff --git a/e2e/solid-start/basic-auth/app/routes/_authed/posts.$postId.tsx b/e2e/solid-start/basic-auth/app/routes/_authed/posts.$postId.tsx deleted file mode 100644 index d9253a40ba..0000000000 --- a/e2e/solid-start/basic-auth/app/routes/_authed/posts.$postId.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { ErrorComponent, createFileRoute } from '@tanstack/solid-router' -import type { ErrorComponentProps } from '@tanstack/solid-router' - -import { NotFound } from '~/components/NotFound.js' -import { fetchPost } from '~/utils/posts.js' - -export const Route = createFileRoute('/_authed/posts/$postId')({ - loader: ({ params: { postId } }) => fetchPost({ data: postId }), - errorComponent: PostErrorComponent, - component: PostComponent, - notFoundComponent: () => { - return Post not found - }, -}) - -export function PostErrorComponent({ error }: ErrorComponentProps) { - return -} - -function PostComponent() { - const post = Route.useLoaderData() - - return ( -
    -

    {post.title}

    -
    {post.body}
    -
    - ) -} diff --git a/e2e/solid-start/basic-auth/app/routes/_authed/posts.index.tsx b/e2e/solid-start/basic-auth/app/routes/_authed/posts.index.tsx deleted file mode 100644 index 4ba0817e28..0000000000 --- a/e2e/solid-start/basic-auth/app/routes/_authed/posts.index.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { createFileRoute } from '@tanstack/solid-router' - -export const Route = createFileRoute('/_authed/posts/')({ - component: PostsIndexComponent, -}) - -function PostsIndexComponent() { - return
    Select a post.
    -} diff --git a/e2e/solid-start/basic-auth/app/routes/_authed/posts.tsx b/e2e/solid-start/basic-auth/app/routes/_authed/posts.tsx deleted file mode 100644 index e32a0ff3c4..0000000000 --- a/e2e/solid-start/basic-auth/app/routes/_authed/posts.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Link, Outlet, createFileRoute } from '@tanstack/solid-router' - -import { fetchPosts } from '~/utils/posts.js' - -export const Route = createFileRoute('/_authed/posts')({ - loader: () => fetchPosts(), - component: PostsComponent, -}) - -function PostsComponent() { - const posts = Route.useLoaderData() - - return ( -
    -
      - {[...posts, { id: 'i-do-not-exist', title: 'Non-existent Post' }].map( - (post) => { - return ( -
    • - -
      {post.title.substring(0, 20)}
      - -
    • - ) - }, - )} -
    -
    - -
    - ) -} diff --git a/e2e/solid-start/basic-auth/app/routes/index.tsx b/e2e/solid-start/basic-auth/app/routes/index.tsx deleted file mode 100644 index a128aeca0e..0000000000 --- a/e2e/solid-start/basic-auth/app/routes/index.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { createFileRoute } from '@tanstack/solid-router' - -export const Route = createFileRoute('/')({ - component: Home, -}) - -function Home() { - return ( -
    -

    Welcome Home!!!

    -
    - ) -} diff --git a/e2e/solid-start/basic-auth/app/routes/login.tsx b/e2e/solid-start/basic-auth/app/routes/login.tsx deleted file mode 100644 index e8478a3933..0000000000 --- a/e2e/solid-start/basic-auth/app/routes/login.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createFileRoute } from '@tanstack/solid-router' - -import { Login } from '~/components/Login' - -export const Route = createFileRoute('/login')({ - component: LoginComp, -}) - -function LoginComp() { - return -} diff --git a/e2e/solid-start/basic-auth/app/routes/logout.tsx b/e2e/solid-start/basic-auth/app/routes/logout.tsx deleted file mode 100644 index 36110f90fe..0000000000 --- a/e2e/solid-start/basic-auth/app/routes/logout.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { createFileRoute, redirect } from '@tanstack/solid-router' -import { createServerFn } from '@tanstack/solid-start' - -import { useAppSession } from '~/utils/session' - -const logoutFn = createServerFn({ method: 'POST' }).handler(async () => { - const session = await useAppSession() - - session.clear() - - throw redirect({ - href: '/', - }) -}) - -export const Route = createFileRoute('/logout')({ - preload: false, - loader: () => logoutFn(), -}) diff --git a/e2e/solid-start/basic-auth/app/routes/signup.tsx b/e2e/solid-start/basic-auth/app/routes/signup.tsx deleted file mode 100644 index f53feffde3..0000000000 --- a/e2e/solid-start/basic-auth/app/routes/signup.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { createFileRoute, redirect } from '@tanstack/solid-router' -import { createServerFn, useServerFn } from '@tanstack/solid-start' - -import { hashPassword, prismaClient } from '~/utils/prisma' -import { useMutation } from '~/hooks/useMutation' -import { Auth } from '~/components/Auth' -import { useAppSession } from '~/utils/session' - -export const signupFn = createServerFn({ - method: 'POST', -}) - .validator( - (data: { email: string; password: string; redirectUrl?: string }) => data, - ) - .handler(async ({ data: payload }) => { - // Check if the user already exists - const found = await prismaClient.user.findUnique({ - where: { - email: payload.email, - }, - }) - - // Encrypt the password using Sha256 into plaintext - const password = await hashPassword(payload.password) - - // Create a session - const session = await useAppSession() - - if (found) { - if (found.password !== password) { - return { - error: true, - userExists: true, - message: 'User already exists', - } - } - - // Store the user's email in the session - await session.update({ - userEmail: found.email, - }) - - // Redirect to the prev page stored in the "redirect" search param - throw redirect({ - href: payload.redirectUrl || '/', - }) - } - - // Create the user - const user = await prismaClient.user.create({ - data: { - email: payload.email, - password, - }, - }) - - // Store the user's email in the session - await session.update({ - userEmail: user.email, - }) - - // Redirect to the prev page stored in the "redirect" search param - throw redirect({ - href: payload.redirectUrl || '/', - }) - }) - -export const Route = createFileRoute('/signup')({ - component: SignupComp, -}) - -function SignupComp() { - const signupMutation = useMutation({ - fn: useServerFn(signupFn), - }) - - return ( - { - const formData = new FormData(e.target as HTMLFormElement) - - signupMutation.mutate({ - data: { - email: formData.get('email') as string, - password: formData.get('password') as string, - }, - }) - }} - afterSubmit={ - signupMutation.data()?.error ? ( - <> -
    {signupMutation.data()?.message}
    - - ) : null - } - /> - ) -} diff --git a/e2e/solid-start/basic-auth/app/ssr.tsx b/e2e/solid-start/basic-auth/app/ssr.tsx deleted file mode 100644 index 6d10bea05f..0000000000 --- a/e2e/solid-start/basic-auth/app/ssr.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { - createStartHandler, - defaultStreamHandler, -} from '@tanstack/solid-start/server' -import { getRouterManifest } from '@tanstack/solid-start/router-manifest' - -import { createRouter } from './router' - -export default createStartHandler({ - createRouter, - getRouterManifest, -})(defaultStreamHandler) diff --git a/e2e/solid-start/basic-auth/app/styles/app.css b/e2e/solid-start/basic-auth/app/styles/app.css deleted file mode 100644 index c53c870665..0000000000 --- a/e2e/solid-start/basic-auth/app/styles/app.css +++ /dev/null @@ -1,22 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - html { - color-scheme: light dark; - } - - * { - @apply border-gray-200 dark:border-gray-800; - } - - html, - body { - @apply text-gray-900 bg-gray-50 dark:bg-gray-950 dark:text-gray-200; - } - - .using-mouse * { - outline: none !important; - } -} diff --git a/e2e/solid-start/basic-auth/app/utils/posts.ts b/e2e/solid-start/basic-auth/app/utils/posts.ts deleted file mode 100644 index 756c830dae..0000000000 --- a/e2e/solid-start/basic-auth/app/utils/posts.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { notFound } from '@tanstack/solid-router' -import { createServerFn } from '@tanstack/solid-start' -import axios from 'redaxios' - -export type PostType = { - id: string - title: string - body: string -} - -export const fetchPost = createServerFn({ method: 'GET' }) - .validator((postId: string) => postId) - .handler(async ({ data: postId }) => { - console.info(`Fetching post with id ${postId}...`) - const post = await axios - .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) - .then((r) => r.data) - .catch((err) => { - console.error(err) - if (err.status === 404) { - throw notFound() - } - throw err - }) - - return post - }) - -export const fetchPosts = createServerFn({ method: 'GET' }).handler( - async () => { - console.info('Fetching posts...') - await new Promise((r) => setTimeout(r, 1000)) - return axios - .get>('https://jsonplaceholder.typicode.com/posts') - .then((r) => r.data.slice(0, 10)) - }, -) diff --git a/e2e/solid-start/basic-auth/app/utils/prisma.ts b/e2e/solid-start/basic-auth/app/utils/prisma.ts deleted file mode 100644 index 74f5137b46..0000000000 --- a/e2e/solid-start/basic-auth/app/utils/prisma.ts +++ /dev/null @@ -1,16 +0,0 @@ -import crypto from 'node:crypto' -import { PrismaClient } from '@prisma/client' - -export const prismaClient = new PrismaClient() - -export function hashPassword(password: string) { - return new Promise((resolve, reject) => { - crypto.pbkdf2(password, 'salt', 100000, 64, 'sha256', (err, derivedKey) => { - if (err) { - reject(err) - } else { - resolve(derivedKey.toString('hex')) - } - }) - }) -} diff --git a/e2e/solid-start/basic-auth/app/utils/seo.ts b/e2e/solid-start/basic-auth/app/utils/seo.ts deleted file mode 100644 index d18ad84b74..0000000000 --- a/e2e/solid-start/basic-auth/app/utils/seo.ts +++ /dev/null @@ -1,33 +0,0 @@ -export const seo = ({ - title, - description, - keywords, - image, -}: { - title: string - description?: string - image?: string - keywords?: string -}) => { - const tags = [ - { title }, - { name: 'description', content: description }, - { name: 'keywords', content: keywords }, - { name: 'twitter:title', content: title }, - { name: 'twitter:description', content: description }, - { name: 'twitter:creator', content: '@tannerlinsley' }, - { name: 'twitter:site', content: '@tannerlinsley' }, - { name: 'og:type', content: 'website' }, - { name: 'og:title', content: title }, - { name: 'og:description', content: description }, - ...(image - ? [ - { name: 'twitter:image', content: image }, - { name: 'twitter:card', content: 'summary_large_image' }, - { name: 'og:image', content: image }, - ] - : []), - ] - - return tags -} diff --git a/e2e/solid-start/basic-auth/app/utils/session.ts b/e2e/solid-start/basic-auth/app/utils/session.ts deleted file mode 100644 index 66578141e1..0000000000 --- a/e2e/solid-start/basic-auth/app/utils/session.ts +++ /dev/null @@ -1,13 +0,0 @@ -// app/services/session.server.ts -import { useSession } from '@tanstack/solid-start/server' -import type { User } from '@prisma/client' - -type SessionUser = { - userEmail: User['email'] -} - -export function useAppSession() { - return useSession({ - password: 'ChangeThisBeforeShippingToProdOrYouWillBeFired', - }) -} diff --git a/e2e/solid-start/basic-auth/package.json b/e2e/solid-start/basic-auth/package.json deleted file mode 100644 index f8d1619d9b..0000000000 --- a/e2e/solid-start/basic-auth/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "tanstack-solid-start-e2e-basic-auth", - "private": true, - "sideEffects": false, - "type": "module", - "scripts": { - "dev": "vinxi dev --port 3000", - "dev:e2e": "vinxi dev", - "build": "vinxi build", - "start": "vinxi start", - "prisma-generate": "prisma generate", - "test:e2e": "exit 0; pnpm run prisma-generate && playwright test --project=chromium" - }, - "dependencies": { - "@prisma/client": "5.22.0", - "@tanstack/solid-router": "workspace:^", - "@tanstack/solid-start": "workspace:^", - "prisma": "^5.22.0", - "solid-js": "^1.0.0", - "redaxios": "^0.5.1", - "tailwind-merge": "^2.6.0", - "vinxi": "0.5.3" - }, - "devDependencies": { - "@playwright/test": "^1.50.1", - "@tanstack/router-e2e-utils": "workspace:^", - "@types/node": "^22.10.2", - "vite-plugin-solid": "^2.11.6", - "postcss": "^8.5.1", - "autoprefixer": "^10.4.20", - "tailwindcss": "^3.4.17", - "typescript": "^5.7.2", - "vite-tsconfig-paths": "^5.1.4" - } -} diff --git a/e2e/solid-start/basic-auth/playwright.config.ts b/e2e/solid-start/basic-auth/playwright.config.ts deleted file mode 100644 index bb77d0cf70..0000000000 --- a/e2e/solid-start/basic-auth/playwright.config.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { defineConfig, devices } from '@playwright/test' -import { derivePort } from '@tanstack/router-e2e-utils' -import packageJson from './package.json' with { type: 'json' } - -const PORT = derivePort(packageJson.name) -const baseURL = `http://localhost:${PORT}` -/** - * See https://playwright.dev/docs/test-configuration. - */ -export default defineConfig({ - testDir: './tests', - workers: 1, - - reporter: [['line']], - - use: { - /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL, - }, - - webServer: { - command: `VITE_SERVER_PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm start --port ${PORT}`, - url: baseURL, - reuseExistingServer: !process.env.CI, - stdout: 'pipe', - }, - - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - ], -}) diff --git a/e2e/solid-start/basic-auth/postcss.config.mjs b/e2e/solid-start/basic-auth/postcss.config.mjs deleted file mode 100644 index 2e7af2b7f1..0000000000 --- a/e2e/solid-start/basic-auth/postcss.config.mjs +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/e2e/solid-start/basic-auth/prisma/dev.db b/e2e/solid-start/basic-auth/prisma/dev.db deleted file mode 100644 index 5f4ab51a02990fc664b53e0583765f4669fd622a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24576 zcmeI(J8v9S6ae5EzXBHPMG=f;x!$p7#^=tQ^UZECbL=~}cRN+c;$YM> zRmPLolQd1PWm%FW5tb!bj>bH!EF5jX&-B##vsPnrCH`Kk|Cubl_FGc>rT%^GYVA8% z#eo7SfC4Ch0w{n2D1ZVefCB%CKzng%ZGAQUtkd`5llC}_%Fr{NZuua5I@wy<+HY>Q znpt~q=XSf9?d)wgKgb$q;~Lqmy~q0+mmAaBjrHODg|*ey^l+;(wi_n#lVxFcwASYJ z-R5jcc6m92F`JmRnjf^Xy<4!fcXzY*_IGY>?%&PcZQgzRaoEs|#~%+yzHv5kdHvGD z(%RM4G?^AM{-_IgZlyV>2Gi|wnlYVxU;N_3^uijY>6hg+w{ke@jC-c+b?%K!)fx21 z=l9H=&HH>nwPOH-YW-iE8x z+PT@BMIBdD_DwGw2R`k9-NC)_allFLlhE10MuTn_PH2sLaQduIoN`>5QFSh_>?rEE zhuCk?hR+BGs9uG0|y#cVK^?`;GjRPbZ4*CywTi;x8j|h z8+*;|^9Ll$>sxb6Ywy!%y$jA?xRRdVH@TO8W%6OG!IIX83A}Ki01BW03ZMWApa2S> z01BW03ZMWA{7V9dwRE|CSX<;tviPc}y2x`UHZz@_>2-+H`ItM`+1 zHo2dq^>niD01BW03ZTIMC-Bei G|NjCh%*#yx diff --git a/e2e/solid-start/basic-auth/prisma/migrations/20240811183753_init/migration.sql b/e2e/solid-start/basic-auth/prisma/migrations/20240811183753_init/migration.sql deleted file mode 100644 index 4512a8f782..0000000000 --- a/e2e/solid-start/basic-auth/prisma/migrations/20240811183753_init/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ --- CreateTable -CREATE TABLE "User" ( - "email" TEXT NOT NULL PRIMARY KEY, - "password" TEXT NOT NULL -); - --- CreateIndex -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); diff --git a/e2e/solid-start/basic-auth/prisma/migrations/migration_lock.toml b/e2e/solid-start/basic-auth/prisma/migrations/migration_lock.toml deleted file mode 100644 index e5e5c4705a..0000000000 --- a/e2e/solid-start/basic-auth/prisma/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) -provider = "sqlite" \ No newline at end of file diff --git a/e2e/solid-start/basic-auth/prisma/schema.prisma b/e2e/solid-start/basic-auth/prisma/schema.prisma deleted file mode 100644 index 3544834310..0000000000 --- a/e2e/solid-start/basic-auth/prisma/schema.prisma +++ /dev/null @@ -1,16 +0,0 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "sqlite" - url = env("DATABASE_URL") -} - -model User { - email String @id @unique - password String -} \ No newline at end of file diff --git a/e2e/solid-start/basic-auth/public/android-chrome-192x192.png b/e2e/solid-start/basic-auth/public/android-chrome-192x192.png deleted file mode 100644 index 09c8324f8c6781bc90fcf5dd38e0702bd5f171f3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29964 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
    -$E{6Mn6vA;gG82)4;e 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 b=FG diff --git a/e2e/solid-start/basic-auth/public/android-chrome-512x512.png b/e2e/solid-start/basic-auth/public/android-chrome-512x512.png deleted file mode 100644 index 11d626ea3d00fddd52861bf0af5554a92fc30d69..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 109271 zcmXt9Ra6{Jw;kLqxDzxG++BhN2@b&_xDF89U4sOd!GgO58Qk6785rDxyIsD2t@~8{ z)LnJz$lm*$j#N>S#Xu!N1pojTa$lv?007wcM_2$d;`?&#G57esz&NYPegRZXkR1X5 zQ~)_C2@Ma!lXfJqxpX}L#(p6RJNwvgXFLrR)jW7Q6ly9HTE@MIgQ}K;Bi5(cmsVTM z&s8`$DyldtE~@b#X~sTL$xm>hbB$q#4YRGkEf%-7l$VuH9y&wX@7C`&njwROZQf6F zhhAb`3Mt6O=L$imWgWgsDKSd?)C;&(jO_i4DtU}W{H`|Fwx}+@VJX4R z<8JM<#r_J6u5GooLn^(U7jHKJ`+{1$)%|*0V{})m6LNjnPI!X0Z~;tJ?fZ4ViB4jj zrhmcv|9G5#APwtwmKBMA3q(e+LCUp=$$%x#>q@`lYdlqrK}%Arw%eI*;Jq9W{4$sA zpEKMD5kPS3*hweD!?$zuhB- z-5&YeM+sc5b@WscW4`6aX0FlDk>f+XgXI>sPu;5=V=MOG!l#RKubQ3FH8I0-Vd<3> zf4P}x{qziUcYr52eu)NpBje$v^q>q46#=N3iT`OJ=QF$(|4ZFnCI{x38>v27Xi|0l ztS_diZfv8P>jo%V@LF7=SKG`vqkL=|nOlzR{fx2wPrNXEc~h&0WK?)_S!<%$yoThc zv!hM#jX^Ka2qV9!%PjA|%@xg8I9X@FQu-U8J75&vj^y8jmFF;==MGKaHz&7YQ2S?g z3@*c6F=e|Ki}Rop!7;j9+6_DS>D%cNH977Bwhe$@=4)f1E2eO#uF43dnxaP7?FqJj zg6DZf>4knZMP0Oi{`*WqXvK7iOrWwb_>~VMnLHIFXcpoeTrRPE&G||m&A~ImD}ptd zJL?TkP*u^Cd_j+KHv1&ld8SyMmFR70h-~R^IoZFL7^No)cdVGSC6kF2&D8@pJB%*j zQD=Dx`QR8Vuop%)Es=>;qz(&Ax6!`{H7FuCiU~fN=0TtrefL~rX!Va630Q;)KjU?d ze4ShWQgpVaE;)~Qc3Q;p5up%V_nmK*A({xIT2PBRq^;s)aADU7NzC`R#j4rriZ9__ zcHaUczXodf^5Zy_?+f=cp0i<9kxnEXxta|*A9y0$D8`ig_U>ukW9)CpQEUtxtE9`$ zJe=_1J&GkKDo9B?(!q`S70}&hdazTu>X8moieO8DW~j)M(z?uj)G( zS><^i6O&*Y;MnB|-I(%RC)#8nkK_wj610aCC6WrOL9p%{o@`>c4j3WIqg?xcK0WuN z1r&@G?@pWDpWW+hodaZg?Tm6KB&0vOz*Ka3Vmop<6AI$3Jj9~#iNLt@1$LFy03S4% zG1g2y&w@<^>9&fJGHws*U!5^W!;15K=J}OHQRgtbcJyW$L#N#dxF0D&e}U{;^83V@ zjm_OkuubzHm^Iw= z%$j?3{NJ;i)PFCP(r;iC-B)JQZ_+chx)!B4Oe8nc3ss6*kutSaRL3UUK84kwvJ^br z0{U!3v@WzElW6p{?gI!o8&w_&1>pyXNIw=@!+xr8PCmm~^AR$Jz)ZQEPqTlUJB_+N zIYc^2>LMF^;wXfIA8I+7&#daP_$2FMw-z8*ZIkwVFRzTO89a};(f=8@o4}4q`Hl;# zx_11?Gj>r8p}5$32#B~@&&#>9)`4gmc<`kZ5Iv5;L!`y3dD7lZCx$6XGBmrP=@>lX zoryLWO=E9RPs*p@AFs3>qxaXvgd1T^v~_k&w_3?unkUe)n%EVVWkDnJsM9(>j_<(P zG(KB}ejIu~@2aYuHX|BO8NM5b@fhpR2!fUN*xaYn{#THf#T-1(xfbwjekIYFTBS-{ zVd0dxRdfZIU>p`U*5;@K{*&GSJ^D9rvE$D#gmgE%0qDonzGX$eKJ}LSRVR5Rcc*>U ztG}}MzqCFayD7%%c>$ypTVhV%HiUy74lu>A;vU*QF>|fZUz6)I#stiM;ATc5oh4p# zY@x;Hn}wzkKurZ*G5K0pf*sU$N>i1f1m|6pucpNqvDt!Z`lf<7pHzeZxIdDMy2!+w zChK&#ipM=zzfVVezNl>GPoPQNUb?ssBB4rmnfbMz@vExqCSxV&I)&klWe6U_buYK1uN3d|l$V&0oq;GvkSc;bGE34-B0wUk_7ICD+D{Pa0s zAB6nbKf^BW2^ao;F5okj9Rd)+*?}%gHx?w$pcq@%oci+5xce{wmSPj!jE(|@@i6)* zw$-C@9F8bln)Emp``5-mxRl4xo%%bMfqT_xLFW8Eh3CIt@}+U{Odb^6m&T*E8|MUu zBum~I{jfmHQ4a#{2NhmQiVZ@|`_%3-r+gnn^L6%9c03LzQ#1~fU0O}gfC|!}tRCg1 zk9{kPlUoWo8&3Ig%JK**H1n?i)z&J#gkiovZ)el{h@>$xovrr+*@^C!eFmp#i&GN> zxe8918{>S2p19;Lq=pmoh(W*Vh{=i_aG3Ku?dOGfH$EA4v0;wnd`w*@G8@3Fh|gF zV9-cy(+iC|oRr7TARN^Hmx!-ZJ`=b{s9^h)A0CR%0wo=h)!xL$0;3{M!ktrKiVZ;g zyu&Arpbps~d6`e8KXbic>@-i|GEFR)cx?|3fo4lS#bRF?030wU|-a$KILQU z0l8!Xr>g?Xj-;_RZ46t_m&qrpS<1t`!TWSeSalbS*AV-tFKaG})(Hd)7JvQ*&N&SE zSBH`Q^*K=X`58!NARGTPhScDLGQ)AK>vYSF=wD!OT&KtAe4?iNoQG@nd(N8j%upWT z^V_Q3$Fwe)K2G@UyzpD^GucCuc+Wmme}s}*z>hbue^`f=8_A|2QROPduMQd}oYLhD zIAaRb2tm?q0J;IcRqF{k{^C)S-*JGtk_ejXoc+mJ^2&M%>>C}Id#L;Rc>^<|TZve`p7UbASI^{EBT|pMBJb4|{wT4wOwv z!Dv1+Kek}SZRm!~#c!dYyyV{PUyd}})PO4vGQLlRe;+*cH9iG0g5p}yOAx@CVf~K5 z5ANG7-)z>B9_I&}R(EdH^=Rgo_$1%aoK={O`h7YZo6n*69O>%WXnyX< zvadbcgpm~_#26t~NjfGV-1P=1mIFS8Kv@($3T)f-~&SO{D zcvKgaL|Fj9UE_8^fh1es-XWmIQWqXwI$Ub<=tnh@0UAsQPL)A6=2;Q@5qn z2epG*@aT^dmt6e+Q`Q~KjvA+5!s^~ z;gh64Q*|_KVwRyR>7f87Vh7>A7pj^b7iMI`EYLW==SO+x?97JK%_sL zBtC%g6GPL{dGP_ZlvY=ndaeFAj7`c>*u+y78mLr3x>xMONPQLfpm59^3%)UHm(6ZC zJrVmR*RM&FCG~zOU#Vy3NL?n{JbnMVui6foUphZoju-4^Q2F(IGu0n5Pi{r~u>?UT z;HU+2Que}{qFp&zuKTRum)h8-PPfs4$@oqt16C+$fPMOdj)}k%oF4Bw-@rw9Z;l-Z zk$<2PP=Pv7$xieG7I-z2mI8YLqgY4Y$yE3NDA&_bjQ_n2-^>Q^J3s6`jK9upEL`+& z@)0??ntV>zndZY=c=5Q3nD7;5gWDcieQ)vrgj)GhK%*by!UDbNZ}yT#>3o@(kRpb% z2#c1fzRde!KIKp0J-;0PN@->%|LlPm*Ww~XkM-|jPjD;r!jDlJLp_5pvHc{Ws-2Ae z+YKzYn2oR_SK5!YF}g2H;wl+4EGVy}1wWn}x-Fzyof0<*x9C~C&=JRSntYj{ofi8v zB6nNLKnugd7_n!|EWkeYQYUG9`s0T8ndWhk!xxnkT3`({6UmD-_$XM}ZhJ5JltJH5 zg(>DHU|8`#GBZ}LhtuR>(~DJVx27_vndUu|^I#_1 z>wT;mZq8-W{WWS$NtD%Xz#e)o&w9(vYU9{_R%0VX@1G68YD{@WFSFRzb;zDn!OSFMBfeg%7Qg^<)r+z#IX9f^*imL5(?|M>)w_%6* ze9LmvA0HYxeUv;&a>kIt2TAA$#(NVsck&tNfMJ;9(3I-v%C7x(^q%0fQsJpG*UvpH zDH)u0Si`ODAwT;e*E!8~Sx|id} z0Qd%h!?1UBUZ6K)64LQNZXVf)qtA1DS$n zKzBfrd$D8r2xN)a7qYB3gAOQo#YKe;J`s=K?jj44aj1U$#}W6Gw>Z>N2)*qH)#EK( zwvo+ctoybU{?6cfSPdE?-_CHA+eB zZsH#eo;Bg|!8%}4JJIhHr50@ncakwqUlGLvvtX7r-}|m6{H3*AsgJejjpezKK^v{1!k{p7iT@oF7%) z78M#)TQ*;6Kd!BuO_q8X>R*N#;luqzX%v?x{-4cI>a|PNvlgwcMeh5cA7|&#`7*kj z_B`o;1NY<3?jTaBpSN}i3P{Ii529?MKQKuTZeI|&JM;$#KJ=_*;(lp|`)hgf3p0yV z_UhuBi*C4{5>^OYNU(>fz|Dz;N7EMl^stX5_2Va?m4PWZQn+ivji8zo7;N^ymD`ZT z0F~R^b=$OPLHWXf+$u+VzH{8+nw0VPgt9;=SDQ*(X`^vXz|;AchSDTV>cCdWg1VPJf&SKDHYWm9+m z?lbj;6f;iPcdXvUw+R{9xyXI;KNe!XoeuCCbNnTJ9Qy<$z0%sbpn>RjwxjQ7 z5JZaS&^)Y!vo^NEUp?`ZR(ec7jbaHpuL>SYJk@Jr0%u)Ro@y-2Qmw;`aw0UfZ*CvO zu1|bnhO7HU zq5V~J{Go^U*mPYoYGWN$8$jdHnPrshDZK~B0LK(xix2KbTMYui7;X<}xdv6Nru=fy zkbQd{cR2>&;~F}5xzFe8QI1A)>T@Qswt}jSat+(>rJx?k1IOCVeKYp4O{%goBJHWE zU#e-tZZ;>>-z7Fd<*StT+G#5@F~gQ2Pbiq}QQK(7zNW=qRMaWxb`C`A6{AP8)e@s* zny&md#l$n$`A5=+xdt$#N4ddz;<`)3GviI2@|v0gpL!6m?U;KseEjQpGUR{y;E=ic zQBWaTIB85+EX~?3zjO&Uf@DkN)V?~N>NJs>uonBrpMT;|9Qvn|_h&8E9B0!w7dYlu zIf0soNu9Q+TVd9E=}1dG1;UhV)iK8ltZPdC9#5Pp=>85dhZMtxRo8z89=W>z$KCC^ zuU**mAGNg)2FrGxUl8P(w^@A63Y?+lAgyN5%% zWAy94%j7+~`;GiyK1MuEdSS#A3XM7rTO4`wwgbnD|BZZbTpRepkoyGQ-eVgZE5zTM zWd;_Dv`K!LjF%|Y19*4?zAI82-=v7UUdG+wGJ1Km?Q3^J)Hd@9;|xoa1T%1H*IKq+ zCtf2XPFi3%nFW-IRymV8_j%?Fukgl8$riMooHt@h2xs9748RXIy?YCl0&AbfZP!M2 zzr|VMXJw?Wt5cL601iT9?O}{vXfc>Gv1@w$a>xr}OOtQ=a>XRi@ z_?8}VtWGyd=Rg!2k~h2y84X=B?O%KO)CY~=Nj+niCFJBfN3n(@;F}Qe_v|`n1?D&PL%;(9oz7-z%QUFOdKqor zIJJCkVh!u#n7UnbcvFPrRu*f*MI2D(FgzD^LdDa1WO{Qyq4HmVN$!t0G~W-0{}hS( zoc5=Zt=fin{JW#Ko4U#laI{z2#Z#T&O$Za_az?nw_d?sSIa`ntl7rOQ^a6HTX@KV= zoB5MeEM@zB6R8Prg$KsM%4Qv2yHd9)6T**``wG`qgFB$^$GPN+$2U4VHTFG285KBw z$(M?wwuExIu+tJ*Lquu4ws9TM-jo|v#P}1i10}^0aRqgY#*^ziplIl1D8*Eg2Pmlu zUWjyh0jJmEZ-PgIU)Pq{ZMEAx!mA7FMY?jfJ>{NTU)*0wUV&ZGY9D3@{BfA&;uQ(r z<*pb)phu-{EpR&qbd?ukcq?5Ou#4^95q&N3cDWNhEiuz4O3&NAqaFRIRaR1 zQ*uE$^v^S#ed(to9q6vhzx{ogSC1^Kp;&+JTV*;h-bkY~ z1FYPEi8d{FoMk6wrf+5QIU2QzE1qOAb$fjJI((kw`!|VW%a*`0e69&iORwTKM(IQR zZF@%;Iv&_;IT(06cjtZ4T1uZ46Qe)fDPkb0O?<@A+xw-$uBRc8>qS(!+th=f#@4Bt ztA&_PDSSA9^jrZ4SbL4J@#$#5H6JV|Oer{@@b8ct*niZd?>4TR?efOjpZxaW>C0p1 z&@!~MFgm6bk2n0}x0gaLX@${y9q4A#Z+Y$xYgQx3D*I9#l}T;PusGEI8T-1+fO$;F z{q8ImvW~6g6bE52z(4x2%nMHW_0vOZs-NR4O4pJN_|KhYsJXoqgJW=_lR6CFUk4zY zIijcy9afDi&2jiiN1p>L5OdG;A*r}TVUJ~0@7SYGXU)(ZQZ=+qSY@;0aWvg~i__M= z|3X=YKRSSCpZIkGsqJxU1%=4rO8s3S0>h??I^D&Vb)NnBu4uX2y}p_afy9~;GDp7p zxr@VO=Q|0kRRWE7`QTT)wq^eVoqjGY5B)d z{G9dTwywf|iU^BH;va*K=T|y;ooJZ3n|*?y;`eoTBE7Nk$B`Z2`;l6@woTjZKQ zHye=tu=rItF{h^>?DI$h3hI^<=KlbEu*b+pGII}36i`p+a>KnDrdczVRh&0PrR z1euw%eXo|H@y1a@JPHwh?b%u|*ffTeM!y~S#T3MQL7S2s&^f(9a$ZUbf$@~|qM~;g zggmv`kbEtE8=ZMvVvB!vWZj?P4Zg5yhkUyZoszo5f3HXlp=3`7D=W@=^Ne{>k;Lt;`JXi)^kZ~F zl+=PWxpUrIM?bn#WR>7Af%QqA{fpw&?9$BBOS3S1qrx*v_ zIV<$g`Lt)m-9W$jq!?tmpA5&~!eYdMrETqd+4N)q^19{D!GBj!4#;YMIZu*j4JLUk z<14SPb_M>7zuS#=qy0K~OWF#T_Iqh-LNlykBjF0u@GCKMP5P2=7mVs}B5W-;rtN{n zkBt`+uw_~mAna(KzrZecX}M1(Z_AHOp$wthmzaD{SQI`#8~fbAI%@#>J%?;=$0E{7 z>kn8|!u-I&jdpKGwW)_J{+V#3KY%F#PK2Y>ev%!jygSL?z2R9_Td}lq619tqO5*!t zKeAvB0F~9+evo(_-bdN|XZvu`8uu7J_lwvY-v#PgYr`#|wDCDkCx1W7zg@ z=n|o>G@~pR1)VcQ_d@)rLBbf2<=6{VU^J03!Ip*RJpJ_7fsg47Xii)U7<;P(%MJXd z&2Y;IWLCJWEkN-F)3c4<=fr}2)W>IAqySkM%^NE1SUi3o!gCSKXU? z8$2Q=^4$NQ1$g&Xo9i;+M|b7*_D||Ir2e(29cBnGt6`{1o0qA(rxx8Rrn_5+8O7`T z1s+>DQ+2#=ApLt2I^v|N@vQ`R=*1LuiV&u@wW&7kXRT7*qUE z8IM2s$GobJ&caDt%)1Ugjq@Kqjlc!A5rxfR8(lQ#&yN*TsF%r3zj;kV9w8a?_wMKD z&sA?>95Y-Z^5a5Vf(?F;7O!=5G|}I--Np&u>kO>ue0 z8OkqmfZaiD{Jv=yW59*95^V6*Q{q3jGhm7msH(-V{iOD;fCDL-#AF$!4nkW`>@Fo) z&A39cd9Hum-3VWxbH##2PD8Fmh@_>rc|F|#_gkr4p62a*Y`v&C?!~y{meDz4VS4#l zc+`@0yqnU$Hu{10>s9{M%kQuKgs=j{X3ntMk8gh7U$U{X2?tpG7dmor!CMM?>{;u#eiUYFUljA3CKY~bllSK(RJpLTgfnxI9{RW_$MJ$?tXrz z6cyNhtX0-Kxe@ozfY2OZq6CP&f9NdiB+eAe^dQ_CE<0}A&xGmNZuE20KDf(G0Og$`xqA7rhy@{1>D*y3RlRqo}F;&+(#aRH{&4pK-9N zvp2M8yqfEFAc4Qjqc+%t8UB~Z9vLRaC>cXRamH#GDLhk3#(D^d&!VRVe+ng&PSziW z&k}5kUI|!hNMz6k_S~>t9DI>6lLb14l*dI;Gf#7$6I`SvWw3%;6;- zOjoY9V`dyfeC5U>P}h5W8u0UTl4zbAV8&Wg@KSm5dA*L)W@j=78fwN$=a5Qm>2gxl z%AWRxdaGt`jB+0}wVMNTW z0`+lN&M4Y?S=DvdOQqHqv-zkbdH?jKk=JN8^NfY&nu zKYsJHZqFC~UE2Hh=fe0d;%f%#6Rr;$)%fD4d4FgCZ&y`Tz+MXV06}FiZ|FyZmkOTO zgwsYBn0BR|^!rR|j`tJBQbt(@22RqX5N+z zF&2IBwDhhEs6Q`GNNdGXL^cI`<|7baTy$g1M!J2Ja{tt|?rjc+$y%$7&gvi6g>^?Q zm<0!dD#wb6qyc*j* z?BU2=>^il(gELgS)4!8l3DA^P&Z&cbS=5-}??8fhd$mn|WPf0e1-blDLIvt`De|m= z!xZ)Tv>l++7p@f5u8H?TMbi)3cpI7n-va17L!}lFh%wBn6x~|knu6bMsWXC7mteN5 zngHO4pCZURj6tD;Uk{(s1e1&vj;I>x0_F?CU3J|GT1rq{MGGlQ$pfNB{a>PlL{k}W zsxJnkdU~SRXzuPceNz*_^p`$kqW6&54&qF zk1yj6J5hlmk8dx94As7&z0cx}y*1XUXLQ6p%n0WxN6Kap_xeI8ZrV?fHEa2P@?3r^ z`mff4Gx)08wp(Kc4q6Q^vU&Q!L90#a{lo$pgPfPpVIWhnH4{!paG4F;uhJS}H8AID z8-7Di!vwr_oGsXJ@MC70LM~7)vmQ) zJ+9Q_uPbs;9!vNN%GY|EpPn+8z3S$h~`V0B5$#$J4>)|MY@I6E5L>}A`kg|~& zRzg@kWZE8RQ(23>!jmE5XoJSCv)iOjk@%#5rE%1ji~)}q_G&s8xHT|bKE+R!;^4aF z&rSc{LM@Dr!z7%eKWzLBYybFHQukWDHXwKh0Vi|_Ikt*!Ic93*m{7tE&2jauvAw+1 zWEiFd@dtHuRTJC?teM*(L92G1+MIC(>$Uu_D>EG0cXOEwB8)ksNUr6 zt00loaPxj)57B^BP2MTCEEkN#y|Zjv7@!?yNSMGrO2+S5@eJk9PQ5jG$1=st{rn*fW+XVYCsgeVF=Xue67Y-u8-|Fs7<`3Jf`V-|NK>eky& zlYUK|+!FKoA6xLxkuMBKTyfF&=?Fd0s9c3|OYCS6u`H^SIFy5x81zOyU6??8RU5TSDj?YPaJzjVLZyDn1ZI<%1| z^X)AFw{7m=c5?-gK;yjwPvp=IMd13YW5Y8km@ba3XLjvS6k?qD-Q_;pMTmHgXUgR& zm|U^+)5zf4wWufRs1a>ntMg17arGg`NVOpFJhQWjd>rZghaRq9>cT#(`T@6-Bb-IP zKti4?m-p$OBzen)PE)`l&6T`P(?yrPVx4WJvyc-P6cGWzZFil{I2tMQc}Sosb)zw^ zV!gmcBzn#jP8<6+`Y)v(tEbN|D7dOF+*EzjV|jvPmzC zpR$mXMEMU>>m}l53&+Hb_eR~l&ewqBg)yTIQ@_hq{Ou@G^)JvCo=ClqSjL9*`#16) zfGd?Gq#d-vA)j+0(`R#Gw1nH9)9Ngms6|1GPo|dyiE$S9LNcUsZUZPy24_zJ84Z_O ze;>^e2|Nm{(5Mh4AG05eolMgO+0X-f_a-Fb;ouynxR$4HG<$pkZIA*~cN#UXnjxg- zM;0D}tRL5JMhgZoy!c%RhxIr~GtI3~x>#Xntmk#awku?D4G z>W`$^{AHyyxSUu)$)&9z2S~*UX_0Q9%`RsP|KlXbvxY__N^$Ibp>iB!VRnxXcc7;= zpxxobcJuR5x=*0MEl6WQJQR2)@9LIa8*zmD;N;VuXt0gwLt})xTWU}b{2=yY#n~%v zutiQ--0KI`KI&{94LDu8CXhYZfR*O3?pKQ&_s3SljV}}t7typ1=3a-=XtykUu^u5w zFq?Xa9zM(s*|?1>C=-{Zhu;-C%`lGYnejzqff)wwm5lHt%)8$KS^= zVXEOdG|pAv>|F-ExjCMm29(r@GGJFVDh~(kn%Zr^_69wO@4oi+kE>LWI_0MuLl!W~ z<^;wA2g@!4xKN?(=Z>T~uDsIG zYjai2^|EcZ-+Z#|v~&w2V?q)~8e^2j5_3h8J<1P7H^o_?f~%nW1#SaeZqnoGkse7B z_KZd1Tr;!&@|0>(jn%%`l~4R%?2hho%r(Z}q!CLHmqe8?{L#DyLcT~PyeF-h$k_&b zaQ|-GU*Rcc;E(-byz%*UaG=XK__dJ-p{mqd-5UP~6DJ3FA;1DkWaI1ZwCahyG~<08 z^Oxzr=1a*UpPvndV8$67Mp8_Bo?q|g{SgL*hA_LunlXz|4bY4QL1zmb&xJ`vFIdbvM7pg&Lz~q# zuG?JIgQ2vKcU(v~A&VK1>-N=&>K@076d;742!AEUQNc9LtK?)1R&2+X#8FTk@rxr9 zfDuO_cn({#E)#-L7ldYvgrLj zzp|DAp9BY@aAc!S1cuz^aF+#Y2M6okzGyv72-jCbg>4gmR)?Js>-aW0sMZ9Wp={DH z=xQIlCDCV(4QO~sE8kJey?)1e7hPk6me~WE2{W!;~+xD!bT2W zNWop9|12Igxh0v|5pX-`(~*jI)1Q;{C7(8bDBJax0pdeu5j}8YW$Txrv9C}A(DLo1 z@+f}yj;9jc|M3q9F54~z3Cd3@2^vZK;7>}&>6T6Q8JM|PdlFWcM&B9$f>$UCtx(A5 zuZ%MEdaXp#+U9A$uH6nWXS}g-$jkp_oZ8-Wa+l}XpKV_<_}6s4(Nw}ZPgIDGJm6_z zT$2(w#dKPZeulEs^J00}3yxcj676HAli&74Qeu=p_1Jgh{d}&hq9=V#wVOJ(~ zZLSo2IV))9sUvzE%8i6g>khu2t{b0iVrU9C)FCRjcB_TA z$>jM}^3o1<(Vzs(u6A4CrTtC9)K5ib;hGTL#DNvwn~BDC!DacHo-G;5O4#>}av+ zrM?pNl+bd+YD?B5_ZwKX;Jnq;md|P7Jw4g`?C;uKdA^-ui1I!@Lqz1)-w4|G%v(aj zI|DQ&#|`c~Z6=^-d9j?3k97TI?7@ouLYj8M`pQ zaU$>3tqfh)GgnFCOK-0V`0|!6`^^xH#`<YrbR?3sCx1*rOx{Q) zR9N0GcnDyK&B9~YIf2g(L3&jROAZ2w2m~a-j^UFcy}mMm5%fL_^mIJtp;q{KIu^Ew zGoY_#F~L>m@TftLZyz1V*5uO_f?S8w`LJahB9Qoj-AiiO*T<)+YgZ_H`4@&wz+U@S zXGjrZirClL$Bc+CA)IN8^}_cZcuoP$%mBtMfH3%9 z*#BA*LtgGvXMs;2IC@cBt`hLnK5mcyAm08|XlD&F{O%Nkj!h(d#qV%Dx)-eb`q(@V zZtz++yo-@ch;}VU^4&c5SCu{W6t>0I>`MFxuP_8>x+{nL(zaDS0Fj}HrlYx(S<&JP z2PNVseCDY6I;eDRoYuw@ygKHuU)^|r!O)8=YH@BlA#V2E>(HD%BiHp!c-`1(^iOr}o^VJQBuA;hxDi!_c#P?aic4@29^% zMr6yL3MaD>yBS`GzYUywC3{H;0d99>|JJpdaw!4KV8F(M(H3I?@`$=K`Ad;qijg^k zn8<{L6@TL~F>Hv{vJ(dm&b;26OXc%4>lXET0y%PL0O7hk__%FcaYMd&V%au!x&o}a152^c2Z92VInYAc0$O!_!%OCd?VV3zUC zyF$0!Pjhs~b^viUH2SxitrkAdkMBp9SVn`lW5^aVo=a})04oLp30;nzt z9;2Qlb&FhgL2RWM)t$l>3vtAkOfsEa#6(#fdMrLK)fhMG*B|15r!A+BSAXU`!C;1D zbSxpHdB{=+gD!vd33%85i~U_QkXUPC-qB<2;?j*0`^64Avpc}fY;r}!?Dv5A z?{mPUi-Ozm`eX4i-tPzx%O%^ePt4JiM2=ihLlE{MHpZYsm@L64kgM6|Ojg-(m=h@( zg+}$8R{yaE!cFpxnN6?`SEYm8>VCIwEP7mtt1Xlm5Hq=JX=v&r*$2l>lqU?h4N_#R z1AfS2sw5Q@TESCpbX-UG&U3D<|KSLf{=CJ@nb+tK`H>yX-x7DSjMVQVJ>Bv84YzXg zHAet5F3ZnAV zA9LC@@Y(QHzuT?X%ng>l67F0Z`20;uxq7D{17ry{(P7|-Fq5G|NP#!fg^#4XAk&Q@ zeQ$?ebvv{5(6opcA0|hE+(b>R_waOu+JnkRmov!kZoWq|ewaqd*CBN{;FL`O_OfX| z*g1u5Mg6U!^L~>)_kiXvyTS=-8t}y<*&$Yrv~1m(kh9UHv9pw)i=zTp+d&twL@#wg z9c9FT?pke^frdGM(+NfV_BpOLHGAJtC#Q~Bb7>C(D&DHECIyydev4z?g#u2YI`o+c z9_E9}6N+60E&zh6&f5!Sb-iGiVYGOUMN+e9GMwL+P{aj+saj|4XDzWR_msI;%P!o^+v!#7o z;L+hRhHd^ywg8#H#o{ChZawrcIrQxLRA+JOgGM88q6T^f+8o|n99QZbcBKilB;0Bo zZ#A+m3KKz?D`7CQ`V*+EGR!s)-$xc=SV-Z&KQDiC5I&`v1Z$8`mfG0-2W7BvgjZb0 z1*^V~fc_T!M-;tu-KUm)=dG(xpw#pk=!wUypdY6%eHL8JeX}Qsx2V+o6+PJ_GlPVo zF%KM+CWx7m=us%9AZg-QG=;oKfZTL108_KVz%K`Qgy~LzXNH*w>5G}=`*NnS7W2$l`Og}OhF zyYL@oAzyD|WZNEDgWLBfRt|IA zMVQj(%2eND(rc}-Bt6C-UXHMN*2M6Q%ybb(^NJd0d93gy%)V(?!XQ(8&OSAeOe5cD z?DtWS3aP=<(2g?zai5Nb@2-HcnuI2%2{THoCZg#AfzDiib7EeBKFr=cX*}G)_x>XN z+b7!i$e|DZmYQph@;Otj!Ql2sTdTMxJBg_?XFdq6-58O#;gG+rq1WTEB^u;DtLgRY zDZBe1G9+fQ#mQGti{iJr&9l?$QC9fy@#yT$P@3%Iqiv{h4(W!8Wy3SMSVe(?Nu8eb z#W-3)v?iqL!_bvvXbi zsnoz}ROgGX@0GGqWeG;|OqkelKhlPHte!ww}CkFZJ46<3{(ox`3xqlb7;7_2V1 zImbu#P#}kg(*TV3{sM+uIlDj2;;a%Is{@r{Sc0ZgJ|_L72;t~z%9)@d@DHiN;o!>S zt%c2Dr_t$e;Cu`I`)H(`X=kV#AVbdm*q{t=^mmG&a$+-{wz;(*1M`Uo6b z3d!xfk%HD9aaSct<`nykMeTmWQ#sk1F=P9dEEY)4Pjuw1@dOCkGKlmlFF8E;TY^?zD~=#URCbv#xbRQE8K!} zzgvUEgy}Yz#OVHQxxa@{ibi3Ce>X9O%8f!el{4HO>9Cz?B~*Sg5Ih^knE_uCe|&s! z;Y1$o-J=~x>PtEEJ6Ero0cJVdojzT2aRKkMRt{v1@>T(lcXbh+vZa~~ZAO&>_b~$8 z<*=9jO(yH-j~e~<1JKd=3ZxDSPLVjT2Z$h=3Z9N>#0Ft<=;!NRv!$^iLD-hi#!XKR z%3qrgZ6mJ(Xj-vPQ6~*HZY{ir8}W%fFjm~J95r?xks^IT-$_`{3eZ&k;hAFQ-esyE zZ1sf2R-mJuO(stwSG{6yiI-dl1^H0~!mo!BNI(XLYd^qRCl(v;ufC1rlNYV$Nc z+*m(3rh>X@7t6s!k7jBSILJGf5ITywe-R${8d+gZT)A2cOv^D5W}N(WW%l=&Aa%X= z(hL;{YQKt@f|*i>$^xFe=IW~Vnj#tCmzGQ#wA?N1Moa0TLSw%`*v4(Ws~w*%B&o6Z-p3-56G+&ZQiS(EIe$aPG*}c%w?U zM*qh#YtIpgRKY|7Xoz>8-7d`Whh^>cVR5#Hi}hA+xoBB6H5(KrP&5QXZ#WU1Al(z; zl~s=q2Lk^(P)u>zd|{tX?fH4Y{?<|~_mQG`5pL?>2-@!4Q7zNBJnVCZ%Gr$8`5PY9I(>Nmqr=WB{HuN)gq;^;#+Io3q8v_NL7Y-TX|YqHIN|7uoKRC#g)>_^Sz z5~bk!FfDl&ulq;f+2R zaOOt`+YLqWf}oFsIr8~%LoWG%v3K&hGT7OP6T2Q@LOTFY4gc{6O`0HoqNp^XE1|H20s7tUwiKk0OG8g9VD#^#wVpyqiJG;)kz zaU&+b6$bD2y3nW*oRY?RvU%L%G{NuY^5n7d^P)DuP%X42@$uu08+Tl=UgFHfb=;h5 z)8&D{DsEYkEMt>=;P0~&)iDzl#|iBKJTv@9rvY=-OTpIKX<-73WRgqf1CFF{DNuWGBZn6p|B{;zAro&kWjby+nT_x!mnR{bws75! z{Ms>UWOO7+9=F_nb2WYXG!o`uqsoJw!T^Vq+IwDH^w458GNP|qi+`^9r#~9wnZ%R6 z#vpVmU3(={h$^j{-7EtWNMXd+&BJPP-B6$um$&xghWXnHF`|>j%`QBy=is#FjySAt zGq@30NjxB2(1ctXNiWU51~~yGyEu+6`XAZ}h-Cph(y}r!USaWx0_wfSg(M}lkDumI zzJxOt=+rF$j+4$8-W0Vb>i@a)JRO-u$UvfLtDwPv?(%~}5NcwNYp{y7K6KJ54c zx0B_TfUvDUcIcu-r-;o!e%voMAN{?5f0yEB1;WWER-PiA?3bu&1Fnp9C+C`6w|Vzn ztN!DP|4cIeJMXc5u9)yU4ZRJ;Y1%iIcf|2Gb{9Mm9=>;B;e38@`nT1to9`kh$_P(d zp3mRV3{TqU@=1|3@P9k!LDCLwST`dNGJhknju@HnIUo$-Nsy%?*%ffZ#&1lzBxT(K zW0|IxoSrDaZR2^bDlP`}OF;QmHwG%YDi{o6lGH^8VKKr(V&BK+%6Pl!DRIXjVNMg( z37C)$z>1OI>#CMs=UO_+EZeZcmW3;I%vD^mbNACqDn9FNOncxin{=GuHlp!zgAd7) z@;PGZo2s~ZIvx6zqmyKQ*B9>S3~s(Qs4s*2Gha%HM~Z_Q%qe>BfBo9xLgDNF`48sI zJFhEo{DBHX#LZ6Tc(jNoWozM`HYjj%!Q#=E^d3Gpv4FV7Ehp0`~{I>5JQ(uuM`FLV3D#C*I(`;I6 z`5PRopn#th#`7o}yS&ZNsS~&r$u5`w@m#lA%{l;@h|s~BN5|YJiKX=U_fm=;I|s1v zlF=?o*Yhk0cQ_;`A+}vmWvrg(iK>TP;@b7jz=U%ER*qgN)%=7;7oRj?iGiY)yQ0yx zOo)}2gHe>GV!#3=Z=bCK3$_TI+_eBV4y($10L7RAs{*T8+>ZU$8`X2`%EOf1U%X4Y zK#Ey^C{dS+=4dhA{dYcG+Wz&1f4X6-Du6ixey#X@YPgVIFj58)q<#l6QZN{sCs(yMSXVD@w%L6e-zGJv0 z=z(_ORz)vuIIqciwW2F55^#^MdD*kgYy+fAd3N8TnCIc<|W06VL4@LQ?D2nZokO|Fg73e3<>{zF`;>%(VYB<8J`z9jtH5F zW^fbe)H2*`a~z|D+lSjhUw(C|$UlGY-GZul!~FczhXLgR2m24dBjB9?_fkPw+;jT_ zrE_Z6fTLe*|6IMF`MJfDP#0<{hSQtK*-j}ig(oP9~ zHx8Uq=Otw*1bR#)Ct$)j0F6eYXKmxk5s?oPyfyb6N%5kpU&Ii>_8^L8uyGLEWin<; z*_MJw#*7bpeyBV7fOk8{yP>mg=vg=MH3 zPbqcRU++&AGYV+Ivx(uxdo%#cf&IIJ+? z-x#*EnB3Cwc@;4-rU#!7f0usG>+5F?SL;HTPjL>?z_7gBdtXQmm=pztP8R1Fgh~Yl@0(V(zLz-b?rwk^It*PKOo)g zy4gt9WfPptG$(Jac(6=%rQ<%%Fj(Oha5F<%A-e^-L@HKFl-+3;y+vgsYt`UkpH!pU zA84nGED$-afsZ*b3u$o&eb;~QxvF>36{T_`1`NfwSr^uh3N_Mn4xoptIBZ4Fa7P96 z0Dg0o_hE;lz!}J1KQXofCWHg9uJJocYLpg~9ZavjqiZXk>9KZ#IJHHl00x@0aQ-e5 zIbx%3X5vo;MC7lPxM53WUq>C~hFbdEGIia9x2T^jxLhrImNu-}W6RytoA*9J9kA2O z)Z7hbM{x7Xrqj6X|E1TIDzO(G@O<7W$81Mn`IL^+xuu0OrFJ`sp3QWXa?6)KQ`+vg zbh6_#;F_~8Q8NZ+#NUp$iBQ$K=OwRMo8bY@lW@AHS=seEAyM8)w?Q+TR^cYYDmalh zv{p^~-s{aJL~s+K{r})8mnzja&7CSrqBVB3QP48&s&dCU$`EBvKvh0YP_b1(wq*Pm z=QRt=(tHA8FFE5H7R zLVTY8(%saw=>z!~cLj534)qO{^Qvu2>IZzl7+2?}PWu5y3H?>-1M;8mv>( zV-GG?Pc3=6I*}&O6EECyclD#wzk*sJa4ni3Z^F8H>s6*%t?VHg@uwU3yCqm4yRy8& z&5d+ESPux#J8lZ)%`dHFd5ZK(Lk)cCIDhRhMEy7ez_I~@r)VdDy(gd(%qYHY16QA* zRAUTV)!GS=KGD<-s~>2!JQTgR)$dA#m%X98w*tD_0a!nJ#p6n;t$F2XzRod!fZOsS zj}31N$8<`O9N9U*e4Gpx>8BD8Qp64GBCP8etgG+6QJwvp&nL2G`-a{6&sWf{ZH!KV zNJ!p$$-3?L<>Kh&Wryw+-EJX)h0~+BN3I=_ucO}|xb3EYD80u&)jsn-c7^SzX;0KN z$=|r+BjJIV-;4>(qrnu}1sx$dd!3Tqf|y|7zh0}98u6+An9=7Bz^Q2eevG&cTrJ3v zlueMBUUvy>ReUSyb8ekd=am8r>-8clI2KrDZ5C&5RB+iy*Qt)~b^u1Mz|A?`9(ryM z*bUo?q$eo{Q0a9&!7Jjn-yoy`u+Pa zRGZA4J%)AT-j|`!z9q(AKeg{GUu++yVSytp5p34J8U3cmFSz!Oj?lNSzfbk`_oA+V zp1Y*x7bC-?YG}~!jMI+V8l*^l!l~&8Hn@^LtL2Zwumk8c=f%?CA zs>;@_2=@40s-HUo@Hf-~UGU) z&;Ez+?QW)vVNGNhH_t&)|6gyoQwzP~>aKR12kQq`AfZC6ZhzHa1bW|pZk2jIXXUz)F{tM+$w zAJBBkSXX|XE<{Za-Se1Qx@cL;NW9@jGga2pW9JisCAQw~Bhh#cJp+A+fQNYsSnrnw zE$5V4y>fNzYJ;Znyyc^pv(pqPV*yT8&fcg^ApDJaKsYKF$S#a;Qg$7Ug_ALaFjZtX z1vforWJC>o`a~1)Yc-=g#%rUF|6>*Z?l=8Cb`WOFQiE^2$jMNUSt*jO0^A1XozOHN zq^Y+@w*+6Fx#Ra1pq%qo1H(&La zmf*VaoQ+u61Kk}dR)VkT>?O8Uf{mnJ0oPE%UWajrRK9JncFp>h;HiPYdcqo(Zw z-BNi&PN2Wnfu0I2(q@miyNxFJu8`dR8W zWMEpq8kjy{Zm=M>(Eiad>Q5VvwX4>t(b3iwz-_KP$qrFeaaIg{17Upxc~)?sEXvUvAv& z+z$F25V?7cU3lU;(7`I$XW>48-0-958#0=woqxS`Sfp$(D zRvB*R-THHN?oAh_@>S_LZR}^xni+3KR1$9TE$N}bVYRvihU3<=E+?lX$-b^#H^?pO zb=&W2x^g=HIrKIA=RXE@l-{Q?>fW-})61S!f1iJEDg9yt^nB-Rt_vOIkLy3+JVulI z!Tbnuf!hwkJcDzQ*s;JyPr{ct8@}-oE+*vH^gjF4b4q7H#`5{U_nfch&X^s@u9Lxv zvTGC+Y}eQpdN3v;`o^CC$;LHfS6PTds zSFDS7a0*4r5@12#VuA6%bAb87vnA^uz1 zD5Bdw{-hU*+Cw%x*YyLZ_`>hzy{H2RXw zwwj|}xQ}W3%N>?LM>HDh+AIFt+=P`>Ai%Hcrwdu;|i!s>&{qci=rI8k$Lkd_2O}&VViggslJ}Gcfd_pQ}dyfb(Kw zdWmou(kHFJZQ#1G>mpUf31~_hC+I^B_4@2B>Q~(``>t^S)~~yan8YGSj%JZ)q==oc z{NFhJg(%Kp_!K>j$KZ@`S;ogJ(fE9v@^OmSjQ#3$KU6=xJuHl?!XR7kppRvNB^Eqt z6Zq5Y2@kTtg9)-!7H$}ndn3*4%aVmpR++vVd+UcUv-KOsIRXDn|9`)46f318Uw3d& zyvrx?r%P{WYF>H^W$u=9EQz_vl=7Mdp*nh;uZd02s4UMY#Cq9_3n$`*{9(8CfxiRO ziRml>YW7sb?wjqbE;#usE|DS0F8R*x-Bgx0;W_WJf@FmBOw}d8=$t2W!b5)7V8l z`IolJo>h~<7#Dr9(<{O6_*fQ(#pWZdv8@Gz<7hH&`+igJxi$Cc?dw%r@3=+0tl26K zz!lR6-<}xMM+i)x8zZQ!!V1qCDkD!|u>I|5k3X`cGIF<`c`2%LHjnGM?^i;rquy{R zO&Or5SmS%uCD&J;i+*66o#Zp`PP1Wq99kVWOxH~4|8xT%Lw~bR>&U5(n`-6I)1x-r zcmuV;hUS7EQdd#|1Fo;>G|^wUP|}9M;5<8-v}2xltVzdCuc>g7Dp}q@PLbSk9W26v zAOO=XqD%PFxub}Tk3iOYuD(L`Ui<^PBK&Ay*&e_CeS=wQ@T?z;JRj46aLmPTCxP?t z^-TdQ((F)7=Z>yZ{WqLkeuOHBx)_r}CIF9PI>cuh*|X9!k_;4$lMFO|KV~FpUIo5< zp*R-3SE-@wEy`R}GLE0_Z~)d`N=O{UH}33nc#jJ%_6iEWm~qe7nH@>S-^E~Y=N8sW z8V}z|?*Ts3M1RvoXR3#vUewgm>ol}Z^}7$52zcBSQND+t8y0EnO)_R^mb{1zjx+kX z@K8e62n?&L8G(yW{f63k&UOUF{r)HJENN%x$deBCTL#WxDD}`?kE#3r{%~blZ@;VQ z;EA0j3CNn&>pFTjz_b|yYO}58qV>1g0#f=OF0fR57{@K-4$s>SwfK>xmFYSE%u6jj z{mC7Cn#QND4d|F2z%Ndfw<1^^Mw#rw`}^N3@S`SM%;@u4fZiT8^qETtP0DZ+MV;40 zvF%TOvWC;Sh}OLioTqvgk1acyf; z)p)NNe`hA`8@a&Qqrihl5h=U_C&5VpRGx12iU%0Q6Uaxm1{Td3;q$^ahaD8B!o2q_ zFl0px+S}W!cHCoI-zv7qGsT$;hL@Xp;C98{ZgID{le%fbp-c+Kbu$SoqF)bb7ra8n{FILtUCH#zizNH zyiPo@<#wB?foVRVApB@dj58B{TP)1!LPtU<2PxW9dHmrem5+wb|B&ALY>n)CIrNN- zSsLUGq}pT4GF28y=ph2xLs>6*7kJ>kCmGOOi^k6QqwRsA&t7UffxzP)NKx+rs+Z(v z%xe3SCXP-ftrOKt@SI1{^gGfFhd zv%s3oL{KvkoM^~S9$zP5+#P_y)y4sEOzYB7{#%!N#>HvCkwll(MZg%Yhp5lK>LcnqxBN`~V8JhTBG%o%YyX4G#~ig%2#Q{A@Qpu)}WK+MWbv0l0n(O}~}FvhX*5 zkV1R({%V=58{c=a>hJAC#j1#Mf$W-had`_IZm%vxU1CI)!g8G=k-?Or04&#j%ci0F zzIKl4x%W;`UfvPlSpeh^4E~pn_{~<2+gT9xlEE7Ej$1^g_i$DQ)2R|}2|9P+#y2a~ zXwl6-RWH&kGrncNw@WZv@ZKry01U3a#2rfY(KxrPoXS>|$$4B@VJznKJDCSCEO`WV zOa;qsw-xeP=YkZ*ARM=K!|T-{-#NJwTfX3>ySee)S>`I440MHowm}WnB=ROeaCFvy zh?xv1wB+be+Ty)#4eQ=J9!kdlh+Pg;w?A>Wda9rW@^Pmf>ESh$y5gd%lHpJGf!pr9 zmC{9wQ2b|3dBm{B+!b;^7*P)Fk^2^vZk6CSYA0ZzSC_+=$u8zSYiphCOT6c0(3^Ya zg`#)TJ`LrvOXZ-U`rprv>;lQyiGZQcp064+H$>r{Y(0!Y_Zc@SUGL=`U`ToiUvQPC zmpE>GZTfF~xu;*9>wP1xj(uuyAzD=0oO%=Ckj))z16xwcj4}R0BHUkn<=n=At$K z0o#%j61-UmQ_McRVV1)pN1~bB8?xI3v4~PN)LnnMKSh+1#+>lhqjHbccw-sLwX zJ=3W{J-t0jcL|)k`6lF*I2r3*S8L=(8~J?%t5>eAWLV2^+T{g1bLH}=LY+3QF z2p9E_07dfPU5}Pc!To;IdoNb~eSK~_DNsgleb`Et!HO zhtE7mjqawere>e2xK-703Ai;wr~Vt>pzIM9gAn&UWM3pK=+MvVnt+ro0q&P^qrrVv zAT{@o;Cm^S5e<=j0RP5e$GDv@c<5VNL03xMr~0#VT6VE=vi#xYtMpJS1`n?MrQW2a zo5>ikTRcliXGmDb6L?J-betg2D%2`&Bs|Ufp1BaFIalVAZfT&bpkfudal}f zhb>5fR5X}F`n+h(Xk!}l5b0dV1*ZU5;8e%f%_es5zWsisy9$o=bK)6Ct80JzXZ6go z7VK44oXi&6Zk8K(@q?5HWP%_v#RZjKuY{Krak%; z1~U24eGgV6Z~sUlZi3Y$1_N8NNXMy|UgE`+Ezr6BH=SJl_P`>JsJbPP%v}>_V0Oy* z)B=o424LN)OOEVmH2&!93X5dRmJJ&$4g*KEsYZ{FONHU>qHnd@46J>>O_fI({`&Wq z9EuLH7_X~dnE|&8~GneGQmAa%Vtl)G7o-1m7jBhxMEmpVT7SnP0c0g&@ zjS4KOvjNaTI2O5k<>LXjDsq?J$$Z6VwgaOn=}svt0?&APJ1RR>b$s7)TpWPG=YCax44t^dBS ze2KTTr3Yz=Nil{TUo9)UR^2fG03ZNKL_t(Kpq!g=#sl>?U}a&HPB;XHPDE(s>?EFh zW~I9S&gw_?DyW#`q5T7W>Ul3hMlr~@V2P%5H31>COGZ-eHsC4UeeVS6SSEz0tb>zK z(eH}BvW8lDexqI z6)&(KtL&1>JE)1lPOYUxitGmUGogI(1(*!DVI2DI{-VmBxwSH%6+WQjfS3`D1tu&( zk~E?6qd|UP;1sl3FZKW8jixVw#>3Vn3>}9~(=hz1;+Cyl zs`_sKP%Ffc!f3vucl=87Yu9;@sng@uLjG7qqpK6+JZ<%-qn@3(f|38+SGI(oCM=c+BX*;MI1 z4fzKtRo-#Y{f;^)`lG1U5kN;7-J4=j5jQeuYTg|&TT}}ldO|(3EbYEleXa4H`|eIp z{0T&g`O4tPXb14_+wNDxL*-7u*0VNOm!I-2=6p0EyZJn&c$?{xXy*^&@g>X$U*XNY z{&wmmj3>n{TYjJF`_rfC zOsOHQG!M?-DPttXQ8X{i&ca+fodrlG*I4wUdv@^Ii@&9^>;plHM@o^z(koaaI>toP z!#(L!QZL{HU|8rmmkq1}3tNCDaohL1rU6csk5;ya9=pFQ>$hq$8&5D z?a@}iIk`~1Dn3~eBd@V_v{fcK+J=5}dMV4)?Kk}+iDrB4zlX1T;P!?&I=*$T0gdY& z?!5J$q^pK+e)msRU)D?aLt;*7%Vn351nVH~)5jb##ODNB2lZ0VJ-=1Ge?8wDf5-s9 z(L{^a(D6SH8$`ewi`;=7olxiY-GrO~B>PcDE7Pg)`;hGi14N04PT)Sa`3YEPnX%4#BMgOJ&s!Aqx2oYcBS0v^Ogn%zT=j zOxAQdV@2NyZ4Fw3+g-~ZRByZNQEqRPyA>- zVTD^dq<^4az4*WvkS+5#3xf5w{|9Rh!o+PF1BBg&Sm3(gy1$j;_p(D@WIYHE1!om- z4Fu&XUq|xS>uu+}lDFkfK+&g6p2~iY$y=GODWg)A_X>5;Irxe&U`lh96*-zJN^#&e_~B1o*@?;4hR2U zG%v~GhAQ~d>@;QjOgB%alqWmp01Q6;GfyIY>zT_U`{E*Zw|eeu+Tm!diH`jDkot7r zyG20-XTHKMk1E`IaWsqat{7G@9|;zZiraykQtE*3pR7h2x|w$M&(Uvsg?&hfF04ln zryW9DVz^V^>i3WN_f~&-{X{7RbeF!Dy!?gnam4c?k`oOT@HPO6*zxU+mY)p^n{}Rl z&FzKr-S?F*b`}bYV*ZfxQ_MfGxZvEwf@{m2jX%HRBGn6>0@OQFaVplr4EtyX0rT!nBX3pkggC8^gHZw4E{KeGnwOHr2H5&lywj3>Bii46e z2AnY9=^}|pfhUdO-?5;tQ(1tHH~{OPzW9roYJ9AWR%O2vSrtliiq|QA*1F1jWAA7M z4DPrBAA(UHZH%h@e{f2vL5U4G_T-j}WA2N0=r) zlMoosT=JpdfYx+8@Q+8-Q%mLTnhj>nG{u3gc+~c*=SM-2(T1@&`*-WSVn;(iGyh#b zixR(G@+Q5o%Db2zFFh#_M@D11Ert8~zkP~QJ)?5LpPmp;|HEm36Mj}H+0YDbeYcEZ zO8}>?I3f6NIyP!mE0Lo2rYuNn#(XI|U46VYpyE(E;s6Xj^;5e!?vLWp2n8mH@+e~E zsW*}Q=vuDM^46rtZHq)s28xdx%rKh63BF5!6**lw4tmHtoMx((n(qN9{X?alV+8Myd2uX^Fu{8dT!;V=;X*)nLVd4R@6kP0kv(wvO z3($54VCb2P4sDF~T$_>wAt!ghW>#1r@(%9BC3us`9xj6=y*I!&_I~Mm;XCOaV|4_& z!O(Th9Bk2}5joVJ$L>(^dWcTVTNe$!;APKa6?I%V@S!)uDZCgR1ReXw{mJ6w4apO24aiedQ~RmKfnhss-GVOW6M z;TOJ3tsGpFzT)-BoB{*W21xWnqtvjWPhdb%b3V=2+CgaNmWl2)@cg~?jgUgvgL?E} zeU5n)Upxfzgyq8<1Ax!(t95Pf{ZO1yV5th4PZMTsmB9^H&M)N1>3!4G4R8IC#X2Ut zDe~qiV_hWU=8>LO33|zTO^8aY;{eaEn783nztL@D(_}Fjw zZk3(ZxkD>e-(TM|zFJ7{Zk4yC2%V!=&KlT6pAxp+#YJD{*!E@t+Ufu-S@Poz=JfWh zB8UlLwigK6i4nwc^_OP?DCf002^kd*%xi5!MUM$||`1Dq-}+yC&r)r<|MJ0r{!?8E`! zMP1PwA4Szedqp4;0l9KVB4`R3nnv1Ff)eYx-`-M+{`|N9#QEpZoj412OuB?k-F30V z*K0^)fL^r8=ssD?5&);|Ub4UJ43IH2hzCaBh{%Nnjv?3cP-1`6W zE~Q4gC~#S)lv<541yaP%+3!NvS*&aQH>k^DYbs>VClYh+H93N{Iv%;hK*k!Vl9Al5-NYDj}cP6Xl;2~}}JNc+Af827aKSd!#u?sML$ zWjxiA-+TLdRd27!Ux8g5T(8@9jsHhCLW0klRcn(cq@!z}8fK|m!I;JXLqb9NU8jH7 zty!Nur=o$n*Mqc?>wI#mY!~F(MaA+#PwQ&XxKpKS~EhUVCkowDYe^V=- zURg+w`Da|@jump*b^QmBPKfCeEGH>?iEll2S}$ST`aWlkzn&B{`jIjL?Dyg0|5K?= zcQWMX*$RA|ejIgf->s*}=lU^M;EW2-{uYgkIql9Uup=xyD8tNzg-6OjOIZRwH-Hy& zwy{FhV_u_rcn`0ZC=b?3p;a(*ID=1Kl%wb9AzX_|GGvG`ok6xrC&M@CYrY+A}xj&YSvj18#bph&Qfdi|kJc}fW-qO-IEz*Cn1 zG#@BwIIfYn<_vuhC(d*?B}$sBesz5z**tP-K=x7;nk||w)ZsG z2uVySW97|}7k;ZMxGCey<|T-d1U7q3dujpNmH`-CdZBXwuv!T`YKtvZJ3ehGIeJBo zQ>emx5|^(AUwWe& z9xm=XvEXeNI`<@1c1iIa&W13(GoE@0mWt~&FxGwlcADJaM>78CyPl$nr|^Er*ISN*rWvx1}-Z%^fbcw~;D7yVXk7UAd?!JbeKz|hhQ2USD$ zJHk6pUY0LAo zX7kD$kKoE@X?<#J0(v_EwmN(s+NV>EF$$eS5^XWyL12KQQ|$;ed+ZRJJ%w$YZFHQo zxxJ=2aS?H10Ph4?P^1trEclbm4SYvkf-5hcSI7g}0mwfQ*@fQL)o#)Ml-E@Sz9syLrX4nJMXNyCfTlZ@?!-jmq+WCLca}ip$Wnpl#(B3 zbpv{yC@2>Y!7)KJ-cAHZW_XXEoM+C{jOHb*Nhmpc+)mscT(MBS<=53O3F2%6MxWO} z7`lb&ZC%7RI+~v9*U@v8LcG?oHHEa%CbJl5FwI6SuXv_-{}~?MXp^vv*VyeKwzY=} zX!MNt#JL0DY!P}S)j{EOJK%`D)!Z%TkS08?5D-qVV6PZ^Dj_NkJm2yttd)>X9AG4c z%K6{@AKbgZVpdG8*n6~ zN^PzU^K}0OZ!J}lWdqFKIJyql9)>1aUPOPbjMlDBE2PtrvOZd;9ff8J-KI?+$hiSG zpDDv9<@b%}%tF}|AKgK}35{-QCqxa4<`j7f$E+&fPdAYm{mmW+cS9k9##_eEB|0da z4#%H4?pHlFc3wp`*4)DJf$!?j`1Z={`_5X{Nb8Y0UVGnNigYS_eF#Hiu zAB>wsC(u1WkD!xjOgd;b)afqwC{lC$8%x*2j?Mn+TR6o1t4^1<3e}A7z@mo#noh~|Ip50iLxG$}v z{T_QFV9m<4l~dS#{k^KMUxzlTDaWCVFtawbbz0~(Fr&D&ud;NmZhn@h#AJeoczzv|bP%n`t7#-HRy?1|3Woyv|WijEA zltaqclrbr=1-nL#4gwPT+f-Y$xEQeY4#46c8`(!Gz#`@B3a5l3$5a>qzg4i{oU*G$ zUiTsBgGIFb5g3F<_6)bQ|i~o*yJF+o^MVZ~PaPty^N=3I741lv2Zonfc&e6r2w2)VbNJ2UPFTjVF&{_xJ@$(x{=mZTjNJT#coq2(oO*0B)Bc7wRTJ$?wCxG&K z0W>@x7zYJzpr@qwVL+r;fSos=4}sMJ+}`xte^n2!c)T)G&D?ON(&==mS{qMGDgf8d zlr?10d-QlK!Awx6r_(Cx0U_L|Cx_5zC&zx@yb`1?;dXl_vj5@dP_ir=K2skC+1Xxe1vBiFBUO*F<;=MJw^ z{dc~%MO@xKFr%9tr$)DZBMaI>TiKNkz~G|u@6VLlwJevCH?J=A6Y%j;mu~WD;WBg}tx^dt#d6I=9 zPR+Mc&n{n4$VeN{*~n)U+!~rx_cJBWV!+d945(gP=$C(wmOs`6$9MBgQIM&P6}B`PTYw&#(-iC&OLT>H0_TtxO}P za7>J-RSRs{;&R8NoGRlxLOX^Bzj|LaXR}Sz&(6KPa54){`4Nq<%XA4u>5T8fOQ5)3 zLV5!90M0UCYX%WNKg;Zyi0UQL=9>5QebO3QOa)*Zebn#&o!3#lr0LZA*YBxp@lEt- zB4h!;8L;ymex-I!CWg+XJfJTGTAOci^gMK~%0`w`JW@m?o32K-dZTL0ewk7Ov)f`% z`nNlyCAPaIKpOY_VAk*kJ(21pt5$B2P+b>zR_VRycnvu3Iu+jl`zuk1vEb^!DzB@3X(>7utV#y}2}UCLO$oTZDF zmGX&p&ZbT8x6Tgb4SJG=6>2gzH7t*g)*=TOIPJ;Dt+Fbe)x;mL;ACw8xy~rl34k|R zhgRyP!ugcZJk(2l|0?K%o?bQl+3yzsYZGn_rTQ*^yEp#a@Z-OAcH$MVK4^^sT93cZ z^-f9|@5J;sQWDOZmcfcGU%IXATRpl_w#Pfu8ki(-OBr_-{yy~Bc|xv-C3=eJ=bq+? zI5Ad22A26R8wauXR+!H|KYK@|HJH2kCL}AttJuA&v)K#YdmzhH z(H^pH1+4dU`NtHmDfRS|`cmz*Pj39t$IFQ^dMhKXdEq&5Tvo0qt}_4!po$Z)<@TEk z=T8c^Z&^QjrIsvwvXl^8ZnwD`L(lqi9tVY~{|~=G&j#k*30q}L7M43DrIVci0~Wjm z42&SVUaLniLC;J8y_7wCk5U_MtQwnb71wE7^J}R7?cc!+V0FPAGXVX{p+|pc$VAfeu?=<>RzV~m z1kA`AlC6jN7KdjX7D`tW`M^3BD^cRJ@bkf}td;pUW->y^Ci3OzDbq z+EfNe;fd%Q&d}fB`=X+@SfxCmQl&D^BZ1FnTHDx@kqWq=(34L2nd zppU$5wv|oQ3-TkG7ie^Wg=g84r`1r&O^CX00CtAJX#=?_zCcD`B%BdIb6>h>d1*aI zbyd+_Pb_%L599L2j#MV=r6OGd=tuRH8$k6^&Lm8i#q^}4m&ly#06tG#Zl9u;n$Wp@ zzjy~N{KG|n>_)=j!}Zz-K@^FQbLCCbV18Ai|Pm!($2>!-sOyjJLuc8HxGFim!Bvl@$~ z?lDkQsCdAy=nl-%o&SIKz5~pz+GtyM@7fsCA@pW2)iir;dJEOmKni>*UlQ_>KoSVH z=}AaJ5=cTI7>Cf)d!vQkdv~LE(@YH=+qmrR{eK;4q?yr3N9XR{_1gG8Pr~}%b0m#4 z^G?xdq||f6%b9JpwO6)B8WTnyvIN58l)jIbpMR~i!_|hYg3L}_C5;x&#>WX=1}88S zwmy8I!1b-m8|PjwwCWNos4MpUd11i6HtQ{Q(WC!SmrqOI&R0rgFF$b7mWQYV*Y%el zRhHdsVOh@r03ZNKL_t&*;lqN8OF7E}w6*`uDt~_UFpma+_rHfV0HBi$oD|2?OPDL! zCZzu=6?xZEO)vh0~_E$#dtYz$(9!CHQ@84RW;uV+DZ+tW~CTi3JyV{f2t9Ra0 zqAjBctr>=_ZeH>-n4H*nP+PcdLU#q)swjV*PzL+J?nsYSq@esky%vgtY%u#lrIN*p zJ}_gt{3oMfax&ndCt5Do8Y|>fl=)6NL15lBA3U57XUtY}=gy1rdtm@r=?3t+_mg?z zq0hL(2bZhnWnM{QGf0nRF)+vU^~h!U0eTB(&=TgxMVy-vu#Ytr@KH7T4Dv1jgg|@0 znKbKcB(fom+elDcf4pW6AehDbzq$J{YN-Tmsr{6u0!LdaX^}chE&)n|&m^ z66T3Gz{gXgbBm)B{RNjWZEd%nt!gh%@!mhabV!H-bnX4yevP@=>*m0~myqm_`eeY6 zLwlsdd-SrNqj}H&#>btB@`#^-HOLMjqR4ZsEoTs5kuPgQ(jqa7t*kAQ02e=ZoJ-sF zFFl~Xb8oz&875O_tm9}BcKCRNk$WWg->D?Aq7U9M@_MTzm@T{f($-j^_*^uH?ofF5 zZ8>jrnH83134>-Jx-wy^7!Z#DKmg&MOe0{e^;UOhsvBw7E^*+~)BMu2uaqjiHHWTd zWr1xr*ni^l)A&q#X~WjA=<4oLuRQ;H&g1H9n|wkYzR3aU1PE-E39>Y=yHkyy`VaNb zXUZ@3$r@DRp@|3o#{8NqyLjxec3aU_SsT59OT9pw#@zZJ50#Jo3Ch2F!vj?J`ub=a zv)ua$=K7M?w(die2g-C8`n`{7<9cwu8UXX2`*oq0q>Fl-0VP~0VSdBcZTgjv+cIb@ zUI{`J)?{99$s73mF$C|81Psp0K3`vU|4^eZWkbmcYl zkvQ{`-&g=(trdIpWoPh#7=6Z`79+r6b(&WpUA|MiANh23c105g^()(6N>f5`GxvTf zYr56i>#t693d{?Rs9@Uwwp&mw7^0NAyBccxw6r0hjewx+G%-LV^Uv_S!5@6XrGF9I ztU1PrZt>Ow&sMtJZd$G>e^-(J*mfAP zR+_c}>wL9oN51J7HKFsS83O<|G#&IHyutv9&;7fIJb8dQR=k};2C(&I7n0w01_NOs z+G42am_LlieaAxHNF$QFQQ-S#Y0FpsE4c_Eq)}im-S2A9v&xQ6YZ#?6!%cQN_CAq? zVKsSfW_eK;KSWwmI@PKz^}#zcR99DcwQzCl5Dfue)uitUNL`4dBLH}waIRIOO{x1b ziUjHM#%r&)I!S2tv%{{EjvN+op+Y+t{qN_WnqI2#*4|(ZVmyH52U|a`0bl}qe*)gJ zfCgwV$7~6`_)K19$@@QZk*eb@I3le4c0qDSuBS03yUy^B%kH0^xK{o7>Fd>7v(xt; zO#IBRsa!^|#C;8;!??iS!8}9c0wZMtmvCRSEyOIX!X?aqEymnh*F07K{WnbLpBn%| zE#7A`pUgi4;Mt2%8M9DA;K&&9DysGFdi|Y0DG>%9=ag4Bo&YLif`4!OIVDzwm25 z^sl>-5&)55(D`Y|$LL+mZTs(is!QCktsn+J_vnjq*1=Biy^R(bVodeWFLy zzV3X80CY~f$mt-CXwJG4pf0eA+3CR;cp~Wa%a}N+=bpEN!B{S}g@;PKZAL>+X#22p zj@t3^BTF^V>O)pB;LYwfcgbSJI58kV!P0#9N+Kh@FfjOHu%j}sypXqdy!=W=s}|$4 zUTMe^Rw1c|(#d?%&3@2MFhO?l^p&`&s)b|=z%CSD%iOJibvIrsy7D+)g2mJrWb`#; zU%BYSw8vje@_;Uxt1oj38WSuMY2AI}7Vu06q4-*FqvS>cCVu*&=xz?kX~Cz=?r^!X z>@*gZ9dZ)R&KYjUwNH+alGz5Ei-LEgEiWVT)Qh6f0@^s9^+#OdW32z_L{*#lR(R}> zpZSOT2o9#nm>5PlUdHe~u_l935{3p#f;k8Vj>?wcug+9Tw z4jB5i&`T`UG$F!dykZ`i_soSp{O=qU9ptwt*I4Uc%2r0T z=8ay8j0X)a7&9ABRoci&g_=^MEIdhZ81%Z(0?Rv4~7^TDlM0)SP-mYDW$j5rzsax>2`j1j=ox=MtMfShB zM!?WbhL9)=*k={dxhF0*IP zRj-spB4DT!KXoD1x=7ZI@`o=Xa^0J<%jJ_;B6K`eW%r{@Tm4to`^TaGi1P2+?O4@V zX#>L}y@0u@>qAw)|EM@D3|>QZZT@}JJ=Zx+=o7sCNKdsP9n!2m;ku@xuH^az#;V8J z-j@cz?8h(Mpgur7Y1ceB_JK1ML;1%$s>rCbp>A?)j~?qL-iVpYu&9jr8fAGRMHB`D zO&b83e(S;Ub8Bc*N^O7n5o%6X{0JNH20o64xt<(@mR+&8{$4mI@5qt8ddP5+?#$B`g-*qsQlU%0Lu@7sGhfLEM+ znWGqQzWR3dB~IQ5XkKRj5_`7SK+Hlauhdq@9 z6XD0E!Upr6z5s88RqfL~pUQ~LZL}3_c%HHmjNHx>&+&1My0rW{%C&~-Ps8YdLylnyH>Q(7qVMAK_amB9+osSo}78A&H6yqFE}=R z*$)^2y59en^Num6##_b%?ROmRw<0u#!2eLA-9tXBx>w#Mc^WNu&Tu|SPy-~l5&K*j zZB?4fX=}jxhga6;m0!?Ce6z zt(3QxKHf&mHatU~6?LLrS)!VLTJKZCfZZHPi&9LuUl6I7kSWDWvvF~8BO6;R9!V7= z+WH2iY9IKM0FF;ko(ayUd`(vh+#*j5H{4>VQ_`uTRA&FvS1|Mrh+R+ZOub50sP4^w7^KF=bQ9q@d~VK#erfzWzC}=c<{NYs zFHt7gm!Z}+@0Vwwd!eLlSaxE$qV(HfJRa7N>*H$2w4@o_sLR2$=NA3y9?%ldGt_c}tO4h1f22 ziLQ|Z*p%#6#*z}*1^mJDU5&QZ;ZkqYR{!O(O3nTto%n|ggRZY$Kr-=r19R&)A@AQR z`@$f>k${>~-9rvija7Cx#(+gpAMkGuzp?k9O(>U>YKNEaGb4ts z90BNj>U^V0*d_o!sfKfdz_OAuqNO}3>C}_MFgs$lwPO}PF(#)+A@_~7Q!I|~9sVBX zO`;9=kJ}k_`PWO8|2iA4Nu6`@Mi>_$7NnJj=bn5?>_;0GKo}_@>=w*Vh)x`wmngNX zJ6K$Y>x(-5kv%sErdVOXV|`t?u6Cu>brGWYhp5;_BU#$EFaY$?ae=G3HDCPOj`8ly zN`XU%t)&JI(#Hc6)ho%&$Ob}qx&!Dp1A2VQGs!vGXvBKH7vw!M_6OwiZQ;-Z>*0y1 z=|17{i3gk?&xK?%M6$ur0&v+)VE~aU2-Y=;0lcmLf=gJIBn;{aF6Clw`%k`Z&inHU ze|oOj-hbDD$1Amp*%s?f%&p&Gv6Cx({O>lUI=8LHlPTRqJn(*|Pg3&@kkr1x*DLAt zN)PJ+bUt~Wjo{Is7!Fk%hBn@gnNaG*!#Q1Ur9pgCI{wDnf|CYd^QGEys}3HeyrIvb&*YysE%^HtDe z%!t6cV&yefME{H%CzKAQ-h2CfHG9^aK(U$js1HaVq83}Kq9Km}IOdiV9|~aT{&J0g zjXTyybs4a0ZI1vJUomroe^*E=y|9U#*H=o-d~c?DF;PlbM*`|~b1@YDcqb$@QWE;V zXyWn7PbUoz{o`lX-9;U{*}+zKhGvyy+(5{mpVba!SB~n@NDy$AKXK%OQOLvMSVE$c zEyE=r5Gkh(rq<9HMoZ9V1D?Qw}<>dCRRacIN7`W;RoJI9t_BQ**tR!q%+Nx$G41jA{xhtQztf(_ zAdI*NiW_|)RTI=@j=B0G*BoO-X-Azh0JgqfsX?=pnn(4BarDu?owZ4cjnbZZeXt(y zCZje)Ys7UX@&;4L0eU@1W!6M-HTCGX)9d*a>4|6 zp&87rpYjbqRy__l{s(`fs6f}Z&NJicW!maj*(!v(ANZfN4N$MEt{s0R&s7WcNqy2c zLY}wt(A9CaQZ>rCpxL7n z<9*>SChmW(=~IxmqGcw#hDRVO=yM_5b6DQ+k;x3%#rV=H*{wnwNSKIA97`(G=I?vx zE!yfoVaiPtFI@Jc+Uu(G$g>?!_a^4HUum(Eh5L)n(RZD@p6hsx;Jt+374!*itL?ep zs@mHRcO3)#`o#d~jk5Y33!XU9t%azaMgS#g@Pq^DSf-{<``J2yGF)Dzn3KaDsl zU1E5EqS0CWR&tphnIoyx`G@bR`t{!q!~fVYu~C7pJ^Yw*SS4qk_%roM z-}HDYLA4guWrnHl^=OsXoOR0?vzb%5B)r6L?HhkHTd!w7s5|O>>|9^_pIBIKlfrTb zo+baAv0`jKK#*;L@&uewgiS5GEY6#;V2Yo0%7gVmGF6bGaX2ftC61T3g=Y|Dy_XpP z#gVRz58hWQ9=H8H{qS?*+wQ0s{OYtbERFX{AvXH-BXV2Ngiic}OL~mQ&g!%BxMML+ zfAC&10C*{4A3d@F!oz7g+qbJ*j3mG~chr>tyOqKx+i|9>IEI; z&-iSy?J(l$OX&>2M|k}lkXo5AtjN*DpM=mqJ^$oO>iu_TL`SvxHpn1}MLo#L4BHgr zVa;O?-TQbc^^^8L*KJ(8z5kf(B6)Z7(JI;HSmL~FgiArD;ewA#^bB6}nA`p{ecUb* zzqU%Z>szO*#-PEj+_!n2UP4yzfS9pZ4RhNsIig(o+wFj^(dU|@(yi7fUO-iSq8$&$ zR4yGlH=;w!gOTIHlxO^%rB9^e&=vW+eb^D%dOiEW&d1Ktj#b_5tD`rA2tb(yh}|Pl zYQ(z?Z*_~J5#xK#$knV-yJkJ4?U+dys9RrqC<&k&Y#tVuDMeiSK8h$P0AmP%zwfDWWT~z zGri0+E*%SA7Pw-fhqy%c?U7^9cFkW^?ZLZ3RzZT?b>cbx_ZH5Rq)+Pq$}j-5!rj-7 z39q-HPY7=1=q7l<&UgL35(B_K*EI^w5v(_i2e7fMf4&OM&{k7j?HeCqk`hWj`3AtF zzj8O7IE%~fF+v|W0~k=qdq32hH+12!FCA%+O^mI2CSCqnRAI<^R6eL`NR`L#IlXz> zoVlk)UX{M^M<2}_GSBz zcfRb$mQhPCHJG0Hwcb5nBT!Uso>fgODx`A>^xm~sY=Gog@({mRl<1t-rBX?NU;yY= zv=P9a5w86GikW_~0ige#n-8F$9kKnUh8{>qDb^IPf&8~mdSpmp%^@86gN$JP)6}~k zjfyrHBqY~{Uzr_!+mAEV2fV)CQmd(d?DSpInk2h^E~wTgvWr~W081kB>`xM)s9zuB zQp;!~%mXr!!Y$-&BGG9EbL(e}RjPAdm_)B-sZjojb#u`^VLVM>ZvF2^&>RFk1^9p9 z{qGM77jm8Q^a<=u%JYP2F#4o^mmLp>Q~(e#0Jieuad{ySov6%{9-Ts6Ne4n@#f&*O z3XpFAJfaN%^ueKJ#~Untm+&z9jkZKHPzA4mxP}cM{tr3QyJ<-F{7EeqbREk z&4Fu+(l+wyv;nZu@bw~lN|608qqFwZW8wOu&EjJQWtc+vfJLd4nlo!o={@z*-2^#h zDWPZSPJ!5c2O`yIz2?oCr=(f}@E$e*x)YquS%|PTcD4 zGo$|njn*m%_-*o?k3`k@h#fX1d@V!~UH+*LRfh6C<*s-i@bsj8enqpwC3%zVALbp-=C%4{_neilF|kM?2$#YX6;*$Nw5E@!_w~v?WGKWGTRSxj3G4Cskuf#TU(nd z6t8Q@*AOGv$u8_Ex+F1Su+q?Af;~>d8fW?UpX9F_)pMQQjeQ1f{;g|5G^5{S833IRpY4$d z6g37)$rI{9wJ%|a_mDX z6*~_;tYdvQZpd?@N2_Sa-zjt5W=Y|fwbonRs50BI(Lw%bBpd+?NVU@_T`E*4f2+(= zHT9+G)3*rD`e0U+7jOhXm*bZ{e7oJxnSHuDeCv$!&Qe=mH?~Zc3~sg6)X1GSr}V{Gi6Gp(Ooj3P#7RZVD0&IEE33y*t|D7^2U{dbiYl5oVn<2?W zC1tVa^5*fs#cc&igIUY(A8G-XxW-!5)&hXJ^*?;ed)0~Zzg-8JNWn9h{G<3Ak%*qM*1mwB>8&$Vy*ty2A7`hX^|^PyiF1d=kSFoLEsTLPqIvyTN% zNQxp)r`qCz+v>I?ra|K=GXORjW$(^*AkVE0-2wH*x;nd+RH}7_m6lVhti_Az$Xtz= zM*pqb(^>VYkG`M?ZzqV6Eu@xahVQYycQ- zY=3}af)*oSP~tH3v#bm4*`IOV1XZ@S(5}8WVA#m@Da$hd7=Gl@6t`Po|DXpf;<3vI zZk-xk$}K;hdE=3Egv)mMz^xA^c}4#?X|G?h0t~_{t&m;L%)Nz6y^ST!;^jh$x%EGt zpwv8lohJ&db0fe;2D%RZrD3jER&NXSN&Uv7Y+sC@VVw5su3gV2^=Ja_m-wkfpNK-2 zNn877tu_GMXhP^rU!$0V_@31C9&>+d3&vAZyWRY4_3qsGF|8$+Swa=H`JD#Ji-5$= zWpA?OhS9UQUctyGD$34!lM95fpn2!jpG_4URGqIN?fNkG}7 z)a!*|(JNw6={2TA887fGz##66hl0k{Bid?eBrr+@hQnelao~a`9=`txu{GTmp7j!a z6eRN$g=G?-AseiTew94W$eP-fmP*RPjfWD`<@8WirdBcl5GRGk5|gXq*`nS+I5B6Q zXFU#W1Y9?!bh`Be`pV>@)HB9)Z_iMXh|73sUpNc+-kTtPEO{GF>bJ@i?c3;$qVKmfgnD@l*%m%aFc|jX~|J{j0)>6 zGy_rE;Nl>+7mh4SBo`yEPb&sjInMS;96A96G_XH>U#O_{ZG7eiL0VbPSc8T*xQ^FD zr5~u&CT5Z)mKt0rFQ)_Wc-es<-zqgnbkQ3A8M2`P16>(XrPMt)%0_@L554-@tJtBTqf-VfBoiI2mErk&BW(k{ZK|dwj-KG=DSE;H z+w2_6Q}R|-c6kyc6tar6BkIqd;8G54P6mS0c0^u+(@pc6HV-c5FKOK}X=}UV5>%)Cn|8Kd!ur0c9v9b9}cBH^;PiUL^ z?lg7i{fIFGPEx8U(h?sUc|(J3tQs4%gDXzqYI8D_`{v84ypdZ3iCW<7BA}b$L?zuU zsErmcxU4M<6li&i9_*vfJt1Ft43Fm+0A$M$d&9O5{U2=rNI76@uw=uW7Q*Cp@ zF~TZ`Uwv$q<<*8;tSh$3pGJo_vf4Hed8Pux;O^@uitlf?$4JNe#sKw3GJZiaQuKFs z-!Q3kT$A?Xav;U3V)3go*_FGmEW6HFZVs3HDEc97#hT)ILU`(IjK0R)^t#p0wn|nr znf{-22Y~*5(Bi7=3qM1R<>-o>XQ&+E(bg|#jvYW3jMYBewIAjC9G=8{l+=}Ow>WRz zz!~hZ0nnu*0JfL|(@^H07{|#4F!Z-*jz9#2NuCSpIJnj^6=lGcD%n`+ek+j{Uh~3X zEGQ3`sAb2c+v+Is{!6RHvE(S7p!d*yVL4XU4ulWDBN2Yp$epNnv{M!!R?RSF8Zt&W&BW*v*GPTVMca zk3<6rvr}-SkVGR8%XUfI0Oy=#Sl$ZbjIIyW&NqL%R3YjI78^8B>0~@NTkr?E>XR65 zIHE^hLY4|)L`x)Ew!5x-AQ;>Bqemfg0h!=Tc7178CZLUmx+`K_Kxg_i!2P?QNh7lE zY=bYf;^-AH0GCscx0l$laeaq%pWZq=z(&4!EDgyzUV~m) zH)J1Z+Uh-t0CZ2C@KB@C*v#mdu)qn-QK%~LF*ghHaGZ?q$0N{XIBiuBojsUypAh9S zj)OTyKl7V!%eB)Y+B$AX9}^qC{U$c=Khzh}x%yZE7GLGfx8r9RRr_=E>SsmwqwDg& z@$#Edr8RJ|0cy>ms|8r$=be=KcjXMg=+IwCR||ttLAU9sw@0>p$Ow&*@a$vhD~p31 z0PRTV;)qC@#zJ1)^Ntw+o{vmRFU|U|I`A%fM|jF2*m3Wzik}@P7R`jWP6l>9((g|B zS($VIM@maH)STIK70=$^VK3c(*chqL&x}gw59UrEJDfdhPSWt1yvJFhyg5EWp%^TS zaV%+#?9y}9_PC`prai_nzlL!m#S*L+`Wr5}@nqsR$H~5+t@iGls_l2Dm^qH^|J$fQ z*Wu%xx#jKRJl)hRZS{XyY-b9eFh!eE-JjGv-y&V5=i~CBeTw_|To>-)Wz%#eII)E>VOq z7z=2av|3HwarJ$r#7^2he@>iKNLQ-LDa2y`F)ZN`snWqq`|A)@i{wj`wJBZ~yO7Yv z*N?Gsa%$=qeov{HGiY*<=41RnhyZ-EoTpl?PwM|**bczT41%uD$YX0ppTs7t8QR)A z#zpmf(g5h1Jl+JR-G-9!#OQYxx5(zIFCGK&UFW3$1l5rB9RRZ#ma7s6W3YXgwv0vt zr6Xm%;v1rPTmC(-Ql;;p4?IK%CTKhz2?Ida(;uqC>MYjn_wD@{^k0+jcsN*pU0`PT z_M4LG0e)MBRdkDKjz&YJ%E#(X*V}Z6D-44b5>_zMc0CMu?womwpa0kMY77ALiQOR( zc)cV#)KJepT0H^)1^@}Sp}Yb+Aj`x)S};#A00s;gpiVpUG`eqY8-D1$=haspNaujy zMcsM7ZOZgT*6(cRD0s*4sx=dxallP6Ziz^+Kuyo*pPU}7Gw*!z?&BR+fNaUK8=B@^ zcCqXPatc^Nu!`amuQ*9@3H9G{T|`VWn71%5(mOG1qBM$#h&+jSss6X4y|GRlA-c`o_1W&+Sfos-VYb(#a+l4>()QvKisb z8MD>nlaVmkmPhSm+5p*z>Ug%L!UtAX)ouMv^R6k55iH$RfZN35r)^Nm%xqBhd*+ca zvH)$YJZM?9(ciY(M+Roj0r9d*c{tT$yhqx&ef^;e8*h9-EY zH|9ELp6V0Q7^=tDHRjBokVa1*RcNXHCp$Lg@fS^?@47zU-1u_lr);>Xb!#7aboB_p zyouv~R#V+4VDEt>=6U19jSWW-i@CAg+Asq3MC8aKY0EJs8a}jzGbZ`|Z7ivbj){6k zJ)v#n&1vS{a_22r=Cuz!8z%BBAXQvjPDcfLM&pzW7%N!C>6a3Nk8(1wRTqT z{eH6g;QbFs|9YO<>!=0}%Ui_{lTe*fYOW9kIP;fY$T@#&o}4jnw%Y0T@07`_`(0NV z+8PmdyhvvBM@h|8n+ECH_-GyWYIHTp)b-4| zoyohI`SOtndf`MJYg%&&vh|9<-6 z`2F;gcRf?hojXUJaBP^BU*=>JAwY{R831RDKV8-BzCw(@=X7_f?QT6mlDFu0yB)ZL zQ@`9Jz?sUVfwDCMLFBy&^9_T(wN8Z3# zwf9$4(I*aLF%MWHKyYR-PqzK$7;gY@jfwu};YJMerazHiT_ig^SX{%{N#Pi;49=PQ}m*Y?v4|Wd-gD_mf}3u8!!3rA z;Q1ciw!Dyw+B4Q)UamuC4_*?0L>5g+%QRfIMZAo8~aS!Z!#N5%HLvBv|m>Z=%n&_mpla2%Q}^X7dw#p}a!z}{&1dIkWYq8AP^+wlWW zNXchUObt&iXrqEFdFVIz9@dv@T^;^^_&L=rmFTmLP2S~9(G%Q< zqYd$}mE$?{LbZ10S!Z~K=6F)(?~W4VP*R$Ee0oRpHI@;CHpLU(K^O&>G>bMi zPCcHHw;h)RzwP!MY4)_y8Dao+Uscb@{S*Wx+TdM@ZnE!p8V>RbJgVc!u*Ib%{ls@okb(SYhXK%+;S-< z(^6=oI`%(iIinrt8Rs}6rFrA}f8U-YW%AB;&e&5DW)N9Gd?(2>=fe4cOR*Zy3j=NQ z9dj|i%qM-)b~cLuaDV~>;F}?jCv~NuPqOqI3%}wuO6bZD>RTUYE;B(NF)Yw^-{}q? zprv5u)dr@YcHx1ns7>zWcAXv@H1uL3BTh^Py9}mwqjYE9VG_9_M^8nF%RTC zVPJ6hrae>G%!lW#p;cDIgxfQ*I`FCa8rHFXP?v^#ja*^ke&2lcZS~(*-y&V;ON-WA zch#uEiFvE$1$vX%<5Ql_QZO@S&Q#w!>Ez(q#q2f!k2VF;fY+&^l#T#M%JR%JwZTEV ziSZdc)!pb;qi(O390>33^*Agw;6t>gA|1Yn1~ zv;jatnHU2I$K8&AnKbi%R?E6#7KlUeks~-FaMyM6B`{^6G;HJs_7U#^`cZ=>@EQPT zD(e>?ofOtC&_)G4yJ+XyMLSSF2C-`Wq=47@*yM5p0QlwTqmK{B9W;P>%1lr4NIP!e zLm>)q=9xbav*U}GJWpc&#ynYOG{)AdtzK7q9lUdR&w~(+BW-XCk^P%QzmTcG!661T z`u*H&+Fr+{k0$2k ztQ(-Z;ezVx#$~tdoa0OPaS8*Vhx(+av>9qCf15dIsCvEu@N=A8Pb0knDxeK6izMW1 zF$P*VcO3BoT!<3AV=~2U`9XYc8G9-cVzQXI^SXK4a40m0*98%7uE&dicibK-8$6sH z_u0x-*5g3ybh)%gh&)kHD{ko6I6&rwzWk-p9S`oNjDSr>fdPOGA_Qn8Y=I978{2Qeju(R8m=^Q7=hG2&~wx}`=~?K^d9oKhyfriHH)UXDZ*?k835ydVKbGRy&WCTq#b{zYVPuIrN$j2M*yURKM9x3p#8DCSyCRrFHW(gbM$UGf48*{7+TwM97#k941lhCPW!KF zsE;{GPIv^80avDIa@u7PbK0ad9;7Tp6DQ^rSEWsOzT?g~fNM`}<<&n%(tvqIG=6*| zoWIyCdr-Xd+WVEhKDZ|zopCOzEuI7dTl4lDzt-+E-RKOh*-pVa66f z0%9y2rn5)xeEXo+)Rv`#OOik;=6U$8KRCl;v4aCRp7~s zUb;!0_rmo}FW(m14pVEbw>sHU3y*8kBVkf!#avkHOoUI##%2?E<)3fNUaR843iFgZ zL9M_gv5f>?h?vzZZB=n8cVeoJpNS&rrmtvAjk&kzUXW-@%>kdgntHRo?FlR5S3(2e z?$eCy$5MtNP1;Ea%7k`?3UY06T6JIqqnT#l<6a9kXKf=+|GBCtM$x!%Gs~48&3chDN+HVAVvXB{~0Oc9EQ_9e-|_(^h# z<A^ig>5)MqbM7rz$27Il|T zY)4CAb|r7Zf0d|>V{{R0VVj9bGIwDB;bC88^{#s z_~taU$1|lJS`PHl>zmx2W;=Cq30dU*DhO=?CG=LxO-xUtCixLufLPUz>~K7 zsTz5ASR)@^)_e8!b||;W_uwr%u*_0i#`VE_9}C8@#Wur&+x*H0i%Od!Mf}jcp@9Ky zEjM5Zb<>t71&IdZjt}WwLK^^)k8=?0@7TcCOE4p50F?1tll*=DOE;+tUrLA5%jZ+6 z0}jzg??CCJdJYA_zABQBbg{$~sgMoZCUDJL-n{Jh*CmHGdApzb-??0;^Npblg zqR47oV7!38Q_;D8h)eVct>Di!$&+MJdvo1lJODbRif)SSC+DtPj0YrrBI&pa`h*{S z4z^B%b&KqeBxtMe@a;?k0Ijyzjq_$Bvd;I8?PEb(sdgf5I2pwed=f4Z#*{lAXBY^n z`Is4MHPw+&s`}Ag58U1#3vq-?ra(Ae^muUmM3>xs<@wig@Ly@Lz3uJC)wn1A7=3>D zwwqXwJiS|AG=LN{R407!9>D>7L@BN;d+bvv6n6>);p(U0c ztoHoO&V0|y{~M2!TR(2y>0S+sZo2ip^mo^m8UO?b`a*VrZ*y^=hwJ7@>I>Q!F7e4< zm@qe+HvJ%XZ>#8&GCfeGtyb&G6m2E^oiqSC@BGP^YYp{#Gs$@Na&`p>WU_U_2%m#o zz0nr#w<>L-$)P&#NniWA+b4I3<}*fYx6;XZ_R;6n^G{7LeNI1c&(=Rui?y|@(+^KmGiQ8A-XA(_ZKaCslRA3JfKPpE@e*(PJi(!i?9Zf{_TXhK?ga_4J7pbg_5)`(?G z*9zKX%4z?Q&!tIR6K z-Y#iN;<;EKwjVcKXYt#~PVpprHkY=Jd(#GhzSITgh>DEQv?_*Wz1=X2>Vsr#`ghm{zhEh}R3L^_6b}$Bow|)ql3CV^S75%gCzcWKU``GhoMY7GQQ4f6FiQ)PzY4ZRu zy`%$LftbX>q6R>z^kL-)n#W8~v;1AQrpZYYek_cEukGu`#2+ynPqmk1Q^On)X$Ej5pTh`6Brfo!t zq0+I#W^}SN{5?$#*^+yldLSV1(TaFJPtzYNonq!~f4@pK@n>+d{W8Y3oy5 zBBiv3Cxml|LkebO0oLdB|o8f_MnVqxJRcQ7Jr}{q9yI zR0ZfjzX1vxq6U2d=a<9r;I@#*Vl6ly`t`K{nx$<-i2*SBV4ZY@y%iLFKu6xZ^=^L$ zYqQZ%Gv1%2bcx@b&p$T(mfB#6RZQJ;+pVUpY@8!^)C$`l?UG^pIe2Q>id z^){uq0I07R-*)Fd3j{)R`@C&F2UE5_6~#!v1M+hmGXO|D0xby+#CSgO2Mjv9l$t43 z1^nqL1-phk2_tSLPs*$Fljkm1|9<2C95$)^%zB1|zJ826oUeU=^+dY$5S08uhl^cqUN zC*=YBL`NcI-s@}r?re0atseM67SA;E(Aw*-uD0F7TyYIx>^X-TyMI6T@?;lNwmOB! ziL!>&3fYzL19#rn>y{c%vt#KmJlR*YVeZRfq>_iK%zGJ6wz93B(pLZYNzMmye>846 zZH;aY|B_Dum|o*^Ech`Jq!c+J~ZNg=WSIR52!U0 zmo(lWYuo+XEN909-%GB=-k;l*U27}RA~!L6)@*gl0M|e$zklw@TI*k|_j$F;avP&( z&G=y5MwA#TqjuihzH79;n@888)rKf)p`V+0`$NI_M($+VD%J;`GY7r`&#|!MdDt9j zbE$3I*_B%yqgG#RIirM-0fE=;nD6}{Fi-}zE48@28kh`T3TO#uVcw$$9*QQ$pZep< zJSp=xz=XixE;8q3rHsFKntY0y(N%tpX%>@39{S|}?X3ps3QNK^*5A%a8vv8H)bobq z4dFl;Z3_%8HL-3B3~hkgUh>!_+L+tk1{ zYrC9K93^uGK-5xH*l`0mh57X0T4d7d^PUx{2%S-eq zg+5XSfL@fnKf8-C*wSfHvp)Dx-Fju7@*lhAX!WI4c1Si_W~k^(Wy;napZ_n|rqNIj z+(xtPbsJmmY}$fd46bBd$RAy{Be9&+Hg3x)58+fFF!l>CO;_X3zaW^KWfr%3CrE3f zm3`fdmLarJ^Ukbrs*gYFNVV$9E7KqPf+tz@BesURj=(mPept?UEqG%8FYaNEfGVZ_ zdiJHs7N>0ae*-jfDc4VMy;pDH}i1KM1oZpQk}7upizlJtoi zV=wBH*pTL@KA{*Zs}8OQE9*D2JA7N)uHP%JPjv=B*UdjhL0a!Uf)T^d2f{$G_2I2{ zyEnqF?*#`kw=LM}0gKpOd5PMU+roti`JRbP8=lx)+D0TI0PXE{HTr)(P7|lyYzvyl z?rqJR)2VK_`1TA@R_b#rZKIA`YhM(8mTBvl`2A#iTkSI3{SKEE84up#;>9)syH;mFOYYhMi zbEV`_JP@4&FjuI{bJnRpw)aM$oAvCAm{vTeoWHRG2VTb&7d^c)e;d0jExUhy{cd&U zOX)&AS8P;cpj_!&{22!(6;>RdrK;*C%~R zTYcB>7uv`h05|6U#{0nVC;9+8~<4X&aGHtcwjAsET8NI9h}a03*NTM97@kbJY$1&Qt!MUU5rx z(%SpG(}TovWv)=iq!I&Q*Ae709q*R01H%q!&BW=8i1-H^6V2)fsa@RGUKjuo(!_iKMu-#z82{rR86$+CM{f%XQ(TXvW=UJw?9RsJuXGG#H8tZx3LGkerFAUMvF)5yG zwbHxURRR(d^%kG@psjf|AP2>66ahVO&g!R4=s|M)e%%dNDGT7T1Z+}U;-@UV9bKvcjbk(outNs{;992yqH0O|vY>DQ#jZ7K&BeD%(l)5qWTSc3T5+Z|}x7`z5>dke-K0*v9#d$(F z)f3uq+x-D^39Z&Kg?ge~O|>eS-KlXv^hSp@=2dl|HOtbTRWtT0xs` zw{15b0ni43{XqEFZuRYv{FBt%5{rHD)>c)+YB#{cKHC#^Y)CTWI z`J3^i6)R^%_}obA?a=_5<3QV?+lo2+NU9a$9YkJ1NO(D(2J1h3djT#*q<;LbP|cQA=c|%b_tImg+=5op(|thBu<+hr}tT-vMcd`-}>;0`t{v^K>Qxn zjSK)T)Zo?N;vWXAd>S{mRARtDymmWdeha{7p{IaFvVvedxGf%Wm)gvs4ZJq~e#!t? zeDENp3jw(gj#t8$wVKkN|5bmzDGNtCmRwu?di}4sQwn7-0d1*q>NbjFU9jIzG797# zG%5E8#}l{Ny@N(2@asg|3GLcNEj#u&#VY^Yb^Gy|jWsPzyaUNGcz$LzJy@5+*d<65iFkx=1`MZz>w_S~L6|8>ijdklQ*(LDHB9m=_ z0dT{~*sv+yO*n0?Z49FL`Cy>{Oe2R3sO28sH(phjI2{`~poa;tzL&k{i zxnWZ9lyAo-5BgY}f7VHx=P*b#eBr6->XjE>Pxd}}#L0we$;ji3JnjeKIxRmqxIwt+ zmq)*_ne3LYq10Q_Hoy=4*!Z||_?t`(^UNHWv)Rezfz&*?#JR}KV;TR}vwu-{zW;Ov zhvYqS>X1*XeU>woDEYdzhPLFPCSn9i_?5G6d~ITm2X9L{KTrji*k)od;bqv|pJ|I> zE}m;_t2ZNW^K0E&shg_EE@qInJx=z}HtV|Yt~sbS;6=y)q`B2b)xbV2ps$pj?0*9w z^+_h48PR^bOX(HJ_{v>4PH(CWy8a$m(EUDm@uGBTSsD?dH~z) zIm(X1&!%yrB(X9f&X@s^3a>gIuwhodts=FFC+J6O9jNwO-X2*LdD1*>1K!3!(kB5V z17Hb~e;FDmsACSanhq%nF#hBpko*S|>O-d~98cn`Xf)@pPf6b7wx|Y+$(zl;h{zk~ zeJpR-W_8)#Fv&`(gP%B0J^f+YXiG79z1F54*y8)*Jk4*~T&BItiK6uxLR|9eHa~GG zG$bX=H71uwj0Kst=J^}*Cg$&??79r%D*D883zjzzSbZnEnzlKOd20i3fn=333bGpIeBV~^6C0k|!a{Y;J$ zfgyw9T1&e79>nw2t zL_a-e@6H7*USw?)vEI>fKHuJd)t{$y@v&I6cAn8&XIm*~LwBOaR)XdBV(^ zLt8jc{S%jX$-f`sH^^=dPo`xzm$up!ZF`>liDEXo zuKPhz0{}sf^P@Z{PzmM`0r3Q!d}_%wi81K^ITqlyq*35;;aqa;!%;uVhyYn=7JiTH z0K0dv=@JVyjd(B~0njz^5B$nr0qp6p|7E|+JBPi_VjokNY;=TcgU^#^;?#^b)QAVo z5w+N7`_Wq_e2c^;WQJfgvkOD!39XI(x#OX<0kFefTiWSHDh`KZ zU=;Q{ZN%_!{Zh*;p*G)km>VxuuGlN}ePGDD>)P~%M^iTcesCP5lqc;xjr~zbzfJ7<)4fZt#R49rD#K74fBMKS0H@q8_YHQjhGo_+lxHeG;LV;C~cx{Vwjs;x29#6 z(v7^_0<7|NrJ>Cm0M~xM7?N~`Ij*gAEqA|fMD6OZGNvm}RT4i?t_-8#W%7d{(9{a($mts%)G)?z45#pEH(=4h;3qU&rQUqoMA& zhAQnH_t}al^`b#{w2&rcc+cI8nYYD>pS0Qc6s<0hHI?R}no_gp%vC3zc3SY#il$_} zxJ!Wz0RCbTEl^6m@j*C_v%VLLsDZ3tQX_Tg~X=`%^z||)~=`FN{h_d`# zXiKQG%h3cIU=g#;gpyF*-+&8*$}FrcVu8s$%f-x^D|S=@Q!ZdUAq#W1gU`Y)d=ss|^5lJCEzdf6qgxw_kl< z@sxg_|K^3yFM|uS>#yvX@40c3nkN<4M>95M)9=~mg>`coF_Cj~g@SLWv8RMO8#c+`w3B!L~hya{^;)!ZNyS;8OwQhXet>kaUZRPSNrEbucQlen5>KS8_A zuUoP3JRCzJp=DT-Ph0ssEf}~&LgaRh7SRUEwl{dPB7bvdG~sQublq5Rgc$%J&3Mx! zOK;hSoUe#pO0^|CpR}l?_VJiw0c%V{YiS$tNIC*=(Bb=KEB_S-ETyjBaEvhFv3Zre zG?~70U%ypd^xr$9i@EcD+Z4Y6b~_9w#~%GMbGyzNW5^H##-#}`;pHQ@U3qVk$<7<{ z1+~qR>(H~5;v4CCiWvad&VhYq7s$f&<325&?|!J%9Ck5~KH50`yZY3|g23NZWhxi$i>Z63#3IEOY)KB2?V^T@21)fcDzE^YwqyA4W7 z_KW9^l<9N(Ft(zI0ETYX_!efPO5$3WSq2_oJ@taj>Ez>6%m@DE^j;@q2&S7t>bfuB`wtbxe z@ZF3}KM0{fxFh40z-!|08Hii*}PdtMo9*jTO z+>AXnTr0f=fD?*i9U-v`9*G(NMgN???duiRE!?DFr)7c>CAxb#NbBI}SfyDrl_5ZnrFwSu-0kM+y|D2n`Sd;*y? zGp^WN!=veWl4whfb=eh`Q6skBL_A6&R(y1)@gYsnlVHx1n(vKCW6wc5kIw+^Gmku{ zrcZk{3HMWmnF2@z5=rDMf1XbJ^+oE{H{Kv^>3fS-uqAQ+paRhZ3(NIMU(!~%Xdgb+UTa+O9bIbfSBTAu zeQ2^@!ZD6*mHVIRYC8s&;;bl>ObW*M%E+emWDqTSNEF{QZ%0{5Ta5OQw&6`007bU{ z2L7mDGLTX_Pgo(x*R9N0+4aEfBL))8TO!LIhJdFiwk8dwUVP?d^~|Hb5R9aPr*8Zm zn#R> z^+_!3*CNaX*_Cyv%mcFQ_8M(RY6IZLMx(Kt(XzO%B&#S*5;J@v7*75oiD33hPgpR* znCzS}ycf0w3=aL_WN?-!g%dqc0S9M6Bih`xp-yq(Fhd?~TJAcY_=%WnD=9SiZn^qk zb;|~0ZQ#Q6>%yXJ&U|#kur!`0qZh2%Rz_Q`rpgR}-9EX43tn zY3(%nQvueDvsx9#d-G*?rKU&~-v34?TFcM#q_}R3cIX@W!aOzhXFm=4)dm3dvgK@y z)PSWZz**V=upi3bY~Jh|_LMeJb{$(2^PcdLG*56{X?58}3dvcu<+2;fKaz=lgGKi8Tjo?zoTEhNl9&@}nO1Qx8p3FFpH8a?Yk~1ScZA zP!#h7CT%#=U8wEQJG5CTZ$h6q&f}*`@VZGd(I>R!=@X{%!iX%=V>s|b^)8AO zmC~l3uXSI3B6SapPin7R;W%we|60$`fqeA&cw0~ii0p9{4N8y;--W-KD`i1Z8B*fo zFZs>2<)7xjkS0c!u4(4FjXf=#gH@N3PXdHP%WgAm6l1Ehq=A4Ph=j5uPIJz?kVQBn!mZOtj^#5k}OAC_hlzG0PXB;GFZ=84a)G?No=%Y*M z=SsBk{&V@84=3UYi9E^eII#DCcQa+zEF_B@q`a71+Jev&BUZqrfTvg+t-~d>dER4O z67p^qVJ>kxTDNYrbc|!&3Owm`WxhTkyWtr;N%XO#Pmt{5XZbwAZ?(o{CIY}MXZX$) zgJH+Iir?*#FR(g646)^ezr$_0K5f>y)Rxo^9~bluA$^y*X(IvowE5@(r!8g`>gR?( zmG-QQB0uXL>+~boVAW_dAr6@~P`7@R;BDjo>Ze9L71qOt_1S6PZPcK}2L=%stP8ke zhX_+bM>=HI&)#tH?V*-r-%Q!y1bZ&S{J`faMw^>g#FLmW^?MIK`K0>Y<(H8jwIHmf z^8m0@U%tXlpmjq87WgN$47ZqW1#-;hg>u=vSpC{m1I60;DNv-?R6N+^d@Gk&7 z;zT4W5n@9o4B7+e(B`qB#oq^5Dl`10Q%b!}+wiC3N_EJ<<<(_FzDDj=X_~f-lUp9X z>DieqiM9mE=u-k+6JqEpuQp??x7v2RI^ zX~MB3eZjnsJw05nHI@+~0LhX1AEE!31Os5gah@K+-@+a4Ikc7WH|BLd{8q@Wi}JC? zKQgolF|t;y8;+y(#=1e9DOqV6mm+##zUmV;rlx6Y0p=ES-%4FsLR%zw7oIS*Oi^oG zdO}0Y1jd|*W-!P;L}x(bnTe<|)>fflaFG~GflDA{Ck8mG7oGq~3WEmhe!sN@-E)$J zp+3o6pmU@6eN5+wXTr1Px{DR-OR5{zJ))ep#AY+}8O#L~+L7p%*3CYjG63{zqYv7d z-g@H@14ERY3Fx8#_EDFAV(0d{f8CZ;sqM8k^}tXQDT&f8wSeaWGM5wUHYQ>K;Wx z^q?qR?dwCdgtlgJ31~wY7sHa&T#LBG+9s#468POGn9J#|s!t@jmUKZg`XmE$$w?Ti z#G|?b%2-B>hC?o?H4c@7&Id4L_LB11JJ#iJ%2y z+bLm|xwE+Z8A-`{GZ2>gppY3lD_sD%P^SV~lF`=k`TzhR07*naR3Z%YG8>wrsOM?; z)1d*d?tm54C2Jo_GwAyPZNnqR3eZ3L;E%iYFaBV+mBReN^EldGth=2+9%{S(-?ypm z?(~XHS?_q$w{0BrWYu*uO<*4I>o)d`aJ_T{!1Wz5U`VkYJ4TliqLg|`FaY-6ZhJL) zyX_sLdj6vP4g8Efw3sEIq77shGhUBm7l~N2vYVn!mS9MZmLm2-yRfL}t}*1;4c1efU1F&O)Gwjam;Ts)J+l1FS8ij!9wO&)fPH>&EYN+AQ zo=&EBgMlllf39_iQ9&paj-6>9adDp_vKnwmWHShN?-ZVlw7Wt0bC{hd1Zbqwr!jQs^&Fa4x80Ep3x zF2sR60F#3eugRlt1(dD_KI@p{tZ`vD01PE6KRNvERWRoP&XYEbf~;}R-yjPTe?!K3 zRoTV+>}HOCRcxg$(y6CA}%eW4fB9k6ER%MTQ}fnmcxJ}P5n@x5dQ9E z%%#{J$0g!E7tk33POs>b#>K~(prCL88L=QNMqw`{hU(<_A*-MU{D5?KQaRvboy6X( zFdmE*#S$PQY(zA`m0a3JO!Eq5$bjY5rE4CVb-@hGIgE$Tnboh!xe4Z@gf?!h@W%K4 z+^+2S1@rr2;U&AMZW{nKe9hl)DWzfRP@^PMbKa_T8zUJ2%PQ4|Wxx)E@H!%nSkNe< z^n$bvU=4sOv^n1JXQIVEv_A6#_4diMi*gU`}~l+womQwoWP=gD@6H4a{Q zCnR;k(Z}`~ZCG}_y&}vLG32}~#bwvz+(RUY8hYjO#-ER4iL`DIIq|rZq>XGW=FkS~ zF5nW`_sNspByAQOWBg6Irv!76XT5M?ExZ|2y<~40 zgXK$i>;0(Wsa8$U<}SF!9*%^X%~FOvEB;0DQq*3qd}-dtej%5Dm%{MnswSfBf>hs?RSoB8V#(U#~iCcsO7e zOFS@U{4W9$*Irhb5oX|o(JcCoz7%NIys(`K<8&0jOj<5&SXSuC-X{3Fcm;d$evICK z;PH>DvO9liOW{%&+o?)hbF$mxbu;elBFqgka8W&cQYRt=s*t}iG8nm_@o4aoTZg&1 zxCFTExy|!wYjvfkt=9PGF`=J|ybcEoWaAU1;p6W)_94V1(@Axxf2&GcvGG7_v9>ic=R^wJr=8U=jyyw#{CNeZsBcq^qN!4t`-Fi@M5pTvomR{5FWeF4$%i3 z@5`pm#ztMAcsxKGl2in-y+<3qM23#XYm0E*K%>C8+!4HvZQHj5dg#0-v51=prD1Uo-gC0Zv#_N_#o0naBt0h@fuoBRj*nD|o zMr`8)B(7izTh0&19L!ammTiM;>!g=54@73lHX2lil9b zm7zYVHU4q5OYlu1w*4^{4VG1Py2Vsvxh&io!lq<)@Wip^#={*b9xRnwZ3S&*?czS9 z{_y+o`(-m8Q76B3eRLvpHNcftU7^q;c+ZLCjj^}sZw+lc;W)x`YUX{h!KD>M-z{{nRS!a68bK5O@{Eck|$#{ENrx;<~=p% zIXs!OW|+TXV~IUeh93(8ZCrL!Y~D)#&Y7EB%&VoY7XZKbH(!32n(^MuWcyRsvdTZ;nT&Ny z(gum@NHhgr8uQDLejNdjlzl4k6o`b-y&jr20Pw8oXBJ;aMZY*q4{U3@-@<7Hva65-u}>5 zN_KN+rj_#6%CwvmaYr?I75h^4} zkTB#~$pH8kG5`qgAS>UEUEqP3yh*alt3pcAuONcP&^+L8?wX)zK&5r-A#K4tNLYes z3FcaoH*UtYkhWkg@Om@LL8NUF=2G^Rtl6sir0+4;S5`!pdei!(Dt}`%NAss|18n?l zoXM(jkk2O=Np~R^{)RWA)J5>ZZ820W_!?A+{VycNNZ}0kIv)=^VT}#(6u|A1b zYnC>q5-Zat%##}?S34+KQJkk#TV~F;n*K{Qw;^0$Z|7p4&+V!P4jLGq1}I)f372}k z(6+#3zrQwV01T?Nt9w^J(m%r!F5&QP<#hv_f{rIBZ)tg>tGinreeN%Vv97zkJp5#U zLRcQj02qJd7}ZvUimUQuHQEB1!u-wU4Ie)sq(@{V)mBsT)Zl(~oACDnK^tB>K2L0Jtn%qr4b?GyLQ+|O z@+*4{-ij&0 zEzD+o!br{lG6bf*t5kP4eY&kyQ{#^q;a2fCH&qZ%J9%T^QVbJi2NQQom}|G! zDao$we}2&xGpR}adP_uXancH@e*nba9s2PyH42FuwFO`L?A?V8CTGcp!9GGdp2!6 zl7QNG<&}C!8_}S6{Fc}(+F-5FJURS8#0EhfI^V;mpOrQM{%>!i8qnn~z5uitFZ?!p z<{Wj+Khib-KfmM%b^NkBn?4=SFlKjh)ic^$`+VKdJjDzEq7MZf=afFvwVchKek+{b z2}juN4}Ii~U>aLDyi_C+L5TjO_#5eOaYLsm*_Gt20#BxBgUL^<8`-N2umk~fZ_-A* zsVcH7U!^VPYyhviqkegVGk-BR2ba?PjQ}ai-_>-bH2`S6dYTYGB$jJH zp(2o=*u_0bG*G7%ENBu-o_j1D@rb2>EeZ2CwxWrpgRO3<4Q}N*v|-gHBs%&(ZNpzq zp9J{Sq5B{MKv2{G)~cZ{`OP&+V?zHpb#=W59#|!epAdt$WZf`7;q_E%%sHU}w4UAq zh}e@cu6ENvn^$oIAQl3KDa@6(IBkA0GTVyot2-?Oov}lY!mKnUyAl3s2@fP>aS<*d zR>50Si!fKtS!=OAA^eTypX-|5(o)rJU#r)~2JHeAZ z)+Zt6)*62vYf%VU8O5JM{Iie&p2UQ$6KD#Dx}>Bha42GJA@EFk0T_WZt=5k`ZGOFm zzmkpueEMtq(r5>(&ZrH5i+_D})QEr|rmlv)f2^pae3FQk&006ki`-!H{3#g#>mvgI z+u+V7VItwSjI~%W`}vu-lB4;na7o%YplviXvXRL=iALWTb=SRs{VSL%txq1RAt#k+59ouQape?QYVx0y2-qRjFl)Z^^i6-oZ2G!C{=0WiMRq!F2Y=zS?oNEX81cHyG~bD(IwFR_DS(q@07Sra&)I-{csx6_7 z%qM=4(l-3nbOhj_!}g1^hW7sdePP-F7+9;T`&T)N83|E~$v&4s8<|tIZlXCN@lit^ zeZkqPtD7E$Ty1HkmK-PygSE@0MeJ57*#P(k)B}*bcyuyWI=K=!nlf#2R=>pGz0H%j z{r*fFw@%o&#X`=_(uOTFVg5zlwiqV`8b~2(Y>~v~n_C4vgL(c#PpY1W8GENwtI}U7s>j|ob(I-y5P#_d+@{5RqV09sPO{I7JD$rNW*mj zlFB{S4sMI`_m%hErEZvfKWTMfT`9N}DCXR8)qwL#NiJO@W&l7Tq^kUl6o(sJ6a0-0 z`rw3tbQumzkzj0y7+~kTcsxV?F;BLbzy0FsxlYl>X~fYsPD8Wn)>_#mw2?fDj5%-F zx-G(783cPmF%p_KckM=0SP*Yw2x(|M})8&<>Dc zihrExTEXv9*=V)W!x=f;RCaLNoH# z$GAkC9%~4-43{`h_AzY>G3J)xQcyL}v0@HSR?{c7#vjH$r)sq!#-L_aMQ~0rbMG8H z73>lbi;Y=+JjZ}%TRD#iCIBf$H%8;x`48#?XbW67_KtWi(+U>%RC zduo!l;jgC?0S`E2v|4=dV$SjW`?(p)e`TxSf_3X#+Q?qGz>{MxkPQH`%YqzCuJ&9W z6S4Wv*ampLb*IKsib6+2-m+VH&h1eeHB z4V;ZGVGsvch9~`vHYqw(hD%=iP|LKpFc(KX`>j4fxD?eVNQM?3ee!&*@%wQ%s#;?= z@kVhRZj|<(Ndk)C#b6eTwgr;z%vtkR;AUZzMJ^Zi26#dYv4$zXlTd0YZQQELZL7b~ z#-k*Du64!QjuIo_vqv0Ih;es!BhUL^Idb!z$OCS%5d$TnIzbz8+WaXHL5nM;Bm-ch z75Unt>krsZ&LD`Kdev<8*c;(X2getU^c_DpH8$UkADmXSt+wj-Z+W?`&qwzV9uqsbpP)r-KajqDTBb_wZBRntgA~g(2PA0FYE1 z>5I*R%LBl=(2n()x0RYJ+`Ba4Ff>m>V5_IJkuYMmydlcTJII@t+DduZeMuYUfhaCv ze89#h|K9$_6B%*RGUq{&ClRxh@g(s6BFwdhWPi{ny@R=4$s-ryDjSx^w{l5}Nd~~iJOaRWS_+KkN{u_!OqWPefC-23zgX!vjgpqMVeXsm~@W55ryFsyh~*aj#J0L)*Kyp^n5 zoHog0M~WsU&wM=C6UDX!ObFgLj_&o^3Cxi3Y~hJ} zUu=&MM`rZ9QgjU;>2uGs8OHA+<1!czx710-rhQu2hs~l5w-p-?XAG&h5Q#Vlt|815 z_NZX*Qq06f>(Yy~_1FOTm^AqcfWSKgAe;nNRI(3Zj_ zjwK5dZLko%h)dzlv#`~a%LDxkbLTg0wg1Bg0F^2sLz$pcg#s&Zzrg(Zha2If3yJ* z`{3OGZXY|+nSlrIJR(E^CVaIl4}c?9&hk{5|Grp@vhkGt-s~8geXndhMB6uslRcI5 zW@#(qpEBB;?YFGm>atr#N%(u&7(RNmQ3IgxyzL~BeNXmXu;namh9WmU*h7^pILT9l z@u0ma@RZ&8*VINVr==O%VwvU^<%4;`8Df^9fbV!Dge%K_K-=&)t8W90U1CRd$e@uv zGY*y>8v-fXxQ+|v$u=0A&t+Auur;{^(ku^PyqER^6Fe~HH`M6S%e=|TzEpqqWY`CZQGlXlugy^EjhSI<6Al#& zC@F$b@Khi1WDb8zvP0IPWS3BqH=Y-eE4x+XEr&Kdwq|*f;03$)p$e9Kq%fDsN0oW1 z!UJ3kvAp%#y3HrqP0&{RDi{F2ZqX@#s%W|B`F=M8!{5$}0@PKTnzX#W)Mml{jQMRy z?`GNnkgy0<{Bydl45U!B_QBT#1K|I1BPa{@JTDskq8V5;1O00TYF~vUKx_bDi&q*i z-_s`>r0fN$E&e`!<^QqwC2)2XRr=?4r?ZD7Y_bTV2s#eRrsMv1XBF91+{RIKTm}L` zWYmBWA_yoDTtEeNM#ptT+(2<<2Sq^K71;?OA_>{kN#Fn6`)<|y>eM+^b>Dma`gQl~ z-!H#(zgM@a&Z$%9`_8H5N_FvQD5OJ(xbRrA_q!(JM3zlLG$9&FB>=b1FMzA6pzgRy zHo)t^zSXe-gUq2w4|K8M^D+UzF7l|}dH)Ps2MF;2kpTQ#^~&Vfyfb~SJKv=F6{;(w z>)5zS(Q0VEC)#9LET*57xjR;}Nqz}s9}C%9wE>YCv(=_Lb2nw&Oq=5kPdaJewyw_k z-0W>l0`MKmsrla5I+NC!oq~62tFB6FtTr}TY=4wl*HV8vS(l9+UGqrpmo&SWo|1{C zU)KobwAZ8I7Xs&9+fQa9KEBZ#r z8b}0SuO~i*{`DmX!~xE!ywiH>ZJU(FEow@hUt&+xo^PDpunk>F9U8516Vl5`ev%_A z%-z1vg%R!axb*;@Wc1za=yP9H_9Xx0!X0R8eh()Z;=2{lX0(VK{RwB?0zQaN7VG99 zk`i%htXt|2j~2D@jzu^rvPo=~hnunJb+lxw>fPhf@G?Ln`iH2D%Z<*5kJT8<=J3Ou zXu%hJSqlPy2HEz6qk(O&Y#I{|(#y&m|@EDO*L`<5_Y8#e*_9A6>LckU;c zF2oluX!EO;x{Q5tKZ3r5&pQs{q5g1rc)g)M^Sz}r|FS=jVRTv^I*+-9LmOpk_f==h5#tz>fo~mB8)_uY{4xL_?0d#2!Q!%W8A!& zJ5g7!&bPj7Dc)5s(*M z09te=^+sct7-35cCdRVy`%d(SGx3=57h1s(Ayi1p3_Wl`KJ&0BH8qf^1?{LOmPr zzn|7#dmT;Rb1xd&W#^O`(pekmv~1EmspCt=*e2Bh==rZKqu=;Xf9f$ll|#rUD1vw6oCHhvBD<+x6ZAxW4)uY)uFwb zoN^rt{>zuM$N+eA8^V5DQw1tmR=OP;dn6Hn`LpKG;vsS7Ayw@xfJ ze_@TWR=dTHO~g7h+TVB5COcVIC*z!LU$(FYI;ra#b!Gp~GXNJ>?*q^xcyDcr zg+Zm~W07I%dM|&_)P+$SuZyKvn;x6xx1=>SsV);Wl;25$jAPkQC5f_qXXEeblr=xJ z##@5F$3PVuNEtV=E(pD(@r~L*{c)2Bz#hfjJj`zJk~j(EwUq!q`-_W*ZckZV664ZIeWiyqJ zf?w+UT*vfW41fuoVgsGVlNNk#+M0i7e6CCYtV`%b#AVjBYt$8DFrjLs=hX5w0FXd$ zzxoq$k!3YC>guR$T4YtDKQU&WWwghN21pE0MmKK;VBkF)Xt(ybbs=hmu~_imeg{e`<_tLKLv}sw-*TuWpXCH&EL5{DsFvq_4hC zXjg8qv$Dm^+Hw|5@j4F){b_aXI)RPRlX9f7W&JUU$J8%r$2Dt=@g(=Tjmy4Le%kZ7 z**l~R05@0@4NFWD`zU4&B&K2(QLBeT(SjF=1Cx$1-)Mi#xmD;-t@X8T`BxB`sW7se;{F0Tj6|9W=+@ELfJohCA=^FvV`Nuq-hNj%>J`1h$wKvWTV_1oX z=F}ByLyWoH$VSqZ*k1)1C$nrV>j^*rnQG7uSVTJ7HoaJvDaRw6RFU z%HfSWWXFE|K8FC5zUfoiT>#ar@YW|-RGJc`rn2d1UHGat6IlqCwHpttvf2qO8@@K# z;XWDHOEbp zO`PVf*rXO2$LcEOVDqG-vh{tgGseA!C*#29X76gG|8)W&t#P2%z3(MYSXHk@ho0BO zNh2|wz|7O?3a!;>Ko*pZrI%{U2F}GT_JMh*nQ|B(sg;wUSb!nq$lVc@Y-{)2t zsp_;DpCzg<>HAzqRfYQAtrCERx!r|my;n7vI6m18Mi6l!8^g$>90Os)9fnf&RlZe2 zJaTb1G)Z+;P6qo`I%TL2f43o>5X5vtHX9{Cw5RH+P1zjY!Zw+hDACbVg=VdeTP@iP z6~$Y2YK&W(vMuBu)|JG2|u;4%DfmB zB=xOu0pLqQt&85I-pm$FR&zvTH$Gj0<=wpxB^;`&^j0DPjl3jh~8 zEaMfTC@+TqWh`R_r{KLjK98I&hu}gY6#THZ!vX~-XaLQ)uCB{w84o)*t+7-d5h1E@ zU9-W^WW(VA_wTMbap=Q{iV;E;CE=unaVlnEoVVMNaSLRjoV7W(!wddL-UBdD6mvp; z(7g#lJlXvS+>DYnJpkihNc#X@XYK=V5VBEmR8RHBZDjRoAOS4xUo8QY>;EPjKm#=A zC%4eR6Q1bOn}Pt8Kax#?U5Yl@+PF1OH?TH6pCt8*4=gey=#Nv^Xv&s2cUfXX)_I$< zS>MQ(k30iT9k;QTt?zT)W!sFhcb{V&h2t^uf&i4`rS0&95C``I;EpH86V$rAwMk%H z#@X4i6QWl9&VZV6^gJ4J=67KCs<2+_^uJ}*>KnCg!8!pJhA>R(Z@;t&DdWnc!IVx27bvg z*E+H#_}l;iIN@K3@-GOXhH^;T@lpMkLz&#u(D2F zUM{!lB-bbRb_h*U9TQbHWd9C4TLmrnvY!Fibqk`o(02U^}IM(qvQv)HXdn65k^!tK~GXd*K4wQXXd1F8%^1SNQ~L! z`@VSrty%BhhrjcDqRnTr^Zhud07J^DESWowDZ?C7)#_oQ-#p|4_R^z@Wq^nsqb*xo z6)r8{^G%t%`GyU&;=)f?>;Dk_+w_0Y>p_}(`y*wCH{Eg*tv>vf)e*q~%7&)WoMR6I ze#@1@92r+y`8NQilp(ZZ0Cwao4Jz3ZpJObDHBh?ktO{^|0SKSM;q5kWP8|oxHhG+fYOLYIE4?8Hl<2<5m zX2D05c_G+4T++vI=MzRb9h|#C$O4oEp#0%;#8k_`(mApwMVnGxe2(Na^4Lmx>>+2s^pg0i6>(CiykT*sG4ec}3%=3}qS zU4PtMTjBj2PqM{qWxUIl?w9=e-x-^X6M*w0Zw2(2ENk++ z?>w%I1vegf_ZYAW$3!;GVZ&YwNl$jiaa}!`sJI*B7LX0ss>*p`7&nevo3fRUhi&pR zSACm)x9kqb5Vo5`w8LDsoNRng)m02e90~=0a5~A&x2WTKcJ)?KX6b9;y#U-MB|VjW z3+!#NcnW9K`TBVIJW;-W9j*9uu^iCI0u(^sI2BNVy>vf-LlOpPvlGoLqYB4EUv5;}()lh*tYPm%W@0$1TC<^7H)YOSCn4X@WbMuCe#I z*|E(HfZ)=aJxzVK%6-|?&pGvAa8 zgsgOF^KE3+N_y;bU$*xEn416_k$!bFu(}(-5CcmRDuOWQJqIiM0r=dOB3dQu*pgpT z+8cHI#;k-EwS&R5B%6}G>&vFh9h|j1i#9p%Bs(^?1h91B!<5mM9)ZNQM28)l1g9Yl zc&;73GtU^~46gC<{Q=-hOvk+b7?Qw`ukffxpvJ0fU@+QRD({c9$APcQ@VyI6O^qW? zg6>Sj;OMfFo0nJh#3F61i~k;!4M-VwwIj0Oa&X{AoeqrK6~DiczJJ@bj#12?LG+X@ z+^w*syO4Z3_+ajLP0--arRK5uk(@}{47l_))jj|m1B(|B%}8tD_8Fr`&|x|J7)@4% z%^G|KL?f%$(8`OyLPa!CoB(iA{pM!Ca)Ds5d2c{juK6ONIVZkKm~27{Mce-~O0tP_ zE9RFhUI@BFDK(pv=B_s1qI0+Jb4wN3y=iFQ5X^MkFIDZBIW*pUZccyC7M!ycnk$E% z;yU0b(J=kh-2%lUL`SpXoxk4UVw+L!c_Y$2c9il6L+Y?IXI z4$;opyUumiJ0(p}eqMDFK#F=XQb4=qCk6P9-lVz=!(vQJ+8u0t1!H{3)HPNwea`n1 zXtI^(oQ1HDM=PG5dPFvlH-N73HnXyva66xeNBGxH@+%{{?jFB_j~x1LnlW`6EXBbc`c;(P z=p6EIh)z2+-$quhqLrV&q}of-d^=#{-hjqNz{V|tuwk(Jt~^ed4btp;e%BQ6{1f1< zf?b}w&ikg#;4=Lgog%jLNYiFffE{WXJG++5vN2MoFnPZ5x*TUE8!pPgci3aQj%-S{ z!)9zWn-n7Qc4X`OT)1Zg{8-I#%NDrn{}O#|F#q$4lK@iGmKa?c2i0()%)4<$h>^T0 z$|jaK;W%UGkpruTJ4{hFXU4NWgABbUTVurht60tFRuUnU<>NE1WRq+lU`vW@x7_<@ z`oi_!bH=;41Zekdni)p?(wPC(6bVK59{3AW;@1xnt=iy|fTgc4vH;q+K}DWyot?YI z5y8hU_^NX?FkTbP_XyY*3Qd_xbME|&E7;N#0;dE7OrNNa+ejdDNA z);68DqdAMV5IRbn1)XfyWrKYUC|@UJv)m~bJ(_U~>4`CaO;5&N?7R{ZQMF}LbcAgQ z{kd(~U+Lm&%?oWB9hxzPXpe21iq_1*fh*zIe)&E6f0m@c=dTsA085WFuLq9FCS^z1 ze8eM#o8L|Mnj5x{RUcj;WvQ< zG~XC#mIE69XEf*DKY6gXdiy5L9iDu=$4)kz@laQd@+LR4#Ka72vJBLErZq&AA zV{2^ar^_}Ay^eR;*ba5$=rq~5&6PS)PEAW`Gnfq70M00-{&Jlf}}&z4VwB*>;_;XL~`_gBT|+sI?9Y1QYy-@GS)T@+;A zA24O_y=cazU$!l++I|mvsnD$VzPR#Pt&iJi$mV4S+t5?_sI*6;qvOW1tlaO8nqNwg zZQS~$i78ulydV85Ua{g7=a?G-!JCyt1H7LTm9ey*dW)I$;Ei8!(@psSXEPS5-TeeX zrmlHt{2XSBB*$3F=1gSHjAODf89ON(*465`1!Z$QZsc?c1d?boHOK8w58X|ly4ocG zQwNEjxn0E>5fcT~a-SRe+gWik)2gcf(X~Xs{hR;v(j(wjz?!nz+QjE@9!f$gbA^aL zcjg!A$mug0SpjoNQ1!k5`yK(a5peE2${$?`uNQ7CAvv1?3YRp|LIDba* zC7w-6eu?=$XG0pw3qF&?dSUX~2osEwN03^7EI$!sv;LGtH8I(Y@j7}^ z&c2I`z87U_dv@cLmobJ2w~XK;|-vd!b*ErRTXfFT4XLjYF>n|%bc zj(;xE(4c!Nz{@yt`)2!!J27`PUJ#qRDZYgJ1t;U!g}K}Jxy?ys@MIGD-0TE@1E8Uw zzSk)*`C}{tATnMp+7zmk*dvejC1+}?!tcboI71i3Qa+SCK#FXkb*q%;n}8u+m(GtZ z1rL{L@^(SCkgB8O#xp)09yi&#?Rw*Hm)$|1z0TbOFnvlR1K|A#_UY`ALR(cHXVl0U z(W@i^a5z5+AVgD6TT-Olstx$|x#Ybcx2XHcI1r|4UxHP;e<18aKRA!>DS#*Fo9^QaU3k=k|U^JvM&&h)10 z$wnu_-UpQnL^2C8d9^Xtp3|a_R9(mRXSCX1sQ0#Ix6{Sfvr_;=Y!jgIITgMHO&4r5 z2GyJ|TqzKMSyN}w>92f;d@pe;^R3PtV)Y~Vo4(H&7^Jsv^Y{h_Z}jg0*sItBkP^6o*xqDP zmIp9k!PSxCT3~S*;IL{LU<_5f!fuwJahcGR zy|$sLFW+GdCG0od*TBToK=vLv`Y)u76MzsKDr*p`DA!uxJ1JU4hLl=QQ7#mI@pgF| zyq%87=ALSD**m1SxtdI0rCYfqJ5*&WmE!6-ifx< zDcP#N-hA&L>A!w?nWORICcr(n5l$tsaY>2rgPXotwxIa)%Deo*Us@1=I^%}p;GINK z?8ScU&)utkdJ|oF#SN-=Ty*9!l&>GA)ffEOzDl@}5hxr? zbEj1|0al-CK##lnOMip+XYyrfG;cNqu<@S@1mL_wkFR#oLx-#lvw5M-1##R&zV>%@?k2rvB|ELGA@X&HVt4=-=EO;DEpL!l41mw%?kj{knKf zapPzG5WR#-0k~Z3nAR+OFlp>GckQ` zc4Dg;fU_3fMj1U_#bynUG{1tAG9`Qp;hm|gP9Fpe>X>XeN&B+NqXGd>tHb8q;@qaL zfAFUp=<+{Y=X7VwneZ|ogSoK&@0ebo{nGzOmI$%oqI|IrpjZlUKhLNj0kwXdb;?+- zPp>%*{F~3`Yu3~1PhV9w=%xPIodM;01ggCOCOd%H0we@OpM5V49%haX60P6-2%A-C zY(T8q%LZuh<#TB8*~KwM<>#0W6Mg&xjW5wAYcBbCwfCTXmwlATXf0!s@0zJwMW^Sk zbD?CLe!%Lfwougr9hz?*9<^_izscTv&NDQzqH4>FEPxkn;7voU3S_d-S$L=}J7FSqbxJlqK~+6w_*e3xYm>H z8^8VuU32HJo%ylN9HLz}XRrTNW{?5-au}ntsZoi^7I==i((BKpWq?D?GC*Ly8Cy!S z?}(?%h(h3h@M~T{Pv7Gy{yiQU(a7?(wD$AYxyu1%L|-il!0UzWtA^q9Km+)z>_GMT z^hdtyba?&zBiw6=@wj36Vj`%cT4os7ZHNYM_++!5k5(@x673tWN3IIAVw2^V*0V`3 zo8`=}F8L)M(2S;E>igWvF#wt9kk1W8|L87MZ;+pruo19Mn3GaArHpa*;#yCQo$(Pr zSSy4}>31rlQ`gESuxvOMDY^`eUE=c>-}qg+?Sa2IHSI8$=t=VeQF9=LZ+$gHU3STp z&p04bh%G<6(-&;%A+Qk;WsBKUi{GDl$^|q$yiwKgqBBhDpB1V4HnL(Zt@+#y_B{a} zsF#s{V{d@X0u-kP%-(^LBs5<)>=S?^tM7~U2dfQ;g%;sm1#Is@(bmxF3vGut&N-fF zV@}yt84bKXqipjCgG;n2b)~@>w;HJ-X%tfX67rL+%s0)RoCvtrWJ}C|O+cTUy;o2F zrxAcN12+POqBbewxUXo4HQz(NX=?k+Yy)GL@Jhf~f z61537aNgBlr91zz%+c7B<`eBWSA7$Pr(J0;fD^?jDOj65VEd$gDiDCz?D2AX-cHZ( zJEdh~+OUCT_dP_P`_$KU@IU<+cGHoNl_=ugRqJW(XKyTz0hUJs&CP)oxEruw`MH45&X2i3Kw;KvSq4_yc=B_~y%(_h!*(Qk3%}#2c02H~m zS0>%l5sRjK_4`f(o>!ONaMi!n5w4^%eL@4=h3DdB0M76Z7ZU!X2a60+1)RUAtqBWqA%CGrP&4K z!OIBSh>GYZ(lWrSfB>Y-w-&}N3jQtF_lA{gY51aB0PrhthP?pIBZ3C-n*_mLFl3Jm zLe$T$8ZKW^Y%U~%%tCQX;m~8BBpPTA%ElQD0?@dy2(uJGE}(vsrN}leYFgpoS&a-w zx$ofrHcm_-q7UES{2u2CjIXzWE#^zm2XH&I(W>HKC0j|IZ=tz6*0Qx=-_E|99`;Mt zfLfSuM#k(v!_mJn$0#|>Ka#KyAX-bmKwksBr-23EImheNGq)w0Gp+hq0C6Rw9~7>v zrh3rv^}PP-%bzovE*by;AOJ~3K~&t~SGN=iz$mCP1pdW_zk`q5r{z|!`}Vc8#~lSa zHlqG=*FL{C*hckLL=J2C&JuwIY>3=pxqxphw6_#)J(s53d4`o?(~K&UAT2+bn+)b9PMb27&f5P!MrFQz{Om8#iWMtWpR;I5weBxPeSE$t z<95ymFQj+O-@B3ax8PqiX*T~=?-VE__-2&PuNby#{N*cWo6&kB5@#s>8`2C2o%hMT&gcj*fyCpkWSF3%%=Q`P+PS!P%W$T2VoNob_ZgI*6>T=YmtJ1f zJgv|qBR0KXw7<(1l>OlyO>GQ(w0W74r=x1L&$zcG?bUmzJsarz+rl%E0j0U*OqiNY<@R5An@W;tuiP`O=~kFe-aU0zl|sY~jw(WF5DQ&-Ef z!O--oYlR|AnT}P-9I!6J9W6&iI??zufuX zJT8cr2bt7=^HL&~Rj}!O7wpYR0!tLk`o75~oVO3rF?&-p`Y-B5SIV5?>G$awG%<1> zOTRRceeO7rtwTOHdw)Fo=dL6@CGijgX->8ZI6Hx+Em-AIrYG2kw|kk z1ksUv&YCf2ERSQ3%H_M+i|=@UCY)@7o-|$K&b4tdZgp%RqXplCGu@S4yzEKt;8BR}nk64^~+Nd#bB;nHW-_QxJgr-7HGqFEx!# z^63(P$InpT0sBrmxyJoH4kh3J)z-Lm(dSaWDSQ99#g5zP55jGAMT~FiB8lNi$pb<`?953L;HGK@O^&?pv(Xmw;q@w{2qW369Xi*>@6dc zro1M}4}~ni(gW+>2f*_YIbCFXQ zPhSfs4Vw1?G){fW*9G(U6L>EuvhO&+ej86apFYv=LEq_-cv$EpI`BkqjrCle_3Sx;- z1p8ic^cZEC;-QEHWx?{Cy{4?2d;MYSxl(_m5tU?+`e{Cs#?3e{UYC}+QR~viEnov^ zom?lPYzca@^<=c*`+f%C#a?NzT=)<2#uWdWD9lKkV|mq>UYMAk3xS8kaj{(9h0rPhqhf^cT%=;xD=Koh0%6+ zd6XKLO(FoVKmwqVj>>tP2wu8IoW~_)H=&DmAE^|;R5D2f;5j?&Mn^q;->NQ-B9-0( z_MHU&xW*P-bLKjS&4h2h13l%J`m)K?GCto%j{q!QJcXt_ zHr(*dg4gmQvVb%G+h1urt;Jf~vkyUB)jt&bdcqlh(KR=7$tLwJcj~7r6j& zDg9BA5kbPdOSc+SV{_8|4(zeU6ICOUts1n>b6Rk@zXb4#og*)BtFgmh%>)293B5(| z(RD;O3irt@J;3Y(V9Es9q@#%;@IUOx{pe}C?NTYcyL0!FuU}5T|K07f&nq?se#o3g zsB4qv8b=1yp?g9w^HDgvWrLJ|WggM;=0!#g`%*`@(b#F#?tku=JexH3HU_fUxzeHe zX6ozvT;N!B@T9H{uyNbC*-*B4vHlh>Hp3zo?9a!1$q9L5AyZ<$()>l?*)t*&mdXpP z$C*^-kCZhotv`lwA|pWL91Gb@`w~m3)1SUy?d1mvo0HDscYnI0~X=fxj<=DgP+PG}|nVymj!^~=oYROiOz9_Ts zeuA!!TZ(M3E6{XGEt6d8c(2XVy%eC`*}XM(AVTNNTui&cvRV84aU9fyi+ zmZ(=Yxn!|^l^>2q1#$u_`ci(tcq^SMHHyo)|JJ7u&9z^C6nL=Vl` zPMKirb}iU!v-wtcyf0hl^CL}$OE0&Un~J~Ft8 z9@zd5^zfVqXvN$IY3=ltw03Zv_+ld4M)eDqclxI3G;`e?+HCnYwAIROX`B0=O4+6< z%?pM~q1bqP(K9GbjnZA|V_>`JgP{`&F`H$8dV)zO9r$&+h{z3QkrOTWp-%wzEiwQC zu34+Du?GO*###Esp%%6rpmvIj@g9CL(afFYRcDRMVi0OfMVj!=XFzQD1aeHW`ckTI zhvhrbZZ{o7gBzzdZ5#AYjrax@1{QRfa(=}KBGU!JJ z1~Pj8$;Hx%Hh0n9s`ryv`xXG-8cB!dZks;0(fjVnwt8?1o%pmD(giy`qo@3LN@qTI z>yPND`~O7K3m@OEO}5~3>qh_Rn^NSnW%3Uh0uYKYH5My4eqh2E@3Ch_sN(rbPR=}t5(o-9YXSV7agzFggg|lp@{KQ5&DbWl zpH}+Q?4w8kp4=3uE<-j!E?U24#*QYoC0hG(qT!i7$n!{Do4Ba}Z7{(Dc=G`W9V84X zCcU)rBFr!f7h2E{>+2ulI>uzbyAr_y0G(x*VChy z7cXkTwZuxlJftkEb(1sAV`umbjGI9Xr!E9+0fU_#_JR$Rk7x#t847TmHYvjU$?IbM z!G7C$Vs_F>dU9ox2*7@oPq!IdmJmRkAid=T%0t^PKgdx(Wq2X!tiYwmTs2*%`$!YqYfET{-S@fcpJO{LhJN{eq zOGRZ%P7UuvD9S841(#nK-E(Y`-=bJ^?%KK38QGlEF-F(UUaL;c-Hhh%{|;I{WZtWt z?(-&F$-|c2PT#!gd%lln?&5CD4X$T5#r{G3F7B@>BYQ znBj}+D2@{juWeX@*nIVt^z5I#i9#e`!EtY)Idf)J`x1=ZwPuqec}}M<(Jv)3f|TIj zj!oia5GnTEk@?03UCLTu+BsE^-Fd{13Ro;N%#ElVz#50pMgeqE2R0n zUD~X?m~lX-3%_GsxJK{oVbIcB;AF&I?jhNXN^M#J_Cny=D$#-~{8IpB27m#Xo1BJS z92nsb5k2~*#w+UaM*=xszgS+0^sQ!C#VTQ7GujRtg=7luhBui;w z?v~X^BdXOUz(Xb5j{Cfo?wv8GBYvx2NxBAJ{_s8YqwBs^J>ZU8wz%Hz|B(Ml?$O8x zPsryV_zqgpPxB<}teXXw{qQ2%cI_&c6gbf^0HwqJ8S>anJ?wsy>cMCR^XKSG3^Cwj z!#XIAyyn$=?DOJJhh$);fH~5DY#ITO*7DJPmTr6&(LauA^h*Rp0iZeE32xvE0XM3H z`E`{DbfQ1tIaCrLc$XCYNKVO)gCf*#Upa-=Kb+B`lZ&XlGz!U3Sf;G1tom=sY5HCR z)2BlZ-IxA(x2H5C|CoI{n9uvf=aItnoH0OpUZUcS{10(@DF%kj~m=a_!3RRlnR)zs8IJ^hrQVtlh8hrd*qNhEb=#c{| zpUZr55D?jy5&%liJD)-KgJlC?YP%fkj{)q2a_t@+H28%9vnpR3{n6&u3$8ng=KOs- zYlBNZ9FF|6ETa#+zdGOVRobpiVuf|)kTidE`wq>wsRtiJ!vmA+sCBRT-H!C94e<8} ze@h+eX8~GLTQ5J9U$QhYB8QL+BO(lA9sd-L$V-RiMCokF!L0GB(N9GrwC@=QIq~P` z(}qori?ar%5bb%YKPnELXaRJ!Nfu3;S8Zv8BRTURbU*}Nr6EpE)c&CA0(~#Pis(_x zf{3~dTwJ8hv3?7w9@zfGrg~Y;cuN!Q%Z18%>ZZ-4ReIaS5VZ6 zkR*NT{aNtSkI|k>*m`^*&1`WiB|>G~r4HyiK$p@VcpwPSEICrAPqAzUtJ*5H?fxhX ziq{+4EU3-2Up;5YIkCFv{pNgsXl}-AGBgKp9`N6>&GzTp#w??uLsR7GeLvLip{Ri? zXwMz@ZAUlS{H#}JKMPP#U*oWJaaZ=nGBW@sgAs;viw&;Hvt42(nGnJ@^EuyD#@9FP zj(%{SVg$yJ6EYNEe98H=X~2DBrUY?z@Mg3kpT_3Jii zGhf`kQ|24?>HjV=nsV3zs{5{!ap;ozU2Ze_SGaWxdhHW{7L+!AA9=!>m2}tTpA!U- z)+0mF5_jX;dS+~q5sG@*TC&R_yX;`YK6OJ zz`lSaaEGpmTD)D-4B0I93-${*e?=z^*KVkk;J*cME4HE#6 zNQq)SvQd3dJ}ZEiLnIX7_Q*yD0N&~*lF3Nj;HWLJNEs7R=jOwN%!;Vv8WZiMKYTk) zTf-x4q2VsrcXmX7GhdCNo0vsA5dF71CtStea!>Ih0=PtVV(PswRa^s)eCI;ieEmB8 z@)Vv`4K21y&3QKfHVR#~_t$9Iy!#r!FFa>~g?$$Um#VBr8iHiNw4+44wfe(^5{X5q z9h6xp2SK-}Jl-Dfdt3cM!lR81kS{eOpy>!mW2?&ordwM*@_2gYRl>dZ-mQHYu?N@} zw`Ga+}_nPli z(lOM>)PFNWFND0>`e!rz`|L{_xA`SUjsipQqH1M>oAYP{SfY_IjC)F1X)~My+%dv| z*^@vvqm(gLHkE9E!bB1Qeb$JZr*nyvRsBsRCgon!P=g6`a{o(CfKrNmd-ZtiSD>p+ zn?L9U2hsPoeNsYi`b8#<209e|w`u-@y88rR(%4`fK?m}jHvO-1@e0r4^YAXPe*4*% z`Y-aa;Ke$qG2JNerUG36P`z}#lA&-&43Yzs#Q+@>s7?efwWI2Xd5M-%+dKIzJK5d1rz0q7HeNoS9(3Ow@l zrT#j-yr<7L@#!k!cYJ>gP|ec`fDzqMuYiTYH6F!k6HH1yw-d!Ck(d}UVR};g6Wef? zvUCE9Ns07R$|%EJek>6pZ)^hkscYjZ5m4;A_;{cX{B_klIm?d&0&q4Fy=P2E=XbxO z?E348Ry;e*K*9Rxt8cRzad(=l4$MPR;oyQX7+5N zxtme`+~*Sg^FOCOGkT>yN_*~jVkfKDd}VuWjK(s+#*IXO_#&Lf&Tqq(Wg)WJ9M+0VfQP@m>^d|v8LQEtUrqB|ZTS~Kk2q${7k zP`r%yaZHzyD7{vFDSc|;JpzQPZ?(|8JZ?*=zd|fA-a{5&h{;b=5ci%fI+Vl)vajt;#V>d+s>3 z)rYm`MkfPMv=++(tXoIfnP+uco(Wf9{-!q(J#nWACzmA9C;zsi{Wx_!UVTk10{{UJ znr)fiG*XcgEe7h4B2y?{0{K5Zt`W^!zzqO1UT%bSOF(Ub4`h>~e%qf)QHEe3Kzq+s zeROoIwF3gLT2FM#{f*1-JYa^(Jc1MOVj{_79|mneD9fAc)TaCc2Je{^^O^3vi?aXz z@5v!+8&c*cy_abE^l%NM5dCYUxD8R@Q6vDv!;~#OpQuRj^ncpcK>o%z5>Ig>RtYPaN~ zs=p)>&{iRXsB0l};bb7B{%`^V-p1&v{J;9+v2_hnpk*~AOP7e17SUi^n&V5#FaSYn zXZGDNcmSPB7q(MHZ#{d*os@m?i@nvS@!FeNvnXG1EYT1o(Z?MDD16BwM~{rxF&J0< z-eV8S4}R6S>aywVCm!0d;q_tjmXj7nhuiKB^n|pq1&T;psT<*n`u2Q$Ls|!%GNOS3 zfZ3))3yy&lzXfv88ShUC=2fG--9pzz+x*882?69nOAcJNvMlPFk%3^_rm~#q%D+}U z41lgATX3I9$OcS13PW%Rg3*28o{E$SIbO2?*|)z#bnC4tBKFI)se$~`iv}tW*=9|` zobPyzP+@k+(aK5g(QLI}cYff3M90{Fqd`v}?Mb-qHhDz+2bEgk~_({~1> zLjy#+ZQE2m0y+)|0xDvXlxDZf8~}u^JcD`=mkWeQ9>r6a7@!a*9hM02y#(wTQ_jmk zRDCLj>)N}BhBxXrO!Ej{;5bdL$-d-ed0n0j0a3Aptc@Ah^F-jLUs3k`@3$dg|It=8 zkY9R{WXi2p=R7xR6_>5-^Tt`(%xR-|4zD9RnAnwM8Re&(+%qzs{kNyl^jtOx8+{{* z6nc(p;0~w?e`p|p>_Z3&n_owOq9|CT^rug008S@i58Xd2_Utx*{@-^#c(-mJFwYBrnB>)BM?`Hv0#<^={@;Co0 z(G#A~wf0OyTa4UCANXk5IP3svUIkv)f&hcKR8zH-Q95GD^2I3f!QyiFTdpTc%t`59ZanQ8#Dm-FH*= zh5zbii~Y7D8W_#!U$ozjkI2DCzc9<_s*cy&udsa$Joh=2@3T+)^+ak|`k;{RXV#bM zbT#KHZeBy@&H$8m8Npd{;zhefY9Av^3;GPaEFc1)V%7rF;@Fl{x2a2tz7cq_O->*} zvZbu;O91X2FEo)n0J;{<9HK=;KfT@G ze5rx92>3wa>~pwzlg$`u?4>vUrcSggro55P%Rii2JgwZXe@)q?-%cyrFEwTw813j^ z^UECr^`F<5rM#UuTS2BXnJ)5Cj?+T(k{CP5wMBw zv78lwt&!Vj1;BB#1RTr(1SQ}V+lCVYs8SB{V&q>u(88VsX(<8$mFp4-lj)}PhPi`{MG?t3V^{ukYCvxV0351vx%sU;8XMYBf!Zl95&D(^D|^ejLKJ&?ec7qeh2 zfF_nL$`uiJaT$;EIodCeP7^P|aUy1jbWLz$g4IU z$R5Nd+Nyyq>DAfIv0|sH<=kz^f zZhjCf8;H`J=RyDvVB&yG^~Y1UCK@PC*%m*-wYeUES4F*OS*W}yqY)Jj$e~SdwrU>j z3UYfu569@*?T;3LOCZnnCIO<;*U05Yk{;{y2c4ZE6C*j%&&}q+fDNVEnk~nK4UvIX zrg$_P@XKFPcG=~vR6TmnJo}lHPm&u=QK3BEn2Rph6B%8Sf9s;dQ|f8AjAond z)+;`rz2;qce$WA<^P+@N>2Z%$`m0+h`wQFd-0dz3#|q#1kJr;K>#DsfLWG806b&x% zUVj@63Nmx&8v>l-@dEfw8E+#T0Ij#qF73uQq(C4n7^NBzve7ns4#mZv(yxW}U0I0&|@EnD#=9wZjrEbH22 zovHkr!57j=^gqrcy>M(V-{r|fd+$+4lzv&GfsGp}`|f3wf9+FU!{x3uRLc0ZAAEvx zdUt=sJEwu{+8c=Oy{~f>)~G3e?}_xWN1tM!XbIc{k(UKA@0avTg0N5XXA(WmyjVwz zI01PqK|rtr{lubn1C0E-T?t&Yy#^9^BRCe%aYDhfFb)YxICo!)D~yXnJ`il17db|+ z5{~LDqF?WM(~n9ij?T4gl+yP?Z-4$9(fyA#I}vh_MjwKCx^qIPBt}qEr{Mt`{zTbX z#T(ei;Ae-&Q~vztj6uT_xXFw@Nw3-OKe{DhcL_it)mMMeza4NiM|agK%D(r5(QKit zd;2Z^LB}6PTvX$s(1t(Jza%rMD%@2hRCBX-bRHqHlKs~q%%KT=+(n1cUmo0i)aQKu%KeCD&mQ%5CUpa^ z-LI>d+||a)f&I!4PopefG^q!<%fe@0`({UP^g8?Ki$n=hom{kBU`e3#(Iy10yt`(^ zPuYq^vZkQ&irf%bNSoqpBo6(Fk^v+U(sRORWxtq^#h>2tL{TvG`d^8#NTfzt8t~ey z+VYMovN7_AR_OR3^wEk8<7SUXN_!isdd2<-1V>w{FrS-#|MV}7hc&x(Y*HXsI9Zai z1}%E!1D71#InPoo^}VJm%g~+c^LVc*&z8_p`*!u())q~zO-=W+0BtI;jmL{)idSCK zas_7r_-n?aMbA#7vA6d@DCkAE)pyqMGHG@#fFC|{K)J@oJlh2X8|L6m%Gq)9P{!Hq zA4|X2e9Iq#l9AT{jG7TJKq%-}O0UQ2v3&@di3 zvLBB@%l#&q2C}bTLNpS8Y2&_o-%Kyt<7VgjTfh)euQ4J}e6L-SA^{I?#3_>z73cRY zAWH;8%Pg4BD^!tE%Gx?R%HWX>2t_4ewxG?UdkMTS+KvF8NPcX(qM^l<{z$Uf2$sp_ zkq1LGxXuHzaiYNWqmmIAH-5a5J2@{K!HIxk18D1^jDXXgq;o$1SQ%|jNu)w^X5YMa zFZ$fi_KN$ufdR^2Eg#H^Kin_TQ4Qqz%ju~7f6~!96Dk{<3_#Jbeik5Mc)D98`{l2Q z{(7g^=ttgqksDFNrmv(+egrG?K8l?L5Ta_+(I;1xF#XoifKueFx@46rFsQ18?f^eDSuXPzbQ0Tz6n)s45EMT z1G}rw#Xp-uPhCIU-A4PZC2QdKf1>QR-%tVmAtz@YehW@cVxKAL)3QGq(axJ!5kFLf z5h{W;&o*$UD@L0p_0L|4qt(R$R*G0X;(>t0qE88w%~;@`grxo?>>04_Dd~LMcO|>R zWpl1G2H6Z5!tXVpv+MLocKCQF%4Ux`;`4&-7=nW!ZZcKePBx7YU0EFGX2b*MnPUgB za>AtG&zJfO++>Mu7bGBm#?y(O);z6~>;w7*yQ6_WP8*`dFWK0g#@tpUAKblu^s@ln z({;_3b{PCKy?ffR&dYCtM1XmJ5!{6eL<5Xu1Gbn&G;exUnFb63m>Pg{zy|tVI(~yr z%uzd$tpwB{8wUwgjkheK9aRf#lnX99G}Y4hjq8VlJ0G`zY@QBSH0$lP5P%v%Rn!yc zpOXTOP{oLg(+)pr!sIQ>wy>w*zU4%JE-t_foMhp)6`W5IA`n_W<5PX%{pZsk*X~-= z1N4h`O#@>T{j;gxHF4dB`XpeC6t6eCc*fy0J!`&#+Bn71xfy_A4$8qz;X7>2I>H!f zV2-$m;C7lXpHY|6wb&;K!0m=IQ;^AqfF@ntYc?Trpt5>m;4Ae<12rk?lfcO%H2`R# zapS3bGZYfYJ7W$PAxQvk%6g|J*beaefoQ9+e_2xIFJhMKaKz*&v0Lk_e(8j063OXN$kDN7I&Wb<^% zo)MQN7yMH(a7Yb|z7*}6y47bmP_yHn4nb+(?H)1x@Bsz+Iu?}h=8>ZLpJGFxqwp5kk z+FPoty@&c)fcEs);?r-KdKx`#@LKU{FItHa0Y)jsC5c0yX~ zb$}2Wb|T(N3sPihKC|ToS;cj2X)|A7BOdRMHU_*u2*zBmDI`XWlJRI_1T3*lo>__Y z#~2?jGo)mDL>*KL3lTA|KW5xHX^3ToV&60Ug)R~j!8|~$`FL9$Dt>^^m7>@$);ymU zFYD^Xb1izQ3HVqKME{!q9UEfWE^7b7)0-)O4ql=|QKD_pkQC?T02+{i z;@9ow5=|+?Lx{u~SbN|Oo6@k)xGaHDYzU~z?yw_&WqFq3=4-nu5V2_6*+?iXi+I2( zY`8Q1LC};ooQYAa5;^lR;bci7RO*$(W=tce7|cdVv2Fxpg)Di*O(++vTLOLQ*e4PS zLxKVX(%PCJfARRbjYQWKS8XH^VXP}Pb0!iA?2C|k>n{m((dm{=<%iR2_rIjutxD}l zLE6~$tDgnvn(miW;IBoPQDcc++{0LbWN7%=j`N!>YAIMwG^C7x2jG$O;|TD!2Pu(V zAkvg{YCnry0%T=4ED7{0MXws^ z;^f6U(x(wqKAy_jzog6HXNELjeHqcus`m>7H}z>~F6_7A_JCxV&t*xUeGHxZk50k~3<8_=Jfn#b0mb2(wu^QC!7A#v;Ac((DBx}TV7|x5 zRsz__J@pnOw6$CYlwR~Hq{_=j%!WP22sn6oYtur+R;q~W62Jv@f@5T#39Pjj<(-R# zByh({>+I0icKH|e1x_(Y%PAr$s_Nm{lMpC}mKPiXnLzT?DjO0XD6_*Hs zZbK27B}?ETQV6_Qr!a5(Bw*BCZ%={$=yp;8Z@tkQA;uAy6XBZ!5-nC;*X0ZU9auWgVm1k_eW)@PF) z(E7yXM%(tr&Dek;PX>Q+95}!#L?C37&ItpouR1X^V1&h!0(IsSQwEo_BryA#dbEai!=lriAX8%v2S5OkTJYW!)uOJL`*De|~EL_>qk z$Gpf6>NC%nK04sZoHr*1{PG^`rWQ?_bytUs2(mdk72Vn6gBb~UFu=LE82iSpd3dW|M1%iEg+%otX3e0U>A3=eI`$7%}WA60qVi%5M2J(ZdfnK)hFercWo@ zZd=Nqxo3k&r2O<4_=jvJMV!$q^VJ{x8xei30QKz0KOAh2ra^8`vMht<-f&-TGH~t}P%rjpM=aaJ+ctHX7Ys=t zt?3hQp46PWwbQ`-wxp1V-f-tv=pUP4W6o%KsgE| zkQUA(OU7iJ%B&%xc{2j9q2>X1P6$*YXvVV!sr=*Lh#q*X(SkFl1RE5~FdOjZx4;slx%0glwITwV4+hi$}8l?M4oW^&R>65oU{aZ5>Qy32p9Ko9bzPG z)545+U_{2pjknKrZtGWwgV&$RO0+Bt2W9g-_7OqR>rZH}g+#8j{s4I>+m2k4#K#W! zRpZhpA!~%PdEgnbNt_qb{L$>j_7gUa+Is^G5nxJ{bZq+zUrN(dn!8fD8`i#nPFuF9 zhS#A+M89%Xt!E~3;a&xQ&9C>&SY(GBeSdL0yk|PpV;#v~{6eB_9w+wdxT$ZUd9j0zny02Z#X1fW$ap({(Q!z(7VLJiH!J1}POU z^@nf#Gkp*MFBL4j{=j&vWQlDAEdkylc=LA)vH<|+AglE!N&lb+?2qYMvD(UQQX?l0 z7#rm0`!l2^9X2K$oSh64vq?u6@Q1a*>a$dvv{wmB^Ehx?2Qw0(cM@@lK0{mpd)oWP?^ z^55-~{6Z@P0#v*}fY#+R>F_%)A-eZ|%C5aW*(Xi*g1sF5t9H^;gZARZQ?hyY4fj-^ zdbl&Eyy4<14=zOh)g%w5|5((DqS^>KvOi#(IgO`6;ANsVX)T%qtmA|M=v0TQ$iVyK z0VvZCW(1r*RewnBOOc%f)b=yw%Hve9u>CUNZDintfH`;$I-1U7<0yV_Didy1^njO* zm6Rp0fD6RP(WxL?a<(CIQB=z|AVlY4w#&$mGN9hTCNUczI`7a?T+ZZTnL$>*>_(o^ zL8rg9hrM5KbuC~1_#yiF;*Gu4E4Md-S@d3aC-sKF8pE;Z^`E1G0sp9s6fH;&#Fm2x zguozphd^<}PXjls-vP?N-T0=TVio~N29M5heK-iF1}GV*OBn>GFR))hB{2YQsK-#< zs5|Lxb=vn55IbY60i6WEY%d14i{ft)U{g0c^7_%<7zs1xFOoMPIaa@@a}Huc-KtG8 zfYh=TI6Y0)O3OZV!X`NhKmsUT3C-O{ko+^!D7l`IkpVjRjJMSAN|U!}uSfqPiR{(k z>XU%*$ejF!i<$+b=)>_;)gQ{4{ZztW!Kg~ZZZ)gf2yU=KCnY*+=vycP#P-m5ML=bn z!_fIUP7?_LKPqTarYw^6z#v2(1lc4)qDGns(B?s*KcUTV4%y;PUb-bwxj7N_&m#vZ zy7g=TWGOg+xCu|`k4lCF(6Zyh>f-(J?1>S!vTBw#mq1?9ONvdJW0sEY82R?<&WNtN zi)j6@Pr_JY&ZYnzdz1Wtk`0jNiTUi6A3nNQp`s`HH6Fl!ToC|Tym%nH^}bDfbo2Rh zh-S|unmMf}2EOb>F*{eUqwJC8jr%@F`R4<#znHQt_cr@+P%+)$&Kn(_)K$s3W4SG6 z5>0`-Fud}yG^^nBjN>y7*Y-UY;UVBMaIkeL7xg%8@DmA8mzq%s+_Y!MLRyoD$W#Pt z03tp4@lTp;JJNA4%DThBTuLB42~O)k=UQeZI2-vK!@l7 zIF6fDiu=~exovaq3qF>p3BVlc>EwOUfe^s7U=+_*~TbCR*Ti2bn57% zX^UNJ>9NOFQ}*CL8fTeW{8@yNzX2&$7mt3?(P2?3J!s1sr^mo(tJ#e}1AU^FF2zl9 zUc{<6GD{}RkuU9xIIMwNM~Y+sNd|Pb?7Jcx5*dD6Oa(?ZqtL0Njx5133f4{$2w)M^ zFDDZL*(7qHk{f0;X3Ql3Dn7Ft^H_gW8-Q|Xlg>K|;MiZ;PmqmA(VlFA-2`pc%q3ww zJ)2ZEd0IdX=l}@~e>Z}35fZn_pWmKF0-Sj+_y@d9C8KsxiADkx%gr8MPV|rJHV~!E`(IZ@{=z6q18bco0wb@xdQftp zwrKUGN%`}%sC`zt-d0&BHvYUME832IbjpE!0|>g-!7}2K0^kTTJgXr^dt%yGkOfJV z1j6Fo_mTDWXnz2}9sLNI4k_bfGYvvLNCH&dn^PXj>z4UAA>WAhCm@@r7s0-{Yz_(1 z>X!BhC>uH@S9mB_T`51Z=(;hk7?Vu(`t*VSpI(1y6S30hNJg4l>_mibQZQ;`Q2LI*2ZpKxNhjQjYftM-916im7x+S#y$sgwxdJtoLheYD+Fo!bi0=K$|Lk8`2w&&stk=ym75zWgTN zbqD>1u#P*#9;@)oQ_M9LJ=QV3I}#D>{m!3sYH_rpdYw$~cem3$JZhIO(v#*kUMTKG zkix|zQaJJIRn8i z&aqLu0Y)X|?C2VuFasTDd?L5qc@Z)PFiCHf4y0(;10`bvf@}r^L-p__DCZeS_h=xh z_6L+H>A)FpX^9E+WcrAD7yA=ff-+{Ml32buS0N^`tc8~C`Ncg%Yc`m{cK0WUKpwhC z2Ok`i#pjpb@f`ZpEeCWCoyJp5<1FG?5pk&(R4wproWlk%XuzWh5iYkw29j zUZ{3jT`A?}SFWRL4oD7VnYVw@a;!c{A_Z`xF_tBjC-QpGFoW zP$@p!ym*VB5a~JKE;vh#WQ3whFO{oA?kH0b;Dx0DEsEvN+)*owZY7oT%tf%BO4*qC zaKJz~)xf{^HVZ~>pNkP8N2g#UBFG8fbDhLwt2=HSEMv<@o;=c?5QR{9BwE_^$AL0( z+!aBT&!_Oi=blE=~4-XW$V`d{IO<+d;?d2y1{L84tSPH+UG~yW5w7IaZZ2&|{ zAA9M3=D02RC`Ym^I13T81rm&0BVdZ)q5&t$gTqjCLDGu^Iyk;!Rw1CFR0|pa03ZNK zL_t(Xr;icYoa1&}x0QiN?OQU3b_Zlhx?4a34_7^-ap zD?;MS!P+Cf>;j^oY-j_Fs27_9Jtxof;SnqNAOsNHmrP0$Fpi^tFwNe<^Okxt!AZb> z7U0h}omGnRA_tB%S~^b{eO{N@>?b;($Yna_84c9b2rJ>Dm?TH90Jx~H&=EVfQOmBa zM~>x5grsE>!$2ffPf+LJ{8K>AVnK*FaZok3DFBVwX2h1HtdL}1QbxcVZ>dj6co^9+ zAje6vnB54-i}g5DG}a$UmrP&M1xyI)SKiTVQPYP+%B<57mUKKf!zwQRobZ`|aSr|y zhX5>IJeAfCtZ#`t^!P&t{6&_|oKBDWW^zsf#_SZV@{In*b7GhQC?@GCXx^i z`oKQyn4nH!;%l}VA_3YBfR3z6dyedIRMmX`s)@QBFk@sIEnaNST=Y1K=-tzBVf+bn z(y7adXzuvyxZdNF_N*d*VU(PYV#j#|_TV^mc64xF1^(RWHW-1MQuKh%RIFUo!`n6a zNfFE$2!J1K_M_+zJ4)ugagOy{0yG2wRF}Af5xZ`dlMEmgC_^Z+wfzZz8YdhYI3Zc# zvLg{F_Y??YZ2F?v9Jc`nOqqEj@sqAD4gxF#I+P!u6KzsE$Inj^#O#~dq%*Z(-$v+K zW{G^Mw$1eyS;F8`9c>IrfZ!71ON>T#KHqR$go-C130OgAo-!eBHkvpDpfJ1%v=m^# z-+(T6D2}5v-h)N#p-C=&M?~|gGyZj*b5Wmk49;c{AhL5JGXO^;lLxxeWh#M7Q8<7* zAXj{;t(jI2N`To?wgF@lPdl)2Q$b$?9Rw@$dnKFMx8V^k!A64VY@v_XPigHQZPwU@ z-A5u^7KB+Q)dm9N=72x*Xr4`CbT8>2K#q^OlL3oF_tpkke^Ll@ESqMk^lTDj6SFJS zI33_zwTU*5052S|bbO9tlb&Cy#_d&?OrVW{6Cm}k`G4SYh;P0L5 zR;5&No%FGkC9qC90NyymJYZ0OS{ic?z$+bvB7g48Lyd-lH~vTG|K7y+zSFm$k&S;Jud=*ihwsy#Ti$59Z=+Z4 zorKMO-UO~e1U9Q)rpTSQ7j3``S%OYcaK>KUOgIjLXrNlOV*wGOZ3GGjFcBC*pI&4K z4m_C%L{1;@K3Q-Mobg88OBeeD^#g>6wKv{2za1x*SLlO}XfkE*lD4R3a zaDG+4Uvby7=@T~}I9^>d(ZG}~>5TV3GJXZ{doGf`aWCLWr`}IQ+l@Cl2D6Q{aQ9}a zOamV)7mqZUbF^>xilp=AO!GIa@#xAp1u;Dt*%6Q6t!{*48?QvC3)u*}^VvlAl|wg9 zE#!zDGGR!FwmA$)10IJ&a5lf=2?6tVWHA-b0fcV=G@GeFHtlU~YML4yKFff3eFq|D zzY_pf(yh1OLLp*EC>)Sx`bSRa*%~8e<|UfJj05ivpp;`H40usi9CvB?$P(9_ZJ0GS zoj~%u6AZAS&KVON7ezKCwB9^{{XQNEa6)_7>w^Z~=F-3s-#&#lY#Mhr<`!CCMrWR~ z{dmKaSbrxb0Vt#%hf4t`?$Oxv=P_wEO46W3Q+f(nD8L^bGQa+o-ve$Oc`%BL91FAU zF_5t!5xHAH1RcJdAY1^%&t6NL1w)`?$3@VkM2z@n;ux{IG(dNBrfdWYrF{*cIku&2 zc++;LTQb8oCW>`SHlWC=$%drCBW)oYQGho_Ud1tl&V@*&cHaS?s}5O_0GuyzNn>5~ zDfaFQeohpU>MT(281QRafnOgW<;@V<}HdgJhGvX0tfS0dg;{FoCF9?h}X$UpGYDEn;4fQV1k0bhauw< zB9z4T#Hc+I zAz7!X$R_p&%VN~TA|lVxS2N9mxtVzZqQQY5fg$vgB0H%W$x6w7h&CX|rhy!`EoG84 za^lE}rx@_g1L?#Ad@p4`xP#-_6)|d8-n{56UGcpQv8Ww7Qp>)X{i97fr%D1F3Qf~7 zo8%-wn*WRkHfMmaKR_Z3d|7c^WCA8I_)iW3aNHR?(X`<^V?1H3zCZ5ir_$g+@pj|J znwzuUC~WjIPMYt&mj+WJ0Y$mlQ=Pfn_`|Gqv&~Vay)w!|C4qDD9DDUv>`-(i0hI+H z2&v2HUs#*;A~4n;=h&G6F;-V869YKxgQvfY$gJ&)FNWccP}vxWy7n?DmAKvx_1Kbz zLY6Gi!DDO6<~$87oCsXfvpKQAk5{I=Ka+&_Ko(3;A(Bs*el3j?GGN12owUiV*>%3EJy&K%XR#XG$%a21oLfpWC|T|sk`hs zR(@D)cm2>4>Ad&d)l+@yaA&0P?r;-5P~AzVK1f85qv1_Nn>G<`+(b0Ik!Zs((Z~p8 zn{uK}BjxYqVfOOhAmxJ@(a;p4sY66l2AlshWUVRcRx=)?H|_cdUS+$d;C6%Z5a%;8ywFi59Aa04aNrI&dD9Y{9d>6Xg^e+L26^pg<=1SThC zFA%Y)4M^mz(wjtlLbAm&R34x*(vzY$Gv3?=tbJn2T#-#92Zl^|nI0`WAT9k7(bad# zBmm`i=C0!NRKLWG?u4Jdi~hQNo0^_eDP9O)_;&NJ@bAML8~%M{6H)P+#Nyw;0MXz; z!@n257yiBQ^M#Ke93&bl-+BHJoq0-!I@THSMx&O=K>&(LaLCcHw|X>GDbzV_i1IDx z63w0|Hn^}#{fn%d0f3!=LQPQ;P&F`f3Pik;gNZm{Ck?8Aq6fAb=;G%QP%^3rMcpwl z;gP<@+*Zhnv;oaHZ&NYPE<}hD5-uaJ9am+|9QEP=kf2)}7fUkWA^w^D3ISY@EjZq6 z>`NKpBuDvmjm>NvWVJDrWEDS4luaT^F`MMF8A9TXlS0N(|K;z2jEGpbRZcbMF$2Um z%$jkkgfhzkqF*u}Y9|H+zrbCVV_QUYh_)$k)f&qFzJh4|aF-l*vzh$TizWp&P6{C{ zc#Z`LP)zsuBQI)xBRFCa@D+f@CbWpJi2Usug8)v}p&__woAsb`MVBmi1$KTL2P^@V zvB8nm_>n**8>p^E^lI7^SL0PspLyuqp!p#=+LJ4+I!(t=<3&jup?)L`X#lFr9UOHev84JWXeg4$RyFfb*hlg~S=% zc>NJ%6K##nC&^a4Il=%PzsLSaZyof=vanQBWCGZ9asOsE$!(CkyaNApL< z?C;3}{;@F|vjG^*hQ}lmzyvxD!QVJZ!3lVih>e)W-uPsjQ60PG4$AqPIdt0{Bps1+ z%}|B-&H(MGl;}WHfPs~fKntC`bbQb}qea^YsHILK%LR6bLU$u*vO)bez0TT{h>&y} zqc^riJ=(=L#s--|aou>j!Zbe+A$&U>nyi$*?u1R6I}-TPmib%TS5wN9uup?;0OWQc z)&qizdKBiHsmGZQCT}T_akMq=%q2T1`z>69ZQEr;!0rh^0w*|UDd=Dcg2q!qwgda! z3nXANfj_sI(Zt=Js(UO+z)>&1lpZ(Vzo^jhy^#rLLCrXB`Fij2+ZjdV&r*KcyU3+_ z&A@Tzs+BFvDYvYp)}KU70&AOUl#FBsbSF6`DGJvJhS5`WBIOx1z1=@5031n|P(uFZx~b4;zC^#2%37i&YjdW-{1!ho~MG#=c)+K13vAfg?157oq8DX zTD_PyrMi@*i{M#!6P;=x#{s^kY$D(*M93KOZ{ zq5&xlZ{;cX_SKE}tq8Qs}t$v{% z4@?r?DkrV`OKp1~QaM%$z>IE;NcI94e8xE0#Ckje6UaDdWj%7n+E!&@AmS~X2$8kf z(C0dXVhCJXg=L<_;e# zk4y+QiS{cVp-mw$u%G8z$@i# z7;O@8%;B9`_xA=&VDbk1ZCSH<2^fR@wfGKp8S`g&dpuQ+-^U^*r!KakN&{u5e%6`d z83~jm$LIlF!11K6jY`oEcZB@ZfqIWZfIN8VeLQj}rON|^qD*^3M9Gf{TEd|biqKAU zqf;169E%X~Df(p}V}wewF)(G<2H3bJ*(946NJ=#O3z0?XnV3yVM9BFprc>~$A^YX_ z>G%QWnN3+fhD9cT&sW=5=1Vky;d4jyYdO*3XO8aCz{w8&eFA_JXOxx$Eco*YCd?w| zBf&9Y6^_HKoO-e8k55=>(wFrJIrkLr*Gpyp-m+A_$-Ka*rj0!w8b-;2N{0B8MS=IGds1h6VFN_L6_PXi}r ze<83oO_-tgKrSE~+K*@7-2Q~MaPA}}fc5M}kPUTeRKois+64$8f_qh&mW3%Em+S+{ zAojXo>Pt$vL2?jA zeW{L7*+uX5N07~XJ0Az-z%wC7t)=OU_N2@S0u4{sN+9cGM~uP1F-&n2m|#ne-7uRp zK*6IqwLb})HznCb8^FB}X77rUxxjwnoWsv~R!DtDm?si349z#IFXgm}eU^=z`JCIN zHs8<&_#EY9B<)ABL_n|`DI-;E3UvGz`a4j(WBYI-N%V&bA|26z{Q!ON_ch)p0Fo6R zdE|_2co9iSpMi$wl$3@<&~R7aJI?K>JP@!(OU^^k3ELAMX=Jf~Wtu#k8a_Y??!A8ur6lg$A{MYhnm@wz3@bwFD=o#ADB*tqez6`I>l#!s+glt+`z%iKsd$z-wn3xGg+ zB)}*`$N;610LhNe`S#1`x<5X#u1lsoA9x0R`!m0({oo{cpiclogM7%*M`c7`u44!9 zJhY1b1@oY@NR_+p%io!loX;Mkrk{9K}U*W zLu5v)Ta3t%OejRmzG;pFSRzrV-5+J#q+9$9M`(!{@-#cj&rW+tHXzdH0J6>wg|ZRdz&^X^@lIHm_Ejq!dkUST$XT1Q=(0(#Edy6t81wD(X~W>c&OECjhb0KJ;y`&+`0Bv0}yd1^7G0 zEleJ^7tj_Dru(5%j7B^#HkWq9BC;CCv>azz?FLsb-C=-f!6>kqh=ZX7j+#R;5tI6c zGp09mu2wGdiz1su+Cl(s*0i1arK4DESpctu#Z722*(4i4vol~|0T+&Umd~;mNpVui z>yqqSvRzy%P(crYw?-7}fE9F*lOApC#3cq+80(UBgtjN1*Z|q0OLL|%TzhAV=KbO0 z5E?gcqo$xUBr$wmq5NJ(ruy=Rdx(m0W-hOpqKvKmOURGp5eQo<3COeT4fL(gRy(m% zJow}*(1f0$NH87ZaiIl@?mqv{WD7agLvF%Se&35=-!;^e^)pAZOZvL)H1U;`YeoG~l@ z5KfVVvRAggK-QwDmT%fD^$q}qdXmP(*i)%Pfp{Q!a$YZqZL^Gw?L){9v|*F>Jp+t9 zAUU}1Ze=sFLT0$Lw7OtX68B@@ylyZ0>NR@>J$Ihbhv{1vElP3kletWv0Hluo!AE~K z%SgSN>K%tP&-`oPZks|76xv5e#O6-=N>p>fkcFQwes(>fhyr)YAtogjl!nKa(TXT^(yxQ~LN^S;yoi2FcZ?#Q$BTLSZ(E~0N= z^tM#jKAFq)2|%6EKlp8*$+Fxttbad5*!9l<(*yDt2{7Ly8|LeII2qZ*6jOqAU!21n zB}Si)Je(W|vSI2FnLh8Q>V%C~5!N)zZU%NkOJL#jhdDZQSf0Zc#?{Fl7zZqD1d~V) zI%)OjR&0Z%w8WBdSxz(>W;#y+3l2I~_jn-?f{iks8ll9sbx?H~mGe^%9Eh~Yx~M0W zz<9OwgkJg`xh*%v+M2UTdc5QUYqjWyrE7ueK_JIc54}v>m1yWi2nc_<& z5|HQFXX)FYeS00ZKAFq*2|(S^XUhTGZ*@Pt^7)tBT0Dg+#_N;|kO6fPX+VTv7Kuo$ zgOmWAgARC8(aB3>gn3}=XiZrScq6j$_8nkly40e-*iCyOa6}M55gq3$$X0d@WJ3qX z$C=l~pB4KcIe%sUfd-DI-lYC;5Rvwl2vM>Y1xuvW(NQ4n{r}s$vRKQG>RR`8w^48) z5h4KvB9Ravj==#L{SZ|zlUSDpKMdiUP)kNcf_PMxYy75b=27WvQ|7`>NTNSC9Z2KI>q@M3DT81a&EHQ^0Ioe?Q>$!X zNCdJJxrJZm&s(CVc%Fc|K~V?CWG zF7joCQ6Tnj>pLlVt*&6$YwxHU*5FVq2=H=ZXZVQkM(zW*>3 z1*-poEz790k)3qcr_Q|okKf+6`t1>SI|10E;D*QU`e=IEBBkBKf`Ca!Va_HIz!{lV z#JO|3)!euM)v05)Ktcc@z{^0=I&en;tXjx5qljRD;amc1qiebj@wgN&S$YJ>pm=0DBaiJ$Bc<>EY>$^aEz5kzm{~VtG%yJ!f@H`Y2Nh5^hh+pmyU#9i`_LyjUIIaE z+W`dkhlmLmLGoN8U|_D_8LcQk3uALK4M45{TM@qHu{rP%GAqE63~&q7S(kIPAsCw^ zM<{cew*oXk=a(Iu1IHzbV+$QvLMLk^hwTNgxBPqHJ&)s=u}v_1584=9Z^QLQF(c|t z{`X|JKL78ffvt^25(8l;hWmc(JL$?-zB29k$oBIz{cG_!>%&?M>T(R?u8;0yqfxn~ zGEWy7Jk-6z*S6T`GB9R?1Pv%-IQNT&uT0Da9a4Mzok?Z5RQL2N5omzZ=6wy1;u zg#)cl04|&?$CfY>z@BJC1e}4K`g4Z8f6SS4PFNW(7*}nPBf-dvQ5k39SyMiTdKeT#bXS z<^Htt=Aurb3gD&-+=oVi8%I*|Tw6*@c+R*Er2Jd^>V?dU6jeZ=)EaR*i)}|lfzmKa zCmooAQY>cZWU+;m(*Z0~A~>z}Hg*TrejcW>2TInglORBh0w|C#fJme|hUO1HuY3L% z60$l0xRBDE+NwnY7#Q*OHW{bSwm-O&F$92#3;)mv+j@<{eabMkyn`qtoe9B`Kn*r! z#}>8n-^$a&cTWE$%$ownIpFOG8;wWRi?nngN7@9qhTx6hT+vCV1Jk-{ z_Gfwsk3LppvC!inHDDxw*9h<+Z;gp~bnw6MVATo0g_r5rB9Z{9;H2Dk&zZAyqFKcl zNPxE#c2HTb^21fo5orjSvz+>w4JUiHXdnrZBoGSuFzHcXq6{4aV>YHLEwM)d0Vn?0 z&$jlJH@CShb%1vKcIYC{bBIJc`J(5$g{11QA;Or&e-@UQp*I#+HXZ@ zP=>6NPQHQHUL$8Jz0e}dMSrFX&9=n)&wUfAIpY9P-9VuuxNd{giA<-cS_7ezHtwN6 z4?O6<8W`pM-I|yNbmaQ(`CkmE>IC3oNOfxS&QrrK6_G5gVTz2Ny_g^-f0j$2lPy`R`Off%XLwBQD;>bKIHu6J zMkR0%SeDLy;rBsi>Lic#3&U3fjr@QfVx10}GE$f)4^LE0P%w22V6h?zak<^4MJ4 zG&c2K?x%r{^Cm0XtJol+QIj&Lct>dSikvRYd431yh85@|F~bQmuwarv?>^Nr zFJ%$*5uXpYoL1647+uJ~EXKC94j@S;d%z?_fp?zM+S(ENeZG3^bSct-X>3dqnBbru z+rQnKhOrgvb{U;aDGekb0v>XD?mK#Ydi+H;(Hq@-CjggV`tLlIZb>P9hCO;Tyu+5i zqVSIa!5-jZC^9YuNOR#68igRdTPTJoFj8SpXPBXF8FB^zj2?tg$iev@g%0x+iA{X^xt`0yS9Ptsrz@&o#MtQtNzd^1pcZ{BREjLX@*m)bN&5`-Vs%r zH^5i|$TV-b4ho$li^ZXQoY$O|fO(4w_iFQ|QV)yaunJWlqjUuHfalY#K|wT!-=iWt zNrHj3Ndg6ZQK^GrOj}#K-yY!|GG>>GG~|*x`NeO-@rr0CQc8E+(DVJ53YIznxKxs! z-cyi(l+uHzcc^eS{$vPL;hx*)OO_nl2|XqncacYp>H*Ne(V}-%%+S6{BV;=T9Ymtz z`ZK3ci)Ds&78shP`ilrakjxZdMPtk2S9w*IBaTIQO-xJ*vP8x*%UrDFh_Kn%5I`Jx z6Ck3%q5v4oy(^uZks8VU5bHq^ObK$R+n^U&zgjvF=%kZ1);Y97;9@P{I3r!lUj0mh zDkF@wbk5XnK|+?;N~+|a^^3{c^n=aA7yiwkb29vVtb_k0MWjvuE~&V8UWO!Kup{RE zqs^3_k2=T$tOq_bwB_0$;2D1pNPvOPus3~_CJ{UrW?Op}IL;s?K zWNhA5(P<_oOadvjz&e?^g#uZ7HUdo4fnc9F$~C_m=d+PwaQzbncS;A?OLkGSC>1na zM^ds^ogDgvU#yg#y~#kNLa^vCw=>)?&Zrn=+R(||9{~KkD+T8LOW)c_dzx+@yRm!z zml}>b0l3r>pWb)psq`NyrN{g)d@Li%;{iq#)x1rw#ylYMSmqRAW#tIGG3yuhI|GC8 zvT9#6jDUfS84?O-Ji);6*lZjsr0=eotQde$c50g-0eH4(N(O9*vEKsB5xBFA=)RiF zaSpjH7nuQ&bYy{u7ru-8(*lMfl#W0Wfj)qUTL(A~jXBBVQQq(p-fq+m%U_W_*6(F? zGMW?BCnT+oyhr+7s|PSgf{0zAOK^e5h9JZJ`QlDN;C~*wF+KJ&nc|IpZ6^SiarzIR zOiwttNMBcZc4_!+0kiiMEZ``|XI@gZ5_k#2s6v%U4wMI(jRc|ydPM|`3_J{8GF(d9 zoyRST&}#j`TrV55(6BqS+TDhw_UGH!GS&uLv>ebV9RMOC`$b@m9@R}lIv_8Fqr^}X^{h|1FXl^9|qk9!O8EY z2vtodl@1`mXz8RRC@`J}f0Tai8CRumTt+jz(XZ|VU>{8Xv3t`tpRtdCrs@NbAQZ-1 zgr=p(-zpXZ!xw>tGXRm14hn>nab|@qyhhR1*iBI^s0?+!78rFVO@+ZG(2>>pR2XLl zv(_IBl@~@+0F>iRGdx>0Pbzesi4Xv~BXhVHT+KItodf+DMS)0!^0Exbkea7z1R|(E zD2RZF!Z@9r(;r37BDqklN)aVf^#wE5ZJ}!yNGEwcNRn640o1u)dSVaxUvBv61mJSd z!1Vq*?%CZTP%*4df84RM=S&fdX_QkJn^l52&3Dt?*^5 zZ3KOqiz>+w^M+OSD&^xqlOj9psYPBp_bDP(k`#z2OmtrX*j85nb5RtqM(K+hK~VCq z0II!S6{XGwuv$8X$YXwA0M@+T3LO9)&Ll<<4di6C^bPh$lB&wsBoS5iht``Tj?w*a z{pt?>`vQ470oWJQu+xd7_on+6+v#V*kUA7~4HGxZn3>fO#%V5qut9a(tIRlC-^BiO zBL^8sf~{i;OqDUi{>U%=H7cKZJWN2`JEx<2#|a`&9KpI3xsV=N#goHuEzgqz0#swx zjUp8UhFFD#^SB6Pk>o{b9R`r9tfOeoOu{Dn1Dy|LSOK(}4kBSwppSZv1$5)}be+s_1r8{!BcgVo5H>wEKdw)d;0pyEbJH zol8IU%%`Mp?=zFT(cS3;V4qI^kP@inG||&yu*g_9gFGkzDIx<%3dD9o6d@aG6=hHc zHjlw9NUI1jFq+CJy1Xh89v7bF?R$+X{s_#F&`408S+CG)XPjr+s)o9NbrA~mXA>c~ zQOY-v6N9cJ1reFSNL6sSQ7d&K0UTwH;{k#Z>2Y39ja?ctA{Tl7qCXLttC5Q0qd!E{ zqz9G*ur9-Qc;OF-H+0 zlK>rf+fF{T-ek%`tT!25LI5|NjM^9LTCKN;kf;biC35!Nr$0IUM`y<5~1a0hMDY(^j~KQ3?;8i3+Uj`iz|h zbAW*jnc6C8!)m1q_8Kb1=fc+9Iw(zYI_+S2vZu0JOMBlB(4&kcSVE2w+SNhD`j3Gri$dEoQHo$pBm<3 z$A_Is54-y6^w2&xu^ZjJP5@S9`j4DU+toZMdN8dpy#jpRCM1j%8-h#%(15ZrMfr*- zL}a++y9f-Mo{%*zLhk%sg_>up#_4cGg;Yi8*DW?BizrM@poZSCJX}d^)oWGbM`883 zK&3oLBuce2@1+jVTxWeydP-|}c(j_viXsq-WI!EF0j8i|S-wHezS$ppw@5<~XUQjX z4!0s0vrcCD3({lqtj$|KA<$&dB4HqpYk#qa{8s^5IssUPS(^HIEU$mcHjv1B={#CE9=B-n|O1o7dyG z43i!PCNR2eWF3GH{hsE{dG}H7*odCVQF)q32H}2HP#TTR$$uggj&Mzbn7)aV3swuV zL^`Psok5v99iR_5A&wQ^eFJ#+5^X@8cjS8sos5cju|J9^DPU$}o%^J8z&bx8y61FY zddVD}(f}Pwt)>fO+iy^=fPN%8lw)<|iN{998_9tK0UHD8 ztGids+h~w!BGfe`hWyULbH(?oLQLoN;ETu;U1!3a zb|F*q+~OGkl)~r&!i7ectBMK0TEM#$p&;)+BmyQ6F}D27 z9#5>}$U#8_h$!TFQymVV&=A`gMX0=aT4G@6)!1;^NFA6@Q! z&Gb-%7&#&Yf3TjY`*1BW87#;t>ZJWBK0*``G1(uMDUg@BHA4l_twp33>Fjk^rGqPK zN;kUOodB%%^e>K_Oyl>o7*M5W5rDcR5okoka3% z_+sE(owRwgHCWq4Ben$E+I2daQo8P{w6$13L~^z5>BjZG6M!|C^1~-~3Is#&4Dx`{ zk-$~&r%@pu^+AF-hDpINI{=vBkxTEWb&i0FP>5?PG#z>O==tR_z&{GPF5AZnShv2k zI6SIC!Q9VcyscxIGm>++!76|acB&}- zWzXKt6Ohwz&68zka%Sz%)k#HIpM7-?`L8;BbONyIGdT7C`0=#$)yJf>wop0bZyqUv zSN2lPSSiCI0E|Pzp+^H=W)43?cvJPURV2pVA$ombbiz^u7+QsSSI_Y^Whl+d%;1Z= zFO(NWzgypV5zgsPtKbv8a?^=%?!~tfa0#HESQU7oC&Ablg!%86egyWn&+^dex^i+e@!Qoeoiew+uaJVZ9A;68+2aQ`61ZpX$ed6fqOmssu^s37_Mxq6P{GtDfW>9Z@>jxTeO2 zo~=uh1hp2Cj^z1PQHww)YyE+#aXTh>=Lh#^&;d(Jt83x z=mcP$W^)?L@xM(6zw(uIdYD`{!NBIxP&B_5Hoc$uZ2K&aL!m~Ymp7R*CGWtwkYbB^+6D0rIW2% z1J{RX&1bO$d-zc&RiPs5AsYvuM3jbFd9Ug zeGE+lNx&263Qv#>zXUM|v#Q^-{hNLq>aXxWmuf&mZ+#R7ls#%$bU0&=y)(qnshmqi zIe~1A7@$#RKq=ZrVrJu{oehpKz`Pl7NwA3kX1Z=VkPG~RR1lBF0X36MBo|bKms1I~ zIua}_ zTzk*hUWJ%AAV*I|kre|m{J-#iT=Vp5yYyl@iS392zjhElA*{ejwRFqwRMnn4GFkw_cXi z$+?6=Mf{M!*ig1pdfW4#o=$CuY24^xb^@?r(?4t(9ES{EW8sMtfGSW^VMYZy**FDZ z-w;HJcwjpi&gC=xtAItG9DAhW&%AxD6@~_I4ao|0-T+bN-tj*3NQIYJeGh^?0w#=Y zE~%;l3+PB8N447lbPkD*npb>pzITRBs)$QS*ho&Tu`1&0`X-_B+l@X*juwrU9sz^!=>v4(_# zJbino`;uC4Phl9DtbhloLcSrO1@bk*H8v~2l4Xz&2r{w$sj-C-6{?$=t2d71kbJFXg!`$^C{W{Q zl0hy6Rm5kJ(k&hMHxdat0ocfC-x>7ncc)tyi}dG(!3Arbjp1a)B4a9zW?*cV`4S%+ zPY(OK{#5$yhy(ngJjCqnsV!h4T%DVt1+m`ospC!HphzCjc8g{X2s{d}91&_aQv^g%yKyy-|3RbG_LpazT_bxMnq@qBrn9 z6%hbi>-Im6-qrLhK9>u`q58e)QSlbE;iXa90F88IyiP_~MX!L1+Cgu-23Q555T3sy zK3u`y03w!amO03NY@$H~YH1l4SrGFOk-0)A?Zz28iD1pTq2|qzL9#78w()uE1NMo= z*3!vpDl0xmdDx7iZt1Yoo0-;JK@1G4c`Ib8uDc7JHF2!xffG@vfLq>3jP z8C~hgqsJ)vV{Og#qO7?nRZ$q{iFH&746RB8QywdybI|Hf4bXDiDg}atnv>S>7^28d zL>8H_6#$Mzkcq?0Is-LcH7AN76|$_PhK$^x3|W_)PDj98(MgW14DGpd2bg3A>+L1a z?0Nof_ID)E2|(9`LINWZn2d)5b-O0QuxKpD2FZAjf1?3a#wiGkI{*RHKo3lMSJV5{ zKKJ2!=e7c@o$(qHguvj-9T4o15@1Ficm@gkhd6Y|`cx1Bnj;37qF9nhRE?aVYzwbF z*xpv?0E!dlyV^F@kretzx>XDLn|SgOzzC3}e~K)+cN6I;$P#TsRo~fAgd)op&p*KP z7&uK*A!iZ!Grf6f2mWsDNdlb!bd``2xaoL0_}HILr!(Fnco2{{;0f~PY)mCs1pAdU zKtoTR!4Aj-7ueN|d`|naIFPSYWoxSi?=-@F;GMx8|Nh|)(a2O0EmF7YQ$?EJCY$j)F~*GQ$Xd$);cf0 zLi}(XDt%Ju#vB4Q19T&OShhcrPEt@Pe_|L35fhz)Xeok6gl!I?;6Cy*7V$8T|I8x2 z@HNj$pY0~DOCXX!CjebHW=Y`4-RbwwEz<9qaWUXP1DOqf;E`9q^he%*t_46ZG4k}y zJMw1=<)ehYtFUqCE8$?*$Vv7QVY+n_~yIF)eU3G#AVKBW`U77a17Ei zxr;Df*X^m1BD`LakMmxY_iytDTi4Y04O-C;MZTiw$6TEpbznHFn$tu$UMlD-NC+Pa zCDKI8=%mvD(2=c_-usH{(tBnt?0(ZmOP~{gjh_DH2Y>tB>F&iM{Th0XXmFJ$Bfaw0 zwy;J$KLEpM>pZL4rD9;YmUk@B`kio*U_QKCf&1n45^Qt~K38IE&y-a8c9wY=2Ux%)P@ z=KL`6P=un9_HlYfV*_#dVp9TR)|;@vQjiIh|M2hp)l0e;BTgzp*3?Oe3OPa)5S;Cl zK6&f2)2-*Zr2FF=D1lA@HgM7}SbJ!mwE4H(N(;5z>tUopRf(39JO$8ib78ukDX;83R4~FR690i=%hT;pr!*+)FOyT72oYB zqXh52^*QNX7jixKwl+%wod9gsd|$ND9{oVN;oMgGT=rNH)Fw0ty?;ApP5}A<3_16z zLZXTUM8I1EW9qMgI(p6s8nX6bN)cPVfFlM0Sy@G`1o#G4;b8ISkw2idqfoPe=>{4>^j`%pIFM9(z)VK_`pt zrMsCDzds*j6%w}A8bOk}oo;;HbJO2lw8h*T-WUmV0&s-gt zp_BMNoWm$;^x*f?kH7K7=?9l)CHJ>%f&@AN*o0ZWJY!rOK9SC$adtq@A@E=enlg-R zAwFDS=J^)^lF>)u_gA z4J^{3!L#9TDjUTjT+4up-Y5KK|I8pb8DsE%?ZWhP3GU$(~3hWQGr-~C1 ztgGNDh*CtV5acl@&qbOJSSkN4!{YII@>$RL9sE^EoR=}BL;tDD_dKV|>HrXrYLOq1 z0Y#o7;!_m88t7ZP3JBOBUfe3FQY=a*Q%a|gT;I>?-&clg4Lj2bz#2~KejSn(3`%bg ziv`Wg0z=w7VGaiED!s7{QFw=7>J|g{Cx{|sB4Bk46_1Y7BH8cb z50L;o?pP5IM-EJOz;+zL0_18&Iv|6 zr3;FF&Y2TIT2T*DO7FkphF;jWUv1dB^{W$rb(_%@8PAcEX&VoDk;a*c0OW1wc!z*1 z?kjW|^kFNR{E&^x?FZ)IgFTyc~am^K7BCRNC=XEq>TFL^9m>-J2FE_84QUHa!Y z+?1ZQ2Fth)VMQg-3BZa@+`4u9RUb)@xbmU&fb-HC#baEE6_N;{K!h>IdZTud3HC5r0$fK(yzK_V9tis^w| zhambRi4E%j6F(55ngiFLVBUtVoIR8t`L^e!@2%S!?&DcK33LLmdULk+JwAF*`tTvWS0VbH zIlx}W2B`oU5vYKrGdeTsAJo`forg+Gz&z$Ky{Ip6gu-Mohrk5nG}6hfl>X+;&rdI2 z`^DP_xKa}61Yo76Yx6og)C!DBS^V$N;EjRW3A-RZl$HePCpbtQUIV)hG$csrcYfaIQlkiy1ok zwioo={^nV&)fsUo0IM@s8{gBT_oVHjSkJt+j7oZ+yaOt9r(qv=L^3idU@*2u`JkSt zl3i7abMAS1HY7c*BWiIsLI9ufPLx!0jJMPd;!kebpImHvB=@ zTjlT>_<&37;0dgfFyznVAOa#g_y-TPsgRgzc)hYU5xw+GVE8@3*d+N-^0hGs2L~aM z;Qk<~%5@01(?I3ldHW!YC#ua*pAMc|7|DHPy>R5us^4`78y;${+qQ3;F! zz=$!#?cS{pM}j_KabgH+@c!{Vc4I5zzy*H6)=5mmMBC&XVGsmP6@7ByB@zv50*?T8 zWJn#cEYdzvI$`Tk>56weGo9|svP)o9CC~}Ls?J>RcOikBj;DiRb&^0;U!36FQ>M!LS24dmLdp_%uIibA#@m9t(`0j=nIDYo#)>G{aR`yiXb>I zANp_d$~!)wmI24+fIa|fB!D>H+SpVf=G6SzUw=tD*q2zBz&c8x6M%J`)jp;|0(YE9 zFWowj{xSooI8`tz_;R3ypfiE43PUST-r83o`Ah_${bfSsDzDv2Ulb$--;G~u#6n5( zs*QJKU`ClBz}G&-OtWP*0sLKv4Ca-smH8Q9N<)`q?MI*k=eE{4FT7jj zu~UV5;h9@hgZI8egdz*jhyaXD9{U@mV8nCEyGLSz-n^Wj>0>)%A6e#N-c&?jk>(x`%>u=*fa@r0Mp5Xq7G1xn+DV#<6$KQH+ zx~4CME`d##Kqmm3t^s}gQUY)MVEV&FO20FQ1mvpzT4;KD3m2<#Xc~72jG^m{vOE8< zaCWpmzgW^Yg&r?5V!H!uw#NfAgm3dJLta0fSm+zN647c7#%W1EP_AHC~U z>D^t`y9BxfyaYM{=o--_FjE3=`cQfxZKp?!+sZ_k8^PLuIybGz!EkMhPAa4=cwrtN zKpE2uOjzu1M;wIbi~yPs^2siIxvRo?*ec=xV=F*EAQX;-KuV$}Ypy*=DShwVuTGDg zIi>x!E`g1fKqmkjJ^g*~=Ocl)o=QKzeKvhQrSxbCavb6&KxI$bTzu#G6IiQ=KyH@O z3>8i!E?_0ExWM28=V zoej0xhLJFk<(anreJ-VR?D*@_w=T~N^|y8jY?cH%0obhh?xWqu5_rq0^s`qi(%mWT zZb@UJkOKt4sek$G`zX&2Zg<{-CkO=A6{z&a*e_~pfq#RX;rUOcv+14hJ(9k;kEfw` ztxI5IB+v=K#!Ph|=sHQ@^&d+I9{t1gtBXZ?ZA$5h|DSSr0+I!DvLnJU{vOl}@Ne$x ze!CHfZ=^-~m+kHJ$!{D;_k8@ebatKO+sD!+un7|QKjQ7a{t~vDDgXcg07*qoM6N<$ Ef@mE(_W%F@ diff --git a/e2e/solid-start/basic-auth/public/apple-touch-icon.png b/e2e/solid-start/basic-auth/public/apple-touch-icon.png deleted file mode 100644 index 5a9423cc02c40ea066d2a061cdcdf621c0718cb1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27246 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⪼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 N002ovPDHLkV1k;v<;Va4 diff --git a/e2e/solid-start/basic-auth/public/favicon-16x16.png b/e2e/solid-start/basic-auth/public/favicon-16x16.png deleted file mode 100644 index e3389b00443d602a249b48ae0e21e333971c9c1e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 832 zcmV-G1Hb%Px%`bk7VR5(v%lW%BSRTRK~=OuZ4FYhI3vvy^4t8t~UDHGBS#44K%hSiDal!9Nj zeb^RZ;3&cb72F3w6s4;uUA7>Cf>1=Z56;a$8wZ=NKHtq z33GDV=_|V0E(QPTEB1=edhG{{=-+C??RQi7*e9J7YBnZR+PppIKjA8EV+}Yt`zH<7 zGR%O5D3|Ef)99hUXu19=Cxaki@6toZIe&u-X0(oDFRl|x6P7Bwc^mB(i9|gOY)Jh6DaPkmt z8Wr!@TP1|RPlYRlJe^eRB0K(gn91{(=zi{HrkpHIflw8&q%;+U!V&dFPYDQAWJh@< zwcklN9VEX?vG;}75GqCepKC25M^xV%BcR%A8c!)S5k4P0$g}HS#lA6zy`~K@9&EX4 zOrvr%_1U2dp~gg6G*;&`Jag*?jZTJv+$S`*#)zme_SFWOCm*F4U&hJihqxAu;8z}x zWv7!u>YKRa3PUJQs55wC)i4QvFOAV0jW$?1cnUJJ7Vl?z_ge{9q!zpfCJxd8vJ>-ql@0&`Bttn1~4;s>8tax%hr(^q#A_*wqAv( zh(T&>hJD?OhiP$sxAzkd1W1^EHV1WL3xM&6$*HhG@|8}OjPpG{+!;GC>w;ha0000< KMNUMnLSTZGXp5cz diff --git a/e2e/solid-start/basic-auth/public/favicon-32x32.png b/e2e/solid-start/basic-auth/public/favicon-32x32.png deleted file mode 100644 index 900c77d444c9b31e6144b6ac0bb1ab7bb7ca767f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2115 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 diff --git a/e2e/solid-start/basic-auth/public/favicon.ico b/e2e/solid-start/basic-auth/public/favicon.ico deleted file mode 100644 index 1a1751676f7e22811b1070572093996c93c87617..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15406 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 diff --git a/e2e/solid-start/basic-auth/public/favicon.png b/e2e/solid-start/basic-auth/public/favicon.png deleted file mode 100644 index 1e77bc06091ade4496525a09d8900675afcf03f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1507 zcmV<91swW`P)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91AfN*P1ONa40RR91AOHXW0IY^$^8f$^O-V#SR9Fd>S3Qp$MHK9ro!#4$ zEP@L_hX|b@f=!*_42h6mKu7{)4)_U*>1>0bCkUj;Z1X!7 zHe(Ew^Oi(|bW3J~xu+)XbtFF?4>!7TH$>(D_atUQVEj(8fGvYu2NF33#JZX>)(Vj8 zIi@z>Glt?6t~;Lf(|C8F>;WF^8F<^s7Scr!sZc01uB?HMHoL5+FZ>B(g+r-)?Sn)#3Zal#?G@GAwO5U27MpGOlC2+_saA)rl zP-<@-n~;PQOlm|Hi<+W;NdR;5+=zADzM&?!+CPD36=cGwHy6!D^vPEHG?rO`K>G|M z3FposX{yT132wuw1OR3Um_5JoKB#6?!QgBupIT;?YIr;WcpmuCE>S75mZid+ens#E zGPuYjiG0UNNVWu=f!Id^?9)34)eIpu-`j_~W0iAQzK(}XYc_!;87Tk~?4tq|h=2(! zuq0HCiNK)@+ocCKR3q1REdUju>HdYxd>JX@%oOibg+J~D+}rhz54D!NfC{h-OYk{M zkzmFtdrL@nL0bm8nF@pob1CeLC>12ef#in-Bzv2!wi)Iuwq24)`AH}|0QNQ^f$KHv z?5PBPo1*#GAuAk+Poe`?UJ>mP`@~d4a(103j0lwUx@_+$#B&VC%7r>#2$HIiD`KO8L|s3Yp%M}BT0;NJDzZtPnx=4%enhU zhW*pNN0t`^4%5MKAR+}=^Q?QeqQ`>bbK zf+-ji$Uz8V0?LpX@kh`k%DL)GCA2=@SJNKg56Wh>>pr=7{1PmHqG|~=AdLV3002ov JPDHLkV1ivgp)>#h diff --git a/e2e/solid-start/basic-auth/public/site.webmanifest b/e2e/solid-start/basic-auth/public/site.webmanifest deleted file mode 100644 index fa99de77db..0000000000 --- a/e2e/solid-start/basic-auth/public/site.webmanifest +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "", - "short_name": "", - "icons": [ - { - "src": "/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} diff --git a/e2e/solid-start/basic-auth/tailwind.config.mjs b/e2e/solid-start/basic-auth/tailwind.config.mjs deleted file mode 100644 index 07c3598bac..0000000000 --- a/e2e/solid-start/basic-auth/tailwind.config.mjs +++ /dev/null @@ -1,4 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -export default { - content: ['./app/**/*.{js,jsx,ts,tsx}'], -} diff --git a/e2e/solid-start/basic-auth/tests/app.spec.ts b/e2e/solid-start/basic-auth/tests/app.spec.ts deleted file mode 100644 index e96cb400b0..0000000000 --- a/e2e/solid-start/basic-auth/tests/app.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { expect, test } from '@playwright/test' -import type { Page } from '@playwright/test' - -async function signup( - page: Page, - baseUrl: string, - email: string, - password: string, -) { - await page.goto(baseUrl + '/signup') - await page.fill('input[name="email"]', email) - await page.fill('input[name="password"]', password) - await page.click('button[type="submit"]') -} - -async function login( - page: Page, - email: string, - password: string, - signupOnFail = false, -) { - await page.goto('/login') - await page.fill('input[name="email"]', email) - await page.fill('input[name="password"]', password) - await page.click('button[type="submit"]') - - if (signupOnFail) { - await page.waitForSelector('text=User not found') - await page.click('button:has-text("Sign up instead?")') - await page.waitForSelector('text=Logout') - } -} - -test('Posts redirects to login when not authenticated', async ({ page }) => { - await page.goto('/posts') - await expect(page.locator('h1')).toContainText('Login') -}) - -test('Login fails with user not found', async ({ page }) => { - await login(page, 'bad@gmail.com', 'badpassword') - expect(page.getByText('User not found')).toBeTruthy() -}) - -test('Login fails with incorrect password', async ({ page }) => { - await signup(page, 'test@gmail.com', 'badpassword') - expect(page.getByText('Incorrect password')).toBeTruthy() -}) - -test('Can sign up from a not found user', async ({ page }) => { - await login(page, 'test2@gmail.com', 'badpassword', true) - expect(page.getByText('test@gmail.com')).toBeTruthy() -}) - -test('Navigating to post after logging in', async ({ page }) => { - await login(page, 'test@gmail.com', 'test') - await new Promise((r) => setTimeout(r, 1000)) - await page.getByRole('link', { name: 'Posts' }).click() - await page.getByRole('link', { name: 'sunt aut facere repe' }).click() - await expect(page.getByRole('heading')).toContainText('sunt aut facere') -}) diff --git a/e2e/solid-start/basic-auth/tests/mock-db-setup.test.ts b/e2e/solid-start/basic-auth/tests/mock-db-setup.test.ts deleted file mode 100644 index 742381a047..0000000000 --- a/e2e/solid-start/basic-auth/tests/mock-db-setup.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { test as setup } from '@playwright/test' - -import { PrismaClient } from '@prisma/client' - -const prismaClient = new PrismaClient() - -setup('create new database', async () => { - if ( - await prismaClient.user.findUnique({ - where: { - email: 'test2@gmail.com', - }, - }) - ) { - await prismaClient.user.delete({ - where: { - email: 'test2@gmail.com', - }, - }) - } -}) diff --git a/e2e/solid-start/basic-auth/tests/mock-db-teardown.test.ts b/e2e/solid-start/basic-auth/tests/mock-db-teardown.test.ts deleted file mode 100644 index d933c7cb08..0000000000 --- a/e2e/solid-start/basic-auth/tests/mock-db-teardown.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { test as teardown } from '@playwright/test' - -import { PrismaClient } from '@prisma/client' - -const prismaClient = new PrismaClient() - -teardown('create new database', async () => { - if ( - await prismaClient.user.findUnique({ - where: { - email: 'test2@gmail.com', - }, - }) - ) { - await prismaClient.user.delete({ - where: { - email: 'test2@gmail.com', - }, - }) - } -}) diff --git a/e2e/solid-start/basic-auth/tsconfig.json b/e2e/solid-start/basic-auth/tsconfig.json deleted file mode 100644 index 86fe6d2cf5..0000000000 --- a/e2e/solid-start/basic-auth/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "include": ["**/*.ts", "**/*.tsx"], - "compilerOptions": { - "strict": true, - "esModuleInterop": true, - "jsx": "preserve", - "jsxImportSource": "solid-js", - "module": "ESNext", - "moduleResolution": "Bundler", - "lib": ["DOM", "DOM.Iterable", "ES2022"], - "isolatedModules": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "target": "ES2022", - "allowJs": true, - "forceConsistentCasingInFileNames": true, - "baseUrl": ".", - "paths": { - "~/*": ["./app/*"] - }, - "noEmit": true - } -} From d08a20a7a2ccaae4b40879bf57ed99ea40e53b41 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Sat, 1 Mar 2025 04:34:09 +0100 Subject: [PATCH 118/124] lock --- pnpm-lock.yaml | 55 -------------------------------------------------- 1 file changed, 55 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b121a28ad8..c667fb14e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1936,61 +1936,6 @@ importers: specifier: ^5.1.4 version: 5.1.4(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) - e2e/solid-start/basic-auth: - dependencies: - '@prisma/client': - specifier: 5.22.0 - version: 5.22.0(prisma@5.22.0) - '@tanstack/solid-router': - specifier: workspace:^ - version: link:../../../packages/solid-router - '@tanstack/solid-start': - specifier: workspace:^ - version: link:../../../packages/solid-start - prisma: - specifier: ^5.22.0 - version: 5.22.0 - redaxios: - specifier: ^0.5.1 - version: 0.5.1 - solid-js: - specifier: ^1.0.0 - version: 1.9.5 - tailwind-merge: - specifier: ^2.6.0 - version: 2.6.0 - vinxi: - specifier: 0.5.3 - version: 0.5.3(@types/node@22.13.4)(db0@0.2.3)(ioredis@5.4.2)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0) - devDependencies: - '@playwright/test': - specifier: ^1.50.1 - version: 1.50.1 - '@tanstack/router-e2e-utils': - specifier: workspace:^ - version: link:../../e2e-utils - '@types/node': - specifier: ^22.10.2 - version: 22.13.4 - autoprefixer: - specifier: ^10.4.20 - version: 10.4.20(postcss@8.5.1) - postcss: - specifier: ^8.5.1 - version: 8.5.1 - tailwindcss: - specifier: ^3.4.17 - version: 3.4.17 - typescript: - specifier: ^5.7.2 - version: 5.7.3 - vite-plugin-solid: - specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) - vite-tsconfig-paths: - specifier: ^5.1.4 - version: 5.1.4(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) - e2e/solid-start/scroll-restoration: dependencies: '@tanstack/solid-router': From 78e3d91542da96306ef2a3820ae76a3d83f64129 Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Fri, 28 Feb 2025 22:05:24 -0600 Subject: [PATCH 119/124] fix version mismatch --- e2e/solid-start/server-functions/package.json | 2 +- pnpm-lock.yaml | 26 ++++++------------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/e2e/solid-start/server-functions/package.json b/e2e/solid-start/server-functions/package.json index 4e9d908851..d46a4a8f85 100644 --- a/e2e/solid-start/server-functions/package.json +++ b/e2e/solid-start/server-functions/package.json @@ -14,7 +14,7 @@ "@tanstack/solid-router": "workspace:^", "@tanstack/solid-start": "workspace:^", "js-cookie": "^3.0.5", - "solid-js": "1.9.4", + "solid-js": "^1.9.5", "redaxios": "^0.5.1", "tailwind-merge": "^2.6.0", "vinxi": "0.5.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c667fb14e1..1a906a3409 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2009,8 +2009,8 @@ importers: specifier: ^0.5.1 version: 0.5.1 solid-js: - specifier: 1.9.4 - version: 1.9.4 + specifier: ^1.9.5 + version: 1.9.5 tailwind-merge: specifier: ^2.6.0 version: 2.6.0 @@ -2050,7 +2050,7 @@ importers: version: 5.7.3 vite-plugin-solid: specifier: ^2.11.6 - version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.4)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) vite-tsconfig-paths: specifier: ^5.1.4 version: 5.1.4(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) @@ -16329,6 +16329,11 @@ snapshots: '@testing-library/dom': 10.4.0 solid-js: 1.9.5 + '@solidjs/testing-library@0.8.10(solid-js@1.9.5)': + dependencies: + '@testing-library/dom': 10.4.0 + solid-js: 1.9.5 + '@stylistic/eslint-plugin-js@2.13.0(eslint@9.20.0(jiti@2.4.2))': dependencies: eslint: 9.20.0(jiti@2.4.2) @@ -22064,21 +22069,6 @@ snapshots: transitivePeerDependencies: - supports-color - vite-plugin-solid@2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.4)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)): - dependencies: - '@babel/core': 7.26.8 - '@types/babel__core': 7.20.5 - babel-preset-solid: 1.9.3(@babel/core@7.26.8) - merge-anything: 5.1.7 - solid-js: 1.9.4 - solid-refresh: 0.6.3(solid-js@1.9.4) - vite: 6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) - vitefu: 1.0.5(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) - optionalDependencies: - '@testing-library/jest-dom': 6.6.3 - transitivePeerDependencies: - - supports-color - vite-plugin-solid@2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)): dependencies: '@babel/core': 7.26.8 From 3fe58fb00bf2d09b455f2511fec242e08cb0b7c4 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Sat, 1 Mar 2025 05:08:23 +0100 Subject: [PATCH 120/124] remove react easlint from solid-start-server --- packages/solid-start-server/eslint.config.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/solid-start-server/eslint.config.js b/packages/solid-start-server/eslint.config.js index 931f0ec774..c97d4f0d82 100644 --- a/packages/solid-start-server/eslint.config.js +++ b/packages/solid-start-server/eslint.config.js @@ -1,25 +1,18 @@ // @ts-check -import pluginReact from '@eslint-react/eslint-plugin' -import pluginReactHooks from 'eslint-plugin-react-hooks' import rootConfig from '../../eslint.config.js' export default [ ...rootConfig, { - ...pluginReact.configs.recommended, files: ['**/*.{ts,tsx}'], }, { plugins: { - 'react-hooks': pluginReactHooks, + }, rules: { - '@eslint-react/no-unstable-context-value': 'off', - '@eslint-react/no-unstable-default-props': 'off', - '@eslint-react/dom/no-missing-button-type': 'off', - 'react-hooks/exhaustive-deps': 'error', - 'react-hooks/rules-of-hooks': 'error', + }, }, { From a6bf5211c3195dd3c08c36eecb13881baac955b3 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 1 Mar 2025 04:09:26 +0000 Subject: [PATCH 121/124] ci: apply automated fixes --- packages/solid-start-server/eslint.config.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/solid-start-server/eslint.config.js b/packages/solid-start-server/eslint.config.js index c97d4f0d82..bd7118fa1a 100644 --- a/packages/solid-start-server/eslint.config.js +++ b/packages/solid-start-server/eslint.config.js @@ -8,12 +8,8 @@ export default [ files: ['**/*.{ts,tsx}'], }, { - plugins: { - - }, - rules: { - - }, + plugins: {}, + rules: {}, }, { files: ['**/__tests__/**'], From 22e50153d717155e6eb52cdc3c5399b4ca08afde Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Sat, 1 Mar 2025 05:24:28 +0100 Subject: [PATCH 122/124] use solid 1.9.5 --- e2e/solid-start/basic/package.json | 2 +- .../scroll-restoration/package.json | 2 +- e2e/solid-start/website/package.json | 2 +- examples/solid/start-bare/package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- packages/solid-start-server/package.json | 2 +- pnpm-lock.yaml | 32 +++++++++---------- 8 files changed, 23 insertions(+), 23 deletions(-) diff --git a/e2e/solid-start/basic/package.json b/e2e/solid-start/basic/package.json index b99fb82624..b8321983bc 100644 --- a/e2e/solid-start/basic/package.json +++ b/e2e/solid-start/basic/package.json @@ -13,7 +13,7 @@ "dependencies": { "@tanstack/solid-router": "workspace:^", "@tanstack/solid-start": "workspace:^", - "solid-js": "^1.0.0", + "solid-js": "^1.9.5", "redaxios": "^0.5.1", "tailwind-merge": "^2.6.0", "vinxi": "0.5.3", diff --git a/e2e/solid-start/scroll-restoration/package.json b/e2e/solid-start/scroll-restoration/package.json index 5fce28623b..a9aac0817b 100644 --- a/e2e/solid-start/scroll-restoration/package.json +++ b/e2e/solid-start/scroll-restoration/package.json @@ -14,7 +14,7 @@ "@tanstack/solid-router": "workspace:^", "@tanstack/solid-start": "workspace:^", "@tanstack/zod-adapter": "workspace:^", - "solid-js": "^1.0.0", + "solid-js": "^1.9.5", "redaxios": "^0.5.1", "tailwind-merge": "^2.6.0", "vinxi": "0.5.3", diff --git a/e2e/solid-start/website/package.json b/e2e/solid-start/website/package.json index fc60a05ff8..0cc2b7e73b 100644 --- a/e2e/solid-start/website/package.json +++ b/e2e/solid-start/website/package.json @@ -13,7 +13,7 @@ "dependencies": { "@tanstack/solid-router": "workspace:^", "@tanstack/solid-start": "workspace:^", - "solid-js": "^1.0.0", + "solid-js": "^1.9.5", "redaxios": "^0.5.1", "tailwind-merge": "^2.6.0", "vinxi": "0.5.3", diff --git a/examples/solid/start-bare/package.json b/examples/solid/start-bare/package.json index dd18fc7807..480804cfea 100644 --- a/examples/solid/start-bare/package.json +++ b/examples/solid/start-bare/package.json @@ -11,7 +11,7 @@ "dependencies": { "@tanstack/solid-router": "workspace:^", "@tanstack/solid-start": "workspace:^", - "solid-js": "^1.0.0", + "solid-js": "^1.9.5", "redaxios": "^0.5.1", "tailwind-merge": "^2.6.0", "vinxi": "0.5.3", diff --git a/packages/solid-start-server-functions-fetcher/package.json b/packages/solid-start-server-functions-fetcher/package.json index 26a1433d64..5f4c29826e 100644 --- a/packages/solid-start-server-functions-fetcher/package.json +++ b/packages/solid-start-server-functions-fetcher/package.json @@ -68,7 +68,7 @@ }, "devDependencies": { "vite-plugin-solid": "^2.11.2", - "solid-js": "^1.0.0", + "solid-js": "^1.9.5", "typescript": "^5.7.2" }, "peerDependencies": { diff --git a/packages/solid-start-server-functions-handler/package.json b/packages/solid-start-server-functions-handler/package.json index b76d1ec535..ff7d79fc41 100644 --- a/packages/solid-start-server-functions-handler/package.json +++ b/packages/solid-start-server-functions-handler/package.json @@ -69,7 +69,7 @@ }, "devDependencies": { "vite-plugin-solid": "^2.11.2", - "solid-js": "^1.0.0", + "solid-js": "^1.9.5", "typescript": "^5.7.2" }, "peerDependencies": { diff --git a/packages/solid-start-server/package.json b/packages/solid-start-server/package.json index 858f9ccbfa..91d659b396 100644 --- a/packages/solid-start-server/package.json +++ b/packages/solid-start-server/package.json @@ -76,7 +76,7 @@ "devDependencies": { "@types/jsesc": "^3.0.3", "esbuild": "^0.25.0", - "solid-js": "^1.0.0", + "solid-js": "^1.9.5", "typescript": "^5.7.2", "vite-plugin-solid": "^2.11.2" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1a906a3409..bb0af21695 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1893,8 +1893,8 @@ importers: specifier: ^0.5.1 version: 0.5.1 solid-js: - specifier: ^1.0.0 - version: 1.9.4 + specifier: ^1.9.5 + version: 1.9.5 tailwind-merge: specifier: ^2.6.0 version: 2.6.0 @@ -1931,7 +1931,7 @@ importers: version: 5.7.3 vite-plugin-solid: specifier: ^2.11.2 - version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.4)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) vite-tsconfig-paths: specifier: ^5.1.4 version: 5.1.4(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) @@ -1951,7 +1951,7 @@ importers: specifier: ^0.5.1 version: 0.5.1 solid-js: - specifier: ^1.0.0 + specifier: ^1.9.5 version: 1.9.5 tailwind-merge: specifier: ^2.6.0 @@ -2067,7 +2067,7 @@ importers: specifier: ^0.5.1 version: 0.5.1 solid-js: - specifier: ^1.0.0 + specifier: ^1.9.5 version: 1.9.5 tailwind-merge: specifier: ^2.6.0 @@ -4964,7 +4964,7 @@ importers: specifier: ^0.5.1 version: 0.5.1 solid-js: - specifier: ^1.0.0 + specifier: ^1.9.5 version: 1.9.5 tailwind-merge: specifier: ^2.6.0 @@ -6160,7 +6160,7 @@ importers: dependencies: '@solidjs/meta': specifier: ^0.29.4 - version: 0.29.4(solid-js@1.9.4) + version: 0.29.4(solid-js@1.9.5) '@tanstack/history': specifier: workspace:* version: link:../history @@ -6196,14 +6196,14 @@ importers: specifier: ^0.25.0 version: 0.25.0 solid-js: - specifier: ^1.0.0 - version: 1.9.4 + specifier: ^1.9.5 + version: 1.9.5 typescript: specifier: ^5.7.2 version: 5.7.3 vite-plugin-solid: specifier: ^2.11.2 - version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.4)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) packages/solid-start-server-functions-client: dependencies: @@ -6231,14 +6231,14 @@ importers: version: link:../solid-start-client devDependencies: solid-js: - specifier: ^1.0.0 - version: 1.9.4 + specifier: ^1.9.5 + version: 1.9.5 typescript: specifier: ^5.7.2 version: 5.7.3 vite-plugin-solid: specifier: ^2.11.2 - version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.4)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) packages/solid-start-server-functions-handler: dependencies: @@ -6256,14 +6256,14 @@ importers: version: 1.3.3 devDependencies: solid-js: - specifier: ^1.0.0 - version: 1.9.4 + specifier: ^1.9.5 + version: 1.9.5 typescript: specifier: ^5.7.2 version: 5.7.3 vite-plugin-solid: specifier: ^2.11.2 - version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.4)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) packages/solid-start-server-functions-ssr: dependencies: From 198a44af2c80b48813fb2e273a6e729c51db4c41 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Sat, 1 Mar 2025 13:13:06 +0100 Subject: [PATCH 123/124] lockfile --- pnpm-lock.yaml | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bb0af21695..7da5dc17b8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1931,7 +1931,7 @@ importers: version: 5.7.3 vite-plugin-solid: specifier: ^2.11.2 - version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) vite-tsconfig-paths: specifier: ^5.1.4 version: 5.1.4(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) @@ -4993,7 +4993,7 @@ importers: version: 5.7.3 vite-plugin-solid: specifier: ^2.11.2 - version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) vite-tsconfig-paths: specifier: ^5.1.4 version: 5.1.4(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) @@ -5981,7 +5981,7 @@ importers: version: link:../start-server-functions-server solid-js: specifier: '>=1.0.0' - version: 1.9.4 + version: 1.9.5 vite: specifier: 6.1.0 version: 6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) @@ -6022,7 +6022,7 @@ importers: version: 3.1.0 solid-js: specifier: '>=1.0.0' - version: 1.9.4 + version: 1.9.5 tiny-invariant: specifier: ^1.3.3 version: 1.3.3 @@ -6035,7 +6035,7 @@ importers: devDependencies: '@solidjs/testing-library': specifier: ^0.8.10 - version: 0.8.10(solid-js@1.9.4) + version: 0.8.10(solid-js@1.9.5) '@testing-library/jest-dom': specifier: ^6.6.3 version: 6.6.3 @@ -6044,7 +6044,7 @@ importers: version: 3.0.3 vite-plugin-solid: specifier: ^2.11.2 - version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.4)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) packages/solid-start-config: dependencies: @@ -6077,7 +6077,7 @@ importers: version: 1.4.1 solid-js: specifier: '>=1.0.0' - version: 1.9.4 + version: 1.9.5 vinxi: specifier: 0.5.3 version: 0.5.3(@types/node@22.13.4)(db0@0.2.3)(ioredis@5.4.2)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0) @@ -6086,7 +6086,7 @@ importers: version: 6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) vite-plugin-solid: specifier: ^2.11.2 - version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.4)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) zod: specifier: ^3.24.1 version: 3.24.1 @@ -6203,7 +6203,7 @@ importers: version: 5.7.3 vite-plugin-solid: specifier: ^2.11.2 - version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) packages/solid-start-server-functions-client: dependencies: @@ -6238,7 +6238,7 @@ importers: version: 5.7.3 vite-plugin-solid: specifier: ^2.11.2 - version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) packages/solid-start-server-functions-handler: dependencies: @@ -6263,7 +6263,7 @@ importers: version: 5.7.3 vite-plugin-solid: specifier: ^2.11.2 - version: 2.11.2(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) packages/solid-start-server-functions-ssr: dependencies: @@ -16329,11 +16329,6 @@ snapshots: '@testing-library/dom': 10.4.0 solid-js: 1.9.5 - '@solidjs/testing-library@0.8.10(solid-js@1.9.5)': - dependencies: - '@testing-library/dom': 10.4.0 - solid-js: 1.9.5 - '@stylistic/eslint-plugin-js@2.13.0(eslint@9.20.0(jiti@2.4.2))': dependencies: eslint: 9.20.0(jiti@2.4.2) From 910b6123aaff3cfed8269f3e419460043e66c94c Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Sat, 1 Mar 2025 13:31:30 +0100 Subject: [PATCH 124/124] remove --compat from preview release flow --- .github/workflows/pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index b906ea1159..e031ae811f 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -48,4 +48,4 @@ jobs: - name: Build Packages run: pnpm run build:all - name: Publish Previews - run: pnpx pkg-pr-new publish --pnpm --compact './packages/*' --template './examples/*/*' + run: pnpx pkg-pr-new publish --pnpm './packages/*' --template './examples/*/*'